From 3a21643158bbc83ac7542c75e6c2281f1a745bfc Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Wed, 18 Sep 2024 10:38:16 +0200 Subject: [PATCH] Add shared error handling --- .../app/main/ui/workspace/tokens/errors.cljs | 22 ++++ .../app/main/ui/workspace/tokens/form.cljs | 119 ++++++++++-------- .../ui/workspace/tokens/style_dictionary.cljs | 34 ++--- 3 files changed, 106 insertions(+), 69 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/tokens/errors.cljs diff --git a/frontend/src/app/main/ui/workspace/tokens/errors.cljs b/frontend/src/app/main/ui/workspace/tokens/errors.cljs new file mode 100644 index 000000000..471d9c59f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/errors.cljs @@ -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')))))) diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs index faaf664af..efbca9d7a 100644 --- a/frontend/src/app/main/ui/workspace/tokens/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.tokens.form (:require-macros [app.main.style :as stl]) (:require + [app.main.ui.workspace.tokens.errors :as wte] ["lodash.debounce" :as debounce] [app.common.colors :as cc] [app.common.data :as d] @@ -25,7 +26,8 @@ [malli.core :as m] [malli.error :as me] [promesa.core :as p] - [rumext.v2 :as mf])) + [rumext.v2 :as mf] + [app.main.ui.workspace.tokens.tinycolor :as tinycolor])) ;; Schemas --------------------------------------------------------------------- @@ -87,36 +89,39 @@ Token names should only contain letters and digits separated by . characters.")} ;; 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+ "Validates token value by resolving the value `input` using `StyleDictionary`. Returns a promise of either resolved tokens or rejects with an error state." [{:keys [input name-value token tokens]}] - (let [empty-input? (empty? (str/trim input)) - ;; Check if the given value contains a reference that is the current token-name - ;; When creating a new token we dont have a token name yet, - ;; 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)] + (let [ ;; When creating a new token we dont have a token name yet, + ;; 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)] (cond - empty-input? (p/rejected nil) - direct-self-reference? (p/rejected :error/token-direct-self-reference) - :else (let [token-id (or (:id token) (random-uuid)) - new-tokens (update tokens token-name merge {:id token-id - :value input - :name token-name - :type (:type token)})] - (-> (sd/resolve-tokens+ new-tokens {:names-map? true - :debug? true}) - (p/then - (fn [resolved-tokens] - (js/console.log "resolved-tokens" resolved-tokens) - (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)] - (cond - resolved-value (p/resolved resolved-token) - (sd/missing-reference-error? errors) (p/rejected :error/token-missing-reference) - :else (p/rejected :error/unknown-error))))) - (p/catch js/console.log)))))) + (empty? (str/trim input)) + (p/rejected {:errors #{:error/empty-input}}) + + (token-self-reference? token-name input) + (p/rejected {:errors #{:error.token/direct-self-reference}}) + + :else + (let [token-id (or (:id token) (random-uuid)) + new-tokens (update tokens token-name merge {:id token-id + :value input + :name token-name + :type (:type token)})] + (-> (sd/resolve-tokens+ new-tokens {:names-map? true}) + (p/then + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)] + (cond + resolved-value (p/resolved resolved-token) + :else (p/rejected {:errors (or errors #{:error/unknown-error})})))))))))) (defn use-debonced-resolve-callback "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 :token token :tokens tokens}) - (p/finally (fn [x err] - (when-not (timeout-outdated-cb?) - (callback (or err x)))))))) + (p/finally + (fn [x err] + (when-not (timeout-outdated-cb?) + (callback (or err x)))))))) timeout))))] debounced-resolver-callback)) @@ -149,7 +155,6 @@ Token names should only contain letters and digits separated by . characters.")} (mf/defc ramp [{:keys [color on-change]}] - (js/console.log "color" color) (let [dragging? (mf/use-state) [r g b] (cc/hex->rgb 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-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/wrap-props false} [{: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])) set-resolve-value (mf/use-callback (fn [token-or-err] - (let [v (cond - (= token-or-err :error/token-direct-self-reference) token-or-err - (= token-or-err :error/token-missing-reference) token-or-err - (:resolved-value token-or-err) (:resolved-value token-or-err))] - (when color? (reset! color v)) + (let [error? (:errors token-or-err) + v (if error? + token-or-err + (:resolved-value token-or-err))] + (when color? (reset! color (if error? nil 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 (mf/use-callback @@ -237,8 +256,7 @@ Token names should only contain letters and digits separated by . characters.")} (fn [hex-value] (reset! value-ref hex-value) (on-update-value-debounced hex-value))) - value-error? (when (keyword? @token-resolve-result) - (= (namespace @token-resolve-result) "error")) + value-error? (seq (:errors @token-resolve-result)) valid-value-field? (and (not value-error?) (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} :render-right (when color? (mf/fnc [] - [:div {:class (stl/css :color-bullet) - :on-click #(swap! color-ramp-open? not)} - (if @color - [:& color-bullet {:color @color - :mini? true}] - [:div {:class (stl/css :color-bullet-placeholder)}])]))}] + [:div {:class (stl/css :color-bullet) + :on-click #(swap! color-ramp-open? not)} + (if-let [hex (some-> @color tinycolor/valid-color tinycolor/->hex)] + [:& color-bullet {:color hex + :mini? true}] + [:div {:class (stl/css :color-bullet-placeholder)}])]))}] (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}]) - [:div {:class (stl/css-case :resolved-value true - :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])] + [:& token-value-or-errors {:result-or-errors @token-resolve-result}] + [:div [:& tokens.common/labeled-input {:label "Description" :input-props {:default-value @description-ref diff --git a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs index 919cfd738..8c6a126f4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs @@ -2,8 +2,8 @@ (:require ["@tokens-studio/sd-transforms" :as sd-transforms] ["style-dictionary$default" :as sd] - [app.common.data :refer [ordered-map]] [app.main.refs :as refs] + [app.main.ui.workspace.tokens.tinycolor :as tinycolor] [app.main.ui.workspace.tokens.token :as wtt] [cuerdas.core :as str] [promesa.core :as p] @@ -61,19 +61,15 @@ (js/console.log "Resolved tokens" resolved-tokens)) resolved-tokens)))))) + (defn humanize-errors [{:keys [errors value] :as _token}] (->> (map (fn [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)) errors) (str/join "\n"))) -(defn missing-reference-error? - [errors] - (and (set? errors) - (get errors :style-dictionary/missing-reference))) - (defn resolve-tokens+ [tokens & {:keys [names-map? debug?] :as config}] (p/let [sd-tokens (-> (wtt/token-names-tree tokens) @@ -85,15 +81,21 @@ (uuid (.-uuid (.-id cur)))) {:keys [type] :as origin-token} (get tokens identifier) value (.-value cur) - parsed-value (case type - :color (wtt/parse-token-color-value value) - (wtt/parse-token-value value)) - resolved-token (if (not parsed-value) - (assoc origin-token :errors [:style-dictionary/missing-reference]) - (assoc origin-token - :resolved-value (:value parsed-value) - :resolved-unit (:unit parsed-value)))] - (assoc acc (wtt/token-identifier resolved-token) resolved-token))) + token-or-err (case type + :color (if-let [tc (tinycolor/valid-color value)] + {:value value :unit (tinycolor/color-format tc)} + {:errors #{:error.token/invalid-color}}) + (or (wtt/parse-token-value value) + (if-let [references (seq (wtt/find-token-references value))] + {:errors #{:error.style-dictionary/missing-reference} + :references references} + {: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)] (when debug? (js/console.log "Resolved tokens" resolved-tokens))