🐛 Migrate legacy stroke width token references (#12018)

Stroke width tokens used to be applied to the single `:stroke-width`
token attribute. Applying tokens per side replaced it with the four
`:stroke-width-*` attributes, but no migration moved the existing
references, so files written before 2.19 look unapplied: the token
pill stays off, the Design tab shows a plain number and propagation
finds no update action.

Add migration 0031, which copies the legacy reference to every side,
keeps any side reference already present and drops the old key. It
covers page and component shapes.

Closes #12016

AI-assisted-by: deepseek-v4.1-flash
This commit is contained in:
Luis de Dios 2026-10-07 11:17:40 +02:00 committed by GitHub
parent 3f6b7a7959
commit f962cf487c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 125 additions and 1 deletions

View File

@ -35,6 +35,7 @@
[app.common.types.shape.shadow :as ctss]
[app.common.types.shape.text :as ctst]
[app.common.types.text :as types.text]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
@ -2151,6 +2152,33 @@
(update :pages-index d/update-vals repair-container)
(d/update-when :components d/update-vals repair-container))))
(defmethod migrate-data "0031-migrate-stroke-width-token-attr"
;; Stroke width tokens used to be applied to the single `:stroke-width` token
;; attribute, replaced by the four per-side attributes. The editor reads only
;; the new keys, so a file written before the change keeps the token on
;; `:stroke-width` and the token looks unapplied even though its value is
;; still set. Copy the legacy reference to every side (matching how applying
;; the token resolves all sides) and drop the old key. A side that already
;; carries a reference wins over the recycled global one.
[data _]
(letfn [(repair-shape [shape]
(let [applied (:applied-tokens shape)
token (:stroke-width applied)]
(if (some? token)
(update shape :applied-tokens
(fn [applied]
(-> (zipmap ctt/per-side-stroke-width-keys (repeat token))
(merge applied)
(dissoc :stroke-width))))
shape)))
(repair-container [container]
(d/update-when container :objects d/update-vals repair-shape))]
(-> data
(update :pages-index d/update-vals repair-container)
(d/update-when :components d/update-vals repair-container))))
(def available-migrations
(into (d/ordered-set)
["legacy-2"
@ -2238,4 +2266,5 @@
"0027-separate-tokens-status"
"0028-normalize-constrained-values"
"0029-move-background-blur-out-of-blur"
"0030-remove-stroke-per-side-attr"]))
"0030-remove-stroke-per-side-attr"
"0031-migrate-stroke-width-token-attr"]))

View File

@ -8,8 +8,10 @@
(:require
[app.common.data :as d]
[app.common.files.migrations :as cfm]
[app.common.files.tokens :as cfo]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[app.common.types.token :as ctt]
[app.common.uuid :as uuid]
[clojure.test :as t]))
@ -459,3 +461,96 @@
(t/is (nil? (:stroke-per-side stroke'))
"obsolete attr removed before schema validation")
(t/is (= 5 (:stroke-width stroke')) "stroke width preserved")))
(t/deftest migration-0031-legacy-applied-token-is-not-recognized-before-migration
;; Reproduces #12016: the editor reads only the per-side token attributes, so
;; a shape carrying the old global `:stroke-width` reference looks unapplied.
(let [shape (-> (cts/setup-shape {:id (uuid/next) :type :rect})
(assoc :applied-tokens {:stroke-width "30"}))]
(t/is (not (cfo/shapes-token-applied? {:name "30"} [shape]
ctt/stroke-width-keys))
"a legacy applied token is invisible to the token lookup")))
(t/deftest migration-0031-recycles-global-stroke-width-token
(let [migration-id "0031-migrate-stroke-width-token-attr"
page-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width "30"
:fill "red"}))
data {:pages-index {page-id {:objects {shape-id shape}}}}
shape' (get-in (cfm/migrate-data data migration-id)
[:pages-index page-id :objects shape-id])
applied' (:applied-tokens shape')]
(t/is (nil? (:stroke-width applied')) "legacy key removed")
(t/is (= "30" (:stroke-width-top applied')))
(t/is (= "30" (:stroke-width-right applied')))
(t/is (= "30" (:stroke-width-bottom applied')))
(t/is (= "30" (:stroke-width-left applied')))
(t/is (= "red" (:fill applied')) "unrelated tokens preserved")
(t/is (cfo/shapes-token-applied? {:name "30"} [shape']
ctt/stroke-width-keys)
"the token is recognized after migration")))
(t/deftest migration-0031-keeps-side-token-that-was-already-set
(let [migration-id "0031-migrate-stroke-width-token-attr"
page-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width "30"
:stroke-width-top "10"}))
data {:pages-index {page-id {:objects {shape-id shape}}}}
applied' (get-in (cfm/migrate-data data migration-id)
[:pages-index page-id :objects shape-id :applied-tokens])]
(t/is (= "10" (:stroke-width-top applied'))
"an explicit side reference is not overwritten")
(t/is (= "30" (:stroke-width-right applied')))))
(t/deftest migration-0031-repairs-component-shapes
(let [migration-id "0031-migrate-stroke-width-token-attr"
component-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width "30"}))
data {:components {component-id {:objects {shape-id shape}}}}
applied' (get-in (cfm/migrate-data data migration-id)
[:components component-id :objects shape-id :applied-tokens])]
(t/is (nil? (:stroke-width applied')) "legacy key removed")
(t/is (= "30" (:stroke-width-left applied'))
"component side reference created")))
(t/deftest migration-0031-leaves-shapes-without-the-legacy-token
(let [migration-id "0031-migrate-stroke-width-token-attr"
page-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width-top "30"
:stroke-width-right "30"
:stroke-width-bottom "30"
:stroke-width-left "30"}))
data {:pages-index {page-id {:objects {shape-id shape}}}}]
(t/is (= data (cfm/migrate-data data migration-id))
"a shape without the legacy token is left untouched")))
(t/deftest migration-0031-runs-through-file-migration
(let [migration-id "0031-migrate-stroke-width-token-attr"
shape-id (uuid/next)
file (ctf/make-file {:name "Legacy stroke width token"})
page-id (first (get-in file [:data :pages]))
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width "30"}))
file (-> file
(assoc :migrations (disj cfm/available-migrations migration-id))
(assoc-in [:data :pages-index page-id :objects shape-id] shape))
file' (cfm/migrate-file file {})
applied' (get-in file' [:data :pages-index page-id :objects shape-id :applied-tokens])]
(t/is (cfm/need-migration? file) "new migration detected")
(t/is (not (cfm/need-migration? file')) "new migration recorded")
(t/is (contains? (:migrations file') migration-id) "migration id persisted")
(t/is (= "30" (:stroke-width-top applied'))
"legacy token applied to the sides")))