Merge pull request #5784 from penpot/niwinz-tokens-changes-2

 Several performance oriented changes to tokens code (part 2)
This commit is contained in:
Andrey Antukh 2025-02-13 11:47:28 +01:00 committed by GitHub
commit 3ea52a0198
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 849 additions and 699 deletions

View file

@ -26,13 +26,15 @@
(def valid-groupable-item? (def valid-groupable-item?
(sm/validator schema:groupable-item)) (sm/validator schema:groupable-item))
(def ^:private xf:clean-string
(comp (map str/trim)
(remove str/empty?)))
(defn split-path (defn split-path
"Decompose a string in the form 'one.two.three' into a vector of strings, removing spaces." "Decompose a string in the form 'one.two.three' into a vector of strings, removing spaces."
[path separator] [path separator]
(let [xf (comp (map str/trim) (->> (str/split path separator)
(remove str/empty?))] (into [] xf:clean-string)))
(->> (str/split path separator)
(into [] xf))))
(defn join-path (defn join-path
"Regenerate a path as a string, from a vector." "Regenerate a path as a string, from a vector."
@ -104,7 +106,10 @@
(defn get-token-path [path] (defn get-token-path [path]
(get-path path token-separator)) (get-path path token-separator))
(defn split-token-path [path] ;; FIXME: misleading name, we are spliting name into path, not
;; spliting path into path
(defn split-token-path
[path]
(split-path path token-separator)) (split-path path token-separator))
(defrecord Token [name type value description modified-at]) (defrecord Token [name type value description modified-at])
@ -247,71 +252,35 @@
set-name (add-set-prefix (last paths))] set-name (add-set-prefix (last paths))]
(conj set-path set-name))) (conj set-path set-name)))
(defn split-token-set-path [token-set-path] (defn split-token-set-name
(split-path token-set-path set-separator)) [name]
(split-path name set-separator))
(defn split-token-set-name [token-set-name]
(-> (split-token-set-path token-set-name)
(add-token-set-paths-prefix)))
(defn get-token-set-path [token-set] (defn get-token-set-path [token-set]
(let [path (get-path token-set set-separator)] (let [path (get-path token-set set-separator)]
(add-token-set-paths-prefix path))) (add-token-set-paths-prefix path)))
(defn set-name->set-path-string [set-name] (defn get-token-set-final-name
(-> (split-token-set-name set-name) [name]
(join-set-path))) (-> (split-token-set-name name)
(peek)))
(defn set-path->set-name [set-path]
(->> (split-token-set-path set-path)
(map (fn [path-part]
(or (-> (split-set-prefix path-part)
(second))
path-part)))
(join-set-path)))
(defn get-token-set-final-name [path]
(-> (split-token-set-path path)
(last)))
(defn set-name->prefixed-full-path [name-str] (defn set-name->prefixed-full-path [name-str]
(-> (split-token-set-path name-str) (-> (split-token-set-name name-str)
(set-full-path->set-prefixed-full-path))) (set-full-path->set-prefixed-full-path)))
(defn get-token-set-prefixed-path [token-set] (defn get-token-set-prefixed-path [token-set]
(let [path (get-path token-set set-separator)] (let [path (get-path token-set set-separator)]
(set-full-path->set-prefixed-full-path path))) (set-full-path->set-prefixed-full-path path)))
(defn get-prefixed-token-set-final-prefix [prefixed-path-str] (defn prefixed-set-path-string->set-name-string [path-str]
(some-> (get-token-set-final-name prefixed-path-str) (->> (split-token-set-name path-str)
(split-set-str-path-prefix)
(first)))
(defn set-name-string->prefixed-set-path-string [name-str]
(-> (set-name->prefixed-full-path name-str)
(join-set-path)))
(defn prefixed-set-path-string->set-path [path-str]
(->> (split-token-set-path path-str)
(map (fn [path-part] (map (fn [path-part]
(or (-> (split-set-str-path-prefix path-part) (or (-> (split-set-str-path-prefix path-part)
(second)) (second))
path-part))))) path-part)))
(defn prefixed-set-path-string->set-name-string [path-str]
(->> (prefixed-set-path-string->set-path path-str)
(join-set-path))) (join-set-path)))
(defn prefixed-set-path-final-group?
"Predicate if the given prefixed path string ends with a group."
[prefixed-path-str]
(= (get-prefixed-token-set-final-prefix prefixed-path-str) set-group-prefix))
(defn prefixed-set-path-final-set?
"Predicate if the given prefixed path string ends with a set."
[prefixed-path-str]
(= (get-prefixed-token-set-final-prefix prefixed-path-str) set-prefix))
(defn replace-last-path-name (defn replace-last-path-name
"Replaces the last element in a `path` vector with `name`." "Replaces the last element in a `path` vector with `name`."
[path name] [path name]
@ -358,7 +327,7 @@
(defrecord TokenSet [name description modified-at tokens] (defrecord TokenSet [name description modified-at tokens]
ITokenSet ITokenSet
(update-name [_ set-name] (update-name [_ set-name]
(TokenSet. (-> (split-token-set-path name) (TokenSet. (-> (split-token-set-name name)
(drop-last) (drop-last)
(concat [set-name]) (concat [set-name])
(join-set-path)) (join-set-path))
@ -401,7 +370,8 @@
(vals tokens)) (vals tokens))
(get-set-prefixed-path-string [_] (get-set-prefixed-path-string [_]
(set-name-string->prefixed-set-path-string name)) (-> (set-name->prefixed-full-path name)
(join-set-path)))
(get-tokens-tree [_] (get-tokens-tree [_]
(tokens-tree tokens)) (tokens-tree tokens))
@ -721,6 +691,7 @@ used for managing active sets without a user created theme.")
(filter some?) (filter some?)
first)))) first))))
;; DEPRECATED
(defn walk-sets-tree-seq (defn walk-sets-tree-seq
"Walk sets tree as a flat list. "Walk sets tree as a flat list.
@ -749,7 +720,7 @@ used for managing active sets without a user created theme.")
;; Set ;; Set
(and v (instance? TokenSet v)) (and v (instance? TokenSet v))
[{:group? false [{:group? false
:path (split-token-set-path (:name v)) :path (split-token-set-name (:name v))
:parent-path parent :parent-path parent
:depth depth :depth depth
:set v}] :set v}]
@ -769,6 +740,67 @@ used for managing active sets without a user created theme.")
(cons item (mapcat #(walk % (assoc opts :parent path :depth (inc depth))) v'))))))))))] (cons item (mapcat #(walk % (assoc opts :parent path :depth (inc depth))) v'))))))))))]
(walk (or nodes (d/ordered-map)) nil))) (walk (or nodes (d/ordered-map)) nil)))
(defn sets-tree-seq
"Get tokens sets tree as a flat list
Options:
`:skip-children-pred`: predicate to skip iterating over a set groups children by checking the path of the set group
`:new-editing-set-path`: append a an item with `:new?` at the given path"
[tree & {:keys [skip-children-pred new-at-path]
:or {skip-children-pred (constantly false)}}]
(let [walk (fn walk [[k v :as node] parent depth]
(lazy-seq
(cond
;; New set
(= :is-new k)
(let [tset (make-token-set :name (if (empty? parent)
""
(join-set-path parent)))]
[{:is-new true
:is-group false
:id ""
:parent-path parent
:token-set tset
:depth depth}])
;; Set
(and v (instance? TokenSet v))
(let [name (:name v)]
[{:is-group false
:path (split-token-set-name name)
:id name
:parent-path parent
:depth depth
:token-set v}])
;; Set group
(and v (d/ordered-map? v))
(let [unprefixed-path (last (split-set-str-path-prefix k))
path (conj parent unprefixed-path)
item {:is-group true
:path path
:id (join-set-path path)
:parent-path parent
:depth depth}]
(if (skip-children-pred path)
[item]
(let [v (cond-> v
(= path new-at-path)
(assoc :is-new true))]
(cons item
(mapcat #(walk % path (inc depth)) v))))))))
tree (cond-> tree
(= [] new-at-path)
(assoc :is-new true))]
(->> tree
(mapcat #(walk % [] 0))
(map-indexed (fn [index item]
(assoc item :index index))))))
(defn flatten-nested-tokens-json (defn flatten-nested-tokens-json
"Recursively flatten the dtcg token structure, joining keys with '.'." "Recursively flatten the dtcg token structure, joining keys with '.'."
[tokens token-path] [tokens token-path]
@ -875,7 +907,7 @@ Will return a value that matches this schema:
this))) this)))
(delete-set-path [_ prefixed-set-name] (delete-set-path [_ prefixed-set-name]
(let [prefixed-set-path (split-token-set-path prefixed-set-name) (let [prefixed-set-path (split-token-set-name prefixed-set-name)
set-node (get-in sets prefixed-set-path) set-node (get-in sets prefixed-set-path)
set-group? (not (instance? TokenSet set-node)) set-group? (not (instance? TokenSet set-node))
set-name-string (prefixed-set-path-string->set-name-string prefixed-set-name)] set-name-string (prefixed-set-path-string->set-name-string prefixed-set-name)]
@ -983,13 +1015,13 @@ Will return a value that matches this schema:
(->> (tree-seq d/ordered-map? vals sets) (->> (tree-seq d/ordered-map? vals sets)
(filter (partial instance? TokenSet)))) (filter (partial instance? TokenSet))))
(get-path-sets [_ path] (get-path-sets [_ name]
(some->> (get-in sets (split-token-set-path path)) (some->> (get-in sets (split-token-set-name name))
(tree-seq d/ordered-map? vals) (tree-seq d/ordered-map? vals)
(filter (partial instance? TokenSet)))) (filter (partial instance? TokenSet))))
(get-sets-at-prefix-path [_ prefixed-path-str] (get-sets-at-prefix-path [_ prefixed-path-str]
(some->> (get-in sets (split-token-set-path prefixed-path-str)) (some->> (get-in sets (split-token-set-name prefixed-path-str))
(tree-seq d/ordered-map? vals) (tree-seq d/ordered-map? vals)
(filter (partial instance? TokenSet)))) (filter (partial instance? TokenSet))))

View file

@ -19,13 +19,25 @@
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.tokens.selected-set :as dwts] [app.main.data.workspace.tokens.selected-set :as dwts]
[app.main.data.workspace.undo :as dwu]
[app.main.ui.workspace.tokens.update :as wtu] [app.main.ui.workspace.tokens.update :as wtu]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
(declare set-selected-token-set-name)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS Getters
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FIXME: lookup rename
(defn get-tokens-lib
[state]
(-> (dsh/lookup-file-data state)
(get :tokens-lib)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Helpers ;; Helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -43,15 +55,6 @@
(watch [_ _ _] (watch [_ _ _]
(rx/of (dwsh/update-shapes [id] #(merge % attrs)))))) (rx/of (dwsh/update-shapes [id] #(merge % attrs))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS Getters
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-tokens-lib
[state]
(-> (dsh/lookup-file-data state)
(get :tokens-lib)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS Actions ;; TOKENS Actions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -108,17 +111,21 @@
(dch/commit-changes changes) (dch/commit-changes changes)
(wtu/update-workspace-tokens)))))) (wtu/update-workspace-tokens))))))
(defn create-token-set [set-name token-set] (defn create-token-set
(let [new-token-set (-> token-set [set-name token-set]
(update :name #(if (empty? %) set-name (ctob/join-set-path [% set-name]))))] (ptk/reify ::create-token-set
(ptk/reify ::create-token-set ptk/UpdateEvent
ptk/WatchEvent (update [_ state]
(watch [it _ _] ;; Clear possible local state
(let [changes (-> (pcb/empty-changes it) (update state :workspace-tokens dissoc :token-set-new-path))
(pcb/add-token-set new-token-set))]
(rx/of ptk/WatchEvent
(dwts/set-selected-token-set-name (:name new-token-set)) (watch [it _ _]
(dch/commit-changes changes))))))) (let [token-set (update token-set :name #(if (empty? %) set-name (ctob/join-set-path [% set-name])))
changes (-> (pcb/empty-changes it)
(pcb/add-token-set token-set))]
(rx/of (set-selected-token-set-name (:name token-set))
(dch/commit-changes changes))))))
(defn rename-token-set-group [set-group-path set-group-fname] (defn rename-token-set-group [set-group-path set-group-fname]
(ptk/reify ::rename-token-set-group (ptk/reify ::rename-token-set-group
@ -138,17 +145,21 @@
changes (-> (pcb/empty-changes it) changes (-> (pcb/empty-changes it)
(pcb/update-token-set token-set prev-token-set))] (pcb/update-token-set token-set prev-token-set))]
(rx/of (rx/of
(dwts/set-selected-token-set-name (:name token-set)) (set-selected-token-set-name (:name token-set))
(dch/commit-changes changes)))))) (dch/commit-changes changes))))))
(defn toggle-token-set [{:keys [token-set-name]}] (defn toggle-token-set
[name]
(assert (string? name) "expected a string for `name`")
(ptk/reify ::toggle-token-set (ptk/reify ::toggle-token-set
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [changes (clt/generate-toggle-token-set (pcb/empty-changes) (get-tokens-lib state) token-set-name)] (let [tlib (get-tokens-lib state)
(rx/of changes (-> (pcb/empty-changes)
(dch/commit-changes changes) (clt/generate-toggle-token-set tlib name))]
(wtu/update-workspace-tokens))))))
(rx/of (dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn toggle-token-set-group [group-path] (defn toggle-token-set-group [group-path]
(ptk/reify ::toggle-token-set-group (ptk/reify ::toggle-token-set-group
@ -164,20 +175,14 @@
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [data (dsh/lookup-file-data state) (let [data (dsh/lookup-file-data state)
update-token-set-change (some-> lib
(ctob/get-sets)
(first)
(:name)
(dwts/set-selected-token-set-name))
changes (-> (pcb/empty-changes it) changes (-> (pcb/empty-changes it)
(pcb/with-library-data data) (pcb/with-library-data data)
(pcb/set-tokens-lib lib))] (pcb/set-tokens-lib lib))]
(rx/of (rx/of (dch/commit-changes changes)
(dch/commit-changes changes) (wtu/update-workspace-tokens))))))
update-token-set-change
(wtu/update-workspace-tokens))))))
(defn delete-token-set-path [group? path] (defn delete-token-set-path
[group? path]
(ptk/reify ::delete-token-set-path (ptk/reify ::delete-token-set-path
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
@ -185,9 +190,8 @@
changes (-> (pcb/empty-changes it) changes (-> (pcb/empty-changes it)
(pcb/with-library-data data) (pcb/with-library-data data)
(pcb/delete-token-set-path group? path))] (pcb/delete-token-set-path group? path))]
(rx/of (rx/of (dch/commit-changes changes)
(dch/commit-changes changes) (wtu/update-workspace-tokens))))))
(wtu/update-workspace-tokens))))))
(defn drop-error [{:keys [error to-path]}] (defn drop-error [{:keys [error to-path]}]
(ptk/reify ::drop-error (ptk/reify ::drop-error
@ -221,53 +225,54 @@
(ptk/reify ::drop-token-set (ptk/reify ::drop-token-set
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [undo-id (js/Symbol)] (try
(try (when-let [changes (clt/generate-move-token-set (pcb/empty-changes it) (get-tokens-lib state) drop-opts)]
(when-let [changes (clt/generate-move-token-set (pcb/empty-changes it) (get-tokens-lib state) drop-opts)] (rx/of (dch/commit-changes changes)
(rx/of (wtu/update-workspace-tokens)))
(dwu/start-undo-transaction undo-id) (catch js/Error e
(dch/commit-changes changes) (rx/of
(some-> (get-in changes [:redo-changes 0 :to-path]) (drop-error (ex-data e))))))))
(ctob/join-set-path)
(dwts/set-selected-token-set-name))
(wtu/update-workspace-tokens)
(dwu/commit-undo-transaction undo-id)))
(catch js/Error e
(rx/of
(drop-error (ex-data e)))))))))
;; FIXME: the the name is very confusing
(defn update-create-token (defn update-create-token
[{:keys [token prev-token-name]}] [{:keys [token prev-token-name]}]
(ptk/reify ::update-create-token (ptk/reify ::update-create-token
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [data (dsh/lookup-file-data state) (let [data (dsh/lookup-file-data state)
token-set (dwts/get-selected-token-set state) selected (dm/get-in state [:workspace-tokens :selected-token-set-name])
set-name (or (:name token-set) "Global")
changes (if (not token-set)
;; No set created add a global set
(let [tokens-lib (get-tokens-lib state)
token-set (ctob/make-token-set :name set-name :tokens {(:name token) token})
hidden-theme (ctob/make-hidden-token-theme :sets [set-name])
active-theme-paths (some-> tokens-lib ctob/get-active-theme-paths)
add-to-hidden-theme? (= active-theme-paths #{ctob/hidden-token-theme-path})
base-changes (pcb/add-token-set (pcb/empty-changes) token-set)]
(cond
(not tokens-lib) (-> base-changes
(pcb/add-token-theme hidden-theme)
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} #{}))
add-to-hidden-theme? (let [prev-hidden-theme (ctob/get-theme tokens-lib ctob/hidden-token-theme-group ctob/hidden-token-theme-name)] tokens-lib (get-tokens-lib state)
(-> base-changes token-set (if selected
(pcb/update-token-theme (ctob/toggle-set prev-hidden-theme ctob/hidden-token-theme-path) prev-hidden-theme))) (some-> tokens-lib (ctob/get-set selected))
(some-> tokens-lib (ctob/get-sets) (first)))
:else base-changes)) set-name (or (:name token-set) "Global")
(-> (pcb/empty-changes it) changes (if (not token-set)
(pcb/with-library-data data) ;; No set created add a global set
(pcb/set-token set-name (or prev-token-name (:name token)) token)))] (let [token-set (ctob/make-token-set :name set-name :tokens {(:name token) token})
hidden-theme (ctob/make-hidden-token-theme :sets [set-name])
active-theme-paths (some-> tokens-lib ctob/get-active-theme-paths)
add-to-hidden-theme? (= active-theme-paths #{ctob/hidden-token-theme-path})
base-changes (pcb/add-token-set (pcb/empty-changes) token-set)]
(cond
(not tokens-lib)
(-> base-changes
(pcb/add-token-theme hidden-theme)
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} #{}))
add-to-hidden-theme?
(let [prev-hidden-theme (ctob/get-theme tokens-lib ctob/hidden-token-theme-group ctob/hidden-token-theme-name)]
(-> base-changes
(pcb/update-token-theme (ctob/toggle-set prev-hidden-theme ctob/hidden-token-theme-path) prev-hidden-theme)))
:else base-changes))
(-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token set-name (or prev-token-name (:name token)) token)))]
(rx/of (rx/of
(dwts/set-selected-token-set-name set-name) (set-selected-token-set-name set-name)
(when-not prev-token-name (when-not prev-token-name
(ptk/event ::ev/event {::ev/name "create-tokens"})) (ptk/event ::ev/event {::ev/name "create-tokens"}))
(dch/commit-changes changes)))))) (dch/commit-changes changes))))))
@ -307,41 +312,81 @@
(update-create-token (update-create-token
{:token (assoc token :name copy-name)}))))))) {:token (assoc token :name copy-name)})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKEN UI OPS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-token-type-section-open (defn set-token-type-section-open
[token-type open?] [token-type open?]
(ptk/reify ::set-token-type-section-open (ptk/reify ::set-token-type-section-open
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:workspace-local :token-type-open-status token-type] open?)))) (update-in state [:workspace-tokens :open-status-by-type] assoc token-type open?))))
;; === Token Context Menu (defn assign-token-context-menu
[{:keys [position] :as params}]
(when params
(assert (gpt/point? position) "expected a point instance for `position` param"))
(defn show-token-context-menu
[{:keys [position _token-name] :as params}]
(dm/assert! (gpt/point? position))
(ptk/reify ::show-token-context-menu (ptk/reify ::show-token-context-menu
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:workspace-local :token-context-menu] params)))) (if params
(update state :workspace-tokens assoc :token-context-menu params)
(update state :workspace-tokens dissoc :token-context-menu)))))
(def hide-token-context-menu ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ptk/reify ::hide-token-context-menu ;; TOKEN-SET UI OPS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn assign-token-set-context-menu
[{:keys [position] :as params}]
(when params
(assert (gpt/point? position) "expected valid point for `position` param"))
(ptk/reify ::assign-token-set-context-menu
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:workspace-local :token-context-menu] nil)))) (if params
(update state :workspace-tokens assoc :token-set-context-menu params)
(update state :workspace-tokens dissoc :token-set-context-menu)))))
;; === Token Set Context Menu (defn set-selected-token-set-name
[name]
(defn show-token-set-context-menu (ptk/reify ::set-selected-token-set-name
[{:keys [position _token-set-name] :as params}]
(dm/assert! (gpt/point? position))
(ptk/reify ::show-token-set-context-menu
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:workspace-local :token-set-context-menu] params)))) (update state :workspace-tokens assoc :selected-token-set-name name))))
(def hide-token-set-context-menu (defn start-token-set-edition
(ptk/reify ::hide-token-set-context-menu [edition-id]
(assert (string? edition-id) "expected a string for `edition-id`")
(ptk/reify ::start-token-set-edition
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:workspace-local :token-set-context-menu] nil)))) (update state :workspace-tokens assoc :token-set-edition-id edition-id))))
(defn start-token-set-creation
[path]
(assert (vector? path) "expected a vector for `path`")
(ptk/reify ::start-token-set-creation
ptk/UpdateEvent
(update [_ state]
(update state :workspace-tokens assoc :token-set-new-path path))))
(defn clear-token-set-edition
[]
(ptk/reify ::clear-token-set-edition
ptk/UpdateEvent
(update [_ state]
(update state :workspace-tokens dissoc :token-set-edition-id))))
(defn clear-token-set-creation
[]
(ptk/reify ::clear-token-set-creation
ptk/UpdateEvent
(update [_ state]
(update state :workspace-tokens dissoc :token-set-new-path))))

