From edfa437ce7d87443ffbef5c5306cf649269801a7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 18:17:10 +0100 Subject: [PATCH 1/3] :books: Improve CONTRIBUTING.md file --- CONTRIBUTING.md | 294 +++++++++++++++++++++++------------------------- 1 file changed, 139 insertions(+), 155 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 187778913b..159eff63df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,211 +1,195 @@ -# Contributing Guide # +# Contributing Guide -Thank you for your interest in contributing to Penpot. This is a -generic guide that details how to contribute to the project in a way that -is efficient for everyone. If you are looking for specific documentation on -different parts of the platform, please refer to the `docs/` directory, -or the rendered version at the [Help Center](https://help.penpot.app/). +Thank you for your interest in contributing to Penpot. This guide covers +how to propose changes, submit fixes, and follow project conventions. -## Reporting Bugs ## +For architecture details, module-specific guidelines, and AI-agent +instructions, see [AGENTS.md](AGENTS.md). For final user technical +documentation, see the `docs/` directory or the rendered [Help +Center](https://help.penpot.app/). -We are using [GitHub Issues](https://github.com/penpot/penpot/issues) -for our public bugs. We keep a close eye on them and try to make it -clear when we have an internal fix in progress. Before filing a new -task, try to make sure your problem doesn't already exist. +## Table of Contents -If you found a bug, please report it, as far as possible, with: +- [Prerequisites](#prerequisites) +- [Reporting Bugs](#reporting-bugs) +- [Pull Requests](#pull-requests) +- [Commit Guidelines](#commit-guidelines) +- [Formatting and Linting](#formatting-and-linting) +- [Changelog](#changelog) +- [Code of Conduct](#code-of-conduct) +- [Developer's Certificate of Origin (DCO)](#developers-certificate-of-origin-dco) -- a detailed explanation of steps to reproduce the error -- the browser and browser version used -- a dev tools console exception stack trace (if available) +## Prerequisites -If you found a bug which you think is better to discuss in private (for -example, security bugs), consider first sending an email to -`support@penpot.app`. +- **Language**: Penpot is written primarily in Clojure (backend), ClojureScript + (frontend/exporter), and Rust (render-wasm). Familiarity with the Clojure + ecosystem is expected for most contributions. +- **Issue tracker**: We use [GitHub Issues](https://github.com/penpot/penpot/issues) + for public bugs and [Taiga](https://tree.taiga.io/project/penpot/) for + internal project management. Changelog entries reference both. -**We don't have a formal bug bounty program for security reports; this -is an open source application, and your contribution will be recognized -in the changelog.** +## Reporting Bugs +Report bugs via [GitHub Issues](https://github.com/penpot/penpot/issues). +Before filing, search existing issues to avoid duplicates. -## Pull Requests ## +Include the following when possible: -If you want to propose a change or bug fix via a pull request (PR), -you should first carefully read the section **Developer's Certificate of -Origin**. You must also format your code and commits according to the -instructions below. +1. Steps to reproduce the error. +2. Browser and browser version used. +3. DevTools console exception stack trace (if available). -If you intend to fix a bug, it's fine to submit a pull request right -away, but we still recommend filing an issue detailing what you're -fixing. This is helpful in case we don't accept that specific fix but -want to keep track of the issue. +For security bugs or issues better discussed in private, email +`support@penpot.app` or report them on [Github Security +Advisories](https://github.com/penpot/penpot/security/advisories) -If you want to implement or start working on a new feature, please -open a **question*- / **discussion*- issue for it. No PR -will be accepted without a prior discussion about the changes, -whether it is a new feature, an already planned one, or a quick win. +> **Note:** We do not have a formal bug bounty program. Security +> contributions are recognized in the changelog. -If it is your first PR, you can learn how to proceed from -[this free video -series](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) +## Pull Requests -We use the `easy fix` tag to indicate issues that are appropriate for beginners. +### Workflow -## Commit Guidelines ## +1. **Read the DCO** — see [Developer's Certificate of Origin](#developers-certificate-of-origin-dco) + below. All code patches must include a `Signed-off-by` line. +2. **Discuss before building** — open a question/discussion issue before + starting work on a new feature or significant change. No PR will be + accepted without prior discussion, whether it is a new feature, a planned + one, or a quick win. +3. **Bug fixes** — you may submit a PR directly, but we still recommend + filing an issue first so we can track it independently of your fix. +4. **Format and lint** — run the checks described in + [Formatting and Linting](#formatting-and-linting) before submitting. -We have very precise rules on how our git commit messages must be formatted. +### Good first issues -The commit message format is: +We use the `easy fix` label to mark issues appropriate for newcomers. + +## Commit Guidelines + +Commit messages must follow this format: ``` - +:emoji: [body] [footer] ``` -Where type is: +### Commit types -- :bug: `:bug:` a commit that fixes a bug -- :sparkles: `:sparkles:` a commit that adds an improvement -- :tada: `:tada:` a commit with a new feature -- :recycle: `:recycle:` a commit that introduces a refactor -- :lipstick: `:lipstick:` a commit with cosmetic changes -- :ambulance: `:ambulance:` a commit that fixes a critical bug -- :books: `:books:` a commit that improves or adds documentation -- :construction: `:construction:` a WIP commit -- :boom: `:boom:` a commit with breaking changes -- :wrench: `:wrench:` a commit for config updates -- :zap: `:zap:` a commit with performance improvements -- :whale: `:whale:` a commit for Docker-related stuff -- :paperclip: `:paperclip:` a commit with other non-relevant changes -- :arrow_up: `:arrow_up:` a commit with dependency updates -- :arrow_down: `:arrow_down:` a commit with dependency downgrades -- :fire: `:fire:` a commit that removes files or code -- :globe_with_meridians: `:globe_with_meridians:` a commit that adds or updates - translations +| Emoji | Description | +|-------|-------------| +| :bug: | Bug fix | +| :sparkles: | Improvement or enhancement | +| :tada: | New feature | +| :recycle: | Refactor | +| :lipstick: | Cosmetic changes | +| :ambulance: | Critical bug fix | +| :books: | Documentation | +| :construction: | Work in progress | +| :boom: | Breaking change | +| :wrench: | Configuration update | +| :zap: | Performance improvement | +| :whale: | Docker-related change | +| :paperclip: | Other non-relevant changes | +| :arrow_up: | Dependency update | +| :arrow_down: | Dependency downgrade | +| :fire: | Removal of code or files | +| :globe_with_meridians: | Add or update translations | +| :rocket: | Epic or highlight | -More info: +### Rules - - https://gist.github.com/parmentf/035de27d6ed1dce0b36a - - https://gist.github.com/rxaviers/7360908 +- Use the **imperative mood** in the subject (e.g. "Fix", not "Fixed"). +- Capitalize the first letter of the subject. +- Do not end the subject with a period. +- Keep the subject to **65 characters** or fewer. +- Separate the subject from the body with a **blank line**. -Each commit should have: +### Examples -- A concise subject using the imperative mood. -- The subject should capitalize the first letter, omit the period - at the end, and be no longer than 65 characters. -- A blank line between the subject line and the body. -- An entry in the CHANGES.md file if applicable, referencing the - GitHub or Taiga issue/user story using these same rules. +``` +:bug: Fix unexpected error on launching modal +:sparkles: Enable new modal for profile +:zap: Improve performance of dashboard navigation +:ambulance: Fix critical bug on user registration process +:tada: Add new approach for user registration +``` -Examples of good commit messages: +## Formatting and Linting -- `:bug: Fix unexpected error on launching modal` -- `:bug: Set proper error message on generic error` -- `:sparkles: Enable new modal for profile` -- `:zap: Improve performance of dashboard navigation` -- `:wrench: Update default backend configuration` -- `:books: Add more documentation for authentication process` -- `:ambulance: Fix critical bug on user registration process` -- `:tada: Add new approach for user registration` - -## Formatting and Linting ## - -You will want to make sure your code is formatted and linted before submitting -a PR. We use [cljfmt](https://github.com/weavejester/cljfmt) and -[clj-kondo](https://github.com/clj-kondo/clj-kondo) for this. After installing -them on your system, you can run them with: +We use [cljfmt](https://github.com/weavejester/cljfmt) for formatting and +[clj-kondo](https://github.com/clj-kondo/clj-kondo) for linting. ```bash -# Check formatting +# Check formatting (does not modify files) +./scripts/check-fmt + +# Fix formatting (modifies files in place) ./scripts/fmt # Lint ./scripts/lint ``` -Ideally, you should run these commands as git pre-commit hooks. A convenient way -of defining them is to use [Husky](https://typicode.github.io/husky/#/). +Ideally, run these as git pre-commit hooks. +[Husky](https://typicode.github.io/husky/#/) is a convenient option for +setting this up. -## Code of Conduct ## +## Changelog -As contributors and maintainers of this project, we pledge to respect -all people who contribute through reporting issues, posting feature -requests, updating documentation, submitting pull requests or patches, -and other activities. +When your change is user-facing or otherwise notable, add an entry to +[CHANGES.md](CHANGES.md) following the same commit-type conventions. Reference +the relevant GitHub issue or Taiga user story. -We are committed to making participation in this project a -harassment-free experience for everyone, regardless of level of -experience, gender, gender identity and expression, sexual -orientation, disability, personal appearance, body size, race, -ethnicity, age, or religion. +## Code of Conduct -Examples of unacceptable behavior by participants include the use of -sexual language or imagery, derogatory comments or personal attacks, -trolling, public or private harassment, insults, or other -unprofessional conduct. +This project follows the [Contributor Covenant](https://www.contributor-covenant.org/). +The full Code of Conduct is available at +[help.penpot.app/contributing-guide/coc](https://help.penpot.app/contributing-guide/coc/) +and in the repository's [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). -Project maintainers have the right and responsibility to remove, edit, -or reject comments, commits, code, wiki edits, issues, and other -contributions that are not aligned with this Code of Conduct. Project -maintainers who do not follow the Code of Conduct may be removed from -the project team. - -This Code of Conduct applies both within project spaces and in public -spaces when an individual is representing the project or its -community. - -Instances of abusive, harassing, or otherwise unacceptable behavior -may be reported by opening an issue or contacting one or more of the -project maintainers. - -This Code of Conduct is adapted from the Contributor Covenant, version -1.1.0, available from [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) +To report unacceptable behavior, open an issue or contact a project maintainer +directly. ## Developer's Certificate of Origin (DCO) By submitting code you agree to and can certify the following: - Developer's Certificate of Origin 1.1 +> **Developer's Certificate of Origin 1.1** +> +> By making a contribution to this project, I certify that: +> +> (a) The contribution was created in whole or in part by me and I have the +> right to submit it under the open source license indicated in the file; or +> +> (b) The contribution is based upon previous work that, to the best of my +> knowledge, is covered under an appropriate open source license and I have +> the right under that license to submit that work with modifications, +> whether created in whole or in part by me, under the same open source +> license (unless I am permitted to submit under a different license), as +> indicated in the file; or +> +> (c) The contribution was provided directly to me by some other person who +> certified (a), (b) or (c) and I have not modified it. +> +> (d) I understand and agree that this project and the contribution are public +> and that a record of the contribution (including all personal information +> I submit with it, including my sign-off) is maintained indefinitely and +> may be redistributed consistent with this project or the open source +> license(s) involved. - By making a contribution to this project, I certify that: +### Signed-off-by - (a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - - (b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - - (c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - - (d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. - -Then, all your code patches (**documentation is excluded**) should -contain a sign-off at the end of the patch/commit description body. It -can be automatically added by adding the `-s` parameter to `git commit`. - -This is an example of what the line should look like: +All code patches (**documentation is excluded**) must contain a sign-off line +at the end of the commit body. Add it automatically with `git commit -s`. ``` -Signed-off-by: Andrey Antukh +Signed-off-by: Your Real Name ``` -Please, use your real name (sorry, no pseudonyms or anonymous -contributions are allowed). - -The commit Signed-off-by is mandatory and should match the commit author. - +- Use your **real name** — pseudonyms and anonymous contributions are not + allowed. +- The `Signed-off-by` line is **mandatory** and must match the commit author. From 01284e2a000f299257f6b713982142210ec197fc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 19:54:05 +0100 Subject: [PATCH 2/3] :sparkles: Improve error handling and exception formatting (#8757) * :sparkles: Improve error handling and exception formatting - Enhance exception formatting with visual separators and cause chaining - Add new handler for :internal error type - Refine error types: change assertion-related errors to :assertion type - Improve error messages and hints consistency - Clean up error handling in zip utilities and HTTP modules * :bug: Properly handle AbortError on fetch request unsubscription When a fetch request in-flight is cancelled due to RxJS unsubscription (e.g. navigating away from the workspace while thumbnail loads are pending), the AbortController.abort() call triggers a catch handler that previously relied solely on a @unsubscribed? flag to suppress the error. This was unreliable: nested observables spawned inside rx/mapcat (such as datauri->blob-uri conversions within get-file-object-thumbnails) could abort independently, with their own AbortController instances, meaning the outer unsubscribed? flag was never set and the AbortError propagated as an unhandled exception. Add an explicit AbortError name check as a disjunctive condition so that abort errors originating from any observable in the chain are suppressed at the source, regardless of subscription state. Signed-off-by: Andrey Antukh --- common/src/app/common/exceptions.cljc | 13 +++++++++++-- frontend/src/app/main/data/auth.cljs | 2 +- frontend/src/app/main/errors.cljs | 9 +++++++-- frontend/src/app/main/repo.cljs | 16 ++++++++-------- frontend/src/app/rasterizer.cljs | 4 ++-- frontend/src/app/util/http.cljs | 4 ++-- frontend/src/app/util/webapi.cljs | 4 ++-- frontend/src/app/util/zip.cljs | 14 +++++++------- 8 files changed, 40 insertions(+), 26 deletions(-) diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index e104be775b..a07eda8c0e 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -245,8 +245,10 @@ (defn format-throwable [cause & {:as opts}] (with-out-str + (println "====================") (when-let [exdata (ex-data cause)] - (when-let [hint (get exdata :hint)] + (when-let [hint (or (get exdata :hint) + (ex-message cause))] (when (str/index-of hint "\n") (println "Hint:") (println "--------------------") @@ -273,7 +275,9 @@ (when-let [trace (.-stack cause)] (println "Trace:") (println "--------------------") - (println (.-stack cause)))))) + (println (.-stack cause))) + + (println "====================")))) (defn first-line [s] @@ -297,6 +301,11 @@ (js/console.group title) (try (js/console.log (format-throwable cause)) + (loop [cause (ex-cause cause)] + (when cause + (js/console.log "\nCaused by:") + (js/console.log (format-throwable cause)) + (recur (ex-cause cause)))) (finally (js/console.groupEnd)))))) diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs index a933b83590..e3aa763ad6 100644 --- a/frontend/src/app/main/data/auth.cljs +++ b/frontend/src/app/main/data/auth.cljs @@ -178,7 +178,7 @@ (rx/map (fn [{:keys [redirect-uri] :as rsp}] (if redirect-uri (rt/nav-raw :uri redirect-uri) - (ex/raise :type :internal + (ex/raise :type :assertion :code :unexpected-response :hint "unexpected response from OIDC method" :resp (pr-str rsp))))) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 2327911e6f..63402ae3f2 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -139,6 +139,12 @@ :level :error :timeout 5000}))) +(defmethod ptk/handle-error :internal + [error] + (st/emit! (rt/assign-exception error)) + (when-let [cause (::instance error)] + (ex/print-throwable cause :prefix "Internal Error"))) + (defmethod ptk/handle-error :default [error] (if (and (string? (:hint error)) @@ -240,8 +246,7 @@ (st/async-emit! (rt/assign-exception error)))) ;; This is a pure frontend error that can be caused by an active -;; assertion (assertion that is preserved on production builds). From -;; the user perspective this should be treated as internal error. +;; assertion (assertion that is preserved on production builds). (defmethod ptk/handle-error :assertion [error] (when-let [cause (::instance error)] diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 0ad10286aa..ad252e2a04 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -57,13 +57,13 @@ :else (rx/throw - (ex-info "repository request error" - {:type :internal - :code :repository-access-error + (ex/error :type :internal + :code :unable-to-process-repository-response + :hint "unable to process repository response" :uri uri :status status :headers headers - :data body})))) + :data body)))) (def default-options {:update-file {:query-params [:id]} @@ -156,11 +156,11 @@ tpoint (ct/tpoint-ms)] (when (and response-stream? (not stream?)) - (ex/raise :type :internal - :code :invalid-response-processing + (ex/raise :type :assertion + :code :unexpected-response :hint "expected normal response, received sse stream" - :response-uri (:uri response) - :response-status (:status response))) + :uri (:uri response) + :status (:status response))) (if response-stream? (-> (sse/create-stream body) diff --git a/frontend/src/app/rasterizer.cljs b/frontend/src/app/rasterizer.cljs index e2bf9ede6e..fa1e233df4 100644 --- a/frontend/src/app/rasterizer.cljs +++ b/frontend/src/app/rasterizer.cljs @@ -44,8 +44,8 @@ (rx/end! subs))) (obj/set! image "crossOrigin" "anonymous") (obj/set! image "onerror" #(rx/error! subs %)) - (obj/set! image "onabort" #(rx/error! subs (ex/error :type :internal - :code :abort + (obj/set! image "onabort" #(rx/error! subs (ex/error :type :abort + :code :operation-aborted :hint "operation aborted"))) (obj/set! image "src" uri) (fn [] diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs index 7ed68dbc7d..8b9c590895 100644 --- a/frontend/src/app/util/http.cljs +++ b/frontend/src/app/util/http.cljs @@ -106,10 +106,10 @@ (p/catch (fn [cause] (vreset! abortable? false) - (when-not @unsubscribed? + (when-not (or @unsubscribed? (= (.-name ^js cause) "AbortError")) (let [error (ex-info (ex-message cause) {:type :internal - :code :unable-to-fetch + :code :fetch-error :hint "unable to perform fetch operation" :uri uri :headers headers} diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 0833ac70d0..1b3a63b97a 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -42,8 +42,8 @@ (obj/set! reader "onerror" #(rx/error! subs %)) (obj/set! reader "onabort" - #(rx/error! subs (ex/error :type :internal - :code :abort + #(rx/error! subs (ex/error :type :abort + :code :operation-aborted :hint "operation aborted"))) (f reader) (fn [] diff --git a/frontend/src/app/util/zip.cljs b/frontend/src/app/util/zip.cljs index ae0d484133..2b8e67ce8a 100644 --- a/frontend/src/app/util/zip.cljs +++ b/frontend/src/app/util/zip.cljs @@ -8,6 +8,7 @@ "Helpers for make zip file." (:require ["@zip.js/zip.js" :as zip] + [app.common.exceptions :as ex] [app.util.array :as array] [promesa.core :as p])) @@ -27,9 +28,9 @@ (reader (js/Uint8Array. blob)) :else - (throw (ex-info "invalid arguments" - {:type :internal - :code :invalid-type})))) + (ex/raise :type :assertion + :coce :invalid-type + :hint "invalid data received for zip/reader"))) (defn blob-writer [& {:keys [mtype]}] @@ -62,10 +63,9 @@ (.add writer path (new zip/TextReader content)) :else - (throw (ex-info "invalid arguments" - {:type :internal - :code :invalid-type})))) - + (ex/raise :type :assertion + :code :invalid-type + :hint "invalid data received for zip/add fn"))) (defn get-entry [reader path] From 6b609566e1d161fbf6aad3e4710bdc256866a302 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 24 Mar 2026 15:53:07 +0100 Subject: [PATCH 3/3] :bug: Fix resize text modifiers --- .../workspace/get-file-13755-fragment.json | 19 +++ .../data/workspace/get-file-13755.json | 135 ++++++++++++++++++ .../ui/specs/workspace-modifers.spec.js | 27 ++++ .../src/app/main/data/workspace/texts.cljs | 11 +- 4 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 frontend/playwright/data/workspace/get-file-13755-fragment.json create mode 100644 frontend/playwright/data/workspace/get-file-13755.json diff --git a/frontend/playwright/data/workspace/get-file-13755-fragment.json b/frontend/playwright/data/workspace/get-file-13755-fragment.json new file mode 100644 index 0000000000..ccfb4561a0 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-13755-fragment.json @@ -0,0 +1,19 @@ +{ + "~:file-id": "~u7fd33337-c651-80ae-8007-c357213f876e", + "~:id": "~u7fd33337-c651-80ae-8007-c3578977e5be", + "~:created-at": "~m1774363460068", + "~:modified-at": "~m1774363460068", + "~:type": "fragment", + "~:backend": "db", + "~:data": { + "~:objects": { + "~#penpot/objects-map/v2": { + "~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ub98e38af-59e9-8056-8007-c3577ef85c83\"]]]", + "~ub98e38af-59e9-8056-8007-c35778509984": "[\"~#shape\",[\"^ \",\"~:y\",522.9999389648438,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:auto-width\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:key\",\"lu847h6p2o\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"normal\",\"~:typography-ref-id\",null,\"~:text-transform\",\"none\",\"~:font-id\",\"sourcesanspro\",\"^8\",\"20x1m8p51r5\",\"~:font-size\",\"14\",\"~:font-weight\",\"400\",\"~:typography-ref-file\",null,\"~:font-variant-id\",\"regular\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-opacity\",1]],\"~:font-family\",\"sourcesanspro\",\"~:text\",\"uno dos tres cuatro\"]],\"^<\",null,\"^=\",\"none\",\"~:text-align\",\"left\",\"^>\",\"sourcesanspro\",\"^8\",\"5tp2r0veqv\",\"^?\",\"14\",\"^@\",\"400\",\"^A\",null,\"~:text-direction\",\"ltr\",\"^7\",\"paragraph\",\"^B\",\"regular\",\"^C\",\"none\",\"^D\",\"0\",\"^E\",[[\"^ \",\"^F\",\"#000000\",\"^G\",1]],\"^H\",\"sourcesanspro\"]]]],\"~:vertical-align\",\"top\"],\"~:hide-in-viewer\",false,\"~:name\",\"uno dos tres cuatro\",\"~:width\",113,\"^7\",\"^I\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",861,\"~:y\",522.9999389648438]],[\"^Q\",[\"^ \",\"~:x\",974,\"~:y\",522.9999389648438]],[\"^Q\",[\"^ \",\"~:x\",974,\"~:y\",539.9999389648438]],[\"^Q\",[\"^ \",\"~:x\",861,\"~:y\",539.9999389648438]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ub98e38af-59e9-8056-8007-c35778509984\",\"~:parent-id\",\"~ub98e38af-59e9-8056-8007-c3577ef85c83\",\"~:position-data\",[[\"^ \",\"~:y\",540.659912109375,\"^:\",\"1.2\",\"^;\",\"normal\",\"^=\",\"none\",\"^J\",\"left\",\"^>\",\"sourcesanspro\",\"^?\",\"14\",\"^@\",\"400\",\"^K\",\"ltr\",\"^O\",112.989990234375,\"^B\",\"regular\",\"^C\",\"none\",\"^D\",\"0\",\"~:x\",861,\"^E\",[[\"^ \",\"^F\",\"#000000\",\"^G\",1]],\"~:direction\",\"ltr\",\"^H\",\"sourcesanspro\",\"~:height\",18.1199951171875,\"^I\",\"uno dos tres cuatro\"]],\"~:frame-id\",\"~ub98e38af-59e9-8056-8007-c3577ef85c83\",\"~:x\",861,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",861,\"~:y\",522.9999389648438,\"^O\",113,\"^W\",17,\"~:x1\",861,\"~:y1\",522.9999389648438,\"~:x2\",974,\"~:y2\",539.9999389648438]],\"~:flip-x\",null,\"^W\",17,\"~:flip-y\",null]]", + "~ub98e38af-59e9-8056-8007-c3577ef85c83": "[\"~#shape\",[\"^ \",\"~:y\",512.9999568462372,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",10,\"~:p2\",10,\"~:p3\",10,\"~:p4\",10],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Board\",\"~:layout-align-items\",\"~:start\",\"~:width\",132.99999487400055,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",850.9999846220016,\"~:y\",512.9999568462372]],[\"^J\",[\"^ \",\"~:x\",983.9999794960022,\"~:y\",512.9999568462372]],[\"^J\",[\"^ \",\"~:x\",983.9999794960022,\"~:y\",549.9999556541443]],[\"^J\",[\"^ \",\"~:x\",850.9999846220016,\"~:y\",549.9999556541443]]],\"~:r2\",0,\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:auto\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",0],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:layout-item-v-sizing\",\"^N\",\"~:r3\",0,\"~:layout-justify-content\",\"^C\",\"~:r1\",0,\"~:id\",\"~ub98e38af-59e9-8056-8007-c3577ef85c83\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:row\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",850.9999846220016,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",850.9999846220016,\"~:y\",512.9999568462372,\"^D\",132.99999487400055,\"~:height\",36.999998807907104,\"~:x1\",850.9999846220016,\"~:y1\",512.9999568462372,\"~:x2\",983.9999794960022,\"~:y2\",549.9999556541443]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^18\",36.999998807907104,\"~:flip-y\",null,\"~:shapes\",[\"~ub98e38af-59e9-8056-8007-c35778509984\"]]]" + } + }, + "~:id": "~u7fd33337-c651-80ae-8007-c357213f876f", + "~:name": "Page 1" + } +} diff --git a/frontend/playwright/data/workspace/get-file-13755.json b/frontend/playwright/data/workspace/get-file-13755.json new file mode 100644 index 0000000000..9234febfe2 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-13755.json @@ -0,0 +1,135 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "render-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~ud715d0a5-a44e-8056-8005-a79999e18b64", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 10", + "~:revn": 2, + "~:modified-at": "~m1774363460059", + "~:vern": 0, + "~:id": "~u7fd33337-c651-80ae-8007-c357213f876e", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content-v2", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0008-fix-library-colors-v4", + "0009-clean-library-colors", + "0009-add-partial-text-touched-flags", + "0010-fix-swap-slots-pointing-non-existent-shapes", + "0011-fix-invalid-text-touched-flags", + "0012-fix-position-data", + "0013-fix-component-path", + "0013-clear-invalid-strokes-and-fills", + "0014-fix-tokens-lib-duplicate-ids", + "0014-clear-components-nil-objects", + "0015-fix-text-attrs-blank-strings", + "0015-clean-shadow-color", + "0016-copy-fills-from-position-data-to-text-node", + "0017-fix-layout-flex-dir" + ] + }, + "~:version": 67, + "~:project-id": "~u76eab896-accf-81a5-8007-2b264ebe7817", + "~:created-at": "~m1774363353342", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~u7fd33337-c651-80ae-8007-c357213f876f" + ], + "~:pages-index": { + "~u7fd33337-c651-80ae-8007-c357213f876f": { + "~#penpot/pointer": [ + "~u7fd33337-c651-80ae-8007-c3578977e5be", + { + "~:created-at": "~m1774363460064" + } + ] + } + }, + "~:id": "~u7fd33337-c651-80ae-8007-c357213f876e", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } +} diff --git a/frontend/playwright/ui/specs/workspace-modifers.spec.js b/frontend/playwright/ui/specs/workspace-modifers.spec.js index 4b0fbb6f87..8e5f871fd8 100644 --- a/frontend/playwright/ui/specs/workspace-modifers.spec.js +++ b/frontend/playwright/ui/specs/workspace-modifers.spec.js @@ -112,3 +112,30 @@ test("BUG 13272 - Fix problem with snap to pixel", async ({ page }) => { await expect(workspacePage.rightSidebar.getByTitle("Width").getByRole("textbox")).toHaveValue("197.5"); await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("128.28"); }); + +test("BUG 13755 - Fix problem with text change modiifers", async ({ page }) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockGetFile("workspace/get-file-13755.json"); + + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-13755-fragment.json", + ); + + await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json"); + + await workspacePage.goToWorkspace({ + fileId: "7fd33337-c651-80ae-8007-c357213f876e", + pageId: "7fd33337-c651-80ae-8007-c357213f876f", + }); + + await workspacePage.clickToggableLayer("Board"); + await workspacePage.clickLeafLayer("uno dos tres cuatro"); + + await workspacePage.page.keyboard.press('Enter'); + await workspacePage.page.keyboard.type('test'); + + await workspacePage.clickToggableLayer("Board"); + await expect(workspacePage.rightSidebar.getByTitle("Width").getByRole("textbox")).toHaveValue("23"); +}); diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index af03561722..1690398f8a 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -1026,10 +1026,15 @@ :stack-undo? effective-stack-undo? :undo-group (when new-shape? id)}) - ;; When we don't update the shape (no new-size), still update WASM display - (when-not (some? new-size) + ;; When `get-wasm-text-new-size` reports a change, `update-shapes` above resizes the + ;; shape data; the WASM renderer still needs matching modifiers. While editing, use + ;; `set-wasm-modifiers` for a temporary preview; on `finalize?`, `apply-wasm-modifiers` + ;; commits layout (flex parents, sidebar width, etc.) like other transform flows. + (when (some? new-size) (when-let [modifiers (dwwt/resize-wasm-text-modifiers shape content)] - (dwm/set-wasm-modifiers modifiers {:undo-group (when new-shape? id)})))) + (if finalize? + (dwm/apply-wasm-modifiers modifiers {:undo-group (when new-shape? id)}) + (dwm/set-wasm-modifiers modifiers {:undo-group (when new-shape? id)}))))) (when finalize? (rx/concat