Compare commits

..

1 Commits

Author SHA1 Message Date
Alejandro Alonso
fe6fb0534c 🐛 Fix focus mode for simple component 2026-02-17 07:23:09 +01:00
12 changed files with 154 additions and 149 deletions

View File

@@ -35,18 +35,12 @@
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
## 2.13.3
### :bug: Bugs fixed
- Revert yetti (http server) update, because that caused a regression on multipart uploads
## 2.13.2
### :bug: Bugs fixed
- Fix modifying shapes by apply negative tokens to border radius [Taiga #13317](https://tree.taiga.io/project/penpot/issue/13317)
- Fix arbitrary file read security issue on create-font-variant rpc method (https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)
- Fix security issue (Path Traversal Vulnerability) on fonts related RPC method
## 2.13.1

View File

@@ -28,8 +28,8 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.8"
:git/sha "1d1b33f"
{:git/tag "v11.9"
:git/sha "5fad7a9"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}

View File

@@ -54,6 +54,19 @@ export class WasmWorkspacePage extends WorkspacePage {
await this.hideUI();
}
async getRenderCount() {
return this.page.evaluate(() => window.wasmRenderCount || 0);
}
async waitForNextRender(previousCount = null) {
const baseCount =
previousCount === null ? await this.getRenderCount() : previousCount;
await this.page.waitForFunction(
(count) => (window.wasmRenderCount || 0) > count,
baseCount,
);
}
async hideUI() {
await this.page.keyboard.press("\\");
await expect(this.pageName).not.toBeVisible();

View File

@@ -356,3 +356,39 @@ test("Renders shapes with multiple fills and blur", async ({
await expect(workspace.canvas).toHaveScreenshot();
});
test("Keeps component visible when focusing after creating it", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json");
await workspace.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspace.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspace.waitForFirstRender();
await workspace.clickLayers();
await workspace.clickLeafLayer("Rectangle");
await page.keyboard.press("ControlOrMeta+k");
const componentLayer = workspace.layers
.getByTestId("layer-row")
.filter({ has: page.getByTestId("icon-component") })
.first();
await expect(componentLayer).toBeVisible();
await componentLayer.click();
const previousRenderCount = await workspace.getRenderCount();
await page.keyboard.press("f");
await workspace.waitForNextRender(previousRenderCount);
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -867,15 +867,15 @@
:on-change on-change
:name (dm/str "flex-align-items-" type)
:options [{:id (dm/str "align-items-start-" type)
:icon (get-layout-grid-icon :align-items :start is-column)
:icon (get-layout-flex-icon :align-items :start is-column)
:label "Align items start"
:value "start"}
{:id (dm/str "align-items-center-" type)
:icon (get-layout-grid-icon :align-items :center is-column)
:icon (get-layout-flex-icon :align-items :center is-column)
:label "Align items center"
:value "center"}
{:id (dm/str "align-items-end-" type)
:icon (get-layout-grid-icon :align-items :end is-column)
:icon (get-layout-flex-icon :align-items :end is-column)
:label "Align items end"
:value "end"}]}]))

View File

