🎉 Rename token group (#8275)

* 🎉 Rename token group

* 📎 Add to CHANGES
This commit is contained in:
Xaviju
2026-03-19 22:54:21 +01:00
committed by GitHub
parent 8e7e6ffc2f
commit f8913c755d
22 changed files with 687 additions and 59 deletions

View File

@@ -456,6 +456,34 @@
(rx/of (dch/commit-changes changes)
(ptk/data-event ::ev/event {::ev/name "edit-token" :type token-type})))))))
(defn bulk-update-tokens
[set-id token-ids type old-path new-path]
(dm/assert! (uuid? set-id))
(dm/assert! (every? uuid? token-ids))
(ptk/reify ::bulk-update-tokens
ptk/WatchEvent
(watch [it state _]
(let [token-set (if set-id
(lookup-token-set state set-id)
(lookup-token-set state))
data (dsh/lookup-file-data state)
changes (reduce (fn [changes token-id]
(let [token (-> (get-tokens-lib state)
(ctob/get-token (ctob/get-id token-set) token-id))
new-name (str/replace (:name token) old-path new-path)
token' (->> (merge token {:name new-name})
(into {})
(ctob/make-token))]
(pcb/set-token changes (ctob/get-id token-set) token-id token')))
(-> (pcb/empty-changes it)
(pcb/with-library-data data))
token-ids)]
(toggle-token-path (str (name type) "." old-path))
(toggle-token-path (str (name type) "." new-path))
(rx/of (dch/commit-changes changes)
(ptk/data-event ::ev/event {::ev/name "bulk-update-tokens" :type type}))))))
(defn delete-token
[set-id token-id]
(dm/assert! (uuid? set-id))

View File

@@ -150,6 +150,18 @@
(rx/of (dch/commit-changes token-changes))))))
(defn bulk-remap-tokens
"Helper function to remap a batch of tokens, used for node renaming"
[tokens-in-path new-tokens]
(ptk/reify ::bulk-remap-tokens
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(map (fn [old-token new-token]
(remap-tokens (:name old-token) (:name new-token)))
tokens-in-path
new-tokens)))))
(defn validate-token-remapping
"Validate that a token remapping operation is safe to perform"
[old-name new-name]

View File

@@ -67,24 +67,38 @@
(mf/defc form-submit*
[{:keys [disabled on-submit] :rest props}]
(let [form (mf/use-ctx context)
disabled? (or (and (some? form)
(or (not (:valid @form))
(seq (:async-errors @form))
(seq (:extra-errors @form))))
(true? disabled))
form-state (when form @form)
disabled? (mf/use-memo
(mf/deps form form-state disabled)
(fn []
(boolean
(or (nil? form)
(true? disabled)
(not (:valid form-state))
(seq (:async-errors form-state))
(seq (:extra-errors form-state))))))
handle-key-down-save
(mf/use-fn
(mf/deps on-submit form)
(mf/deps on-submit form disabled?)
(fn [e]
(when (or (k/enter? e) (k/space? e))
(when (and (or (k/enter? e) (k/space? e)) (not disabled?))
(dom/prevent-default e)
(on-submit form e))))
props
(mf/spread-props props {:disabled disabled?
:on-key-down handle-key-down-save
:type "submit"})]
(mf/spread-props props {:on-key-down handle-key-down-save
:type "submit"})
props
(if disabled?
(mf/spread-props props {:disabled true
:on-key-down handle-key-down-save
:type "submit"})
props)]
[:> button* props]))

View File

@@ -36,6 +36,7 @@
[app.main.ui.workspace.tokens.import]
[app.main.ui.workspace.tokens.import.modal]
[app.main.ui.workspace.tokens.management.forms.modals]
[app.main.ui.workspace.tokens.management.forms.rename-node-modal]
[app.main.ui.workspace.tokens.remapping-modal]
[app.main.ui.workspace.tokens.settings]
[app.main.ui.workspace.tokens.themes.create-modal]

View File

@@ -195,7 +195,7 @@
(dom/set-attribute! checkbox "indeterminate" true)
(dom/remove-attribute! checkbox "indeterminate"))))
[:div {:class (stl/css :fill-section)}
[:div {:class (stl/css :fill-section) :data-testid "fill-section"}
[:div {:class (stl/css :fill-title)}
[:> title-bar* {:collapsable has-fills?
:collapsed (not open?)

View File

@@ -2,12 +2,17 @@
(: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.config :as cf]
[app.main.data.helpers :as dh]
[app.main.data.modal :as modal]
[app.main.data.style-dictionary :as sd]
[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]
@@ -17,6 +22,7 @@
[app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]]
[app.util.array :as array]
[app.util.i18n :refer [tr]]
[cljs.pprint :as pp]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -124,6 +130,17 @@
(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 ".")
_ (pp/pprint {:token-path token-path :count (count token-path)})]
(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
@@ -132,7 +149,7 @@
(->> selected-token-set-tokens
(filter (fn [token]
(let [[_ token-value] token]
(and (= (:type token-value) type) (str/starts-with? (:name token-value) path)))))
(and (= (:type token-value) type) (str/starts-with? (:name token-value) (str path "."))))))
(mapv (fn [token]
(let [[_ token-value] token]
(:id token-value)))))))
@@ -176,7 +193,88 @@
;; Remove from unfolded tree path
(if remaining-tokens?
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
(st/emit! (dwtl/toggle-token-path (name type)))))))]
(st/emit! (dwtl/toggle-token-path (name 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)]
(st/emit!
(dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-node-path)
(remap/bulk-remap-tokens tokens-in-path new-tokens)
(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)
_ (pp/pprint {:tokens-in-current-path tokens-in-current-path})
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)))))
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})))))]
(mf/with-effect [tokens-lib selected-token-set-id]
(when (and tokens-lib
@@ -190,7 +288,8 @@
[:*
[:& token-context-menu {:on-delete-token delete-token}]
[:> token-node-context-menu* {:on-delete-node delete-node}]
[:> token-node-context-menu* {:on-rename-node open-rename-node-modal
:on-delete-node delete-node}]
[:> selected-set-info* {:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]

View File

@@ -160,13 +160,13 @@
on-remap-token
(mf/use-fn
(mf/deps token)
(fn [valid-token name old-name description]
(fn [valid-token new-name old-name description]
(st/emit!
(dwtl/update-token (:id token)
{:name name
{:name new-name
:value (:value valid-token)
:description description})
(remap/remap-tokens old-name name)
(remap/remap-tokens old-name new-name)
(dwtp/propagate-workspace-tokens)
(modal/hide!))))
@@ -203,11 +203,12 @@
is-rename (and (= action "edit") (not= name old-name))
references-count (remap/count-token-references file-data old-name)
on-remap #(on-remap-token valid-token name old-name description)
on-rename #(on-rename-token valid-token name description)]
on-rename #(on-rename-token valid-token name description)
remap-data {:new-name name
:old-name old-name
:type "token"}]
(if (and is-rename (> references-count 0))
(st/emit! (modal/show :tokens/remapping-confirmation {:old-token-name old-name
:new-token-name name
:references-count references-count
(st/emit! (modal/show :tokens/remapping-confirmation {:remap-data remap-data
:on-remap on-remap
:on-rename on-rename}))
(st/emit!

View File

@@ -0,0 +1,117 @@
(ns app.main.ui.workspace.tokens.management.forms.rename-node-modal
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cfo]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.forms :as fc]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(mf/defc rename-node-form*
[{:keys [node active-tokens tokens-tree on-close on-submit]}]
(let [make-schema #(cfo/make-node-token-schema active-tokens tokens-tree node)
schema
(mf/with-memo [active-tokens]
(make-schema))
initial (mf/with-memo [node]
{:name (:name node)})
form (fm/use-form :schema schema
:initial initial)
on-submit (mf/use-fn
(mf/deps form on-submit)
(fn []
(let [name (get-in @form [:clean-data :name])]
(when (and (get-in @form [:touched :name]) (not= name (:name node)))
(on-submit name)))))
is-disabled? (or (not (:valid @form))
(not (get-in @form [:touched :name]))
(= (get-in @form [:clean-data :name]) (:name node)))
new-path (mf/with-memo [@form node]
(let [new-name (get-in @form [:clean-data :name])
path (str (:path node))
new-path (str/replace path (:name node) new-name)]
new-path))]
[:*
[:> heading* {:level 2
:typography "headline-medium"
:class (stl/css :form-modal-title)}
(tr "workspace.tokens.rename-group")]
[:> fc/form* {:class (stl/css :form-wrapper)
:form form
:on-submit on-submit}
[:> fc/form-input* {:id "rename-node"
:name :name
:label (tr "workspace.tokens.token-name")
:placeholder (tr "workspace.tokens.token-name")
:max-length 255
:variant "comfortable"
:hint-type "hint"
:hint-message (tr "workspace.tokens.rename-group-name-hint" new-path)
:auto-focus true}]
[:div {:class (stl/css :form-actions)}
[:> button* {:variant "secondary"
:name "cancel"
:on-click on-close} (tr "labels.cancel")]
[:> fc/form-submit* {:variant "primary"
:disabled is-disabled?
:name "rename"} (tr "labels.rename")]]]]))
(mf/defc rename-node-modal
{::mf/register modal/components
::mf/register-as :tokens/rename-node}
[{:keys [node tokens-in-active-set on-rename]}]
(let [tokens-tree-in-selected-set
(mf/with-memo [tokens-in-active-set node]
(-> (ctob/tokens-tree tokens-in-active-set)
(d/dissoc-in (:name node))))
close-modal
(mf/use-fn
(fn []
(st/emit! (modal/hide))))
rename
(mf/use-fn
(mf/deps on-rename)
(fn [new-name]
(on-rename new-name)))
on-key-down
(mf/use-fn
(mf/deps [close-modal])
(fn [event]
(when (kbd/esc? event)
(close-modal))))]
[:div {:class (stl/css :modal-overlay)
:on-key-down on-key-down
:data-testid "token-rename-node-modal"}
[:div {:class (stl/css :modal-dialog)}
[:> icon-button* {:class (stl/css :close-btn)
:on-click close-modal
:aria-label (tr "labels.close")
:variant "ghost"
:icon i/close}]
[:> rename-node-form* {:node node
:active-tokens tokens-in-active-set
:tokens-tree tokens-tree-in-selected-set
:on-close close-modal
:on-submit rename}]]]))

View File

@@ -0,0 +1,46 @@
// 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
@use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as t;
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
--modal-title-foreground-color: var(--color-foreground-primary);
--modal-text-foreground-color: var(--color-foreground-secondary);
@extend .modal-overlay-base;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset-inline-start: 0;
inset-block-start: 0;
block-size: 100%;
inline-size: 100%;
background-color: var(--overlay-color);
}
.close-btn {
position: absolute;
inset-block-start: $sz-6;
inset-inline-end: $sz-6;
}
.modal-dialog {
@extend .modal-container-base;
inline-size: 100%;
max-inline-size: 32rem;
max-block-size: unset;
user-select: none;
position: relative;
}
.form-modal-title {
@include t.use-typography("headline-medium");
color: var(--color-foreground-primary);
}

View File

@@ -13,6 +13,7 @@
(def ^:private schema:token-node-context-menu
[:map
[:on-rename-node fn?]
[:on-delete-node fn?]])
(def ^:private tokens-node-menu-ref
@@ -25,7 +26,7 @@
(mf/defc token-node-context-menu*
{::mf/schema schema:token-node-context-menu}
[{:keys [on-delete-node]}]
[{:keys [on-rename-node on-delete-node]}]
(let [mdata (mf/deref tokens-node-menu-ref)
is-open? (boolean mdata)
dropdown-ref (mf/use-ref)
@@ -35,7 +36,13 @@
dropdown-direction-change* (mf/use-ref 0)
top (+ (get-in mdata [:position :y]) 5)
left (+ (get-in mdata [:position :x]) 5)
rename-node (mf/use-fn
(mf/deps mdata on-rename-node)
(fn []
(let [node (get mdata :node)
type (get mdata :type)]
(when node
(on-rename-node node type)))))
delete-node (mf/use-fn
(mf/deps mdata)
(fn []
@@ -75,6 +82,11 @@
:on-context-menu prevent-default}
(when mdata
[:ul {:class (stl/css :token-node-context-menu-list)}
[:li {:class (stl/css :token-node-context-menu-listitem)}
[:button {:class (stl/css :token-node-context-menu-action)
:type "button"
:on-click rename-node}
(tr "labels.rename")]]
[:li {:class (stl/css :token-node-context-menu-listitem)}
[:button {:class (stl/css :token-node-context-menu-action)
:type "button"

View File

@@ -20,27 +20,41 @@
[app.util.keyboard :as kbd]
[rumext.v2 :as mf]))
(defn hide-remapping-modal
(defn- hide-remapping-modal
"Hide the token remapping confirmation modal"
[]
(st/emit! (modal/hide)))
;; TODO: Uncomment when modal components support schema validation
;; (def ^:private schema:remap-data
;; [:map
;; [:old-name :string]
;; [:new-name :string]
;; [:type [:enum "token" "node"]]])
;; (def ^:private schema:token-remapping-modal
;; [:map
;; [:remap-data [:maybe schema:remap-data]]
;; [:on-remap {:optional true} [:maybe fn?]]
;; [:on-rename {:optional true} [:maybe fn?]]])
;; Remapping Modal Component
(mf/defc token-remapping-modal
{::mf/register modal/components
::mf/register-as :tokens/remapping-confirmation}
[{:keys [old-token-name new-token-name on-remap on-rename]}]
(let [remap-modal (get @st/state :remap-modal)
::mf/register-as :tokens/remapping-confirmation
;; TODO: Uncomment when modal components support schema validation
;; ::mf/schema schema:token-remapping-modal
}
[{:keys [remap-data on-remap on-rename]}]
(let [old-name (:old-name remap-data)
new-name (:new-name remap-data)
;; Remap logic on confirm
confirm-remap
(mf/use-fn
(mf/deps on-remap remap-modal)
(mf/deps on-remap old-name new-name)
(fn []
;; Call shared remapping logic
(let [old-token-name (:old-token-name remap-modal)
new-token-name (:new-token-name remap-modal)]
(st/emit! [:tokens/remap-tokens old-token-name new-token-name]))
(when (fn? on-remap)
(on-remap))))
@@ -83,9 +97,13 @@
:id "modal-title"
:typography "headline-large"
:class (stl/css :modal-title)}
(tr "workspace.tokens.remap-token-references-title" old-token-name new-token-name)]]
(if (= (:type remap-data) "token")
(tr "workspace.tokens.remap-token-references-title" old-name new-name)
(tr "workspace.tokens.remap-node-references-title" old-name new-name))]]
[:div {:class (stl/css :modal-content)}
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-effects")]
(if (= (:type remap-data) "token")
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-token-warning-effects")]
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-node-warning-effects")])
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-time")]]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}