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")))