2026-05-29 12:28:10 +02:00

329 lines
16 KiB
Clojure

(ns app.main.ui.workspace.tokens.management
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.path-names :as cpn]
[app.common.types.shape.layout :as ctsl]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.helpers :as dh]
[app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.data.workspace.tokens.remapping :as remap]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]]
[app.main.ui.workspace.tokens.management.group :refer [token-group*]]
[app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]]
[app.util.array :as array]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- get-sorted-token-groups
"Separate token-types into groups of `empty` or `filled` depending if
tokens exist for that type. Sort each group alphabetically (by their type)."
[tokens-by-type]
(let [token-shadow? (contains? cf/flags :token-shadow)
all-types (cond-> dwta/token-properties
(not token-shadow?) (dissoc :shadow))
all-types (-> all-types keys seq)]
(loop [empty #js []
filled #js []
types all-types]
(if-let [type (first types)]
(if (not-empty (get tokens-by-type type))
(recur empty
(array/conj! filled type)
(rest types))
(recur (array/conj! empty type)
filled
(rest types)))
[(seq (array/sort! empty))
(seq (array/sort! filled))]))))
(mf/defc selected-set-info*
{::mf/private true}
[{:keys [tokens-lib selected-token-set-id]}]
(let [selected-token-set
(mf/with-memo [tokens-lib selected-token-set-id]
(when selected-token-set-id
(some-> tokens-lib (ctob/get-set selected-token-set-id))))
active-token-sets-names
(mf/with-memo [tokens-lib]
(some-> tokens-lib (ctob/get-active-themes-set-names)))
token-set-active?
(mf/use-fn
(mf/deps active-token-sets-names)
(fn [name]
(contains? active-token-sets-names name)))]
[:div {:class (stl/css :sets-header-container)}
[:> text* {:as "span"
:typography "headline-small"
:class (stl/css :sets-header)
:data-testid "active-token-set-title"}
(tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))]
(when (and (some? selected-token-set-id)
(not (token-set-active? (ctob/get-name selected-token-set))))
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
;; NOTE: when no set in tokens-lib, the selected-token-set-id
;; will be `nil`, so for properly hide the inactive message we
;; check that at least `selected-token-set-id` has a value
[:*
[:> icon* {:class (stl/css :sets-header-status-icon) :icon-id i/eye-off}]
[:> text* {:as "span" :typography "body-small" :class (stl/css :sets-header-status-text)}
(tr "workspace.tokens.inactive-set")]]])]))
(mf/defc tokens-section*
{::mf/private true}
[{:keys [tokens-lib active-tokens resolved-active-tokens]}]
(let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
selected-shapes
(mf/with-memo [selected objects]
(into [] (keep (d/getf objects)) selected))
is-selected-inside-layout
(mf/with-memo [selected-shapes objects]
(some #(ctsl/any-layout-immediate-child? objects %) selected-shapes))
;; This only checks for the currently explicitly selected set
;; id, it is ephimeral and can be nil
;; FIXME: this is a repeated deref for the same `:workspace-tokens` state
selected-token-set-id (mf/deref refs/selected-token-set-id)
;; 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-id
(some-> tokens-lib (ctob/get-tokens selected-token-set-id)))
tokens
(mf/with-memo [active-tokens selected-token-set-tokens]
(merge active-tokens selected-token-set-tokens))
;; Group tokens by their type
tokens-by-type
(mf/with-memo [tokens selected-token-set-tokens]
(let [tokens (reduce-kv (fn [tokens k _]
(if (contains? selected-token-set-tokens k)
tokens
(dissoc tokens k)))
tokens
tokens)]
(ctob/group-by-type tokens)))
[empty-group filled-group]
(mf/with-memo [tokens-by-type]
(get-sorted-token-groups tokens-by-type))
;; Filter tokens by their path and return the tokens
filter-tokens-by-path
(mf/use-fn
(fn [tokens-filtered-by-type node]
(->> tokens-filtered-by-type
(filter (fn [token]
(let [token-path (cpn/split-path (:name token) :separator ".")]
(and (> (count token-path) 0)
(str/starts-with? (:name token) (str (:path node) ".")))))))))
;; Filter tokens by their path and return their ids
filter-tokens-by-path-ids
(mf/use-fn
(mf/deps selected-token-set-tokens)
(fn [type path]
(->> selected-token-set-tokens
(filter (fn [token]
(let [[_ token-value] token]
(and (= (:type token-value) type) (str/starts-with? (:name token-value) (str path "."))))))
(mapv (fn [token]
(let [[_ token-value] token]
(:id token-value)))))))
remaining-tokens-of-type-in-set?
(mf/use-fn
(fn [selected-token-set-tokens tokens-in-path-ids]
(let [token-ids (set tokens-in-path-ids)
remaining-tokens (filter (fn [token]
(not (contains? token-ids (:id token))))
selected-token-set-tokens)]
(seq remaining-tokens))))
delete-token
(mf/with-memo [selected-token-set-tokens selected-token-set-id]
(fn [token]
(let [id (:id token)
type (:type token)
path (:name token)
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
tokens-filtered-by-type (get tokens-by-type type)
remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type [id])]
(st/emit! (dwtl/delete-token selected-token-set-id id))
(if remaining-tokens?
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
(st/emit! (dwtl/close-token-type type))))))
delete-node
(mf/with-memo [selected-token-set-tokens selected-token-set-id]
(fn [node type]
(let [path (:path node)
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
tokens-filtered-by-type (get tokens-by-type type)
tokens-in-path-ids (filter-tokens-by-path-ids type path)
remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type tokens-in-path-ids)]
;; Delete tokens in path
(st/emit! (dwtl/bulk-delete-tokens selected-token-set-id tokens-in-path-ids))
;; Remove from unfolded tree path
(if remaining-tokens?
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
(st/emit! (dwtl/close-token-type type))))))
bulk-rename-tokens-in-path
;; Rename tokens in bulk affected by a node rename.
(mf/use-fn
(mf/deps filter-tokens-by-path-ids selected-token-set-id)
(fn [node type new-node-name]
(let [old-path (:path node)
new-path (ctob/rename-path node new-node-name)
tokens-in-path-ids (filter-tokens-by-path-ids type old-path)]
(st/emit!
(modal/hide)
(dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-path)))))
bulk-remap-tokens-in-path
;; Remap tokens in bulk affected by a node rename.
;; It will update the token names and propagate the changes to the workspace.
(mf/use-fn
(mf/deps filter-tokens-by-path filter-tokens-by-path-ids selected-token-set-tokens selected-token-set-id)
(fn [node type new-node-name]
(let [old-path (:path node)
;; Get tokens in path to remap their names after remapping the node
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
tokens-filtered-by-type (get tokens-by-type type)
tokens-in-path (filter-tokens-by-path tokens-filtered-by-type node)
tokens-in-path-ids (filter-tokens-by-path-ids type old-path)
new-node-path (ctob/rename-path node new-node-name)
new-tokens (map (fn [token]
(let [new-token-path (ctob/rename-path node token new-node-name)]
(assoc token :name new-token-path)))
tokens-in-path)
undo-group (uuid/next)]
(st/emit!
(dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-node-path :undo-group undo-group)
(remap/bulk-remap-tokens tokens-in-path new-tokens :undo-group undo-group)
(dwtp/propagate-workspace-tokens)
(modal/hide)))))
on-remap-node-warning
;; If there are tokens that will be affected by the node rename, we show the remap modal
(mf/use-fn
(mf/deps bulk-remap-tokens-in-path bulk-rename-tokens-in-path)
(fn [node type new-node-name]
(let [remap-data {:new-name new-node-name
:old-name (:name node)
:type "node"}
remap-handler #(bulk-remap-tokens-in-path node type new-node-name)
rename-handler #(bulk-rename-tokens-in-path node type new-node-name)]
(st/emit!
(modal/hide)
(modal/show :tokens/remapping-confirmation {:remap-data remap-data
:on-remap remap-handler
:on-rename rename-handler})))))
on-rename-node
;; When user renames a node, we need to check if there are tokens that will be affected by this change.
;; If there are, we display the remap modal, otherwise, we rename the tokens directly.
(mf/use-fn
(mf/deps selected-token-set-tokens filter-tokens-by-path on-remap-node-warning bulk-rename-tokens-in-path)
(fn [node type new-node-name]
(let [state @st/state
file-data (dh/lookup-file-data state)
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
tokens-filtered-by-type (get tokens-by-type type)
tokens-in-current-path (filter-tokens-by-path tokens-filtered-by-type node)
token-references-count (reduce (fn [count token]
(+ count (remap/count-token-references file-data (:name token))))
0
tokens-in-current-path)]
(if (> token-references-count 0)
(on-remap-node-warning node type new-node-name)
(bulk-rename-tokens-in-path node type new-node-name)))))
on-duplicate-node
(fn [node type new-node-name]
(let [tokens-in-path-ids (filter-tokens-by-path-ids type (:path node))]
(st/emit!
(modal/hide)
(dwtl/bulk-create-tokens selected-token-set-id tokens-in-path-ids type node new-node-name))))
open-rename-node-modal
;; When user renames a node, we display a form modal
(mf/use-fn
(mf/deps selected-token-set-tokens on-rename-node)
(fn [node type]
(let [on-rename-node-handler #(on-rename-node node type %)]
(st/emit! (modal/show :tokens/rename-node {:node node
:tokens-in-active-set selected-token-set-tokens
:on-rename on-rename-node-handler})))))
open-duplicate-node-modal
(mf/use-fn
(mf/deps selected-token-set-tokens on-duplicate-node)
(fn [node type]
(let [on-duplicate-node-handler #(on-duplicate-node node type %)]
(st/emit! (modal/show :tokens/rename-node {:new-node-name (str (:name node) "-copy")
:node node
:variant "duplicate"
:tokens-in-active-set selected-token-set-tokens
:on-rename on-duplicate-node-handler})))))]
(mf/with-effect [tokens-lib selected-token-set-id]
(when (and tokens-lib
(or (nil? selected-token-set-id)
(and selected-token-set-id
(not (ctob/get-set tokens-lib selected-token-set-id)))))
(let [match (->> (ctob/get-sets tokens-lib)
(first))]
(when match
(st/emit! (dwtl/set-selected-token-set-id (ctob/get-id match)))))))
[:*
[:& token-context-menu {:on-delete-token delete-token}]
[:> token-node-context-menu* {:on-rename-node open-rename-node-modal
:on-duplicate-node open-duplicate-node-modal
:on-delete-node delete-node}]
[:> selected-set-info* {:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]
(for [type filled-group]
(let [tokens (get tokens-by-type type)]
[:> token-group* {:key (name type)
:tokens tokens
:type type
:selected-ids selected
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens resolved-active-tokens
:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]))
(for [type empty-group]
[:> token-group* {:key (name type)
:tokens []
:type type
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens resolved-active-tokens}])]))