From f962cf487c2bd41b19594ca7bcd0d21d5b447b0b Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Wed, 7 Oct 2026 11:17:40 +0200 Subject: [PATCH] :bug: 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 --- common/src/app/common/files/migrations.cljc | 31 +++++- .../common_tests/files_migrations_test.cljc | 95 +++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index 649f980021..c0e97bc3d7 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -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"])) diff --git a/common/test/common_tests/files_migrations_test.cljc b/common/test/common_tests/files_migrations_test.cljc index 7e92deda66..02c4a441da 100644 --- a/common/test/common_tests/files_migrations_test.cljc +++ b/common/test/common_tests/files_migrations_test.cljc @@ -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")))