🐛 Keep per-side token attributes when remapping a token (#12091)

Renaming a token with "Remap tokens" grouped the references by container
and wrote the union of their attributes onto every shape in it. A shape
with a stroke width token on one side got the token on all four sides
when another shape in the same container had it on every side.

Group the references by shape, so each shape is only renamed on the
attributes it already had the token on.

Closes #12075

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Eva Marco authored and GitHub committed 2026-10-06 13:49:16 +02:00
1 parent 582f943228
commit ff74c38bbf
2 files changed
+44 -7

No files matched your search

@@ -115,11 +115,14 @@
shape-changes
(reduce-kv
(fn [changes container refs]
(let [shape-ids (map :shape-id refs)
(let [;; Each shape keeps only the attributes it already had the
;; token on, so the rename never applies the token to new
;; attributes (e.g. a stroke-width token on a single side).
attributes-by-shape (update-vals (group-by :shape-id refs)
#(into #{} (map :attribute) %))
;; Find the correct token to apply (new or alias)
token (or (some #(when (= (:name (:token %)) new-token-name) %) tokens-with-sets)
(some #(when (= (:name (:token %)) old-token-name) %) tokens-with-sets))
attributes (set (map :attribute refs))]
token (or (some #(when (= (:name (:token %)) new-token-name) %) tokens-with-sets)
(some #(when (= (:name (:token %)) old-token-name) %) tokens-with-sets))]
(if token
;; Create a new independent changes so we can call `with-file-data` after `with-container`
;; otherwise it causes probelms looking up for objects
@@ -127,10 +130,11 @@
(-> (pcb/empty-changes)
(pcb/with-container container)
(pcb/with-file-data file-data)
(pcb/update-shapes shape-ids
(pcb/update-shapes (keys attributes-by-shape)
(fn [shape]
(update shape :applied-tokens
#(merge % (cft/attributes-map attributes (:token token)))))
(let [attributes (get attributes-by-shape (:id shape))]
(update shape :applied-tokens
#(merge % (cft/attributes-map attributes (:token token))))))
{:ignore-touched true}))]
(pcb/concat-changes changes container-changes))
changes)))
@@ -120,6 +120,39 @@
(let [result (dwtr/validate-token-remapping "color.primary" "brand.primary")]
(t/is (true? (:valid? result)))))))
(t/deftest test-remap-keeps-applied-attributes-per-shape
(t/testing "renaming a token only renames the attributes each shape already had"
(let [stroke-token {:id (cthi/new-id! :sw-10)
:name "sw-10"
:value "10"
:type :stroke-width}
file (-> (ctht/sample-file-with-tokens
:lib-fn #(-> %
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a)
:name "Set A"))
(ctob/add-token (cthi/id :set-a)
(ctob/make-token stroke-token))))
(ctho/add-rect :top-rect)
(ctho/add-rect :all-rect)
(ctht/apply-token-to-shape :top-rect "sw-10"
[:stroke-width-top] [] nil)
(ctht/apply-token-to-shape :all-rect "sw-10"
cto/stroke-width-keys [] nil)
;; The production flow renames the token first,
;; then remaps the shapes that use it.
(update-in [:data :tokens-lib]
ctob/update-token (cthi/id :set-a) (cthi/id :sw-10)
#(assoc % :name "sw-big")))
changes (dwtr/build-remap-changes (:data file) "sw-10" "sw-big")
file' (cthf/apply-changes file changes)
top-rect' (cths/get-shape file' :top-rect)
all-rect' (cths/get-shape file' :all-rect)]
(t/is (= {:stroke-width-top "sw-big"}
(:applied-tokens top-rect')))
(t/is (= (zipmap cto/stroke-width-keys (repeat "sw-big"))
(:applied-tokens all-rect'))))))
(defn- setup-file-with-component-copy-and-token
"Create a file containing a component, an instance copy of it, and a single
`color.primary` token already applied to both the main and the copy shape.