mirror of
https://github.com/penpot/penpot.git
synced 2026-08-31 00:59:02 +00:00
329 lines
16 KiB
Clojure
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}])]))
|