View file

@ -399,6 +399,7 @@
:workspace-media-objects :workspace-media-objects
:workspace-persistence :workspace-persistence
:workspace-presence :workspace-presence
:workspace-tokens
:workspace-ready :workspace-ready
:workspace-undo) :workspace-undo)
(update :workspace-global dissoc :read-only?) (update :workspace-global dissoc :read-only?)

View file

@ -9,11 +9,10 @@
Will default to the first set." Will default to the first set."
(:require (:require
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]))
[potok.v2.core :as ptk]))
(defn get-selected-token-set-name [state] (defn get-selected-token-set-name [state]
(or (get-in state [:workspace-local :selected-token-set-name]) (or (get-in state [:workspace-tokens :selected-token-set-name])
(some-> (dsh/lookup-file-data state) (some-> (dsh/lookup-file-data state)
(get :tokens-lib) (get :tokens-lib)
(ctob/get-sets) (ctob/get-sets)
@ -33,10 +32,3 @@
(defn get-selected-token-set-tokens [state] (defn get-selected-token-set-tokens [state]
(some-> (get-selected-token-set state) (some-> (get-selected-token-set state)
:tokens)) :tokens))
(defn set-selected-token-set-name
[name]
(ptk/reify ::set-selected-token-set-path-from-name
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local assoc :selected-token-set-name name))))

View file

@ -128,6 +128,10 @@
(def workspace-selrect-transform (def workspace-selrect-transform
(l/derived :workspace-selrect-transform st/state)) (l/derived :workspace-selrect-transform st/state))
(def workspace-tokens
"All tokens related ephimeral state"
(l/derived :workspace-tokens st/state))
;; TODO: rename to workspace-selected (?) ;; TODO: rename to workspace-selected (?)
;; Don't use directly from components, this is a proxy to improve performance of selected-shapes ;; Don't use directly from components, this is a proxy to improve performance of selected-shapes
(def ^:private selected-shapes-data (def ^:private selected-shapes-data
@ -453,8 +457,8 @@
(def workspace-token-themes-no-hidden (def workspace-token-themes-no-hidden
(l/derived #(remove ctob/hidden-temporary-theme? %) workspace-token-themes)) (l/derived #(remove ctob/hidden-temporary-theme? %) workspace-token-themes))
(def workspace-selected-token-set-name (def selected-token-set-name
(l/derived dwts/get-selected-token-set-name st/state)) (l/derived (l/key :selected-token-set-name) workspace-tokens))
(def workspace-ordered-token-sets (def workspace-ordered-token-sets
(l/derived #(or (some-> % ctob/get-sets) []) tokens-lib)) (l/derived #(or (some-> % ctob/get-sets) []) tokens-lib))
@ -476,10 +480,7 @@
(def workspace-active-theme-paths-no-hidden (def workspace-active-theme-paths-no-hidden
(l/derived #(disj % ctob/hidden-token-theme-path) workspace-active-theme-paths)) (l/derived #(disj % ctob/hidden-token-theme-path) workspace-active-theme-paths))
(def workspace-active-set-names ;; FIXME: deprecated, it should not be implemented with ref (still used in form)
(l/derived (d/nilf ctob/get-active-themes-set-names) tokens-lib))
;; FIXME: deprecated, it should not be implemented with ref
(def workspace-active-theme-sets-tokens (def workspace-active-theme-sets-tokens
(l/derived #(or (some-> % ctob/get-active-themes-set-tokens) {}) tokens-lib)) (l/derived #(or (some-> % ctob/get-active-themes-set-tokens) {}) tokens-lib))
@ -492,7 +493,6 @@
(def workspace-selected-token-set-tokens (def workspace-selected-token-set-tokens
(l/derived #(or (dwts/get-selected-token-set-tokens %) {}) st/state)) (l/derived #(or (dwts/get-selected-token-set-tokens %) {}) st/state))
(def plugins-permissions-peek (def plugins-permissions-peek
(l/derived (fn [state] (l/derived (fn [state]
(dm/get-in state [:plugins-permissions-peek :data])) (dm/get-in state [:plugins-permissions-peek :data]))

View file

@ -133,13 +133,14 @@
(fn [] (fn []
(st/emit! (dtm/finalize-team team-id)))) (st/emit! (dtm/finalize-team team-id))))
(let [team (mf/deref refs/team)] (let [{:keys [permissions] :as team} (mf/deref refs/team)]
(when (= team-id (:id team)) (when (= team-id (:id team))
[:& (mf/provider ctx/current-team-id) {:value team-id} [:> (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/permissions) {:value (:permissions team)} [:> (mf/provider ctx/permissions) {:value permissions}
;; The `:key` is mandatory here because we want to reinitialize [:> (mf/provider ctx/can-edit?) {:value (:can-edit permissions)}
;; all dom tree instead of simple rerender. ;; The `:key` is mandatory here because we want to reinitialize
[:* {:key (str team-id)} children]]]))) ;; all dom tree instead of simple rerender.
[:* {:key (str team-id)} children]]]])))
(mf/defc page* (mf/defc page*
{::mf/props :obj {::mf/props :obj

View file

@ -34,3 +34,4 @@
(def sidebar (mf/create-context nil)) (def sidebar (mf/create-context nil))
(def permissions (mf/create-context nil)) (def permissions (mf/create-context nil))
(def can-edit? (mf/create-context nil))

View file

@ -38,7 +38,7 @@
Optionally remove attributes from `attributes-to-remove`, Optionally remove attributes from `attributes-to-remove`,
this is useful for applying a single attribute from an attributes set this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set." while removing other applied tokens from this set."
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape] :as _props}] [{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
(ptk/reify ::apply-token (ptk/reify ::apply-token
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]

View file

@ -240,15 +240,15 @@
:no-selectable true :no-selectable true
:action (fn [event] :action (fn [event]
(let [{:keys [key fields]} modal] (let [{:keys [key fields]} modal]
(st/emit! dt/hide-token-context-menu)
(dom/stop-propagation event) (dom/stop-propagation event)
(modal/show! key {:x (.-clientX ^js event) (st/emit! (dt/assign-token-context-menu nil)
:y (.-clientY ^js event) (modal/show key {:x (.-clientX ^js event)
:position :right :y (.-clientY ^js event)
:fields fields :position :right
:action "edit" :fields fields
:selected-token-set-name selected-token-set-name :action "edit"
:token token})))} :selected-token-set-name selected-token-set-name
:token token}))))}
{:title (tr "workspace.token.duplicate") {:title (tr "workspace.token.duplicate")
:no-selectable true :no-selectable true
:action #(st/emit! (dt/duplicate-token (:name token)))} :action #(st/emit! (dt/duplicate-token (:name token)))}
@ -274,8 +274,8 @@
;; Components ------------------------------------------------------------------ ;; Components ------------------------------------------------------------------
(def tokens-menu-ref (def ^:private tokens-menu-ref
(l/derived :token-context-menu refs/workspace-local)) (l/derived :token-context-menu refs/workspace-tokens))
(defn- prevent-default (defn- prevent-default
[event] [event]
@ -377,7 +377,7 @@
selected-shapes (into [] (keep (d/getf objects)) selected) selected-shapes (into [] (keep (d/getf objects)) selected)
token-name (:token-name mdata) token-name (:token-name mdata)
token (mf/deref (refs/workspace-selected-token-set-token token-name)) token (mf/deref (refs/workspace-selected-token-set-token token-name))
selected-token-set-name (mf/deref refs/workspace-selected-token-set-name)] selected-token-set-name (mf/deref refs/selected-token-set-name)]
[:ul {:class (stl/css :context-list)} [:ul {:class (stl/css :context-list)}
[:& menu-tree {:submenu-offset width [:& menu-tree {:submenu-offset width
:submenu-direction direction :submenu-direction direction
@ -416,8 +416,9 @@
(reset! dropdown-direction* (if is-outside? "up" "down")) (reset! dropdown-direction* (if is-outside? "up" "down"))
(mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*))))))) (mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*)))))))
;; FIXME: perf optimization
[:& dropdown {:show is-open? [:& dropdown {:show is-open?
:on-close #(st/emit! dt/hide-token-context-menu)} :on-close #(st/emit! (dt/assign-token-context-menu nil))}
[:div {:class (stl/css :token-context-menu) [:div {:class (stl/css :token-context-menu)
:data-testid "tokens-context-menu-for-token" :data-testid "tokens-context-menu-for-token"
:ref dropdown-ref :ref dropdown-ref

View file

@ -25,7 +25,6 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.workspace.tokens.components.controls.input-tokens :refer [input-tokens*]] [app.main.ui.workspace.tokens.components.controls.input-tokens :refer [input-tokens*]]
[app.main.ui.workspace.tokens.sets :as wts] [app.main.ui.workspace.tokens.sets :as wts]
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.keyboard :as k] [app.util.keyboard :as k]
@ -339,8 +338,8 @@
token-set-active? token-set-active?
(mf/use-fn (mf/use-fn
(mf/deps theme-state) (mf/deps theme-state)
(fn [set-name] (fn [name]
(get-in theme-state [:sets set-name]))) (contains? (:sets theme-state) name)))
on-toggle-token-set on-toggle-token-set
(mf/use-fn (mf/use-fn
@ -381,16 +380,15 @@
(tr "workspace.token.set-selection-theme")] (tr "workspace.token.set-selection-theme")]
[:div {:class (stl/css :sets-list-wrapper)} [:div {:class (stl/css :sets-list-wrapper)}
[:& wts/controlled-sets-list [:> wts/controlled-sets-list*
{:token-sets token-sets {:token-sets token-sets
:token-set-selected? (constantly false) :is-token-set-active token-set-active?
:token-set-active? token-set-active? :is-token-set-group-active token-set-group-active?
:token-set-group-active? token-set-group-active?
:on-select on-click-token-set :on-select on-click-token-set
:can-edit false
:on-toggle-token-set on-toggle-token-set :on-toggle-token-set on-toggle-token-set
:on-toggle-token-set-group on-toggle-token-set-group :on-toggle-token-set-group on-toggle-token-set-group
:origin "theme-modal" :origin "theme-modal"}]]
:context sets-context/static-context}]]
[:div {:class (stl/css :edit-theme-footer)} [:div {:class (stl/css :edit-theme-footer)}
[:> button* {:variant "secondary" [:> button* {:variant "secondary"
@ -432,5 +430,4 @@
:aria-label (tr "labels.close") :aria-label (tr "labels.close")
:variant "action" :variant "action"
:icon "close"}] :icon "close"}]
[:& sets-context/provider {} [:& themes-modal-body]]]))
[:& themes-modal-body]]]]))

View file

@ -7,19 +7,17 @@
(ns app.main.ui.workspace.tokens.sets (ns app.main.ui.workspace.tokens.sets
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.tokens :as wdt] [app.main.data.tokens :as dt]
[app.main.data.workspace.tokens.selected-set :as dwts]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic]
[app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.hooks :as h] [app.main.ui.hooks :as h]
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
@ -27,42 +25,62 @@
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn on-toggle-token-set-click [token-set-name] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(st/emit! (wdt/toggle-token-set {:token-set-name token-set-name}))) ;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn on-toggle-token-set-group-click [group-path] (defn- on-start-creation
(st/emit! (wdt/toggle-token-set-group group-path))) []
(st/emit! (dt/start-token-set-creation [])))
(defn on-select-token-set-click [set-name] (defn- on-toggle-token-set-click [name]
(st/emit! (dwts/set-selected-token-set-name set-name))) (st/emit! (dt/toggle-token-set name)))
(defn on-update-token-set [set-name token-set] (defn- on-toggle-token-set-group-click [path]
(st/emit! (wdt/update-token-set (:name token-set) (ctob/update-name token-set set-name)))) (st/emit! (dt/toggle-token-set-group path)))
(defn on-update-token-set-group [set-group-path set-group-fname] (defn- on-select-token-set-click [name]
(st/emit! (wdt/rename-token-set-group set-group-path set-group-fname))) (st/emit! (dt/set-selected-token-set-name name)))
(defn on-create-token-set [set-name token-set] (defn on-update-token-set
(st/emit! (ptk/event ::ev/event {::ev/name "create-tokens-set"})) [name token-set]
(st/emit! (wdt/create-token-set set-name token-set))) (st/emit! (dt/clear-token-set-edition)
(dt/update-token-set (:name token-set) (ctob/update-name token-set name))))
(mf/defc editing-label (defn- on-update-token-set-group [path name]
(st/emit! (dt/clear-token-set-edition)
(dt/rename-token-set-group path name)))
(defn- on-create-token-set [name token-set]
(st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name})
(dt/create-token-set name token-set)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COMPONENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc editing-label*
{::mf/private true}
[{:keys [default-value on-cancel on-submit]}] [{:keys [default-value on-cancel on-submit]}]
(let [ref (mf/use-ref) (let [ref (mf/use-ref)
on-submit-valid (mf/use-fn
(fn [event] on-submit-valid
(let [value (str/trim (dom/get-target-val event))] (mf/use-fn
(if (or (str/empty? value) (mf/deps on-cancel on-submit default-value)
(= value default-value)) (fn [event]
(on-cancel) (let [value (str/trim (dom/get-target-val event))]
(do (if (or (str/empty? value)
(on-submit value) (= value default-value))
(on-cancel)))))) (on-cancel)
on-key-down (mf/use-fn (on-submit value)))))
(fn [event]
(cond on-key-down
(kbd/enter? event) (on-submit-valid event) (mf/use-fn
(kbd/esc? event) (on-cancel))))] (mf/deps on-submit-valid on-cancel)
(fn [event]
(cond
(kbd/enter? event) (on-submit-valid event)
(kbd/esc? event) (on-cancel))))]
[:input [:input
{:class (stl/css :editing-node) {:class (stl/css :editing-node)
:type "text" :type "text"
@ -72,17 +90,12 @@
:auto-focus true :auto-focus true
:default-value default-value}])) :default-value default-value}]))
(mf/defc checkbox (mf/defc checkbox*
[{:keys [checked aria-label on-click disabled]}] [{:keys [checked aria-label on-click disabled]}]
(let [all? (true? checked) (let [all? (true? checked)
mixed? (= checked "mixed") mixed? (= checked "mixed")
checked? (or all? mixed?) checked? (or all? mixed?)]
on-click
(mf/use-fn
(mf/deps disabled)
(fn [e]
(when-not disabled
(on-click e))))]
[:div {:role "checkbox" [:div {:role "checkbox"
:aria-checked (dm/str checked) :aria-checked (dm/str checked)
:disabled disabled :disabled disabled
@ -91,37 +104,59 @@
:class (stl/css-case :checkbox-style true :class (stl/css-case :checkbox-style true
:checkbox-checked-style checked? :checkbox-checked-style checked?
:checkbox-disabled-checked (and checked? disabled) :checkbox-disabled-checked (and checked? disabled)
:checkbox-disabled disabled) :checkbox-disabled disabled)
:on-click on-click} :on-click (when-not disabled on-click)}
(when checked? (when ^boolean checked?
[:> icon* [:> icon*
{:aria-label aria-label {:aria-label aria-label
:class (stl/css :check-icon) :class (stl/css :check-icon)
:size "s" :size "s"
:icon-id (if mixed? ic/remove ic/tick)}])])) :icon-id (if mixed? ic/remove ic/tick)}])]))
(mf/defc sets-tree-set-group (mf/defc inline-add-button*
[{:keys [label tree-depth tree-path active? selected? draggable? on-toggle-collapse on-toggle editing-id editing? on-edit on-edit-reset on-edit-submit collapsed-paths tree-index]}] []
(let [active?' (active? tree-path) (let [can-edit? (mf/use-ctx ctx/can-edit?)]
editing?' (editing? editing-id) (if can-edit?
collapsed? (some? (get @collapsed-paths tree-path)) [:div {:class (stl/css :empty-sets-wrapper)}
can-edit? (:can-edit (deref refs/permissions)) [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
;; Used by playwright to get the correct item by label (tr "workspace.token.no-sets-yet")]
label-id (str editing-id "-label") [:button {:on-click on-start-creation
:class (stl/css :create-set-button)}
(tr "workspace.token.create-one")]]
[:div {:class (stl/css :empty-sets-wrapper)}
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
(tr "workspace.token.no-sets-yet")]])))
(mf/defc add-button*
[]
[:> icon-button* {:variant "ghost"
:icon "add"
:on-click on-start-creation
:aria-label (tr "workspace.token.add set")}])
(mf/defc sets-tree-set-group*
{::mf/private true}
[{:keys [id label tree-depth tree-path is-active is-selected is-draggable is-collapsed tree-index on-drop
on-toggle-collapse on-toggle is-editing on-start-edition on-reset-edition on-edit-submit]}]
(let [can-edit?
(mf/use-ctx ctx/can-edit?)
label-id
(str id "-label")
on-context-menu on-context-menu
(mf/use-fn (mf/use-fn
(mf/deps editing?' editing-id can-edit?) (mf/deps is-editing can-edit?)
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(when-not editing?' (when-not is-editing
(st/emit! (st/emit! (dt/assign-token-set-context-menu
(wdt/show-token-set-context-menu {:position (dom/get-client-position event)
{:position (dom/get-client-position event) :is-group true
:group? true :path tree-path})))))
:path tree-path})))))
on-collapse-click on-collapse-click
(mf/use-fn (mf/use-fn
@ -130,9 +165,7 @@
(on-toggle-collapse tree-path))) (on-toggle-collapse tree-path)))
on-double-click on-double-click
(mf/use-fn (mf/use-fn (mf/deps id) #(on-start-edition id))
(mf/deps editing-id can-edit?)
#(on-edit editing-id))
on-checkbox-click on-checkbox-click
(mf/use-fn (mf/use-fn
@ -146,24 +179,18 @@
on-drop on-drop
(mf/use-fn (mf/use-fn
(mf/deps tree-index collapsed-paths) (mf/deps tree-index on-drop)
(fn [position data] (fn [position data]
(let [props {:from-index (:index data) (on-drop tree-index position data)))
:to-index tree-index
:position position
:collapsed-paths @collapsed-paths}]
(if (:group? data)
(st/emit! (wdt/drop-token-set-group props))
(st/emit! (wdt/drop-token-set props))))))
[dprops dref] [dprops dref]
(h/use-sortable (h/use-sortable
:data-type "penpot/token-set" :data-type "penpot/token-set"
:on-drop on-drop :on-drop on-drop
:data {:index tree-index :data {:index tree-index
:group? true} :is-group true}
:detect-center? true :detect-center? true
:draggable? draggable?)] :draggable? is-draggable)]
[:div {:ref dref [:div {:ref dref
:role "button" :role "button"
@ -172,7 +199,7 @@
:style {"--tree-depth" tree-depth} :style {"--tree-depth" tree-depth}
:class (stl/css-case :set-item-container true :class (stl/css-case :set-item-container true
:set-item-group true :set-item-group true
:selected-set selected? :selected-set is-selected
:dnd-over (= (:over dprops) :center) :dnd-over (= (:over dprops) :center)
:dnd-over-top (= (:over dprops) :top) :dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot)) :dnd-over-bot (= (:over dprops) :bot))
@ -181,68 +208,64 @@
{:class (stl/css :set-item-group-collapse-button) {:class (stl/css :set-item-group-collapse-button)
:on-click on-collapse-click :on-click on-collapse-click
:aria-label (tr "labels.collapse") :aria-label (tr "labels.collapse")
:icon (if collapsed? "arrow-right" "arrow-down") :icon (if is-collapsed "arrow-right" "arrow-down")
:variant "action"}] :variant "action"}]
(if editing?' (if is-editing
[:& editing-label [:> editing-label*
{:default-value label {:default-value label
:on-cancel on-edit-reset :on-cancel on-reset-edition
:on-create on-edit-reset
:on-submit on-edit-submit'}] :on-submit on-edit-submit'}]
[:* [:*
[:div {:class (stl/css :set-name) [:div {:class (stl/css :set-name)
:on-double-click on-double-click :on-double-click on-double-click
:id label-id} :id label-id}
label] label]
[:& checkbox [:> checkbox*
{:on-click on-checkbox-click {:on-click on-checkbox-click
:disabled (not can-edit?) :disabled (not can-edit?)
:checked (case active?' :checked (case is-active
:all true :all true
:partial "mixed" :partial "mixed"
:none false) :none false)
:arial-label (tr "workspace.token.select-set")}]])])) :arial-label (tr "workspace.token.select-set")}]])]))
(mf/defc sets-tree-set (mf/defc sets-tree-set*
[{:keys [set label tree-depth tree-path tree-index selected? on-select active? draggable? on-toggle editing-id editing? on-edit on-edit-reset on-edit-submit collapsed-paths]}] [{:keys [id set label tree-depth tree-path tree-index is-selected is-active is-draggable is-editing
(let [set-name (.-name set) on-select on-drop on-toggle on-start-edition on-reset-edition on-edit-submit]}]
editing?' (editing? editing-id) (let [set-name (get set :name)
active?' (some? (active? set-name)) can-edit? (mf/use-ctx ctx/can-edit?)
can-edit? (:can-edit (deref refs/permissions))
on-click on-click
(mf/use-fn (mf/use-fn
(mf/deps editing?' tree-path) (mf/deps is-editing tree-path)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(when-not editing?' (when-not is-editing
(on-select set-name)))) (when (fn? on-select)
(on-select set-name)))))
on-context-menu on-context-menu
(mf/use-fn (mf/use-fn
(mf/deps editing?' tree-path can-edit?) (mf/deps is-editing tree-path can-edit?)
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(when (and can-edit? (not editing?')) (when (and can-edit? (not is-editing))
(st/emit! (st/emit! (dt/assign-token-set-context-menu
(wdt/show-token-set-context-menu {:position (dom/get-client-position event)
{:position (dom/get-client-position event) :is-group false
:group? false :path tree-path})))))
:path tree-path})))))
on-double-click on-double-click
(mf/use-fn (mf/use-fn (mf/deps id) #(on-start-edition id))
(mf/deps editing-id)
(fn []
(on-edit editing-id)))
on-checkbox-click on-checkbox-click
(mf/use-fn (mf/use-fn
(mf/deps set-name) (mf/deps set-name on-toggle)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(on-toggle set-name))) (when (fn? on-toggle)
(on-toggle set-name))))
on-edit-submit' on-edit-submit'
(mf/use-fn (mf/use-fn
@ -253,20 +276,14 @@
(mf/use-fn (mf/use-fn
(mf/deps tree-path) (mf/deps tree-path)
(fn [_] (fn [_]
(when-not selected? (when-not is-selected
(on-select tree-path)))) (on-select tree-path))))
on-drop on-drop
(mf/use-fn (mf/use-fn
(mf/deps tree-index collapsed-paths) (mf/deps tree-index on-drop)
(fn [position data] (fn [position data]
(let [props {:from-index (:index data) (on-drop tree-index position data)))
:to-index tree-index
:position position
:collapsed-paths @collapsed-paths}]
(if (:group? data)
(st/emit! (wdt/drop-token-set-group props))
(st/emit! (wdt/drop-token-set props))))))
[dprops dref] [dprops dref]
(h/use-sortable (h/use-sortable
@ -274,201 +291,280 @@
:on-drag on-drag :on-drag on-drag
:on-drop on-drop :on-drop on-drop
:data {:index tree-index :data {:index tree-index
:group? false} :is-group false}
:draggable? draggable?)] :draggable? is-draggable)
drop-over
(get dprops :over)]
[:div {:ref dref [:div {:ref dref
:role "button" :role "button"
:data-testid "tokens-set-item" :data-testid "tokens-set-item"
:style {"--tree-depth" tree-depth} :style {"--tree-depth" tree-depth}
:class (stl/css-case :set-item-container true :class (stl/css-case :set-item-container true
:selected-set selected? :selected-set is-selected
:dnd-over (= (:over dprops) :center) :dnd-over (= drop-over :center)
:dnd-over-top (= (:over dprops) :top) :dnd-over-top (= drop-over :top)
:dnd-over-bot (= (:over dprops) :bot)) :dnd-over-bot (= drop-over :bot))
:on-click on-click :on-click on-click
:on-context-menu on-context-menu :on-context-menu on-context-menu
:aria-checked active?'} :aria-checked is-active}
[:> icon* [:> icon*
{:icon-id "document" {:icon-id "document"
:class (stl/css-case :icon true :class (stl/css-case :icon true
:root-icon (not tree-depth))}] :root-icon (not tree-depth))}]
(if editing?' (if is-editing
[:& editing-label [:> editing-label*
{:default-value label {:default-value label
:on-cancel on-edit-reset :on-cancel on-reset-edition
:on-create on-edit-reset
:on-submit on-edit-submit'}] :on-submit on-edit-submit'}]
[:* [:*
[:div {:class (stl/css :set-name) [:div {:class (stl/css :set-name)
:on-double-click on-double-click} :on-double-click on-double-click}
label] label]
[:& checkbox [:> checkbox*
{:on-click on-checkbox-click {:on-click on-checkbox-click
:disabled (not can-edit?) :disabled (not can-edit?)
:arial-label (tr "workspace.token.select-set") :arial-label (tr "workspace.token.select-set")
:checked active?'}]])])) :checked is-active}]])]))
(mf/defc sets-tree (mf/defc token-sets-tree*
[{:keys [draggable? [{:keys [is-draggable
active? selected
selected? is-token-set-group-active
group-active? is-token-set-active
editing? on-start-edition
on-edit-reset on-reset-edition
on-edit-submit-set on-edit-submit-set
on-edit-submit-group on-edit-submit-group
on-select on-select
on-toggle-set on-toggle-set
on-toggle-set-group on-toggle-set-group
set-node] token-sets
:as props}] new-path
(let [{:keys [on-edit new-path] :as ctx} (sets-context/use-context) edition-id]}]
collapsed-paths (mf/use-state #{})
(let [collapsed-paths* (mf/use-state #{})
collapsed-paths (deref collapsed-paths*)
collapsed? collapsed?
(mf/use-fn (mf/use-fn
#(contains? @collapsed-paths %)) (mf/deps collapsed-paths)
(partial contains? collapsed-paths))
on-drop
(mf/use-fn
(mf/deps collapsed-paths)
(fn [tree-index position data]
(let [props {:from-index (:index data)
:to-index tree-index
:position position
:collapsed-paths collapsed-paths}]
(if (:is-group data)
(st/emit! (dt/drop-token-set-group props))
(st/emit! (dt/drop-token-set props))))))
on-toggle-collapse on-toggle-collapse
(mf/use-fn (mf/use-fn
(fn [path] (fn [path]
(swap! collapsed-paths #(if (contains? % path) (swap! collapsed-paths* #(if (contains? % path)
(disj % path) (disj % path)
(conj % path)))))] (conj % path)))))]
(for [[index {:keys [new? group? path parent-path depth] :as node}]
(d/enumerate (ctob/walk-sets-tree-seq set-node {:skip-children-pred #(contains? @collapsed-paths %)
:new-editing-set-path new-path}))]
(cond
group?
(let [editing-id (sets-context/set-group-path->id path)]
[:& sets-tree-set-group
{:key editing-id
:label (last path)
:active? group-active?
:selected? false
:draggable? draggable?
:on-select on-select
:tree-path path
:tree-depth depth
:tree-index index
:tree-parent-path parent-path
:editing-id editing-id
:editing? editing?
:on-edit on-edit
:on-edit-reset on-edit-reset
:on-edit-submit on-edit-submit-group
:collapsed? (collapsed? path)
:on-toggle-collapse on-toggle-collapse
:on-toggle on-toggle-set-group
:collapsed-paths collapsed-paths}])
new? (for [{:keys [id token-set index is-new is-group path parent-path depth] :as node}
(let [editing-id (sets-context/set-path->id path)] (ctob/sets-tree-seq token-sets
[:& sets-tree-set {:skip-children-pred collapsed?
{:key editing-id :new-at-path new-path})]
:set (ctob/make-token-set :name (if (empty? parent-path) (cond
"" ^boolean is-group
(ctob/join-set-path parent-path))) [:> sets-tree-set-group*
:label "" {:key index
:active? (constantly true) :label (peek path)
:selected? (constantly true) :id id
:on-select (constantly nil) :is-active (is-token-set-group-active path)
:tree-path path :is-selected false
:tree-depth depth :is-draggable is-draggable
:tree-index index :is-editing (= edition-id id)
:tree-parent-path parent-path :is-collapsed (collapsed? path)
:on-toggle (constantly nil) :on-select on-select
:editing-id editing-id
:editing? (constantly true) :tree-path path
:on-edit-reset on-edit-reset :tree-depth depth
:on-edit-submit on-create-token-set}]) :tree-index index
:tree-parent-path parent-path
:on-drop on-drop
:on-start-edition on-start-edition
:on-reset-edition on-reset-edition
:on-edit-submit on-edit-submit-group
:on-toggle-collapse on-toggle-collapse
:on-toggle on-toggle-set-group}]
^boolean is-new
[:> sets-tree-set*
{:key index
:set token-set
:label ""
:id id
:is-editing true
:is-active true
:is-selected true
:tree-path path
:tree-depth depth
:tree-index index
:tree-parent-path parent-path
:on-drop on-drop
:on-reset-edition on-reset-edition
:on-edit-submit on-create-token-set}]
:else :else
(let [editing-id (sets-context/set-path->id path)] [:> sets-tree-set*
[:& sets-tree-set {:key index
{:key editing-id :set token-set
:set (:set node) :id id
:label (last path) :label (peek path)
:active? active? :is-editing (= edition-id id)
:selected? (selected? (get-in node [:set :name])) :is-active (is-token-set-active id)
:draggable? draggable? :is-selected (= selected id)
:on-select on-select :is-draggable is-draggable
:tree-path path :on-select on-select
:tree-depth depth :tree-path path
:tree-index index :tree-depth depth
:tree-parent-path parent-path :tree-index index
:on-toggle on-toggle-set :tree-parent-path parent-path
:editing-id editing-id :on-toggle on-toggle-set
:editing? editing? :edition-id edition-id
:on-edit on-edit :on-start-edition on-start-edition
:on-edit-reset on-edit-reset :on-drop on-drop
:on-edit-submit on-edit-submit-set :on-reset-edition on-reset-edition
:collapsed-paths collapsed-paths}]))))) :on-edit-submit on-edit-submit-set}]))))
(mf/defc controlled-sets-list (mf/defc controlled-sets-list*
{::mf/props :obj}
[{:keys [token-sets [{:keys [token-sets
selected
on-update-token-set on-update-token-set
on-update-token-set-group on-update-token-set-group
token-set-selected? is-token-set-active
token-set-active? is-token-set-group-active
token-set-group-active?
on-create-token-set on-create-token-set
on-toggle-token-set on-toggle-token-set
on-toggle-token-set-group on-toggle-token-set-group
on-start-edition
on-reset-edition
origin origin
on-select on-select
context] new-path
:as _props}] edition-id]}]
(let [{:keys [editing? on-edit on-reset new-path] :as ctx} (or context (sets-context/use-context))
theme-modal? (= origin "theme-modal") (assert (fn? is-token-set-group-active) "expected a function for `is-token-set-group-active` prop")
can-edit? (:can-edit (deref refs/permissions)) (assert (fn? is-token-set-active) "expected a function for `is-token-set-active` prop")
draggable? (and (not theme-modal?) can-edit?)]
(let [theme-modal? (= origin "theme-modal")
can-edit? (mf/use-ctx ctx/can-edit?)
draggable? (and (not theme-modal?) can-edit?)
empty-state? (and theme-modal?
(empty? token-sets)
(not new-path))
;; NOTE: on-reset-edition and on-start-edition function can
;; come as nil, in this case we need to provide a safe
;; fallback for them
on-reset-edition
(mf/use-fn
(mf/deps on-reset-edition)
(fn [v]
(when (fn? on-reset-edition)
(on-reset-edition v))))
on-start-edition
(mf/use-fn
(mf/deps on-start-edition)
(fn [v]
(when (fn? on-start-edition)
(on-start-edition v))))]
[:fieldset {:class (stl/css :sets-list)} [:fieldset {:class (stl/css :sets-list)}
(if (and theme-modal? (if ^boolean empty-state?
(empty? token-sets)
(not new-path))
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message-sets)} [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message-sets)}
(tr "workspace.token.no-sets-create")] (tr "workspace.token.no-sets-create")]
[:& sets-tree
{:draggable? draggable? [:> token-sets-tree*
:set-node token-sets {:is-draggable draggable?
:selected? token-set-selected? :new-path new-path
:edition-id edition-id
:token-sets token-sets
:selected selected
:on-select on-select :on-select on-select
:active? token-set-active? :is-token-set-active is-token-set-active
:group-active? token-set-group-active? :is-token-set-group-active is-token-set-group-active
:on-toggle-set on-toggle-token-set :on-toggle-set on-toggle-token-set
:on-toggle-set-group on-toggle-token-set-group :on-toggle-set-group on-toggle-token-set-group
:editing? editing?
:on-create-token-set on-create-token-set :on-create-token-set on-create-token-set
:on-edit on-edit :on-start-edition on-start-edition
:on-edit-reset on-reset :on-reset-edition on-reset-edition
:on-edit-submit-set on-update-token-set :on-edit-submit-set on-update-token-set
:on-edit-submit-group on-update-token-set-group}])])) :on-edit-submit-group on-update-token-set-group}])]))
(mf/defc sets-list*
[{:keys [tokens-lib selected new-path edition-id]}]
(mf/defc sets-list (let [token-sets
[{:keys []}] (some-> tokens-lib (ctob/get-set-tree))
(let [token-sets (mf/deref refs/workspace-token-sets-tree)
selected-token-set-name (mf/deref refs/workspace-selected-token-set-name) can-edit?
token-set-selected? (mf/use-fn (mf/use-ctx ctx/can-edit?)
(mf/deps token-sets selected-token-set-name)
(fn [set-name] active-token-sets-names
(= set-name selected-token-set-name))) (mf/with-memo [tokens-lib]
active-token-set-names (mf/deref refs/workspace-active-set-names) (some-> tokens-lib (ctob/get-active-themes-set-names)))
token-set-active? (mf/use-fn
(mf/deps active-token-set-names) token-set-active?
(fn [set-name] (mf/use-fn
(get active-token-set-names set-name))) (mf/deps active-token-sets-names)
token-set-group-active? (mf/use-fn (fn [name]
(fn [group-path] (contains? active-token-sets-names name)))
@(refs/token-sets-at-path-all-active group-path)))]
[:& controlled-sets-list token-set-group-active?
(mf/use-fn
(fn [group-path]
;; FIXME
@(refs/token-sets-at-path-all-active group-path)))
on-reset-edition
(mf/use-fn
(mf/deps can-edit?)
(fn [_]
(when can-edit?
(st/emit! (dt/clear-token-set-edition)
(dt/clear-token-set-creation)))))
on-start-edition
(mf/use-fn
(mf/deps can-edit?)
(fn [id]
(when can-edit?
(st/emit! (dt/start-token-set-edition id)))))]
[:> controlled-sets-list*
{:token-sets token-sets {:token-sets token-sets
:token-set-selected? token-set-selected?
:token-set-active? token-set-active? :is-token-set-active token-set-active?
:token-set-group-active? token-set-group-active? :is-token-set-group-active token-set-group-active?
:on-select on-select-token-set-click :on-select on-select-token-set-click
:selected selected
:new-path new-path
:edition-id edition-id
:origin "set-panel" :origin "set-panel"
:can-edit can-edit?
:on-start-edition on-start-edition
:on-reset-edition on-reset-edition
:on-toggle-token-set on-toggle-token-set-click :on-toggle-token-set on-toggle-token-set-click
:on-toggle-token-set-group on-toggle-token-set-group-click :on-toggle-token-set-group on-toggle-token-set-group-click
:on-update-token-set on-update-token-set :on-update-token-set on-update-token-set

View file

@ -4,6 +4,7 @@
// //
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "../../ds/typography.scss" as *;
@import "refactor/common-refactor.scss"; @import "refactor/common-refactor.scss";
.sets-list { .sets-list {
@ -12,6 +13,21 @@
overflow-y: auto; overflow-y: auto;
} }
.empty-sets-wrapper {
padding: $s-12;
padding-inline-start: $s-24;
color: var(--color-foreground-secondary);
}
.create-set-button {
@include use-typography("body-small");
background-color: transparent;
border: none;
appearance: none;
color: var(--color-accent-primary);
cursor: pointer;
}
.set-item-container { .set-item-container {
@include bodySmallTypography; @include bodySmallTypography;
display: flex; display: flex;

View file

@ -1,54 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.workspace.tokens.sets-context
(:require
[app.common.data.macros :as dm]
[rumext.v2 :as mf]))
(defn set-group-path->id [set-group-path]
(dm/str "group-" set-group-path))
(defn set-path->id [set-path]
(dm/str "set-" set-path))
(def initial {})
(def context (mf/create-context initial))
(def static-context
{:editing? (constantly false)
:on-edit (constantly nil)
:on-create (constantly nil)
:on-reset (constantly nil)})
(mf/defc provider
{::mf/wrap-props false}
[props]
(let [children (unchecked-get props "children")
state (mf/use-state initial)]
[:& (mf/provider context) {:value state}
children]))
(defn use-context []
(let [ctx (mf/use-ctx context)
{:keys [editing-id new-path]} @ctx
editing? (mf/use-callback
(mf/deps editing-id)
#(= editing-id %))
on-edit (mf/use-fn
(fn [editing-id]
(reset! ctx (assoc @ctx :editing-id editing-id))))
on-create (mf/use-fn
(fn [path]
(swap! ctx assoc :editing-id (random-uuid) :new-path path)))
on-reset (mf/use-fn
#(reset! ctx initial))]
{:editing? editing?
:new-path new-path
:on-edit on-edit
:on-create on-create
:on-reset on-reset}))

View file

@ -7,76 +7,75 @@
(ns app.main.ui.workspace.tokens.sets-context-menu (ns app.main.ui.workspace.tokens.sets-context-menu
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.main.data.tokens :as wdt] [app.common.data.macros :as dm]
[app.common.types.tokens-lib :as ctob]
[app.main.data.tokens :as dt]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def sets-menu-ref (def ^:private ref:token-sets-context-menu
(l/derived :token-set-context-menu refs/workspace-local)) (l/derived :token-set-context-menu refs/workspace-tokens))
(defn- prevent-default (defn- prevent-default
[event] [event]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event)) (dom/stop-propagation event))
(mf/defc menu-entry (mf/defc menu-entry*
{::mf/props :obj}
[{:keys [title value on-click]}] [{:keys [title value on-click]}]
[:li {:class (stl/css :context-menu-item) [:li {:class (stl/css :context-menu-item)
:data-value value :data-value value
:on-click on-click} :on-click on-click}
[:span {:class (stl/css :title)} title]]) [:span {:class (stl/css :title)} title]])
(mf/defc menu (mf/defc menu*
[{:keys [group? path]}] {::mf/private true}
(let [{:keys [on-create on-edit]} (sets-context/use-context) [{:keys [is-group path]}]
create-set-at-path (let [create-set-at-path
(mf/use-fn (mf/deps path) #(st/emit! (dt/start-token-set-creation path)))
on-edit
(mf/use-fn (mf/use-fn
(mf/deps path) (mf/deps path)
#(on-create path))
edit-name
(mf/use-fn
(mf/deps group?)
(fn [] (fn []
(let [path (if group? (let [name (ctob/join-set-path path)]
(sets-context/set-group-path->id path) (st/emit! (dt/start-token-set-edition name)))))
(sets-context/set-path->id path))]
(on-edit path))))
delete-set on-delete
(mf/use-fn (mf/use-fn
#(st/emit! (wdt/delete-token-set-path group? path)))] (mf/deps is-group path)
[:ul {:class (stl/css :context-list)} #(st/emit! (dt/delete-token-set-path is-group path)))]
(when group?
[:& menu-entry {:title (tr "workspace.token.add-set-to-group") :on-click create-set-at-path}])
[:& menu-entry {:title (tr "labels.rename") :on-click edit-name}]
[:& menu-entry {:title (tr "labels.delete") :on-click delete-set}]]))
(mf/defc sets-context-menu [:ul {:class (stl/css :context-list)}
(when is-group
[:> menu-entry* {:title (tr "workspace.token.add-set-to-group") :on-click create-set-at-path}])
[:> menu-entry* {:title (tr "labels.rename") :on-click on-edit}]
[:> menu-entry* {:title (tr "labels.delete") :on-click on-delete}]]))
(mf/defc token-set-context-menu*
[] []
(let [mdata (mf/deref sets-menu-ref) (let [{:keys [position is-group path]}
top (+ (get-in mdata [:position :y]) 5) (mf/deref ref:token-sets-context-menu)
left (+ (get-in mdata [:position :x]) 5)
width (mf/use-state 0) position-top
dropdown-ref (mf/use-ref)] (+ (dm/get-prop position :y) 5)
(mf/use-effect
(mf/deps mdata) position-left
(fn [] (+ (dm/get-prop position :x) 5)
(when-let [node (mf/ref-val dropdown-ref)]
(reset! width (.-offsetWidth node))))) on-close
[:& dropdown {:show (boolean mdata) (mf/use-fn #(st/emit! (dt/assign-token-set-context-menu nil)))]
:on-close #(st/emit! wdt/hide-token-set-context-menu)}
[:& dropdown {:show (some? position)
:on-close on-close}
[:div {:class (stl/css :token-set-context-menu) [:div {:class (stl/css :token-set-context-menu)
:data-testid "tokens-context-menu-for-set" :data-testid "tokens-context-menu-for-set"
:ref dropdown-ref :style {:top position-top
:style {:top top :left left} :left position-left}
:on-context-menu prevent-default} :on-context-menu prevent-default}
[:& menu {:group? (:group? mdata) [:> menu* {:is-group is-group :path path}]]]))
:path (:path mdata)}]]]))

View file

@ -8,7 +8,7 @@
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.json :as json]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@ -29,9 +29,8 @@
[app.main.ui.workspace.tokens.changes :as wtch] [app.main.ui.workspace.tokens.changes :as wtch]
[app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]]
[app.main.ui.workspace.tokens.errors :as wte] [app.main.ui.workspace.tokens.errors :as wte]
[app.main.ui.workspace.tokens.sets :refer [sets-list]] [app.main.ui.workspace.tokens.sets :as tsets]
[app.main.ui.workspace.tokens.sets-context :as sets-context] [app.main.ui.workspace.tokens.sets-context-menu :refer [token-set-context-menu*]]
[app.main.ui.workspace.tokens.sets-context-menu :refer [sets-context-menu]]
[app.main.ui.workspace.tokens.style-dictionary :as sd] [app.main.ui.workspace.tokens.style-dictionary :as sd]
[app.main.ui.workspace.tokens.theme-select :refer [theme-select]] [app.main.ui.workspace.tokens.theme-select :refer [theme-select]]
[app.main.ui.workspace.tokens.token-pill :refer [token-pill*]] [app.main.ui.workspace.tokens.token-pill :refer [token-pill*]]
@ -46,7 +45,7 @@
[shadow.resource])) [shadow.resource]))
(def ref:token-type-open-status (def ref:token-type-open-status
(l/derived #(dm/get-in % [:workspace-local :token-type-open-status]) st/state)) (l/derived (l/key :open-status-by-type) refs/workspace-tokens))
;; Components ------------------------------------------------------------------ ;; Components ------------------------------------------------------------------
@ -72,6 +71,9 @@
(let [{:keys [modal title]} (let [{:keys [modal title]}
(get wtch/token-properties type) (get wtch/token-properties type)
can-edit?
(mf/use-ctx ctx/can-edit?)
tokens tokens
(mf/with-memo [tokens] (mf/with-memo [tokens]
(vec (sort-by :name tokens))) (vec (sort-by :name tokens)))
@ -80,8 +82,7 @@
(mf/use-fn (mf/use-fn
(fn [event token] (fn [event token]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (st/emit! (dt/assign-token-context-menu
(st/emit! (dt/show-token-context-menu
{:type :token {:type :token
:position (dom/get-client-position event) :position (dom/get-client-position event)
:errors (:errors token) :errors (:errors token)
@ -115,14 +116,12 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(when (seq selected-shapes) (when (seq selected-shapes)
(st/emit! (wtch/toggle-token {:token token (st/emit! (wtch/toggle-token {:token token
:shapes selected-shapes}))))) :shapes selected-shapes})))))]
tokens-count (count tokens)
can-edit? (:can-edit (deref refs/permissions))]
[:div {:on-click on-toggle-open-click} [:div {:on-click on-toggle-open-click}
[:& cmm/asset-section {:icon (token-section-icon type) [:& cmm/asset-section {:icon (token-section-icon type)
:title title :title title
:assets-count tokens-count :assets-count (count tokens)
:open? is-open} :open? is-open}
[:& cmm/asset-section-block {:role :title-button} [:& cmm/asset-section-block {:role :title-button}
(when can-edit? (when can-edit?
@ -168,11 +167,8 @@
(let [ordered-themes (let [ordered-themes
(mf/deref refs/workspace-token-themes-no-hidden) (mf/deref refs/workspace-token-themes-no-hidden)
permissions
(mf/use-ctx ctx/permissions)
can-edit? can-edit?
(get permissions :can-edit) (mf/use-ctx ctx/can-edit?)
open-modal open-modal
(mf/use-fn (mf/use-fn
@ -201,64 +197,45 @@
(tr "workspace.token.no-permission-themes"))} (tr "workspace.token.no-permission-themes"))}
[:& theme-select]]))])) [:& theme-select]]))]))
(mf/defc add-set-button* (mf/defc token-sets-list*
{::mf/private true} {::mf/private true}
[{:keys [style]}] [{:keys [tokens-lib]}]
(let [{:keys [on-create new-path]} (let [;; FIXME: This is an inneficient operation just for being
(sets-context/use-context) ;; ability to check if there are some sets and lookup the
;; first one when no set is selected, should be REFACTORED; is
;; inneficient because instead of return the sets as-is (tree)
;; it firstly makes it a plain seq from tree.
token-sets
(some-> tokens-lib (ctob/get-sets))
permissions selected-token-set-name
(mf/use-ctx ctx/permissions) (mf/deref refs/selected-token-set-name)
can-edit? {:keys [token-set-edition-id
(get permissions :can-edit) token-set-new-path]}
(mf/deref refs/workspace-tokens)]
on-click
(mf/use-fn
(mf/deps on-create)
(fn []
(on-create [])))]
(if (= style "inline")
(when-not new-path
(if can-edit?
[:div {:class (stl/css :empty-sets-wrapper)}
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
(tr "workspace.token.no-sets-yet")]
[:button {:on-click on-click
:class (stl/css :create-theme-button)}
(tr "workspace.token.create-one")]]
[:div {:class (stl/css :empty-sets-wrapper)}
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
(tr "workspace.token.no-sets-yet")]]))
(when can-edit?
[:> icon-button* {:variant "ghost"
:icon "add"
:on-click on-click
:aria-label (tr "workspace.token.add set")}]))))
(mf/defc theme-sets-list*
{::mf/private true}
[]
(let [token-sets (mf/deref refs/workspace-ordered-token-sets)
{:keys [new-path] :as ctx} (sets-context/use-context)]
(if (and (empty? token-sets) (if (and (empty? token-sets)
(not new-path)) (not token-set-new-path))
[:> add-set-button* {:style "inline"}]
[:& h/sortable-container {}
[:& sets-list]])))
(mf/defc themes-sets-tab* (when-not token-set-new-path
[:> tsets/inline-add-button*])
[:> h/sortable-container {}
[:> tsets/sets-list*
{:tokens-lib tokens-lib
:new-path token-set-new-path
:edition-id token-set-edition-id
:selected selected-token-set-name}]])))
(mf/defc token-sets-section*
{::mf/private true} {::mf/private true}
[{:keys [resize-height]}] [{:keys [resize-height] :as props}]
(let [permissions (let [can-edit?
(mf/use-ctx ctx/permissions) (mf/use-ctx ctx/can-edit?)]
can-edit? [:*
(get permissions :can-edit)] [:> token-set-context-menu*]
[:& sets-context/provider {}
[:& sets-context-menu]
[:article {:data-testid "token-themes-sets-sidebar" [:article {:data-testid "token-themes-sets-sidebar"
:class (stl/css :sets-section-wrapper) :class (stl/css :sets-section-wrapper)
:style {"--resize-height" (str resize-height "px")}} :style {"--resize-height" (str resize-height "px")}}
@ -267,12 +244,12 @@
[:div {:class (stl/css :sidebar-header)} [:div {:class (stl/css :sidebar-header)}
[:& title-bar {:title (tr "labels.sets")} [:& title-bar {:title (tr "labels.sets")}
(when can-edit? (when can-edit?
[:> add-set-button* {:style "header"}])]] [:> tsets/add-button*])]]
[:> theme-sets-list* {}]]]])) [:> token-sets-list* props]]]]))
(mf/defc tokens-tab* (mf/defc tokens-section*
[] [{:keys [tokens-lib]}]
(let [objects (mf/deref refs/workspace-page-objects) (let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes) selected (mf/deref refs/selected-shapes)
open-status (mf/deref ref:token-type-open-status) open-status (mf/deref ref:token-type-open-status)
@ -282,16 +259,36 @@
(into [] (keep (d/getf objects)) selected)) (into [] (keep (d/getf objects)) selected))
active-theme-tokens active-theme-tokens
(sd/use-active-theme-tokens) (mf/with-memo [tokens-lib]
(if tokens-lib
(ctob/get-active-themes-set-tokens tokens-lib)
{}))
;; Resolve tokens as second step
active-theme-tokens
(sd/use-resolved-tokens* active-theme-tokens)
;; This only checks for the currently explicitly selected set
;; name, it is ephimeral and can be nil
selected-token-set-name
(mf/deref refs/selected-token-set-name)
selected-token-set
(when selected-token-set-name
(some-> tokens-lib (ctob/get-set selected-token-set-name)))
;; If we have not selected any set explicitly we just
;; select the first one from the list of sets
selected-token-set-tokens
(when selected-token-set
(get selected-token-set :tokens))
tokens tokens
(sd/use-resolved-workspace-tokens) (mf/with-memo [active-theme-tokens selected-token-set-tokens]
(merge active-theme-tokens selected-token-set-tokens))
selected-token-set-tokens tokens
(mf/deref refs/workspace-selected-token-set-tokens) (sd/use-resolved-tokens* tokens)
selected-token-set-name
(mf/deref refs/workspace-selected-token-set-name)
tokens-by-type tokens-by-type
(mf/with-memo [tokens selected-token-set-tokens] (mf/with-memo [tokens selected-token-set-tokens]
@ -307,6 +304,16 @@
(mf/with-memo [tokens-by-type] (mf/with-memo [tokens-by-type]
(get-sorted-token-groups tokens-by-type))] (get-sorted-token-groups tokens-by-type))]
(mf/with-effect [tokens-lib selected-token-set-name]
(when (and tokens-lib
(or (nil? selected-token-set-name)
(and selected-token-set-name
(not (ctob/get-set tokens-lib selected-token-set-name)))))
(let [match (->> (ctob/get-sets tokens-lib)
(first)
(:name))]
(st/emit! (dt/set-selected-token-set-name match)))))
[:* [:*
[:& token-context-menu] [:& token-context-menu]
[:& title-bar {:all-clickable true [:& title-bar {:all-clickable true
@ -328,12 +335,15 @@
:active-theme-tokens active-theme-tokens :active-theme-tokens active-theme-tokens
:tokens []}])])) :tokens []}])]))
(mf/defc import-export-button (mf/defc import-export-button*
{::mf/wrap-props false} []
[{:keys []}] (let [input-ref (mf/use-ref)
(let [show-menu* (mf/use-state false)
show-menu* (mf/use-state false)
show-menu? (deref show-menu*) show-menu? (deref show-menu*)
can-edit? (:can-edit (deref refs/permissions))
can-edit?
(mf/use-ctx ctx/can-edit?)
open-menu open-menu
(mf/use-fn (mf/use-fn
@ -347,34 +357,37 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(reset! show-menu* false))) (reset! show-menu* false)))
input-ref (mf/use-ref)
on-display-file-explorer on-display-file-explorer
(mf/use-fn (mf/use-fn #(dom/click (mf/ref-val input-ref)))
#(.click (mf/ref-val input-ref)))
on-import on-import
(fn [event] (mf/use-fn
(let [file (-> event .-target .-files (aget 0))] (fn [event]
(->> (wapi/read-file-as-text file) (let [file (-> (dom/get-target event)
(sd/process-json-stream) (dom/get-files)
(rx/subs! (fn [lib] (first))]
(st/emit! (ptk/event ::ev/event {::ev/name "import-tokens"})) (->> (wapi/read-file-as-text file)
(st/emit! (dt/import-tokens-lib lib))) (sd/process-json-stream)
(fn [err] (rx/subs! (fn [lib]
(js/console.error err) (st/emit! (ptk/data-event ::ev/event {::ev/name "import-tokens"})
(st/emit! (ntf/show {:content (wte/humanize-errors [(ex-data err)]) (dt/import-tokens-lib lib)))
:type :toast (fn [err]
:level :error}))))) (js/console.error err)
(set! (.-value (mf/ref-val input-ref)) ""))) (st/emit! (ntf/show {:content (wte/humanize-errors [(ex-data err)])
:type :toast
:level :error})))))
(-> (mf/ref-val input-ref)
(dom/set-value! "")))))
on-export (fn [] on-export
(st/emit! (ptk/event ::ev/event {::ev/name "export-tokens"})) (mf/use-fn
(let [tokens-json (some-> (deref refs/tokens-lib) (fn []
(ctob/encode-dtcg) (st/emit! (ptk/data-event ::ev/event {::ev/name "export-tokens"}))
(clj->js) (let [tokens-json (some-> (deref refs/tokens-lib)
(js/JSON.stringify nil 2))] (ctob/encode-dtcg)
(->> (wapi/create-blob (or tokens-json "{}") "application/json") (json/encode :key-fn identity))]
(dom/trigger-download "tokens.json"))))] (->> (wapi/create-blob (or tokens-json "{}") "application/json")
(dom/trigger-download "tokens.json")))))]
[:div {:class (stl/css :import-export-button-wrapper)} [:div {:class (stl/css :import-export-button-wrapper)}
(when can-edit? (when can-edit?
@ -406,14 +419,20 @@
on-lost-pointer-capture-pages :on-lost-pointer-capture on-lost-pointer-capture-pages :on-lost-pointer-capture
on-pointer-move-pages :on-pointer-move on-pointer-move-pages :on-pointer-move
size-pages-opened :size} size-pages-opened :size}
(use-resize-hook :tokens 200 38 "0.6" :y false nil)] (use-resize-hook :tokens 200 38 400 :y false nil)
tokens-lib
(mf/deref refs/tokens-lib)]
[:div {:class (stl/css :sidebar-wrapper)} [:div {:class (stl/css :sidebar-wrapper)}
[:> themes-sets-tab* {:resize-height size-pages-opened}] [:> token-sets-section*
{:resize-height size-pages-opened
:tokens-lib tokens-lib}]
[:article {:class (stl/css :tokens-section-wrapper) [:article {:class (stl/css :tokens-section-wrapper)
:data-testid "tokens-sidebar"} :data-testid "tokens-sidebar"}
[:div {:class (stl/css :resize-area-horiz) [:div {:class (stl/css :resize-area-horiz)
:on-pointer-down on-pointer-down-pages :on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages :on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}] :on-pointer-move on-pointer-move-pages}]
[:> tokens-tab*]] [:> tokens-section* {:tokens-lib tokens-lib}]]
[:& import-export-button]])) [:> import-export-button*]]))

View file

@ -54,12 +54,6 @@
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
} }
.empty-sets-wrapper {
padding: $s-12;
padding-inline-start: $s-24;
color: var(--color-foreground-secondary);
}
.sidebar-header { .sidebar-header {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -5,7 +5,6 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.common.transit :as t] [app.common.transit :as t]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.errors :as wte] [app.main.ui.workspace.tokens.errors :as wte]
[app.main.ui.workspace.tokens.tinycolor :as tinycolor] [app.main.ui.workspace.tokens.tinycolor :as tinycolor]
[app.main.ui.workspace.tokens.token :as wtt] [app.main.ui.workspace.tokens.token :as wtt]
@ -166,10 +165,10 @@
([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]
(-> style-dictionary (let [sdict (-> style-dictionary
(add-tokens tokens-tree) (add-tokens tokens-tree)
(build-dictionary) (build-dictionary))]
(p/then #(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))
@ -177,8 +176,10 @@
(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+ [tokens] (defn resolve-tokens+
(resolve-tokens-tree+ (ctob/tokens-tree tokens) #(get tokens (sd-token-name %)))) [tokens]
(let [tokens-tree (ctob/tokens-tree tokens)]
(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.
@ -267,36 +268,45 @@
:or {cache-atom !tokens-cache} :or {cache-atom !tokens-cache}
:as config}] :as config}]
(let [tokens-state (mf/use-state (get @cache-atom tokens))] (let [tokens-state (mf/use-state (get @cache-atom tokens))]
(mf/use-effect
(mf/deps tokens config) ;; FIXME: this with effect with trigger all the time because
(fn [] ;; `config` will be always a different instance
(let [cached (get @cache-atom tokens)]
(cond (mf/with-effect [tokens config]
(nil? tokens) nil (let [cached (get @cache-atom tokens)]
;; The tokens are already processing somewhere (cond
(p/promise? cached) (-> cached (nil? tokens) nil
(p/then #(reset! tokens-state %)) ;; The tokens are already processing somewhere
#_(p/catch js/console.error)) (p/promise? cached) (-> cached
;; Get the cached entry (p/then #(reset! tokens-state %))
(some? cached) (reset! tokens-state cached) #_(p/catch js/console.error))
;; No cached entry, start processing ;; Get the cached entry
:else (let [promise+ (if interactive? (some? cached) (reset! tokens-state cached)
(resolve-tokens-interactive+ tokens) ;; No cached entry, start processing
(resolve-tokens+ tokens))] :else (let [promise+ (if interactive?
(swap! cache-atom assoc tokens promise+) (resolve-tokens-interactive+ tokens)
(p/then promise+ (fn [resolved-tokens] (resolve-tokens+ tokens))]
(swap! cache-atom assoc tokens resolved-tokens) (swap! cache-atom assoc tokens promise+)
(reset! tokens-state resolved-tokens)))))))) (p/then promise+ (fn [resolved-tokens]
(swap! cache-atom assoc tokens resolved-tokens)
(reset! tokens-state resolved-tokens)))))))
@tokens-state)) @tokens-state))
(defn use-resolved-workspace-tokens [] (defn use-resolved-tokens*
(let [active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens) "This hook will return the unresolved tokens as state until they are
selected-token-set-tokens (mf/deref refs/workspace-selected-token-set-tokens) processed, then the state will be updated with the resolved tokens.
prefer-selected-token-set-tokens (merge active-theme-tokens selected-token-set-tokens)]
(use-resolved-tokens prefer-selected-token-set-tokens)))
(defn use-active-theme-tokens This is a cache-less, simplified version of use-resolved-tokens
"A hook that returns active tokens for the current active theme" hook."
[] [tokens & {:keys [interactive?]}]
(-> (mf/deref refs/workspace-active-theme-sets-tokens) (let [state* (mf/use-state tokens)]
(use-resolved-tokens {:cache-atom !theme-tokens-cache}))) (mf/with-effect [tokens interactive?]
(when (seq tokens)
(let [promise (if interactive?
(resolve-tokens-interactive+ tokens)
(resolve-tokens+ tokens))]
(->> promise
(p/fmap (fn [resolved-tokens]
(reset! state* resolved-tokens)))))))
@state*))