🐛 Fix token unapply for text shapes

This commit is contained in:
Andrés Moya 2025-06-18 09:38:50 +02:00 committed by Andrés Moya
parent 43ba30c694
commit cf78e68787
8 changed files with 107 additions and 26 deletions

View File

@ -57,6 +57,7 @@ on-premises instances** that want to keep up to date.
- Fix button width [Taiga #11394](https://tree.taiga.io/project/penpot/issue/11394) - Fix button width [Taiga #11394](https://tree.taiga.io/project/penpot/issue/11394)
- Fix mixed letter spacing and line height [Taiga #11178](https://tree.taiga.io/project/penpot/issue/11178) - Fix mixed letter spacing and line height [Taiga #11178](https://tree.taiga.io/project/penpot/issue/11178)
- Fix snap nodes shortcut [Taiga #11054](https://tree.taiga.io/project/penpot/issue/11054) - Fix snap nodes shortcut [Taiga #11054](https://tree.taiga.io/project/penpot/issue/11054)
- Fix changing a text property in a text layer does not unapply the previously applied token in the same property [Taiga #11337}(https://tree.taiga.io/project/penpot/issue/11337)
## 2.7.2 ## 2.7.2

View File

@ -161,7 +161,6 @@
(contains? (meta changes) ::file-data) (contains? (meta changes) ::file-data)
"Call (with-file-data) before using this function")) "Call (with-file-data) before using this function"))
(defn- lookup-objects (defn- lookup-objects
[changes] [changes]
(let [data (::file-data (meta changes))] (let [data (::file-data (meta changes))]

View File

@ -16,19 +16,30 @@
[app.common.types.pages-list :as ctpl] [app.common.types.pages-list :as ctpl]
[app.common.types.shape.interactions :as ctsi] [app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
[app.common.types.text :as ctt]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.uuid :as uuid])) [app.common.uuid :as uuid]
[clojure.set :as set]))
(defn- generate-unapply-tokens (defn- generate-unapply-tokens
"When updating attributes that have a token applied, we must unapply it, because the value "When updating attributes that have a token applied, we must unapply it, because the value
of the attribute now has been given directly, and does not come from the token." of the attribute now has been given directly, and does not come from the token."
[changes objects changed-sub-attr] [changes objects changed-sub-attr]
(let [mod-obj-changes (->> (:redo-changes changes) (let [new-objects (pcb/get-objects changes)
mod-obj-changes (->> (:redo-changes changes)
(filter #(= (:type %) :mod-obj))) (filter #(= (:type %) :mod-obj)))
text-changed-attrs
(fn [shape]
(let [new-shape (get new-objects (:id shape))
attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))]
(apply set/union (map cto/shape-attr->token-attrs attrs))))
check-attr (fn [shape changes attr] check-attr (fn [shape changes attr]
(let [tokens (get shape :applied-tokens {}) (let [tokens (get shape :applied-tokens {})
token-attrs (cto/shape-attr->token-attrs attr changed-sub-attr)] token-attrs (if (or (not= (:type shape) :text) (not= attr :content))
(cto/shape-attr->token-attrs attr changed-sub-attr)
(text-changed-attrs shape))]
(if (some #(contains? tokens %) token-attrs) (if (some #(contains? tokens %) token-attrs)
(pcb/update-shapes changes [(:id shape)] #(cto/unapply-token-id % token-attrs)) (pcb/update-shapes changes [(:id shape)] #(cto/unapply-token-id % token-attrs))
changes))) changes)))

View File

@ -37,8 +37,6 @@
(merge shape (merge shape
text-params)))) text-params))))
(defn add-frame (defn add-frame
[file frame-label & {:keys [] :as params}] [file frame-label & {:keys [] :as params}]
;; Generated shape tree: ;; Generated shape tree:

View File

@ -11,6 +11,7 @@
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.test-helpers.files :as thf] [app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.ids-map :as thi]
[app.common.text :as txt]
[app.common.types.color :as ctc] [app.common.types.color :as ctc]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.pages-list :as ctpl] [app.common.types.pages-list :as ctpl]
@ -81,6 +82,21 @@
(:id page) (:id page)
#(ctst/set-shape % (ctn/set-shape-attr shape attr val))))))) #(ctst/set-shape % (ctn/set-shape-attr shape attr val)))))))
(defn update-shape-text
[file shape-label attr val & {:keys [page-label]}]
(let [page (if page-label
(thf/get-page file page-label)
(thf/current-page file))
shape (ctst/get-shape page (thi/id shape-label))]
(update file :data
(fn [file-data]
(ctpl/update-page file-data
(:id page)
#(ctst/set-shape % (txt/update-text-content shape
txt/is-content-node?
d/txt-merge
{attr val})))))))
(defn sample-library-color (defn sample-library-color
[label & {:keys [name path color opacity gradient image]}] [label & {:keys [name path color opacity gradient image]}]
(-> {:id (thi/new-id! label) (-> {:id (thi/new-id! label)

View File

@ -11,9 +11,12 @@
(defn- compare-text-content (defn- compare-text-content
"Given two content text structures, conformed by maps and vectors, "Given two content text structures, conformed by maps and vectors,
compare them, and returns a set with the type of differences. compare them, and returns a set with the differences info.
The possibilities are :text-content-text :text-content-attribute and :text-content-structure." If the structures are equal, it returns an empty set. If the structure
[a b] has changed, it returns :text-content-structure. There are two
callbacks to specify what to return when there is a text change with
the same structure, and when attributes change."
[a b {:keys [text-cb attribute-cb] :as callbacks}]
(cond (cond
;; If a and b are equal, there is no diff ;; If a and b are equal, there is no diff
(= a b) (= a b)
@ -38,18 +41,18 @@
#{:text-content-structure} #{:text-content-structure}
(into acc (into acc
(apply set/union (apply set/union
(map #(compare-text-content %1 %2) v1 v2)))) (map #(compare-text-content %1 %2 callbacks) v1 v2))))
;; If the key is :text, and they are different, it is a text differece ;; If the key is :text, and they are different, it is a text differece
(= k :text) (= k :text)
(if (not= v1 v2) (if (not= v1 v2)
(conj acc :text-content-text) (text-cb acc)
acc) acc)
:else :else
;; If the key is not :text, and they are different, it is an attribute differece ;; If the key is not :text, and they are different, it is an attribute differece
(if (not= v1 v2) (if (not= v1 v2)
(conj acc :text-content-attribute) (attribute-cb acc k)
acc)))) acc))))
#{} #{}
keys)) keys))
@ -57,7 +60,6 @@
:else :else
#{:text-content-structure})) #{:text-content-structure}))
(defn equal-attrs? (defn equal-attrs?
"Given a text structure, and a map of attrs, check that all the internal attrs in "Given a text structure, and a map of attrs, check that all the internal attrs in
paragraphs and sentences have the same attrs" paragraphs and sentences have the same attrs"
@ -79,10 +81,15 @@
(defn get-diff-type (defn get-diff-type
"Given two content text structures, conformed by maps and vectors, "Given two content text structures, conformed by maps and vectors,
compare them, and returns a set with the type of differences. compare them, and returns a set with the type of differences.
The possibilities are :text-content-text :text-content-attribute, The possibilities are
:text-content-structure and :text-content-structure-same-attrs." :text-content-text
:text-content-attribute,
:text-content-structure
:text-content-structure-same-attrs."
[a b] [a b]
(let [diff-type (compare-text-content a b)] (let [diff-type (compare-text-content a b
{:text-cb (fn [acc] (conj acc :text-content-text))
:attribute-cb (fn [acc _] (conj acc :text-content-attribute))})]
(if-not (contains? diff-type :text-content-structure) (if-not (contains? diff-type :text-content-structure)
diff-type diff-type
(let [;; get attrs of the first paragraph of the first paragraph-set (let [;; get attrs of the first paragraph of the first paragraph-set
@ -92,6 +99,24 @@
#{:text-content-structure :text-content-structure-same-attrs} #{:text-content-structure :text-content-structure-same-attrs}
diff-type))))) diff-type)))))
(defn get-diff-attrs
"Given two content text structures, conformed by maps and vectors,
compare them, and returns a set with the attributes that have changed.
This is independent of the text structure, so if the structure changes
but the attributes are the same, it will return an empty set."
[a b]
(let [diff-attrs (compare-text-content a b
{:text-cb identity
:attribute-cb (fn [acc attr] (conj acc attr))})]
(if-not (contains? diff-attrs :text-content-structure)
diff-attrs
(let [;; get attrs of the first paragraph of the first paragraph-set
attrs (get-first-paragraph-text-attrs a)]
(if (and (equal-attrs? a attrs)
(equal-attrs? b attrs))
#{}
(disj diff-attrs :text-content-structure))))))
;; TODO We know that there are cases that the blocks of texts are separated ;; TODO We know that there are cases that the blocks of texts are separated
;; differently: ["one" " " "two"], ["one " "two"], ["one" " two"] ;; differently: ["one" " " "two"], ["one " "two"], ["one" " two"]
;; so this won't work for 100% of the situations. But it's good enough for now, ;; so this won't work for 100% of the situations. But it's good enough for now,
@ -116,7 +141,6 @@
:else :else
true)) true))
(defn copy-text-keys (defn copy-text-keys
"Given two equal content text structures, deep copy all the keys :text "Given two equal content text structures, deep copy all the keys :text
from origin to destiny" from origin to destiny"

View File

@ -6,6 +6,7 @@
(ns common-tests.logic.token-apply-test (ns common-tests.logic.token-apply-test
(:require (:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb] [app.common.files.changes-builder :as pcb]
[app.common.logic.shapes :as cls] [app.common.logic.shapes :as cls]
[app.common.test-helpers.compositions :as tho] [app.common.test-helpers.compositions :as tho]
@ -13,6 +14,7 @@
[app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths] [app.common.test-helpers.shapes :as ths]
[app.common.test-helpers.tokens :as tht] [app.common.test-helpers.tokens :as tht]
[app.common.text :as txt]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.token :as cto] [app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
@ -53,7 +55,8 @@
(ctob/make-token :name "token-dimensions" (ctob/make-token :name "token-dimensions"
:type :dimensions :type :dimensions
:value 100)))) :value 100))))
(tho/add-frame :frame1))) (tho/add-frame :frame1)
(tho/add-text :text1 "Hello World")))
(defn- apply-all-tokens (defn- apply-all-tokens
[file] [file]
@ -64,7 +67,8 @@
(tht/apply-token-to-shape :frame1 "token-stroke-width" [:stroke-width] [:stroke-width] 2) (tht/apply-token-to-shape :frame1 "token-stroke-width" [:stroke-width] [:stroke-width] 2)
(tht/apply-token-to-shape :frame1 "token-color" [:stroke-color] [:stroke-color] "#00ff00") (tht/apply-token-to-shape :frame1 "token-color" [:stroke-color] [:stroke-color] "#00ff00")
(tht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00") (tht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00")
(tht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100))) (tht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100)
(tht/apply-token-to-shape :text1 "token-color" [:fill] [:fill] "#00ff00")))
(t/deftest apply-tokens-to-shape (t/deftest apply-tokens-to-shape
(let [;; ==== Setup (let [;; ==== Setup
@ -171,6 +175,7 @@
(apply-all-tokens)) (apply-all-tokens))
page (thf/current-page file) page (thf/current-page file)
frame1 (ths/get-shape file :frame1) frame1 (ths/get-shape file :frame1)
text1 (ths/get-shape file :text1)
;; ==== Action ;; ==== Action
changes (-> (-> (pcb/empty-changes nil) changes (-> (-> (pcb/empty-changes nil)
@ -190,13 +195,25 @@
(ctn/set-shape-attr :width 0) (ctn/set-shape-attr :width 0)
(ctn/set-shape-attr :height 0))) (ctn/set-shape-attr :height 0)))
(:objects page) (:objects page)
{})
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(txt/update-text-content
shape
txt/is-content-node?
d/txt-merge
{:fills (ths/sample-fills-color :fill-color "#fabada")}))
(:objects page)
{})) {}))
file' (thf/apply-changes file changes) file' (thf/apply-changes file changes)
;; ==== Get ;; ==== Get
frame1' (ths/get-shape file' :frame1) frame1' (ths/get-shape file' :frame1)
applied-tokens' (:applied-tokens frame1')] text1' (ths/get-shape file' :text1)
applied-tokens-frame' (:applied-tokens frame1')
applied-tokens-text' (:applied-tokens text1')]
;; ==== Check ;; ==== Check
(t/is (= (count applied-tokens') 0)))) (t/is (= (count applied-tokens-frame') 0))
(t/is (= (count applied-tokens-text') 0))))

View File

@ -29,11 +29,12 @@
#(conj % line))) #(conj % line)))
(t/deftest test-get-diff-type (t/deftest test-get-diff-type
(let [diff-text (cttx/get-diff-type content-base content-changed-text) (let [diff-text (cttx/get-diff-type content-base content-changed-text)
diff-attr (cttx/get-diff-type content-base content-changed-attr) diff-attr (cttx/get-diff-type content-base content-changed-attr)
diff-both (cttx/get-diff-type content-base content-changed-both) diff-both (cttx/get-diff-type content-base content-changed-both)
diff-structure (cttx/get-diff-type content-base content-changed-structure) diff-structure (cttx/get-diff-type content-base content-changed-structure)
diff-structure-same-attrs (cttx/get-diff-type content-base content-changed-structure-same-attrs)] diff-structure-same-attrs (cttx/get-diff-type content-base content-changed-structure-same-attrs)]
(t/is (= #{:text-content-text} diff-text)) (t/is (= #{:text-content-text} diff-text))
(t/is (= #{:text-content-attribute} diff-attr)) (t/is (= #{:text-content-attribute} diff-attr))
(t/is (= #{:text-content-text :text-content-attribute} diff-both)) (t/is (= #{:text-content-text :text-content-attribute} diff-both))
@ -41,6 +42,20 @@
(t/is (= #{:text-content-structure :text-content-structure-same-attrs} diff-structure-same-attrs)))) (t/is (= #{:text-content-structure :text-content-structure-same-attrs} diff-structure-same-attrs))))
(t/deftest test-get-diff-attrs
(let [attrs-text (cttx/get-diff-attrs content-base content-changed-text)
attrs-attr (cttx/get-diff-attrs content-base content-changed-attr)
attrs-both (cttx/get-diff-attrs content-base content-changed-both)
attrs-structure (cttx/get-diff-attrs content-base content-changed-structure)
attrs-structure-same-attrs (cttx/get-diff-attrs content-base content-changed-structure-same-attrs)]
(t/is (= #{} attrs-text))
(t/is (= #{:font-size} attrs-attr))
(t/is (= #{:font-size} attrs-both))
(t/is (= #{} attrs-structure))
(t/is (= #{} attrs-structure-same-attrs))))
(t/deftest test-equal-structure (t/deftest test-equal-structure
(t/is (true? (cttx/equal-structure? content-base content-changed-text))) (t/is (true? (cttx/equal-structure? content-base content-changed-text)))
(t/is (true? (cttx/equal-structure? content-base content-changed-attr))) (t/is (true? (cttx/equal-structure? content-base content-changed-attr)))