@@ -175,10 +175,6 @@
(defn shape-proxy? [p]
(obj/type-of? p "ShapeProxy"))
;; Cannot use token/token-proxy? here because of circular dependency in applyToShapes in token proxy
(defn token-proxy? [t]
(obj/type-of? t "TokenProxy"))
(defn shape-proxy
([plugin-id id]
(shape-proxy plugin-id (:current-file-id @st/state) (:current-page-id @st/state) id))
@@ -1305,19 +1301,16 @@
tokens)))}
:applyToken
{:schema [:tuple
[:fn token-proxy?]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [token attrs]
(let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id"))
kw-attrs (into #{} (map keyword attrs))]
(if (some #(not (cto/token-attr? %)) kw-attrs)
(u/display-not-valid :applyToken attrs)
(st/emit!
(dwta/toggle-token {:token token
:attrs kw-attrs
:shape-ids [id]
:expand-with-children false})))))}
(fn [token attrs]
(let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id"))
kw-attrs (into #{} (map keyword attrs))]
(if (some #(not (cto/token-attr? %)) kw-attrs)
(u/display-not-valid :applyToken attrs)
(st/emit!
(dwta/toggle-token {:token token
:attrs kw-attrs
:shape-ids [id]
:expand-with-children false})))))
:isVariantHead
(fn []

View File

@@ -16,6 +16,7 @@
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
;; [app.plugins.shape :as shape]
[app.plugins.utils :as u]
[app.util.object :as obj]
[beicon.v2.core :as rx]
@@ -37,14 +38,10 @@
(defn token-proxy? [p]
(obj/type-of? p "TokenProxy"))
;; Cannot use shape/shape-proxy? here because of circular dependency in applyToken in shape proxy
(defn shape-proxy? [s]
(obj/type-of? s "ShapeProxy"))
(defn token-proxy
[plugin-id file-id set-id id]
(obj/reify {:name "TokenProxy"
:on-error u/handle-error}
:wrap u/wrap-errors}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$set-id {:enumerable false :get (constantly set-id)}
@@ -116,10 +113,14 @@
:applyToShapes
{:schema [:tuple
[:vector [:fn shape-proxy?]]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
;; FIXME: the schema decoder is interpreting the array of shape-proxys and converting
;; them to plain maps. For now we adapt the schema to accept it, but the decoder
;; should be fixed to keep the original proxy objects coming from the plugin.
;; [:vector [:fn shape/shape-proxy?]]
[:vector [:map [:id ::sm/uuid]]]
[:maybe [:set [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]]
:fn (fn [shapes attrs]
(apply-token-to-shapes file-id set-id id (map #(obj/get % "$id") shapes) attrs))}
(apply-token-to-shapes file-id set-id id (map :id shapes) attrs))}
:applyToSelected
{:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
@@ -135,7 +136,7 @@
(defn token-set-proxy
[plugin-id file-id id]
(obj/reify {:name "TokenSetProxy"
:on-error u/handle-error}
:wrap u/wrap-errors}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$id {:enumerable false :get (constantly id)}
@@ -256,7 +257,7 @@
(defn token-theme-proxy
[plugin-id file-id id]
(obj/reify {:name "TokenThemeProxy"
:on-error u/handle-error}
:wrap u/wrap-errors}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$id {:enumerable false :get (constantly id)}
@@ -351,7 +352,7 @@
(defn tokens-catalog
[plugin-id file-id]
(obj/reify {:name "TokensCatalog"
:on-error u/handle-error}
:wrap u/wrap-errors}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$id {:enumerable false :get (constantly file-id)}

View File

@@ -223,9 +223,7 @@
(defn display-not-valid
[code value]
(if (some? value)
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code))
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code)))
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code))
nil)
(defn reject-not-valid
@@ -250,12 +248,19 @@
(let [s (set values)]
(if (= (count s) 1) (first s) "mixed")))
(defn handle-error
"Function to be used in plugin proxies methods to handle errors and print a readable
message to the console."
[cause]
(display-not-valid (ex-message cause) nil)
(if-let [explain (-> cause ex-data ::sm/explain)]
(println (sm/humanize-explain explain))
(js/console.log (ex-data cause)))
(js/console.log (.-stack cause)))
(defn wrap-errors
"Function wrapper to be used in plugin proxies methods to handle errors.
When an exception is thrown, a readable error message is output to the console
and the exception is captured."
[f]
(fn []
(let [args (js-arguments)]
(try
(.apply f nil args)
(catch :default cause
(display-not-valid (ex-message cause) (obj/stringify args))
(if-let [explain (-> cause ex-data ::sm/explain)]
(println (sm/humanize-explain explain))
(js/console.log (ex-data cause)))
(js/console.log (.-stack cause))
nil)))))

View File

@@ -10,7 +10,6 @@
(:refer-clojure :exclude [set! new get merge clone contains? array? into-array reify class])
#?(:cljs (:require-macros [app.util.object]))
(:require
[app.common.data :as d]
[app.common.json :as json]
[app.common.schema :as sm]
[clojure.core :as c]
@@ -157,7 +156,6 @@
this-sym (with-meta (gensym (str rsym "-this-")) {:tag 'js})
target-sym (with-meta (gensym (str rsym "-target-")) {:tag 'js})
cause-sym (gensym "cause-")
make-sym
(fn [pname prefix]
@@ -178,7 +176,6 @@
wrap (c/get params :wrap)
schema-1 (c/get params :schema-1)
this? (c/get params :this false)
on-error (c/get params :on-error)
decode-expr
(c/get params :decode/fn)
@@ -217,16 +214,7 @@
(with-meta {:tag 'function}))
val-sym
(gensym (str "val-" (str/slug pname) "-"))
wrap-error-handling
(if on-error
(fn [expr]
`(try
~expr
(catch :default ~cause-sym
(~on-error ~cause-sym))))
identity)]
(gensym (str "val-" (str/slug pname) "-"))]
(concat
(when wrap
@@ -238,13 +226,8 @@
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~get-expr]
~(wrap-error-handling
`(.call ~fn-sym ~this-sym ~this-sym))))
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~get-expr]
~(wrap-error-handling
`(.call ~fn-sym ~this-sym)))))])
(.call ~fn-sym ~this-sym ~this-sym)))
get-expr)])
(when set-expr
[schema-sym schema-n
@@ -258,35 +241,28 @@
(make-sym pname "set-fn")
`(fn [~val-sym]
~(wrap-error-handling
`(let [~this-sym (~'js* "this")
~fn-sym ~set-expr
(let [~this-sym (~'js* "this")
~fn-sym ~set-expr
;; We only emit schema and coercer bindings if
;; schema-n is provided
~@(if (some? schema-n)
[schema-sym
`(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
;; We only emit schema and coercer bindings if
;; schema-n is provided
~@(if (some? schema-n)
[schema-sym `(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
coercer-sym
`(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
coercer-sym `(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
val-sym (if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym `(~coercer-sym ~val-sym)]
[])]
val-sym
(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym
`(~coercer-sym ~val-sym)]
[])]
~(if this?
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
`(.call ~fn-sym ~this-sym ~val-sym)))))])
~(if this?
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
`(.call ~fn-sym ~this-sym ~val-sym))))])
(when fn-expr
[schema-sym (or schema-n schema-1)
@@ -299,12 +275,7 @@
(make-sym pname "get-fn")
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~(if (and (list? fn-expr)
(= 'fn (first fn-expr)))
(let [[sa sb & sother] fn-expr]
`(~sa ~sb ~(wrap-error-handling `(do ~@sother))))
fn-expr)
~fn-sym ~fn-expr
~fn-sym ~(if this?
`(.bind ~fn-sym ~this-sym ~this-sym)
`(.bind ~fn-sym ~this-sym))
@@ -313,31 +284,25 @@
;; schema-n or schema-1 is provided
~@(if (or schema-n schema-1)
[fn-sym `(fn* [~@(if schema-1 [val-sym] [])]
~(wrap-error-handling
`(let [~@(if schema-n
[val-sym `(into-array (cljs.core/js-arguments))]
[])
~val-sym
~(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
(let [~@(if schema-n
[val-sym `(into-array (cljs.core/js-arguments))]
[])
~val-sym ~(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
~schema-sym
(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
~schema-sym (if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
~coercer-sym
(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
~coercer-sym (if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
~val-sym
(~coercer-sym ~val-sym)]
~(if schema-1
`(~fn-sym ~val-sym)
`(apply ~fn-sym ~val-sym)))))]
~val-sym (~coercer-sym ~val-sym)]
~(if schema-1
`(~fn-sym ~val-sym)
`(apply ~fn-sym ~val-sym))))]
[])]
~(if wrap
`(~wrap-sym ~fn-sym)
@@ -410,16 +375,12 @@
(let [definition (first params)]
(if (some? definition)
(let [definition (if (map? definition)
(c/merge {:wrap (:wrap tmeta)
:on-error (:on-error tmeta)}
definition)
(c/merge {:wrap (:wrap tmeta)} definition)
(-> {:enumerable false}
(c/merge (meta definition))
(assoc :wrap (:wrap tmeta))
(assoc :on-error (:on-error tmeta))
(assoc :fn definition)
(dissoc :get :set :line :column)
(d/without-nils)))
(dissoc :get :set)))
definition (assoc definition :name (name ckey))]
(recur (rest params)

View File

@@ -5226,27 +5226,27 @@ type TokenDimensionProps =
/**
* The properties that a FontFamilies token can be applied to.
*/
type TokenFontFamiliesProps = 'fontFamilies';
type TokenFontFamiliesProps = 'font-families';
/**
* The properties that a FontSizes token can be applied to.
*/
type TokenFontSizesProps = 'fontSize';
type TokenFontSizesProps = 'font-size';
/**
* The properties that a FontWeight token can be applied to.
*/
type TokenFontWeightProps = 'fontWeight';
type TokenFontWeightProps = 'font-weight';
/**
* The properties that a LetterSpacing token can be applied to.
*/
type TokenLetterSpacingProps = 'letterSpacing';
type TokenLetterSpacingProps = 'letter-spacing';
/**
* The properties that a Number token can be applied to.
*/
type TokenNumberProps = 'rotation';
type TokenNumberProps = 'rotation' | 'line-height';
/**
* The properties that an Opacity token can be applied to.
@@ -5262,18 +5262,18 @@ type TokenSizingProps =
| 'height'
// Layout
| 'layoutItemMinW'
| 'layoutItemMaxW'
| 'layoutItemMinH'
| 'layoutItemMaxH';
| 'layout-item-min-w'
| 'layout-item-max-w'
| 'layout-item-min-h'
| 'layout-item-max-h';
/**
* The properties that a Spacing token can be applied to.
*/
type TokenSpacingProps =
// Spacing / Gap
| 'rowGap'
| 'columnGap'
| 'row-gap'
| 'column-gap'
// Spacing / Padding
| 'p1'
@@ -5290,17 +5290,17 @@ type TokenSpacingProps =
/**
* The properties that a BorderWidth token can be applied to.
*/
type TokenBorderWidthProps = 'strokeWidth';
type TokenBorderWidthProps = 'stroke-width';
/**
* The properties that a TextCase token can be applied to.
*/
type TokenTextCaseProps = 'textCase';
type TokenTextCaseProps = 'text-case';
/**
* The properties that a TextDecoration token can be applied to.
*/
type TokenTextDecorationProps = 'textDecoration';
type TokenTextDecorationProps = 'text-decoration';
/**
* The properties that a Typography token can be applied to.

View File

@@ -1896,10 +1896,12 @@ impl RenderState {
}
}
let can_flatten = element.can_flatten() && !self.focus_mode.should_focus(&element.id);
// Skip render_shape_enter/exit for flattened containers
// If a container was flattened, it doesn't affect children visually, so we skip
// the expensive enter/exit operations and process children directly
if !element.can_flatten() {
if !can_flatten {
// Enter focus early so shadow_before_layer can run (it needs focus_mode.is_active())
self.focus_mode.enter(&element.id);
@@ -1978,7 +1980,7 @@ impl RenderState {
// Skip nested state updates for flattened containers
// Flattened containers don't affect children, so we don't need to track their state
if !element.can_flatten() {
if !can_flatten {
match element.shape_type {
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
self.nested_blurs.push(None);
@@ -2003,7 +2005,7 @@ impl RenderState {
let children_clip_bounds =
node_render_state.get_children_clip_bounds(element, None);
let children_ids: Vec<_> = if element.can_flatten() {
let children_ids: Vec<_> = if can_flatten {
// Container was flattened: get simplified children (which skip this level)
get_simplified_children(tree, element)
} else {