From 01896501c14868b0ab19c7010d2799fa7df5b376 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Thu, 24 Jul 2025 09:37:38 +0200 Subject: [PATCH 01/17] :bug: Remove image type from inspect tab panels (#6959) --- frontend/src/app/main/ui/inspect/attributes.cljs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/inspect/attributes.cljs b/frontend/src/app/main/ui/inspect/attributes.cljs index 661b808f0a..1e86b08eaa 100644 --- a/frontend/src/app/main/ui/inspect/attributes.cljs +++ b/frontend/src/app/main/ui/inspect/attributes.cljs @@ -27,13 +27,12 @@ [rumext.v2 :as mf])) (def type->options - {:multiple [:fill :stroke :image :text :shadow :blur :layout-element] + {:multiple [:fill :stroke :text :shadow :blur :layout-element] :frame [:visibility :geometry :fill :stroke :shadow :blur :layout :layout-element] :group [:visibility :geometry :svg :layout-element] :rect [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element] :circle [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element] :path [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element] - :image [:visibility :image :geometry :fill :stroke :shadow :blur :svg :layout-element] :text [:visibility :geometry :text :shadow :blur :stroke :layout-element] :variant [:variant :geometry :fill :stroke :shadow :blur :layout :layout-element]}) From d08c94d5a6053f61d626963d11647eb9f5d0adb2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 8 Jul 2025 15:27:23 +0200 Subject: [PATCH 02/17] :sparkles: Change default status filtering for logical deletion --- backend/src/app/features/logical_deletion.clj | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/src/app/features/logical_deletion.clj b/backend/src/app/features/logical_deletion.clj index 8a06f3f30f..a8407cdf62 100644 --- a/backend/src/app/features/logical_deletion.clj +++ b/backend/src/app/features/logical_deletion.clj @@ -10,18 +10,19 @@ [app.config :as cf] [app.util.time :as dt])) +(def ^:private canceled-status + #{"canceled" "unpaid"}) + (defn get-deletion-delay "Calculate the next deleted-at for a resource (file, team, etc) in function of team settings" [team] - (if-let [subscription (get team :subscription)] + (if-let [{:keys [type status]} (get team :subscription)] (cond - (and (= (:type subscription) "unlimited") - (= (:status subscription) "active")) + (and (= "unlimited" type) (not (contains? canceled-status status))) (dt/duration {:days 30}) - (and (= (:type subscription) "enterprise") - (= (:status subscription) "active")) + (and (= "enterprise" type) (not (contains? canceled-status status))) (dt/duration {:days 90}) :else From 1f15e9b81e5a87b9591e6e2a681b2882ffaefe7b Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Wed, 23 Jul 2025 14:52:39 +0200 Subject: [PATCH 03/17] :sparkles: Fix spacing token for frame children --- common/src/app/common/types/token.cljc | 19 +- .../data/workspace/tokens/application.cljs | 199 ++++++++++-------- .../main/ui/workspace/tokens/management.cljs | 7 + .../tokens/management/context_menu.cljs | 44 ++-- .../ui/workspace/tokens/management/group.cljs | 3 +- .../tokens/management/token_pill.cljs | 16 +- 6 files changed, 182 insertions(+), 106 deletions(-) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 9ef1571e53..afd7279808 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -92,19 +92,32 @@ (def opacity-keys (schema-keys schema:opacity)) -(def ^:private schema:spacing +(def ^:private schema:spacing-gap [:map [:row-gap {:optional true} token-name-ref] - [:column-gap {:optional true} token-name-ref] + [:column-gap {:optional true} token-name-ref]]) + +(def ^:private schema:spacing-padding + [:map [:p1 {:optional true} token-name-ref] [:p2 {:optional true} token-name-ref] [:p3 {:optional true} token-name-ref] - [:p4 {:optional true} token-name-ref] + [:p4 {:optional true} token-name-ref]]) + +(def ^:private schema:spacing-margin + [:map [:m1 {:optional true} token-name-ref] [:m2 {:optional true} token-name-ref] [:m3 {:optional true} token-name-ref] [:m4 {:optional true} token-name-ref]]) +(def ^:private schema:spacing + (reduce mu/union [schema:spacing-gap + schema:spacing-padding + schema:spacing-margin])) + +(def spacing-margin-keys (schema-keys schema:spacing-margin)) + (def spacing-keys (schema-keys schema:spacing)) (def ^:private schema:dimensions diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 0ae82fd980..b457963c01 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -31,88 +31,6 @@ (declare token-properties) -;; Events to apply / unapply tokens to shapes ------------------------------------------------------------ - -(defn apply-token - "Apply `attributes` that match `token` for `shape-ids`. - - Optionally remove attributes from `attributes-to-remove`, - this is useful for applying a single attribute from an attributes set - while removing other applied tokens from this set." - [{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}] - (ptk/reify ::apply-token - ptk/WatchEvent - (watch [_ state _] - ;; We do not allow to apply tokens while text editor is open. - (when (empty? (get state :workspace-editor-state)) - (when-let [tokens (some-> (dsh/lookup-file-data state) - (get :tokens-lib) - (ctob/get-tokens-in-active-sets))] - (->> (sd/resolve-tokens tokens) - (rx/mapcat - (fn [resolved-tokens] - (let [undo-id (js/Symbol) - objects (dsh/lookup-page-objects state) - - shape-ids (or (->> (select-keys objects shape-ids) - (filter (fn [[_ shape]] - (ctt/any-appliable-attr? attributes (:type shape)))) - (keys)) - []) - - resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value]) - tokenized-attributes (cft/attributes-map attributes token)] - (rx/of - (st/emit! (ptk/event ::ev/event {::ev/name "apply-tokens"})) - (dwu/start-undo-transaction undo-id) - (dwsh/update-shapes shape-ids (fn [shape] - (cond-> shape - attributes-to-remove - (update :applied-tokens #(apply (partial dissoc %) attributes-to-remove)) - :always - (update :applied-tokens merge tokenized-attributes)))) - (when on-update-shape - (on-update-shape resolved-value shape-ids attributes)) - (dwu/commit-undo-transaction undo-id))))))))))) - -(defn unapply-token - "Removes `attributes` that match `token` for `shape-ids`. - - Doesn't update shape attributes." - [{:keys [attributes token shape-ids] :as _props}] - (ptk/reify ::unapply-token - ptk/WatchEvent - (watch [_ _ _] - (rx/of - (let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))] - (dwsh/update-shapes - shape-ids - (fn [shape] - (update shape :applied-tokens remove-token)))))))) - -(defn toggle-token - [{:keys [token shapes]}] - (ptk/reify ::on-toggle-token - ptk/WatchEvent - (watch [_ _ _] - (let [{:keys [attributes all-attributes on-update-shape]} - (get token-properties (:type token)) - - unapply-tokens? - (cft/shapes-token-applied? token shapes (or all-attributes attributes)) - - shape-ids (map :id shapes)] - (if unapply-tokens? - (rx/of - (unapply-token {:attributes (or all-attributes attributes) - :token token - :shape-ids shape-ids})) - (rx/of - (apply-token {:attributes attributes - :token token - :shape-ids shape-ids - :on-update-shape on-update-shape}))))))) - ;; Events to update the value of attributes with applied tokens --------------------------------------------------------- ;; (note that dwsh/update-shapes function returns an event) @@ -380,6 +298,123 @@ {:ignore-touched true :page-id page-id}))))) +;; Events to apply / unapply tokens to shapes ------------------------------------------------------------ + +(defn apply-token + "Apply `attributes` that match `token` for `shape-ids`. + + Optionally remove attributes from `attributes-to-remove`, + this is useful for applying a single attribute from an attributes set + while removing other applied tokens from this set." + [{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}] + (ptk/reify ::apply-token + ptk/WatchEvent + (watch [_ state _] + ;; We do not allow to apply tokens while text editor is open. + (when (empty? (get state :workspace-editor-state)) + (when-let [tokens (some-> (dsh/lookup-file-data state) + (get :tokens-lib) + (ctob/get-tokens-in-active-sets))] + (->> (sd/resolve-tokens tokens) + (rx/mapcat + (fn [resolved-tokens] + (let [undo-id (js/Symbol) + objects (dsh/lookup-page-objects state) + selected-shapes (select-keys objects shape-ids) + + shape-ids (or (->> selected-shapes + (filter (fn [[_ shape]] + (or + (and (ctsl/any-layout-immediate-child? objects shape) + (some ctt/spacing-margin-keys attributes)) + (ctt/any-appliable-attr? attributes (:type shape))))) + (keys)) + []) + + resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value]) + tokenized-attributes (cft/attributes-map attributes token)] + (rx/of + (st/emit! (ptk/event ::ev/event {::ev/name "apply-tokens"})) + (dwu/start-undo-transaction undo-id) + (dwsh/update-shapes shape-ids (fn [shape] + (cond-> shape + attributes-to-remove + (update :applied-tokens #(apply (partial dissoc %) attributes-to-remove)) + :always + (update :applied-tokens merge tokenized-attributes)))) + (when on-update-shape + (on-update-shape resolved-value shape-ids attributes)) + (dwu/commit-undo-transaction undo-id))))))))))) + +(defn apply-spacing-token + "Handles edge-case for spacing token when applying token via toggle button. + Splits out `shape-ids` into seperate default actions: + - Layouts take the `default` update function + - Shapes inside layout will only take margin" + [{:keys [token shapes]}] + (ptk/reify ::apply-spacing-token + ptk/WatchEvent + (watch [_ state _] + (let [objects (dsh/lookup-page-objects state) + + {:keys [attributes on-update-shape]} + (get token-properties (:type token)) + + {:keys [other frame-children]} + (group-by #(if (ctsl/any-layout-immediate-child? objects %) :frame-children :other) shapes)] + + (rx/of + (apply-token {:attributes attributes + :token token + :shape-ids (map :id other) + :on-update-shape on-update-shape}) + (apply-token {:attributes ctt/spacing-margin-keys + :token token + :shape-ids (map :id frame-children) + :on-update-shape update-layout-item-margin})))))) + +(defn unapply-token + "Removes `attributes` that match `token` for `shape-ids`. + + Doesn't update shape attributes." + [{:keys [attributes token shape-ids] :as _props}] + (ptk/reify ::unapply-token + ptk/WatchEvent + (watch [_ _ _] + (rx/of + (let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))] + (dwsh/update-shapes + shape-ids + (fn [shape] + (update shape :applied-tokens remove-token)))))))) + +(defn toggle-token + [{:keys [token shapes]}] + (ptk/reify ::on-toggle-token + ptk/WatchEvent + (watch [_ _ _] + (let [{:keys [attributes all-attributes on-update-shape]} + (get token-properties (:type token)) + + unapply-tokens? + (cft/shapes-token-applied? token shapes (or all-attributes attributes)) + + shape-ids (map :id shapes)] + (if unapply-tokens? + (rx/of + (unapply-token {:attributes (or all-attributes attributes) + :token token + :shape-ids shape-ids})) + (rx/of + (case (:type token) + :spacing + (apply-spacing-token {:token token + :shapes shapes}) + (apply-token {:attributes attributes + :token token + :shape-ids shape-ids + :on-update-shape on-update-shape})))))))) + ;; Map token types to different properties used along the cokde --------------------------------------------- ;; FIXME: the values should be lazy evaluated, probably a function, diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index bfc533364b..19d606c955 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -2,6 +2,7 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.types.shape.layout :as ctsl] [app.common.types.token :as ctt] [app.common.types.tokens-lib :as ctob] [app.config :as cf] @@ -61,6 +62,10 @@ (mf/with-memo [selected objects] (into [] (keep (d/getf objects)) selected)) + is-selected-inside-layout + (mf/with-memo [selected-shapes objects] + (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)) + active-theme-tokens (mf/with-memo [tokens-lib] (if tokens-lib @@ -148,6 +153,7 @@ :is-open (get open-status type false) :type type :selected-shapes selected-shapes + :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens' :tokens tokens}])) @@ -155,5 +161,6 @@ [:> token-group* {:key (name type) :type type :selected-shapes selected-shapes + :is-selected-inside-layout :is-selected-inside-layout :active-theme-tokens active-theme-tokens' :tokens []}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index bc96b1ad76..86a054e336 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.tokens :as cft] + [app.common.types.shape.layout :as ctsl] [app.common.types.token :as ctt] [app.common.types.tokens-lib :as ctob] [app.main.data.modal :as modal] @@ -34,11 +35,13 @@ (some #(contains? m %) ks)) (defn clean-separators - "Cleans up `:separator` inside of `items` - Will clean consecutive items like `[:separator :separator {}]` - And will return nil for lists consisting only of `:separator` items." + "Cleans up `:separator` inside of `items` with these rules: + - Clean consecutive items like `[:separator :separator {}]` + - Returns nil for lists consisting only of `:separator` items. + - Removes `:separator` at the beginning of the `items`" [items] - (let [items' (dedupe items)] + (let [items' (->> (dedupe items) + (drop-while #(= % :separator)))] (when-not (every? #(= % :separator) items') items'))) @@ -190,7 +193,7 @@ -(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes] :as context-data}] +(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes is-selected-inside-layout] :as context-data}] (let [padding-attr-labels {:p1 "Padding top" :p2 "Padding right" :p3 "Padding bottom" @@ -209,7 +212,9 @@ :m2 "Margin right" :m3 "Margin bottom" :m4 "Margin left"} - margin-items (when (key-in-map? allowed-shape-attributes margin-attr-labels) + margin-items (when (or + is-selected-inside-layout + (key-in-map? allowed-shape-attributes margin-attr-labels)) (layout-spacing-items {:token token :selected-shapes selected-shapes :all-attr-labels margin-attr-labels @@ -224,11 +229,13 @@ :hint (tr "workspace.tokens.gaps") :on-update-shape dwta/update-layout-spacing} context-data)] - (concat gap-items - (when padding-items [:separator]) - padding-items - (when margin-items [:separator]) - margin-items))) + (->> (concat + gap-items + [:separator] + padding-items + [:separator] + margin-items) + (clean-separators)))) (defn sizing-attribute-actions [context-data] (->> @@ -446,9 +453,17 @@ (mf/defc token-context-menu-tree [{:keys [width errors] :as mdata}] - (let [objects (mf/deref refs/workspace-page-objects) + (let [objects (mf/deref refs/workspace-page-objects) selected (mf/deref refs/selected-shapes) - selected-shapes (into [] (keep (d/getf objects)) selected) + + selected-shapes + (mf/with-memo [selected objects] + (into [] (keep (d/getf objects)) selected)) + + is-selected-inside-layout + (mf/with-memo [selected-shapes objects] + (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)) + token-name (:token-name mdata) token (mf/deref (refs/workspace-token-in-selected-set token-name)) selected-token-set-name (mf/deref refs/selected-token-set-name)] @@ -457,7 +472,8 @@ :token token :errors errors :selected-token-set-name selected-token-set-name - :selected-shapes selected-shapes}]])) + :selected-shapes selected-shapes + :is-selected-inside-layout is-selected-inside-layout}]])) (mf/defc token-context-menu [] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index c96ed54cdf..2a55e65960 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -42,7 +42,7 @@ (mf/defc token-group* {::mf/private true} - [{:keys [type tokens selected-shapes active-theme-tokens is-open]}] + [{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens is-open]}] (let [{:keys [modal title]} (get dwta/token-properties type) editing-ref (mf/deref refs/workspace-editor-state) @@ -115,6 +115,7 @@ {:key (:name token) :token token :selected-shapes selected-shapes + :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens active-theme-tokens :on-click on-token-pill-click :on-context-menu on-context-menu}])]])]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs index fa4890d9f0..669eb6056d 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.cljs @@ -21,6 +21,7 @@ [app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] + [clojure.set :as set] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -164,17 +165,20 @@ (cft/shapes-applied-all? ids-by-attributes shape-ids attributes))) (defn attributes-match-selection? - [selected-shapes attrs] - (some (fn [shape] - (ctt/any-appliable-attr? attrs (:type shape))) - selected-shapes)) + [selected-shapes attrs & {:keys [selected-inside-layout?]}] + (or + ;; Edge-case for allowing margin attribute on shapes inside layout parent + (and selected-inside-layout? (set/subset? ctt/spacing-margin-keys attrs)) + (some (fn [shape] + (ctt/any-appliable-attr? attrs (:type shape))) + selected-shapes))) (def token-types-with-status-icon #{:color :border-radius :rotation :sizing :dimensions :opacity :spacing :stroke-width}) (mf/defc token-pill* {::mf/wrap [mf/memo]} - [{:keys [on-click token on-context-menu selected-shapes active-theme-tokens]}] + [{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}] (let [{:keys [name value errors type]} token has-selected? (pos? (count selected-shapes)) @@ -201,7 +205,7 @@ has-selected? (not applied?) (not half-applied?) - (not (attributes-match-selection? selected-shapes attributes))) + (not (attributes-match-selection? selected-shapes attributes {:selected-inside-layout? is-selected-inside-layout}))) ;; FIXME: move to context or props can-edit? (:can-edit (deref refs/permissions)) From 8c96a617be5e252b1ad48f8d72257e06a303af20 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Wed, 23 Jul 2025 15:35:52 +0200 Subject: [PATCH 04/17] :sparkles: Add test for spacing token application rules --- .../tokens/management/context_menu.cljs | 13 +++--- .../tokens/logic/token_actions_test.cljs | 45 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index 86a054e336..c0d5a2cc69 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -456,17 +456,20 @@ (let [objects (mf/deref refs/workspace-page-objects) selected (mf/deref refs/selected-shapes) + token-name (:token-name mdata) + token (mf/deref (refs/workspace-token-in-selected-set token-name)) + token-type (:type token) + selected-token-set-name (mf/deref refs/selected-token-set-name) + selected-shapes (mf/with-memo [selected objects] (into [] (keep (d/getf objects)) selected)) is-selected-inside-layout - (mf/with-memo [selected-shapes objects] - (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)) + (mf/with-memo [token-type selected-shapes objects] + (when (= :spacing token-type) + (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)))] - token-name (:token-name mdata) - token (mf/deref (refs/workspace-token-in-selected-set token-name)) - selected-token-set-name (mf/deref refs/selected-token-set-name)] [:ul {:class (stl/css :context-list)} [:& menu-tree {:submenu-offset width :token token diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs index 8c87194ddd..19b66579c8 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -645,6 +645,51 @@ (t/is (= (:r1 (:applied-tokens rect-without-token')) (:name target-token))) (t/is (= (:r1 (:applied-tokens rect-with-other-token-2')) (:name target-token))))))))))) +(t/deftest test-toggle-spacing-token + (t/testing "applies spacing token only to layouts and layout children" + (t/async + done + (let [spacing-token {:name "spacing.md" + :value "16" + :type :spacing} + file (-> (setup-file-with-tokens) + (ctho/add-frame-with-child :frame-layout :rect-in-layout + {:frame-params {:layout :grid}}) + (ctho/add-rect :rect-regular) + (update-in [:data :tokens-lib] + #(ctob/add-token-in-set % "Set A" (ctob/make-token spacing-token)))) + store (ths/setup-store file) + frame-layout (cths/get-shape file :frame-layout) + rect-in-layout (cths/get-shape file :rect-in-layout) + rect-regular (cths/get-shape file :rect-regular) + events [(dwta/toggle-token {:token (toht/get-token file "spacing.md") + :shapes [frame-layout rect-in-layout rect-regular]})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + frame-layout' (cths/get-shape file' :frame-layout) + rect-in-layout' (cths/get-shape file' :rect-in-layout) + rect-regular' (cths/get-shape file' :rect-regular)] + + (t/testing "frame with layout gets all spacing attributes" + (t/is (= "spacing.md" (:column-gap (:applied-tokens frame-layout')))) + (t/is (= "spacing.md" (:row-gap (:applied-tokens frame-layout')))) + (t/is (= 16 (get-in frame-layout' [:layout-gap :column-gap]))) + (t/is (= 16 (get-in frame-layout' [:layout-gap :row-gap])))) + + (t/testing "shape inside layout frame gets only margin attributes" + (t/is (= "spacing.md" (:m1 (:applied-tokens rect-in-layout')))) + (t/is (= "spacing.md" (:m2 (:applied-tokens rect-in-layout')))) + (t/is (= "spacing.md" (:m3 (:applied-tokens rect-in-layout')))) + (t/is (= "spacing.md" (:m4 (:applied-tokens rect-in-layout')))) + (t/is (nil? (:column-gap (:applied-tokens rect-in-layout')))) + (t/is (nil? (:row-gap (:applied-tokens rect-in-layout')))) + (t/is (= {:m1 16, :m2 16, :m3 16, :m4 16} (get rect-in-layout' :layout-item-margin)))) + + (t/testing "regular shape doesn't get spacing attributes" + (t/is (nil? (:applied-tokens rect-regular'))))))))))) + (t/deftest test-detach-styles-color (t/testing "applying a color token to a shape with color styles should detach the styles" (t/async From 019bc2f1832df77e672e34076ef98ebb7e97a504 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 7 Jul 2025 15:56:37 +0200 Subject: [PATCH 05/17] :sparkles: Add migrations handling on file snapshots --- CHANGES.md | 1 + backend/src/app/migrations.clj | 5 +- .../sql/0140-mod-file-change-table.sql | 2 + .../src/app/rpc/commands/files_snapshot.clj | 52 ++++++++++++++----- common/src/app/common/files/migrations.cljc | 2 +- 5 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 backend/src/app/migrations/sql/0140-mod-file-change-table.sql diff --git a/CHANGES.md b/CHANGES.md index e3806a46e4..9106360ef3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -57,6 +57,7 @@ - Fix unexpected exception on processing old texts [Github #6889](https://github.com/penpot/penpot/pull/6889) - Fix UI theme selection from main menu [Taiga #11567](https://tree.taiga.io/project/penpot/issue/11567) +- Add missing migration information to file snapshots [Github #686](https://github.com/penpot/penpot/pull/6864) ## 2.8.0 diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 795a9bea5c..1c51365a9c 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -438,7 +438,10 @@ :fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")} {:name "0139-mod-file-change-table.sql" - :fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}]) + :fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")} + + {:name "0140-mod-file-change-table.sql" + :fn (mg/resource "app/migrations/sql/0140-mod-file-change-table.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0140-mod-file-change-table.sql b/backend/src/app/migrations/sql/0140-mod-file-change-table.sql new file mode 100644 index 0000000000..6189edb140 --- /dev/null +++ b/backend/src/app/migrations/sql/0140-mod-file-change-table.sql @@ -0,0 +1,2 @@ +ALTER TABLE file_change + ADD COLUMN migrations text[]; diff --git a/backend/src/app/rpc/commands/files_snapshot.clj b/backend/src/app/rpc/commands/files_snapshot.clj index 71689560a5..bcfbad9428 100644 --- a/backend/src/app/rpc/commands/files_snapshot.clj +++ b/backend/src/app/rpc/commands/files_snapshot.clj @@ -8,6 +8,7 @@ (:require [app.binfile.common :as bfc] [app.common.exceptions :as ex] + [app.common.files.migrations :as fmg] [app.common.logging :as l] [app.common.schema :as sm] [app.common.uuid :as uuid] @@ -15,6 +16,7 @@ [app.db :as db] [app.db.sql :as-alias sql] [app.features.fdata :as feat.fdata] + [app.features.file-migrations :refer [reset-migrations!]] [app.main :as-alias main] [app.msgbus :as mbus] [app.rpc :as-alias rpc] @@ -27,6 +29,13 @@ [app.util.time :as dt] [cuerdas.core :as str])) +(defn decode-row + [{:keys [migrations] :as row}] + (when row + (cond-> row + (some? migrations) + (assoc :migrations (db/decode-pgarray migrations))))) + (def sql:get-file-snapshots "WITH changes AS ( SELECT id, label, revn, created_at, created_by, profile_id @@ -74,10 +83,7 @@ (assert (#{:system :user :admin} created-by) "expected valid keyword for created-by") - (let [conn - (db/get-connection cfg) - - created-by + (let [created-by (name created-by) deleted-at @@ -101,12 +107,15 @@ (blob/encode (:data file)) features - (db/encode-pgarray (:features file) conn "text")] + (into-array (:features file)) - (l/debug :hint "creating file snapshot" - :file-id (str (:id file)) - :id (str snapshot-id) - :label label) + migrations + (into-array (:migrations file))] + + (l/dbg :hint "creating file snapshot" + :file-id (str (:id file)) + :id (str snapshot-id) + :label label) (db/insert! cfg :file-change {:id snapshot-id @@ -114,6 +123,7 @@ :data data :version (:version file) :features features + :migrations migrations :profile-id profile-id :file-id (:id file) :label label @@ -159,7 +169,17 @@ {:file-id file-id :id snapshot-id} {::db/for-share true}) - (feat.fdata/resolve-file-data cfg))] + (feat.fdata/resolve-file-data cfg) + (decode-row)) + + ;; If snapshot has tracked applied migrations, we reuse them, + ;; if not we take a safest set of migrations as starting + ;; point. This is because, at the time of implementing + ;; snapshots, migrations were not taken into account so we + ;; need to make this backward compatible in some way. + file (assoc file :migrations + (or (:migrations snapshot) + (fmg/generate-migrations-from-version 67)))] (when-not snapshot (ex/raise :type :not-found @@ -180,12 +200,16 @@ :label (:label snapshot) :snapshot-id (str (:id snapshot))) - ;; If the file was already offloaded, on restring the snapshot - ;; we are going to replace the file data, so we need to touch - ;; the old referenced storage object and avoid possible leaks + ;; If the file was already offloaded, on restoring the snapshot we + ;; are going to replace the file data, so we need to touch the old + ;; referenced storage object and avoid possible leaks (when (feat.fdata/offloaded? file) (sto/touch-object! storage (:data-ref-id file))) + ;; In the same way, on reseting the file data, we need to restore + ;; the applied migrations on the moment of taking the snapshot + (reset-migrations! conn file) + (db/update! conn :file {:data (:data snapshot) :revn (inc (:revn file)) @@ -253,7 +277,7 @@ :deleted-at nil} {:id snapshot-id} {::db/return-keys true}) - (dissoc :data :features))) + (dissoc :data :features :migrations))) (defn- get-snapshot "Get a minimal snapshot from database and lock for update" diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index 5973451a94..a603e9b922 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -81,7 +81,7 @@ (update :migrations set/union diff) (vary-meta assoc ::migrated (not-empty diff))))) -(defn- generate-migrations-from-version +(defn generate-migrations-from-version "A function that generates new format migration from the old, version based migration system" [version] From f6b97af148fc816e8d01908ad7e0632158de7ad2 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Thu, 24 Jul 2025 15:09:09 +0200 Subject: [PATCH 06/17] :bug: Fix spacing menu not available in dimensions token --- .../app/main/ui/workspace/tokens/management/context_menu.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index c0d5a2cc69..7fe47addf4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -467,7 +467,7 @@ is-selected-inside-layout (mf/with-memo [token-type selected-shapes objects] - (when (= :spacing token-type) + (when (contains? #{:spacing :dimensions} token-type) (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)))] [:ul {:class (stl/css :context-list)} From 58a843ea2339a9c5cf739701d6a55dd38cd75af4 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Thu, 24 Jul 2025 15:38:32 +0200 Subject: [PATCH 07/17] :sparkles: Remove token when applying tyopgraphic asset style --- common/src/app/common/logic/shapes.cljc | 12 +++- .../data/workspace/tokens/application.cljs | 56 +++++++------------ 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc index 6590c7bed8..1f41a71c54 100644 --- a/common/src/app/common/logic/shapes.cljc +++ b/common/src/app/common/logic/shapes.cljc @@ -11,6 +11,7 @@ [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] [app.common.logic.variant-properties :as clvp] + [app.common.text :as ct] [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.pages-list :as ctpl] @@ -21,9 +22,12 @@ [app.common.uuid :as uuid] [clojure.set :as set])) +(def text-typography-attrs (set ct/text-typography-attrs)) + (defn- generate-unapply-tokens "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. + When applying a typography asset style we also unapply any typographic tokens." [changes objects changed-sub-attr] (let [new-objects (pcb/get-objects changes) mod-obj-changes (->> (:redo-changes changes) @@ -32,7 +36,11 @@ text-changed-attrs (fn [shape] (let [new-shape (get new-objects (:id shape)) - attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))] + attrs (ctt/get-diff-attrs (:content shape) (:content new-shape)) + ;; Unapply token when applying typography asset style + attrs (if (set/intersection text-typography-attrs attrs) + (into attrs cto/typography-keys) + attrs)] (apply set/union (map cto/shape-attr->token-attrs attrs)))) check-attr (fn [shape changes attr] diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index b457963c01..c011cabcc4 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -250,53 +250,37 @@ (dwsl/update-layout-child shape-ids props {:ignore-touched true :page-id page-id})))))))) +(defn generate-text-shape-update + [txt-attrs shape-ids page-id] + (let [update-node? (fn [node] + (or (txt/is-text-node? node) + (txt/is-paragraph-node? node))) + update-fn (fn [node _] + (-> node + (d/txt-merge txt-attrs) + (cty/remove-typography-from-node)))] + (dwsh/update-shapes shape-ids + #(txt/update-text-content % update-node? update-fn nil) + {:ignore-touched true + :page-id page-id}))) + (defn update-line-height ([value shape-ids attributes] (update-line-height value shape-ids attributes nil)) ([value shape-ids _attributes page-id] - (let [update-node? (fn [node] - (or (txt/is-text-node? node) - (txt/is-paragraph-node? node))) - update-fn (fn [node _] - (-> node - (d/txt-merge {:line-height value}) - (cty/remove-typography-from-node)))] - (when (number? value) - (dwsh/update-shapes shape-ids - #(txt/update-text-content % update-node? update-fn nil) - {:ignore-touched true - :page-id page-id}))))) + (when (number? value) + (generate-text-shape-update {:line-height value} shape-ids page-id)))) (defn update-letter-spacing ([value shape-ids attributes] (update-letter-spacing value shape-ids attributes nil)) ([value shape-ids _attributes page-id] - (let [update-node? (fn [node] - (or (txt/is-text-node? node) - (txt/is-paragraph-node? node))) - update-fn (fn [node _] - (-> node - (d/txt-merge {:letter-spacing (str value)}) - (cty/remove-typography-from-node)))] - (when (number? value) - (dwsh/update-shapes shape-ids - #(txt/update-text-content % update-node? update-fn nil) - {:ignore-touched true - :page-id page-id}))))) + (when (number? value) + (generate-text-shape-update {:letter-spacing (str value)} shape-ids page-id)))) (defn update-font-size ([value shape-ids attributes] (update-font-size value shape-ids attributes nil)) ([value shape-ids _attributes page-id] - (let [update-node? (fn [node] - (or (txt/is-text-node? node) - (txt/is-paragraph-node? node))) - update-fn (fn [node _] - (-> node - (d/txt-merge {:font-size (str value)}) - (cty/remove-typography-from-node)))] - (when (number? value) - (dwsh/update-shapes shape-ids - #(txt/update-text-content % update-node? update-fn nil) - {:ignore-touched true - :page-id page-id}))))) + (when (number? value) + (generate-text-shape-update {:font-size (str value)} shape-ids page-id)))) ;; Events to apply / unapply tokens to shapes ------------------------------------------------------------ From 5ae4dde222203caadf59d964bf07e297c14db96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Fri, 25 Jul 2025 12:30:56 +0200 Subject: [PATCH 08/17] :books: Add font size token doc (#6972) --- docs/user-guide/design-tokens/index.njk | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/user-guide/design-tokens/index.njk b/docs/user-guide/design-tokens/index.njk index 3de6a9dbae..df0d4d7698 100644 --- a/docs/user-guide/design-tokens/index.njk +++ b/docs/user-guide/design-tokens/index.njk @@ -205,6 +205,10 @@ title: 10· Design Tokens

Y Position (dimension)

The Y property specifies the position of the element on the Y axis of the canvas.

+

Font Size

+

Font size tokens allow you to define and standardize font-size values across your design system. These tokens can be applied to the font-size property in text layers, ensuring consistent typography throughout your designs.

+

Font size token values are always computed as px (pixels).

+

Opacity

Opacity tokens allow you to define the opacity of a layer, ranging from fully opaque to fully transparent.

Opacity tokens can be applied to any design element that supports transparency. You can use any decimal value between 0 and 1 to set varying levels of opacity or you can use any value between 0 and 100 with `%` sign at the end of the value. For example, you can use 45% which would resolve to .45.

From b7a8747f005249d7c393992878876b8bb1666a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Fri, 25 Jul 2025 13:20:39 +0200 Subject: [PATCH 09/17] :books: Add doc for tokens zip file import option (#6973) --- docs/user-guide/design-tokens/index.njk | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/user-guide/design-tokens/index.njk b/docs/user-guide/design-tokens/index.njk index df0d4d7698..186de28280 100644 --- a/docs/user-guide/design-tokens/index.njk +++ b/docs/user-guide/design-tokens/index.njk @@ -382,7 +382,15 @@ title: 10· Design Tokens

Import Options

-

Single file

+ +

ZIP file

+

You can import tokens from a .zip file. This file can either contain a single JSON file or a folder structure with multiple files. The ZIP import option provides flexibility for organizing your tokens before importing them into Penpot.

+
    +
  • If the ZIP contains a single JSON file, it will be imported as a single set of tokens.
  • +
  • If the ZIP contains a folder structure, each file and folder will be interpreted as separate token sets, following the same rules as the multifile import.
  • +
+ +

Single JSON file

You can import a JSON file comprising all tokens, token sets and token themes.

When importing a single file, the first-level keys of the json file will be interpreted as the set name.

From 54fcd58531ab0f20ef2404a906332747c6920ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Fri, 25 Jul 2025 13:20:52 +0200 Subject: [PATCH 10/17] :books: Add doc for resizing text (#6974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :books: Add doc for resizing text * :books: Update docs for text resizing Co-authored-by: Madalena Melo Signed-off-by: andrés gonzález --------- Signed-off-by: andrés gonzález Co-authored-by: Madalena Melo --- docs/user-guide/objects/index.njk | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/user-guide/objects/index.njk b/docs/user-guide/objects/index.njk index 5faba1e1ca..9653ec3670 100644 --- a/docs/user-guide/objects/index.njk +++ b/docs/user-guide/objects/index.njk @@ -142,6 +142,11 @@ a design.

+

Tips for resizing

+
    +
  • Double-click on the right side of the bounding box to set the resize setting to auto-width.
  • +
  • Double-click on the bottom side of the bounding box to set the resize setting to auto-height.
  • +

Edit and style text content

Press Enter with a text layer selected to start editing the text content. You can style parts of the text content as rich text.

From 97fc7702b801be0580c76688617891ad751a9195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Fri, 25 Jul 2025 14:53:32 +0200 Subject: [PATCH 11/17] :books: Improve and clarify 'Hide and lock layers' section (#6975) --- docs/user-guide/layer-basics/index.njk | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/user-guide/layer-basics/index.njk b/docs/user-guide/layer-basics/index.njk index 317857423c..1667d8886e 100644 --- a/docs/user-guide/layer-basics/index.njk +++ b/docs/user-guide/layer-basics/index.njk @@ -34,7 +34,13 @@ desc: Master layer basics with Penpot's user guide! Learn to create, manipulate,

Layers are displayed from the bottom to the top of the layer stack, with layers above on the stack being shown on top in the image.

Hide and lock layers

-

Click on the eye icon to change the visibility of a layer. Click on the lock icon to lock or unlock a layer. A locked layer can not be modified.

+ +

Hide and show layers

+

You can control the visibility of any layer by clicking the eye icon next to it in the Layers panel. When a layer is hidden, it will not appear on the canvas, but you can still select it in the Layers panel, move its order, or modify its properties. The eye icon always indicates whether a layer is visible or hidden, making it easy to manage complex designs.

+ +

Lock and unlock layers

+

Locking a layer helps prevent accidental changes or movement on the canvas. When a layer is locked, it cannot be moved or edited directly in the canvas area. However, you can still select a locked layer in the Layers panel and adjust its properties, such as color, effects, or name. The lock icon next to the layer’s name shows its locked status, helping you keep your design organized and protected.

+