mirror of
https://github.com/penpot/penpot.git
synced 2025-05-30 02:06:10 +02:00
♻️ Use rx streams for style dictionary interface
This commit is contained in:
parent
9f414b6ecd
commit
31f642ed25
6 changed files with 115 additions and 122 deletions
|
@ -202,20 +202,20 @@
|
||||||
|
|
||||||
(build-dictionary [_]
|
(build-dictionary [_]
|
||||||
(let [platform "json"
|
(let [platform "json"
|
||||||
config' (clj->js config)]
|
config' (clj->js config)
|
||||||
(-> (sd. config')
|
build+ (-> (sd. config')
|
||||||
(.buildAllPlatforms platform)
|
(.buildAllPlatforms platform)
|
||||||
(p/then #(.getPlatformTokens ^js % platform))
|
(p/then #(.getPlatformTokens ^js % platform))
|
||||||
(p/then #(.-allTokens ^js %))))))
|
(p/then #(.-allTokens ^js %)))]
|
||||||
|
(rx/from build+))))
|
||||||
|
|
||||||
(defn resolve-tokens-tree+
|
(defn resolve-tokens-tree
|
||||||
([tokens-tree get-token]
|
([tokens-tree get-token]
|
||||||
(resolve-tokens-tree+ tokens-tree get-token (StyleDictionary. default-config)))
|
(resolve-tokens-tree tokens-tree get-token (StyleDictionary. default-config)))
|
||||||
([tokens-tree get-token style-dictionary]
|
([tokens-tree get-token style-dictionary]
|
||||||
(let [sdict (-> style-dictionary
|
(->> (add-tokens style-dictionary tokens-tree)
|
||||||
(add-tokens tokens-tree)
|
(build-dictionary)
|
||||||
(build-dictionary))]
|
(rx/map #(process-sd-tokens % get-token)))))
|
||||||
(p/fmap #(process-sd-tokens % get-token) sdict))))
|
|
||||||
|
|
||||||
(defn sd-token-name [^js sd-token]
|
(defn sd-token-name [^js sd-token]
|
||||||
(.. sd-token -original -name))
|
(.. sd-token -original -name))
|
||||||
|
@ -223,12 +223,12 @@
|
||||||
(defn sd-token-uuid [^js sd-token]
|
(defn sd-token-uuid [^js sd-token]
|
||||||
(uuid (.-uuid (.-id ^js sd-token))))
|
(uuid (.-uuid (.-id ^js sd-token))))
|
||||||
|
|
||||||
(defn resolve-tokens+
|
(defn resolve-tokens
|
||||||
[tokens]
|
[tokens]
|
||||||
(let [tokens-tree (ctob/tokens-tree tokens)]
|
(let [tokens-tree (ctob/tokens-tree tokens)]
|
||||||
(resolve-tokens-tree+ tokens-tree #(get tokens (sd-token-name %)))))
|
(resolve-tokens-tree tokens-tree #(get tokens (sd-token-name %)))))
|
||||||
|
|
||||||
(defn resolve-tokens-interactive+
|
(defn resolve-tokens-interactive
|
||||||
"Interactive check of resolving tokens.
|
"Interactive check of resolving tokens.
|
||||||
Uses a ids map to backtrace the original token from the resolved StyleDictionary token.
|
Uses a ids map to backtrace the original token from the resolved StyleDictionary token.
|
||||||
|
|
||||||
|
@ -241,10 +241,10 @@
|
||||||
this way after the resolving computation we can restore any token, even clashing ones with the same :name path by just looking up that :id in the ids map."
|
this way after the resolving computation we can restore any token, even clashing ones with the same :name path by just looking up that :id in the ids map."
|
||||||
[tokens]
|
[tokens]
|
||||||
(let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)]
|
(let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)]
|
||||||
(resolve-tokens-tree+ tokens-tree #(get ids (sd-token-uuid %)))))
|
(resolve-tokens-tree tokens-tree #(get ids (sd-token-uuid %)))))
|
||||||
|
|
||||||
(defn resolve-tokens-with-errors+ [tokens]
|
(defn resolve-tokens-with-errors [tokens]
|
||||||
(resolve-tokens-tree+
|
(resolve-tokens-tree
|
||||||
(ctob/tokens-tree tokens)
|
(ctob/tokens-tree tokens)
|
||||||
#(get tokens (sd-token-name %))
|
#(get tokens (sd-token-name %))
|
||||||
(StyleDictionary. (assoc default-config :log {:verbosity "verbose"}))))
|
(StyleDictionary. (assoc default-config :log {:verbosity "verbose"}))))
|
||||||
|
@ -346,24 +346,23 @@
|
||||||
(when unknown-tokens
|
(when unknown-tokens
|
||||||
(st/emit! (tokens-of-unknown-type-warning unknown-tokens)))
|
(st/emit! (tokens-of-unknown-type-warning unknown-tokens)))
|
||||||
(try
|
(try
|
||||||
(-> (ctob/get-all-tokens tokens-lib)
|
(->> (ctob/get-all-tokens tokens-lib)
|
||||||
(resolve-tokens-with-errors+)
|
(resolve-tokens-with-errors)
|
||||||
(p/then (fn [_] tokens-lib))
|
(rx/map (fn [_]
|
||||||
(p/catch (fn [sd-error]
|
tokens-lib))
|
||||||
(let [reference-errors (reference-errors sd-error)]
|
(rx/catch (fn [sd-error]
|
||||||
;; We allow reference errors for the users to resolve in the ui and throw on any other errors
|
(let [reference-errors (reference-errors sd-error)]
|
||||||
(if reference-errors
|
;; We allow reference errors for the users to resolve in the ui and throw on any other errors
|
||||||
(p/resolved tokens-lib)
|
(if reference-errors
|
||||||
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error)))))))
|
(rx/of tokens-lib)
|
||||||
|
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error)))))))
|
||||||
(catch js/Error e
|
(catch js/Error e
|
||||||
(p/rejected (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e))))))))))
|
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e))))))))))
|
||||||
|
|
||||||
;; === Hooks
|
;; === Hooks
|
||||||
|
|
||||||
(defonce !tokens-cache (atom nil))
|
(defonce !tokens-cache (atom nil))
|
||||||
|
|
||||||
(defonce !theme-tokens-cache (atom nil))
|
|
||||||
|
|
||||||
(defn use-resolved-tokens
|
(defn use-resolved-tokens
|
||||||
"The StyleDictionary process function is async, so we can't use resolved values directly.
|
"The StyleDictionary process function is async, so we can't use resolved values directly.
|
||||||
|
|
||||||
|
@ -382,19 +381,17 @@
|
||||||
(cond
|
(cond
|
||||||
(nil? tokens) nil
|
(nil? tokens) nil
|
||||||
;; The tokens are already processing somewhere
|
;; The tokens are already processing somewhere
|
||||||
(p/promise? cached) (-> cached
|
(rx/observable? cached) (rx/sub! cached #(reset! tokens-state %))
|
||||||
(p/then #(reset! tokens-state %))
|
|
||||||
#_(p/catch js/console.error))
|
|
||||||
;; Get the cached entry
|
;; Get the cached entry
|
||||||
(some? cached) (reset! tokens-state cached)
|
(some? cached) (reset! tokens-state cached)
|
||||||
;; No cached entry, start processing
|
;; No cached entry, start processing
|
||||||
:else (let [promise+ (if interactive?
|
:else (let [resolved-tokens-s (if interactive?
|
||||||
(resolve-tokens-interactive+ tokens)
|
(resolve-tokens-interactive tokens)
|
||||||
(resolve-tokens+ tokens))]
|
(resolve-tokens tokens))]
|
||||||
(swap! cache-atom assoc tokens promise+)
|
(swap! cache-atom assoc tokens resolved-tokens-s)
|
||||||
(p/then promise+ (fn [resolved-tokens]
|
(rx/sub! resolved-tokens-s (fn [resolved-tokens]
|
||||||
(swap! cache-atom assoc tokens resolved-tokens)
|
(swap! cache-atom assoc tokens resolved-tokens)
|
||||||
(reset! tokens-state resolved-tokens)))))))
|
(reset! tokens-state resolved-tokens)))))))
|
||||||
@tokens-state))
|
@tokens-state))
|
||||||
|
|
||||||
(defn use-resolved-tokens*
|
(defn use-resolved-tokens*
|
||||||
|
@ -408,12 +405,12 @@
|
||||||
(mf/with-effect [tokens interactive?]
|
(mf/with-effect [tokens interactive?]
|
||||||
(if (seq tokens)
|
(if (seq tokens)
|
||||||
(let [tpoint (dt/tpoint-ms)
|
(let [tpoint (dt/tpoint-ms)
|
||||||
promise (if interactive?
|
tokens-s (if interactive?
|
||||||
(resolve-tokens-interactive+ tokens)
|
(resolve-tokens-interactive tokens)
|
||||||
(resolve-tokens+ tokens))]
|
(resolve-tokens tokens))]
|
||||||
|
|
||||||
(->> promise
|
(-> tokens-s
|
||||||
(p/fmap (fn [resolved-tokens]
|
(rx/sub! (fn [resolved-tokens]
|
||||||
(let [elapsed (tpoint)]
|
(let [elapsed (tpoint)]
|
||||||
(l/dbg :hint "use-resolved-tokens*" :elapsed elapsed)
|
(l/dbg :hint "use-resolved-tokens*" :elapsed elapsed)
|
||||||
(reset! state* resolved-tokens))))))
|
(reset! state* resolved-tokens))))))
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||||
(get :tokens-lib)
|
(get :tokens-lib)
|
||||||
(ctob/get-active-themes-set-tokens))]
|
(ctob/get-active-themes-set-tokens))]
|
||||||
(->> (rx/from (sd/resolve-tokens+ tokens))
|
(->> (sd/resolve-tokens tokens)
|
||||||
(rx/mapcat
|
(rx/mapcat
|
||||||
(fn [resolved-tokens]
|
(fn [resolved-tokens]
|
||||||
(let [undo-id (js/Symbol)
|
(let [undo-id (js/Symbol)
|
||||||
|
|
|
@ -185,12 +185,11 @@
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(when-let [tokens-lib (-> (dsh/lookup-file-data state)
|
(when-let [tokens-lib (-> (dsh/lookup-file-data state)
|
||||||
(get :tokens-lib))]
|
(get :tokens-lib))]
|
||||||
(let [tokens (-> (ctob/get-active-themes-set-tokens tokens-lib)
|
(->> (ctob/get-active-themes-set-tokens tokens-lib)
|
||||||
(sd/resolve-tokens+))]
|
(sd/resolve-tokens)
|
||||||
(->> (rx/from tokens)
|
(rx/mapcat (fn [sd-tokens]
|
||||||
(rx/mapcat (fn [sd-tokens]
|
(let [undo-id (js/Symbol)]
|
||||||
(let [undo-id (js/Symbol)]
|
(rx/concat
|
||||||
(rx/concat
|
(rx/of (dwu/start-undo-transaction undo-id :timeout false))
|
||||||
(rx/of (dwu/start-undo-transaction undo-id :timeout false))
|
(propagate-tokens state sd-tokens)
|
||||||
(propagate-tokens state sd-tokens)
|
(rx/of (dwu/commit-undo-transaction undo-id)))))))))))
|
||||||
(rx/of (dwu/commit-undo-transaction undo-id))))))))))))
|
|
||||||
|
|
|
@ -36,10 +36,10 @@
|
||||||
[app.util.functions :as uf]
|
[app.util.functions :as uf]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
[app.util.keyboard :as k]
|
[app.util.keyboard :as k]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[malli.core :as m]
|
[malli.core :as m]
|
||||||
[malli.error :as me]
|
[malli.error :as me]
|
||||||
[promesa.core :as p]
|
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
;; Schemas ---------------------------------------------------------------------
|
;; Schemas ---------------------------------------------------------------------
|
||||||
|
@ -94,14 +94,9 @@
|
||||||
(defn valid-value? [value]
|
(defn valid-value? [value]
|
||||||
(seq (finalize-value value)))
|
(seq (finalize-value value)))
|
||||||
|
|
||||||
(defn schema-validation->promise [validated]
|
|
||||||
(if (:errors validated)
|
|
||||||
(p/rejected validated)
|
|
||||||
(p/resolved validated)))
|
|
||||||
|
|
||||||
;; Component -------------------------------------------------------------------
|
;; Component -------------------------------------------------------------------
|
||||||
|
|
||||||
(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 [value name-value token tokens]}]
|
[{:keys [value name-value token tokens]}]
|
||||||
|
@ -110,10 +105,10 @@
|
||||||
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)]
|
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)]
|
||||||
(cond
|
(cond
|
||||||
(empty? (str/trim value))
|
(empty? (str/trim value))
|
||||||
(p/rejected {:errors [(wte/get-error-code :error.token/empty-input)]})
|
(rx/throw {:errors [(wte/get-error-code :error.token/empty-input)]})
|
||||||
|
|
||||||
(ctob/token-value-self-reference? token-name value)
|
(ctob/token-value-self-reference? token-name value)
|
||||||
(p/rejected {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
|
(rx/throw {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [tokens' (cond-> tokens
|
(let [tokens' (cond-> tokens
|
||||||
|
@ -122,14 +117,14 @@
|
||||||
:always (update token-name #(ctob/make-token (merge % {:value value
|
:always (update token-name #(ctob/make-token (merge % {:value value
|
||||||
:name token-name
|
:name token-name
|
||||||
:type (:type token)}))))]
|
:type (:type token)}))))]
|
||||||
(-> tokens'
|
(->> tokens'
|
||||||
(sd/resolve-tokens-interactive+)
|
(sd/resolve-tokens-interactive)
|
||||||
(p/then
|
(rx/mapcat
|
||||||
(fn [resolved-tokens]
|
(fn [resolved-tokens]
|
||||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
|
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
|
||||||
(cond
|
(cond
|
||||||
resolved-value (p/resolved resolved-token)
|
resolved-value (rx/of resolved-token)
|
||||||
:else (p/rejected {:errors (or errors (wte/get-error-code :error/unknown-error))}))))))))))
|
:else (rx/throw {:errors (or errors (wte/get-error-code :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`.
|
||||||
|
@ -148,14 +143,14 @@
|
||||||
(js/setTimeout
|
(js/setTimeout
|
||||||
(fn []
|
(fn []
|
||||||
(when (not (timeout-outdated-cb?))
|
(when (not (timeout-outdated-cb?))
|
||||||
(-> (validate-token-value+ {:value value
|
(->> (validate-token-value {:value value
|
||||||
:name-value @name-ref
|
:name-value @name-ref
|
||||||
:token token
|
:token token
|
||||||
:tokens tokens})
|
:tokens tokens})
|
||||||
(p/finally
|
(rx/filter #(not (timeout-outdated-cb?)))
|
||||||
(fn [x err]
|
(rx/subs!
|
||||||
(when-not (timeout-outdated-cb?)
|
callback
|
||||||
(callback (or err x))))))))
|
callback))))
|
||||||
timeout))))]
|
timeout))))]
|
||||||
debounced-resolver-callback))
|
debounced-resolver-callback))
|
||||||
|
|
||||||
|
@ -442,33 +437,36 @@
|
||||||
;; and the user might have edited a valid form to make it invalid,
|
;; and the user might have edited a valid form to make it invalid,
|
||||||
;; and press enter before the next validations could return.
|
;; and press enter before the next validations could return.
|
||||||
(let [final-name (finalize-name @token-name-ref)
|
(let [final-name (finalize-name @token-name-ref)
|
||||||
valid-name?+ (-> (validate-name final-name) schema-validation->promise)
|
valid-name? (try
|
||||||
|
(not (:errors (validate-name final-name)))
|
||||||
|
(catch js/Error _ nil))
|
||||||
final-value (finalize-value (mf/ref-val value-ref))
|
final-value (finalize-value (mf/ref-val value-ref))
|
||||||
final-description @description-ref
|
final-description @description-ref
|
||||||
valid-description?+ (some-> final-description validate-descripion schema-validation->promise)]
|
valid-description? (if final-description
|
||||||
(-> (p/all [valid-name?+
|
(try
|
||||||
valid-description?+
|
(not (:errors (validate-descripion final-description)))
|
||||||
(validate-token-value+ {:value final-value
|
(catch js/Error _ nil))
|
||||||
:name-value final-name
|
true)]
|
||||||
:token token
|
(when (and valid-name? valid-description?)
|
||||||
:tokens active-theme-tokens})])
|
(->> (validate-token-value {:value final-value
|
||||||
(p/finally (fn [result err]
|
:name-value final-name
|
||||||
;; The result should be a vector of all resolved validations
|
:token token
|
||||||
;; We do not handle the error case as it will be handled by the components validations
|
:tokens active-theme-tokens})
|
||||||
(when (and (seq result) (not err))
|
(rx/subs!
|
||||||
(st/emit!
|
(fn []
|
||||||
(if (ctob/token? token)
|
(st/emit!
|
||||||
(dwtl/update-token (:name token)
|
(if (ctob/token? token)
|
||||||
{:name final-name
|
(dwtl/update-token (:name token)
|
||||||
:value final-value
|
{:name final-name
|
||||||
:description final-description})
|
:value final-value
|
||||||
|
:description final-description})
|
||||||
|
|
||||||
(dwtl/create-token {:name final-name
|
(dwtl/create-token {:name final-name
|
||||||
:type token-type
|
:type token-type
|
||||||
:value final-value
|
:value final-value
|
||||||
:description final-description}))
|
:description final-description}))
|
||||||
(dwtp/propagate-workspace-tokens)
|
(dwtp/propagate-workspace-tokens)
|
||||||
(modal/hide)))))))))
|
(modal/hide)))))))))
|
||||||
|
|
||||||
on-delete-token
|
on-delete-token
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
|
|
@ -30,9 +30,9 @@
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [data (dsh/lookup-file-data state)]
|
(let [data (dsh/lookup-file-data state)]
|
||||||
(->> (rx/from (-> (get data :tokens-lib)
|
(->> (get data :tokens-lib)
|
||||||
(ctob/get-active-themes-set-tokens)
|
(ctob/get-active-themes-set-tokens)
|
||||||
(sd/resolve-tokens+)))
|
(sd/resolve-tokens)
|
||||||
(rx/mapcat #(rx/of (end))))))))
|
(rx/mapcat #(rx/of (end))))))))
|
||||||
|
|
||||||
(defn stop-on
|
(defn stop-on
|
||||||
|
|
|
@ -10,8 +10,7 @@
|
||||||
[app.common.types.tokens-lib :as ctob]
|
[app.common.types.tokens-lib :as ctob]
|
||||||
[app.main.data.style-dictionary :as sd]
|
[app.main.data.style-dictionary :as sd]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[cljs.test :as t :include-macros true]
|
[cljs.test :as t :include-macros true]))
|
||||||
[promesa.core :as p]))
|
|
||||||
|
|
||||||
(t/deftest resolve-tokens-test
|
(t/deftest resolve-tokens-test
|
||||||
(t/async
|
(t/async
|
||||||
|
@ -35,23 +34,23 @@
|
||||||
:value "{borderRadius.sm} * 200000000"
|
:value "{borderRadius.sm} * 200000000"
|
||||||
:type :border-radius}))
|
:type :border-radius}))
|
||||||
(ctob/get-all-tokens))]
|
(ctob/get-all-tokens))]
|
||||||
(-> (sd/resolve-tokens+ tokens)
|
(-> (sd/resolve-tokens tokens)
|
||||||
(p/finally
|
(rx/sub!
|
||||||
(fn [resolved-tokens]
|
(fn [resolved-tokens]
|
||||||
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))
|
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))
|
||||||
(t/is (= "px" (get-in resolved-tokens ["borderRadius.sm" :unit])))
|
(t/is (= "px" (get-in resolved-tokens ["borderRadius.sm" :unit])))
|
||||||
(t/is (= 24 (get-in resolved-tokens ["borderRadius.md-with-dashes" :resolved-value])))
|
(t/is (= 24 (get-in resolved-tokens ["borderRadius.md-with-dashes" :resolved-value])))
|
||||||
(t/is (= "px" (get-in resolved-tokens ["borderRadius.md-with-dashes" :unit])))
|
(t/is (= "px" (get-in resolved-tokens ["borderRadius.md-with-dashes" :unit])))
|
||||||
(t/is (nil? (get-in resolved-tokens ["borderRadius.large" :resolved-value])))
|
(t/is (nil? (get-in resolved-tokens ["borderRadius.large" :resolved-value])))
|
||||||
(t/is (= :error.token/number-too-large
|
(t/is (= :error.token/number-too-large
|
||||||
(get-in resolved-tokens ["borderRadius.large" :errors 0 :error/code])))
|
(get-in resolved-tokens ["borderRadius.large" :errors 0 :error/code])))
|
||||||
(t/is (nil? (get-in resolved-tokens ["borderRadius.largePx" :resolved-value])))
|
(t/is (nil? (get-in resolved-tokens ["borderRadius.largePx" :resolved-value])))
|
||||||
(t/is (= :error.token/number-too-large
|
(t/is (= :error.token/number-too-large
|
||||||
(get-in resolved-tokens ["borderRadius.largePx" :errors 0 :error/code])))
|
(get-in resolved-tokens ["borderRadius.largePx" :errors 0 :error/code])))
|
||||||
(t/is (nil? (get-in resolved-tokens ["borderRadius.largeFn" :resolved-value])))
|
(t/is (nil? (get-in resolved-tokens ["borderRadius.largeFn" :resolved-value])))
|
||||||
(t/is (= :error.token/number-too-large
|
(t/is (= :error.token/number-too-large
|
||||||
(get-in resolved-tokens ["borderRadius.largeFn" :errors 0 :error/code])))
|
(get-in resolved-tokens ["borderRadius.largeFn" :errors 0 :error/code])))
|
||||||
(done))))))))
|
(done))))))))
|
||||||
|
|
||||||
(t/deftest process-json-stream-test
|
(t/deftest process-json-stream-test
|
||||||
(t/async
|
(t/async
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue