From d93fe89c12942773ca894e129fcb227bd9d4ec7e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 24 Nov 2025 10:48:51 +0100 Subject: [PATCH 01/10] :paperclip: Backport CI github workflog from develop --- .github/workflows/tests.yml | 344 +++++++++++++++++++++++++----------- 1 file changed, 243 insertions(+), 101 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 421b1f2598..4222373900 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: "CI: Tests" +name: "CI" defaults: run: @@ -16,125 +16,140 @@ on: - develop - staging +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + jobs: - # lint: - # name: "Code Linter" - # runs-on: ubuntu-24.04 - # container: penpotapp/devenv:latest + lint: + name: "Code Linter" + runs-on: ubuntu-24.04 + container: penpotapp/devenv:latest - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 + steps: + - name: Checkout repository + uses: actions/checkout@v4 - # - name: Check clojure code format - # run: | - # corepack enable; - # corepack install; - # yarn install - # yarn run fmt:clj:check + - name: Check clojure code format + run: | + corepack enable; + corepack install; + yarn install + yarn run fmt:clj:check - # test-common: - # name: "Common Tests" - # runs-on: ubuntu-24.04 - # container: penpotapp/devenv:latest + test-common: + name: "Common Tests" + runs-on: ubuntu-24.04 + container: penpotapp/devenv:latest - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 + steps: + - name: Checkout repository + uses: actions/checkout@v4 - # - name: Run tests on JVM - # working-directory: ./common - # run: | - # clojure -M:dev:test + - name: Run tests on JVM + working-directory: ./common + run: | + clojure -M:dev:test - # - name: Run tests on NODE - # working-directory: ./common - # run: | - # corepack enable; - # corepack install; - # yarn install; - # yarn run test; + - name: Run tests on NODE + working-directory: ./common + run: | + corepack enable; + corepack install; + yarn install; + yarn run test; - # test-frontend: - # name: "Frontend Tests" - # runs-on: ubuntu-24.04 - # container: penpotapp/devenv:latest + test-frontend: + name: "Frontend Tests" + runs-on: ubuntu-24.04 + container: penpotapp/devenv:latest - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 + steps: + - name: Checkout repository + uses: actions/checkout@v4 - # - name: Unit Tests - # working-directory: ./frontend - # run: | - # corepack enable; - # corepack install; - # yarn install; - # yarn run test; + - name: Unit Tests + working-directory: ./frontend + run: | + corepack enable; + corepack install; + yarn install; + yarn run test; - # - name: Component Tests - # working-directory: ./frontend - # run: | - # yarn run playwright install chromium --with-deps; - # yarn run build:storybook + - name: Component Tests + working-directory: ./frontend + run: | + yarn run playwright install chromium --with-deps; + yarn run build:storybook - # npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ - # "npx http-server storybook-static --port 6006 --silent" \ - # "npx wait-on tcp:6006 && yarn test:storybook" + npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ + "npx http-server storybook-static --port 6006 --silent" \ + "npx wait-on tcp:6006 && yarn test:storybook" - # - name: Check SCSS Format - # working-directory: ./frontend - # run: | - # yarn run lint:scss; + - name: Check SCSS Format + working-directory: ./frontend + run: | + yarn run lint:scss; - # test-backend: - # name: "Backend Tests" - # runs-on: ubuntu-24.04 - # container: penpotapp/devenv:latest + test-backend: + name: "Backend Tests" + runs-on: ubuntu-24.04 + container: penpotapp/devenv:latest - # services: - # postgres: - # image: postgres:17 - # # Provide the password for postgres - # env: - # POSTGRES_USER: penpot_test - # POSTGRES_PASSWORD: penpot_test - # POSTGRES_DB: penpot_test + services: + postgres: + image: postgres:17 + # Provide the password for postgres + env: + POSTGRES_USER: penpot_test + POSTGRES_PASSWORD: penpot_test + POSTGRES_DB: penpot_test - # # Set health checks to wait until postgres has started - # options: >- - # --health-cmd pg_isready - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 - # redis: - # image: valkey/valkey:9 + redis: + image: valkey/valkey:9 - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 + steps: + - name: Checkout repository + uses: actions/checkout@v4 - # test-library: - # name: "Library Tests" - # runs-on: ubuntu-24.04 - # container: penpotapp/devenv:latest + - name: Run tests + working-directory: ./backend + env: + PENPOT_TEST_DATABASE_URI: "postgresql://postgres/penpot_test" + PENPOT_TEST_DATABASE_USERNAME: penpot_test + PENPOT_TEST_DATABASE_PASSWORD: penpot_test + PENPOT_TEST_REDIS_URI: "redis://redis/1" - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 + run: | + clojure -M:dev:test --reporter kaocha.report/documentation - # - name: Run tests - # working-directory: ./library - # run: | - # corepack enable; - # corepack install; - # yarn install; - # yarn run build:bundle; - # yarn run test; + test-library: + name: "Library Tests" + runs-on: ubuntu-24.04 + container: penpotapp/devenv:latest - test-integration: - name: "Integration Tests" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run tests + working-directory: ./library + run: | + corepack enable; + corepack install; + yarn install; + yarn run build:bundle; + yarn run test; + + build-integration: + name: "Build Integration Bundle" runs-on: ubuntu-24.04 container: penpotapp/devenv:latest @@ -157,17 +172,144 @@ jobs: run: | ./build release + - name: Store Bundle Cache + uses: actions/cache@v4 + with: + key: "integration-bundle-${{ github.sha }}" + path: frontend/resources/public + + test-integration-1: + name: "Integration Tests 1/4" + runs-on: ubuntu-24.04 + container: penpotapp/devenv:latest + needs: build-integration + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Restore Cache + uses: actions/cache/restore@v4 + with: + key: "integration-bundle-${{ github.sha }}" + path: frontend/resources/public + - name: Run Tests working-directory: ./frontend run: | - yarn run playwright install chromium --with-deps - yarn run test:e2e -x --workers=1 --reporter=line + corepack enable; + corepack install; + yarn install; + yarn run playwright install chromium --with-deps; + yarn run test:e2e -x --workers=2 --reporter=list --shard="1/4"; - name: Upload test result uses: actions/upload-artifact@v4 if: always() with: - name: integration-tests-result + name: integration-tests-result-1 + path: frontend/test-results/ + overwrite: true + retention-days: 3 + + test-integration-2: + name: "Integration Tests 2/4" + runs-on: ubuntu-24.04 + container: penpotapp/devenv:latest + needs: build-integration + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Restore Cache + uses: actions/cache/restore@v4 + with: + key: "integration-bundle-${{ github.sha }}" + path: frontend/resources/public + + - name: Run Tests + working-directory: ./frontend + run: | + corepack enable; + corepack install; + yarn install; + yarn run playwright install chromium --with-deps; + yarn run test:e2e -x --workers=2 --reporter=list --shard "2/4"; + + - name: Upload test result + uses: actions/upload-artifact@v4 + if: always() + with: + name: integration-tests-result-2 + path: frontend/test-results/ + overwrite: true + retention-days: 3 + + test-integration-3: + name: "Integration Tests 3/4" + runs-on: ubuntu-24.04 + container: penpotapp/devenv:latest + needs: build-integration + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Restore Cache + uses: actions/cache/restore@v4 + with: + key: "integration-bundle-${{ github.sha }}" + path: frontend/resources/public + + - name: Run Tests + working-directory: ./frontend + run: | + corepack enable; + corepack install; + yarn install; + yarn run playwright install chromium --with-deps; + yarn run test:e2e -x --workers=2 --reporter=list --shard "3/4"; + + - name: Upload test result + uses: actions/upload-artifact@v4 + if: always() + with: + name: integration-tests-result-3 + path: frontend/test-results/ + overwrite: true + retention-days: 3 + + test-integration-4: + name: "Integration Tests 3/4" + runs-on: ubuntu-24.04 + container: penpotapp/devenv:latest + needs: build-integration + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Restore Cache + uses: actions/cache/restore@v4 + with: + key: "integration-bundle-${{ github.sha }}" + path: frontend/resources/public + + - name: Run Tests + working-directory: ./frontend + run: | + corepack enable; + corepack install; + yarn install; + yarn run playwright install chromium --with-deps; + yarn run test:e2e -x --workers=2 --reporter=list --shard "4/4"; + + - name: Upload test result + uses: actions/upload-artifact@v4 + if: always() + with: + name: integration-tests-result-4 path: frontend/test-results/ overwrite: true retention-days: 3 From 00bbb0bfb650a5063ae5c3da24f715ce42ac1afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Fri, 21 Nov 2025 13:42:01 +0100 Subject: [PATCH 02/10] :recycle: Add format and refactor payments --- .../app/main/ui/settings/subscription.cljs | 157 ++++++++++-------- .../app/main/ui/settings/subscription.scss | 53 +++--- 2 files changed, 121 insertions(+), 89 deletions(-) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index c0ade1f61c..b3fa6e2d44 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -83,74 +83,101 @@ ::mf/register-as :management-dialog} [{:keys [subscription-type current-subscription editors subscribe-to-trial]}] - (let [unlimited-modal-step* (mf/use-state 1) - unlimited-modal-step (deref unlimited-modal-step*) - subscription-name (if subscribe-to-trial - (if (= subscription-type "unlimited") - (tr "subscription.settings.unlimited-trial") - (tr "subscription.settings.enterprise-trial")) - (case subscription-type - "professional" (tr "subscription.settings.professional") - "unlimited" (tr "subscription.settings.unlimited") - "enterprise" (tr "subscription.settings.enterprise"))) - min-editors (if (seq editors) (count editors) 1) - initial (mf/with-memo [min-editors] - {:min-members min-editors}) - form (fm/use-form :schema (schema:seats-form min-editors) - :initial initial) - submit-in-progress* (mf/use-state false) - subscribe-to-unlimited (mf/use-fn - (mf/deps form) - (fn [add-payment-details] - (when (not @submit-in-progress*) - (let [data (:clean-data @form) - return-url (-> (rt/get-current-href) (rt/encode-url)) - href (if add-payment-details - (dm/str "payments/subscriptions/create?type=unlimited&show=true&quantity=" (:min-members data) "&returnUrl=" return-url) - (dm/str "payments/subscriptions/create?type=unlimited&show=false&quantity=" (:min-members data) "&returnUrl=" return-url))] - (reset! submit-in-progress* true) - (reset! form nil) - (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" - :type "unlimited" - :quantity (:min-members data)}) - (rt/nav-raw :href href)))))) + (let [unlimited-modal-step* + (mf/use-state 1) - subscribe-to-enterprise (mf/use-fn - (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" - :type "enterprise"})) - (let [return-url (-> (rt/get-current-href) (rt/encode-url)) - href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)] - (st/emit! (rt/nav-raw :href href))))) + unlimited-modal-step + (deref unlimited-modal-step*) - handle-accept-dialog (mf/use-fn - (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management" - ::ev/origin "settings" - :section "subscription-management-modal"})) - (let [current-href (rt/get-current-href) - returnUrl (js/encodeURIComponent current-href) - href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)] - (st/emit! (rt/nav-raw :href href))) - (modal/hide!))) - handle-close-dialog (mf/use-fn - (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"})) - (modal/hide!))) + subscription-name + (if subscribe-to-trial + (if (= subscription-type "unlimited") + (tr "subscription.settings.unlimited-trial") + (tr "subscription.settings.enterprise-trial")) + (case subscription-type + "professional" (tr "subscription.settings.professional") + "unlimited" (tr "subscription.settings.unlimited") + "enterprise" (tr "subscription.settings.enterprise"))) - handle-unlimited-modal-step (mf/use-fn - (mf/deps unlimited-modal-step) - (fn [] - (if (= unlimited-modal-step 1) - (reset! unlimited-modal-step* 2) - (reset! unlimited-modal-step* 1)))) + min-editors + (if (seq editors) (count editors) 1) - show-editors-list* (mf/use-state false) - show-editors-list (deref show-editors-list*) - handle-click (mf/use-fn - (fn [event] - (dom/stop-propagation event) - (swap! show-editors-list* not)))] + initial + (mf/with-memo [min-editors] + {:min-members min-editors}) + + form + (fm/use-form :schema (schema:seats-form min-editors) + :initial initial) + + form-data-min-editors + (-> @form :clean-data :min-members) + + submit-in-progress* + (mf/use-state false) + + subscribe-to-unlimited + (mf/use-fn + (mf/deps form-data-min-editors) + (fn [add-payment-details] + (when (not @submit-in-progress*) + (let [return-url (-> (rt/get-current-href) (rt/encode-url)) + href (if add-payment-details + (dm/str "payments/subscriptions/create?type=unlimited&show=true&quantity=" form-data-min-editors "&returnUrl=" return-url) + (dm/str "payments/subscriptions/create?type=unlimited&show=false&quantity=" form-data-min-editors "&returnUrl=" return-url))] + (reset! submit-in-progress* true) + (reset! form nil) + (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" + :type "unlimited" + :quantity form-data-min-editors}) + (rt/nav-raw :href href)))))) + + subscribe-to-enterprise + (mf/use-fn + (fn [] + (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" + :type "enterprise"})) + (let [return-url (-> (rt/get-current-href) (rt/encode-url)) + href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)] + (st/emit! (rt/nav-raw :href href))))) + + handle-accept-dialog + (mf/use-fn + (fn [] + (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management" + ::ev/origin "settings" + :section "subscription-management-modal"})) + (let [current-href (rt/get-current-href) + returnUrl (js/encodeURIComponent current-href) + href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)] + (st/emit! (rt/nav-raw :href href))) + (modal/hide!))) + + handle-close-dialog + (mf/use-fn + (fn [] + (st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"})) + (modal/hide!))) + + handle-unlimited-modal-step + (mf/use-fn + (mf/deps unlimited-modal-step) + (fn [] + (if (= unlimited-modal-step 1) + (reset! unlimited-modal-step* 2) + (reset! unlimited-modal-step* 1)))) + + show-editors-list* + (mf/use-state false) + + show-editors-list + (deref show-editors-list*) + + handle-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (swap! show-editors-list* not)))] [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-dialog)} @@ -246,7 +273,7 @@ [:input {:class (stl/css :cancel-button) :type "button" - :value (tr "ubscription.settings.management-dialog.step-2-skip-button") + :value (tr "subscription.settings.management-dialog.step-2-skip-button") :on-click #(subscribe-to-unlimited false)}] [:input diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss index 5bb2443551..e86d54cd16 100644 --- a/frontend/src/app/main/ui/settings/subscription.scss +++ b/frontend/src/app/main/ui/settings/subscription.scss @@ -8,10 +8,12 @@ @use "ds/typography.scss" as t; @use "ds/_borders.scss" as *; @use "ds/spacing.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; .dashboard-section { display: flex; - width: 100%; + inline-size: 100%; justify-content: center; align-items: center; } @@ -20,10 +22,10 @@ display: flex; justify-content: center; flex-direction: column; - max-width: deprecated.$s-500; + max-inline-size: $sz-500; margin-block-end: var(--sp-xxxl); - width: deprecated.$s-580; - margin: deprecated.$s-92 auto deprecated.$s-120 auto; + inline-size: px2rem(580); + margin: px2rem(92) auto px2rem(120) auto; justify-content: center; } @@ -98,8 +100,8 @@ .plan-title-icon { @extend .button-icon; stroke: var(--color-foreground-primary); - height: var(--sp-xl); - width: var(--sp-xl); + block-size: var(--sp-xl); + inline-size: var(--sp-xl); border-radius: 6px; border: 1.75px solid var(--color-foreground-primary); stroke-width: 2.25px; @@ -140,7 +142,7 @@ } .other-subscriptions { - margin-block-start: deprecated.$s-52; + margin-block-start: px2rem(52); } .cta-button { @@ -155,8 +157,8 @@ .cta-button svg { @extend .button-icon; - height: var(--sp-l); - width: var(--sp-l); + block-size: var(--sp-l); + inline-size: var(--sp-l); stroke: var(--color-accent-primary); margin-inline-start: var(--sp-xs); } @@ -179,14 +181,12 @@ .modal-dialog { @extend .modal-container-base; - display: grid; - grid-template-rows: auto 1fr auto; - max-height: initial; - min-width: deprecated.$s-548; + max-block-size: initial; + min-inline-size: px2rem(548); } .modal-dialog.subscription-success { - min-width: deprecated.$s-648; + min-inline-size: px2rem(648); } .close-btn { @@ -232,11 +232,11 @@ .modal-success-content { display: flex; - gap: deprecated.$s-40; + gap: $sz-40; } .modal-footer { - margin-block-start: deprecated.$s-40; + margin-block-start: $sz-40; } .action-buttons { @@ -249,23 +249,28 @@ .primary-button { @extend .modal-accept-btn; + min-block-size: $sz-32; + block-size: auto; } .cancel-button { @extend .modal-cancel-btn; + min-block-size: $sz-32; + white-space: break-spaces; + block-size: auto; } .modal-start { display: flex; justify-content: center; - max-width: deprecated.$s-220; + max-inline-size: $sz-224; svg { - width: 100%; - height: auto; + inline-size: 100%; + block-size: auto; } - @media (max-width: 992px) { + @media (max-inline-size: 992px) { display: none; } } @@ -285,19 +290,19 @@ list-style-position: inside; list-style-type: none; margin-inline-start: var(--sp-xl); - max-height: deprecated.$s-216; + max-block-size: px2rem(216); overflow-y: auto; } .input-field { --input-icon-padding: var(--sp-s); - width: deprecated.$s-80; + inline-size: px2rem(80); } .error-message { @include t.use-typography("body-small"); color: var(--color-foreground-error); - margin-block-start: deprecated.$s-8; + margin-block-start: var(--sp-s); } .editors-wrapper { @@ -316,7 +321,7 @@ @include t.use-typography("body-small"); background-color: var(--color-background-tertiary); border-radius: var(--sp-s); - margin-block-start: deprecated.$s-40; + margin-block-start: $sz-40; padding-block: var(--sp-s); padding-inline: var(--sp-m); } From f58475a7c9a96b19b61288b90d0ad01f59e38a1d Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Mon, 24 Nov 2025 14:36:24 +0100 Subject: [PATCH 03/10] :bug: Fix pasting application/transit+json (#7812) --- CHANGES.md | 2 + .../app/main/data/workspace/clipboard.cljs | 10 ++- frontend/src/app/util/clipboard.cljs | 55 +++++++------ frontend/src/app/util/clipboard.js | 81 +++++++++++++++++-- 4 files changed, 114 insertions(+), 34 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 929385dda7..9c2ed4c55a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -71,6 +71,7 @@ example. It's still usable as before, we just removed the example. - Add new shape validation mechanism for shapes [Github #7696](https://github.com/penpot/penpot/pull/7696) - Apply color tokens from sidebar [Taiga #11353](https://tree.taiga.io/project/penpot/us/11353) - Display tokens in the inspect tab [Taiga #9313](https://tree.taiga.io/project/penpot/us/9313) +- Refactor clipboard behavior to assess some minor inconsistencies and make pasting binary data faster. [Taiga #12571](https://tree.taiga.io/project/penpot/task/12571) ### :bug: Bugs fixed @@ -85,6 +86,7 @@ example. It's still usable as before, we just removed the example. - Fix shortcut conflict in text editor (increase/decrease font size vs word selection) - Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312) - Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294) +- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721) ## 2.11.1 diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index c850b014e5..9e95f9c769 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -254,6 +254,10 @@ (declare ^:private paste-svg-text) (declare ^:private paste-shapes) +(def ^:private default-options + #js {:decodeTransit t/decode-str + :allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")}) + (defn create-paste-from-blob [in-viewport?] (fn [blob] @@ -290,7 +294,7 @@ (ptk/reify ::paste-from-clipboard ptk/WatchEvent (watch [_ _ _] - (->> (clipboard/from-navigator) + (->> (clipboard/from-navigator default-options) (rx/mapcat default-paste-from-blob) (rx/take 1))))) @@ -308,7 +312,7 @@ ;; we forbid that scenario so the default behaviour is executed (if is-editing? (rx/empty) - (->> (clipboard/from-synthetic-clipboard-event event) + (->> (clipboard/from-synthetic-clipboard-event event default-options) (rx/mapcat (create-paste-from-blob in-viewport?)))))))) (defn copy-selected-svg @@ -478,7 +482,7 @@ (js/console.error "Clipboard error:" cause)) (rx/empty)))] - (->> (clipboard/from-navigator) + (->> (clipboard/from-navigator default-options) (rx/mapcat #(.text %)) (rx/map decode-entry) (rx/take 1) diff --git a/frontend/src/app/util/clipboard.cljs b/frontend/src/app/util/clipboard.cljs index 7c7b56da26..e70c567881 100644 --- a/frontend/src/app/util/clipboard.cljs +++ b/frontend/src/app/util/clipboard.cljs @@ -18,47 +18,56 @@ "image/svg+xml"]) (def ^:private default-options - #js {:decodeTransit t/decode-str}) + #js {:decodeTransit t/decode-str + :allowHTMLPaste false}) (defn- from-data-transfer "Get clipboard stream from DataTransfer instance" - [data-transfer] - (->> (rx/from (impl/fromDataTransfer data-transfer default-options)) - (rx/mapcat #(rx/from %)))) + ([data-transfer] + (from-data-transfer data-transfer default-options)) + ([data-transfer options] + (->> (rx/from (impl/fromDataTransfer data-transfer options)) + (rx/mapcat #(rx/from %))))) (defn from-navigator - [] - (->> (rx/from (impl/fromNavigator default-options)) - (rx/mapcat #(rx/from %)))) + ([] + (from-navigator default-options)) + ([options] + (->> (rx/from (impl/fromNavigator options)) + (rx/mapcat #(rx/from %))))) (defn from-clipboard-event "Get clipboard stream from clipboard event" - [event] - (let [cdata (.-clipboardData ^js event)] - (from-data-transfer cdata))) + ([event] + (from-clipboard-event event default-options)) + ([event options] + (let [cdata (.-clipboardData ^js event)] + (from-data-transfer cdata options)))) (defn from-synthetic-clipboard-event "Get clipboard stream from syntetic clipboard event" - [event] - (let [target - (dom/get-target event) + ([event options] + (let [target + (dom/get-target event) - content-editable? - (dom/is-content-editable? target) + content-editable? + (dom/is-content-editable? target) - is-input? - (= (dom/get-tag-name target) "INPUT")] + is-input? + (= (dom/get-tag-name target) "INPUT")] ;; ignore when pasting into an editable control - (when-not (or content-editable? is-input?) - (-> event - (dom/event->browser-event) - (from-clipboard-event))))) + (when-not (or content-editable? is-input?) + (-> event + (dom/event->browser-event) + (from-clipboard-event options)))))) (defn from-drop-event "Get clipboard stream from drop event" - [event] - (from-data-transfer (.-dataTransfer ^js event))) + ([event] + (from-drop-event event default-options)) + ([event options] + (from-data-transfer (.-dataTransfer ^js event) options))) ;; FIXME: rename to `write-text` (defn to-clipboard diff --git a/frontend/src/app/util/clipboard.js b/frontend/src/app/util/clipboard.js index 5fe4d88865..96f4080a7e 100644 --- a/frontend/src/app/util/clipboard.js +++ b/frontend/src/app/util/clipboard.js @@ -27,6 +27,7 @@ const exclusiveTypes = [ /** * @typedef {Object} ClipboardSettings * @property {Function} [decodeTransit] + * @property {boolean} [allowHTMLPaste] */ /** @@ -38,9 +39,7 @@ const exclusiveTypes = [ */ function parseText(text, options) { options = options || {}; - const decodeTransit = options["decodeTransit"]; - if (decodeTransit) { try { decodeTransit(text); @@ -57,18 +56,85 @@ function parseText(text, options) { } } +/** + * Filters ClipboardItem types + * + * @param {ClipboardSettings} options + * @returns {Function} + */ +function filterAllowedTypes(options) { + /** + * @param {string} type + * @returns {boolean} + */ + return function filter(type) { + if ( + (!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) && + type === "text/html" + ) { + return false; + } + return allowedTypes.includes(type); + }; +} + +/** + * Filters DataTransferItems + * + * @param {ClipboardSettings} options + * @returns {Function} + */ +function filterAllowedItems(options) { + /** + * @param {DataTransferItem} + * @returns {boolean} + */ + return function filter(item) { + if ( + (!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) && + item.type === "text/html" + ) { + return false; + } + return allowedTypes.includes(item.type); + }; +} + +/** + * Sorts ClipboardItem types + * + * @param {string} a + * @param {string} b + * @returns {number} + */ +function sortTypes(a, b) { + return allowedTypes.indexOf(a) - allowedTypes.indexOf(b); +} + +/** + * Sorts DataTransferItems + * + * @param {DataTransferItem} a + * @param {DataTransferItem} b + * @returns {number} + */ +function sortItems(a, b) { + return allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type); +} + /** * * @param {ClipboardSettings} [options] * @returns {Promise>} */ export async function fromNavigator(options) { + options = options || {}; const items = await navigator.clipboard.read(); return Promise.all( Array.from(items).map(async (item) => { const itemAllowedTypes = Array.from(item.types) - .filter((type) => allowedTypes.includes(type)) - .sort((a, b) => allowedTypes.indexOf(a) - allowedTypes.indexOf(b)); + .filter(filterAllowedTypes(options)) + .sort(sortTypes); if ( itemAllowedTypes.length === 1 && @@ -96,12 +162,11 @@ export async function fromNavigator(options) { * @returns {Promise>} */ export async function fromDataTransfer(dataTransfer, options) { + options = options || {}; const items = await Promise.all( Array.from(dataTransfer.items) - .filter((item) => allowedTypes.includes(item.type)) - .sort( - (a, b) => allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type), - ) + .filter(filterAllowedItems(options)) + .sort(sortItems) .map(async (item) => { if (item.kind === "file") { return Promise.resolve(item.getAsFile()); From a7552d412a79d1948caed565b06c140aa2801ad2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 24 Nov 2025 11:08:39 +0100 Subject: [PATCH 04/10] :sparkles: Add explicit network asingation and alias on devenv compose --- docker/devenv/docker-compose.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index 33e2036ca8..89580ef8b4 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -115,6 +115,11 @@ services: volumes: - "valkey_data:/data" + networks: + default: + aliases: + - redis + mailer: image: sj26/mailcatcher:latest restart: always @@ -123,6 +128,12 @@ services: ports: - "1080:1080" + networks: + default: + aliases: + - mailer + + # https://github.com/rroemhild/docker-test-openldap ldap: image: rroemhild/test-openldap:2.1 @@ -136,3 +147,9 @@ services: nofile: soft: 1024 hard: 1024 + + networks: + default: + aliases: + - ldap + From dedeae8641c3e972957306d77193407cc354b9a4 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 24 Nov 2025 11:24:55 +0100 Subject: [PATCH 05/10] :bug: Fix incorrect subscription fetching after profile registration --- frontend/src/app/main/data/auth.cljs | 4 +++- frontend/src/app/main/data/profile.cljs | 23 +++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs index cfa11e4263..a933b83590 100644 --- a/frontend/src/app/main/data/auth.cljs +++ b/frontend/src/app/main/data/auth.cljs @@ -195,7 +195,9 @@ (ptk/reify ::login-from-token ptk/WatchEvent (watch [_ _ _] - (->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"}))) + (->> (dp/on-fetch-profile-success profile) + (rx/map (fn [profile] + (logged-in (with-meta profile {::ev/source "login-with-token"})))) ;; NOTE: we need this to be asynchronous because the effect ;; should be called before proceed with the login process (rx/observe-on :async))))) diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs index 01dd5ba7d3..1a77f6b9d3 100644 --- a/frontend/src/app/main/data/profile.cljs +++ b/frontend/src/app/main/data/profile.cljs @@ -77,22 +77,25 @@ (rx/of (rt/nav-raw :href href))) (rx/throw cause)))) +(defn on-fetch-profile-success + [profile] + (if (and (contains? cf/flags :subscriptions) + (is-authenticated? profile)) + (->> (rp/cmd! :get-subscription-usage {}) + (rx/map (fn [{:keys [editors]}] + (update-in profile [:props :subscription] assoc :editors editors))) + (rx/catch (fn [cause] + (js/console.error "unexpected error on obtaining subscription usage" cause) + (rx/of profile)))) + (rx/of profile))) + (defn fetch-profile [] (ptk/reify ::fetch-profile ptk/WatchEvent (watch [_ _ _] (->> (rp/cmd! :get-profile) - (rx/mapcat (fn [profile] - (if (and (contains? cf/flags :subscriptions) - (is-authenticated? profile)) - (->> (rp/cmd! :get-subscription-usage {}) - (rx/map (fn [{:keys [editors]}] - (update-in profile [:props :subscription] assoc :editors editors))) - (rx/catch (fn [cause] - (js/console.error "unexpected error on obtaining subscription usage" cause) - (rx/of profile)))) - (rx/of profile)))) + (rx/mapcat on-fetch-profile-success) (rx/map (partial ptk/data-event ::profile-fetched)) (rx/catch on-fetch-profile-exception))))) From dd5f3396d12d90f1f029a9769e4a563bc43f3117 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 24 Nov 2025 16:11:02 +0100 Subject: [PATCH 06/10] :bug: Fix problem with layout z-index --- frontend/src/app/render_wasm/api.cljs | 2 +- render-wasm/src/render.rs | 6 +++--- render-wasm/src/shapes/modifiers.rs | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index dcae37fd02..6646fdc0b1 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -739,7 +739,7 @@ (d/nilv align-self 0) is-absolute - (d/nilv z-index)))) + (d/nilv z-index 0)))) (defn clear-layout [] diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index d7fc7b30af..3875da7f00 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1641,9 +1641,9 @@ impl RenderState { } children_ids.sort_by(|id1, id2| { - let z1 = tree.get(id1).map_or_else(|| 0, |s| s.z_index()); - let z2 = tree.get(id2).map_or_else(|| 0, |s| s.z_index()); - z1.cmp(&z2) + let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0); + let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0); + z2.cmp(&z1) }); } diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index c7ca3e5484..f090cf98a7 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -332,6 +332,7 @@ fn propagate_reflow( } _ => { // Other shapes don't have to be reflown + reflow_parent = true; } } From 2640889dc8cfda929567dd6440cca643e5892f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Tue, 25 Nov 2025 10:37:26 +0100 Subject: [PATCH 07/10] :bug: Fix backwards compatibility importing files with token themes --- common/src/app/common/types/tokens_lib.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index ec3a3e7bf9..70e36bfc2f 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -1679,7 +1679,7 @@ Will return a value that matches this schema: ["id" {:optional true} :string] ["name" :string] ["description" :string] - ["isSource" :boolean] + ["isSource" {:optional true} :boolean] ["selectedTokenSets" [:map-of :string [:enum "enabled" "disabled"]]]]]] ["$metadata" {:optional true} From 33417a4b2049ff24b6441eb67fa89a9765bc852e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 25 Nov 2025 09:57:36 +0100 Subject: [PATCH 08/10] :bug: Fix svg attrs stroke-linecap stroke-linejoin fill-rule --- frontend/src/app/render_wasm/api.cljs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 6646fdc0b1..cfcd403870 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -468,14 +468,14 @@ [attrs] (let [style (:style attrs) ;; Filter to only supported attributes - allowed-keys #{:fill :fillRule :strokeLinecap :strokeLinejoin} + allowed-keys #{:fill :fillRule :fill-rule :strokeLinecap :stroke-linecap :strokeLinejoin :stroke-linejoin} attrs (-> attrs (dissoc :style) (merge style) (select-keys allowed-keys)) - fill-rule (-> attrs :fillRule sr/translate-fill-rule) - stroke-linecap (-> attrs :strokeLinecap sr/translate-stroke-linecap) - stroke-linejoin (-> attrs :strokeLinejoin sr/translate-stroke-linejoin) + fill-rule (or (-> attrs :fill-rule sr/translate-fill-rule) (-> attrs :fillRule sr/translate-fill-rule)) + stroke-linecap (or (-> attrs :stroke-linecap sr/translate-stroke-linecap) (-> attrs :strokeLinecap sr/translate-stroke-linecap)) + stroke-linejoin (or (-> attrs :stroke-linejoin sr/translate-stroke-linejoin) (-> attrs :strokeLinejoin sr/translate-stroke-linejoin)) fill-none (= "none" (-> attrs :fill))] (h/call wasm/internal-module "_set_shape_svg_attrs" fill-rule stroke-linecap stroke-linejoin fill-none))) From aaca2c41d8d436a37db55e24a90df0772e22360c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Tue, 25 Nov 2025 17:00:14 +0100 Subject: [PATCH 09/10] :books: Add metadescriptions to some help center pages (#7821) --- docs/technical-guide/developer/abstraction-levels.md | 1 + docs/technical-guide/developer/backend.md | 1 + docs/technical-guide/getting-started/elestio.md | 1 + docs/user-guide/design-systems/design-tokens.njk | 1 + 4 files changed, 4 insertions(+) diff --git a/docs/technical-guide/developer/abstraction-levels.md b/docs/technical-guide/developer/abstraction-levels.md index b2cd59d0b6..361b007691 100644 --- a/docs/technical-guide/developer/abstraction-levels.md +++ b/docs/technical-guide/developer/abstraction-levels.md @@ -1,5 +1,6 @@ --- title: 3.07. Abstraction levels +desc: "Penpot Technical Guide: organize data and logic in clear abstraction layers—ADTs, file ops, event-sourced changes, business rules, and data events." --- # Code organization in abstraction levels diff --git a/docs/technical-guide/developer/backend.md b/docs/technical-guide/developer/backend.md index 0f60c928d7..f2e7bc4d15 100644 --- a/docs/technical-guide/developer/backend.md +++ b/docs/technical-guide/developer/backend.md @@ -1,5 +1,6 @@ --- title: 3.06. Backend Guide +desc: "Penpot Technical Guide: Backend basics - REPL setup, loading fixtures, database migrations, and clj-kondo linting to speed development workflows." --- # Backend guide # diff --git a/docs/technical-guide/getting-started/elestio.md b/docs/technical-guide/getting-started/elestio.md index 0723ba0f16..b6dc49af2a 100644 --- a/docs/technical-guide/getting-started/elestio.md +++ b/docs/technical-guide/getting-started/elestio.md @@ -1,5 +1,6 @@ --- title: 1.2 Install with Elestio +desc: "Step-by-step guide to deploy a self-hosted Penpot on Elestio: 3-minute setup, managed DNS/SMTP/SSL/backups, Docker Compose config, updates & support." --- # Install with Elestio diff --git a/docs/user-guide/design-systems/design-tokens.njk b/docs/user-guide/design-systems/design-tokens.njk index ec8a21aa0b..3a328fe5a8 100644 --- a/docs/user-guide/design-systems/design-tokens.njk +++ b/docs/user-guide/design-systems/design-tokens.njk @@ -1,6 +1,7 @@ --- title: Design Tokens order: 5 +desc: Learn how to create, manage and apply Penpot Design Tokens using W3C DTCG format, with sets, themes, aliases, equations and JSON import/export. ---

Design Tokens

From 8880f07a6a39edf01e26916263b41b9c1f98ef8b Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 25 Nov 2025 17:56:00 +0100 Subject: [PATCH 10/10] :bug: Fix problem with thumbnails in parallel --- frontend/src/app/main/ui/dashboard/grid.cljs | 8 +- frontend/src/app/render_wasm/api.cljs | 131 ++++++++++--------- frontend/src/app/render_wasm/api/fonts.cljs | 13 +- frontend/src/app/worker/thumbnails.cljs | 104 +++++++++------ render-wasm/src/wasm/text.rs | 7 +- 5 files changed, 143 insertions(+), 120 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 695e9fb44e..e6cc837b52 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -60,10 +60,10 @@ (defn render-thumbnail [file-id revn] (if (features/active-feature? @st/state "render-wasm/v1") - (->> (mw/ask! {:cmd :thumbnails/generate-for-file-wasm - :revn revn - :file-id file-id - :width thumbnail-width})) + (mw/ask! {:cmd :thumbnails/generate-for-file-wasm + :revn revn + :file-id file-id + :width thumbnail-width}) (->> (mw/ask! {:cmd :thumbnails/generate-for-file :revn revn :file-id file-id diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index cfcd403870..ba5525791e 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -128,11 +128,12 @@ (defn update-text-rect! [id] - (mw/emit! - {:cmd :index/update-text-rect - :page-id (:current-page-id @st/state) - :shape-id id - :dimensions (get-text-dimensions id)})) + (when wasm/context-initialized? + (mw/emit! + {:cmd :index/update-text-rect + :page-id (:current-page-id @st/state) + :shape-id id + :dimensions (get-text-dimensions id)}))) (defn- ensure-text-content @@ -198,70 +199,71 @@ (defn set-shape-children [children] (perf/begin-measure "set-shape-children") - (case (count children) - 0 - (h/call wasm/internal-module "_set_children_0") + (let [children (into [] (filter uuid?) children)] + (case (count children) + 0 + (h/call wasm/internal-module "_set_children_0") - 1 - (let [[c1] children - c1 (uuid/get-u32 c1)] - (h/call wasm/internal-module "_set_children_1" - (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3))) + 1 + (let [[c1] children + c1 (uuid/get-u32 c1)] + (h/call wasm/internal-module "_set_children_1" + (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3))) - 2 - (let [[c1 c2] children - c1 (uuid/get-u32 c1) - c2 (uuid/get-u32 c2)] - (h/call wasm/internal-module "_set_children_2" - (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3) - (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3))) + 2 + (let [[c1 c2] children + c1 (uuid/get-u32 c1) + c2 (uuid/get-u32 c2)] + (h/call wasm/internal-module "_set_children_2" + (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3) + (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3))) - 3 - (let [[c1 c2 c3] children - c1 (uuid/get-u32 c1) - c2 (uuid/get-u32 c2) - c3 (uuid/get-u32 c3)] - (h/call wasm/internal-module "_set_children_3" - (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3) - (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3) - (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3))) + 3 + (let [[c1 c2 c3] children + c1 (uuid/get-u32 c1) + c2 (uuid/get-u32 c2) + c3 (uuid/get-u32 c3)] + (h/call wasm/internal-module "_set_children_3" + (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3) + (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3) + (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3))) - 4 - (let [[c1 c2 c3 c4] children - c1 (uuid/get-u32 c1) - c2 (uuid/get-u32 c2) - c3 (uuid/get-u32 c3) - c4 (uuid/get-u32 c4)] - (h/call wasm/internal-module "_set_children_4" - (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3) - (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3) - (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3) - (aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3))) + 4 + (let [[c1 c2 c3 c4] children + c1 (uuid/get-u32 c1) + c2 (uuid/get-u32 c2) + c3 (uuid/get-u32 c3) + c4 (uuid/get-u32 c4)] + (h/call wasm/internal-module "_set_children_4" + (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3) + (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3) + (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3) + (aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3))) - 5 - (let [[c1 c2 c3 c4 c5] children - c1 (uuid/get-u32 c1) - c2 (uuid/get-u32 c2) - c3 (uuid/get-u32 c3) - c4 (uuid/get-u32 c4) - c5 (uuid/get-u32 c5)] - (h/call wasm/internal-module "_set_children_5" - (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3) - (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3) - (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3) - (aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3) - (aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3))) + 5 + (let [[c1 c2 c3 c4 c5] children + c1 (uuid/get-u32 c1) + c2 (uuid/get-u32 c2) + c3 (uuid/get-u32 c3) + c4 (uuid/get-u32 c4) + c5 (uuid/get-u32 c5)] + (h/call wasm/internal-module "_set_children_5" + (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3) + (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3) + (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3) + (aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3) + (aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3))) - ;; Dynamic call for children > 5 - (let [heap (mem/get-heap-u32) - size (mem/get-alloc-size children UUID-U8-SIZE) - offset (mem/alloc->offset-32 size)] - (reduce - (fn [offset id] - (mem.h32/write-uuid offset heap id)) - offset - children) - (h/call wasm/internal-module "_set_children"))) + ;; Dynamic call for children > 5 + (let [heap (mem/get-heap-u32) + size (mem/get-alloc-size children UUID-U8-SIZE) + offset (mem/alloc->offset-32 size)] + (reduce + (fn [offset id] + (mem.h32/write-uuid offset heap id)) + offset + children) + (h/call wasm/internal-module "_set_children")))) (perf/end-measure "set-shape-children") nil) @@ -1031,8 +1033,9 @@ (into full-acc full))) {:thumbnails thumbnails-acc :full full-acc}))] (perf/end-measure "set-objects") - (process-pending shapes thumbnails full render-callback + (process-pending shapes thumbnails full noop-fn (fn [] + (when render-callback (render-callback)) (ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))) (defn clear-focus-mode diff --git a/frontend/src/app/render_wasm/api/fonts.cljs b/frontend/src/app/render_wasm/api/fonts.cljs index 1962ed224f..1e3bcc6ca5 100644 --- a/frontend/src/app/render_wasm/api/fonts.cljs +++ b/frontend/src/app/render_wasm/api/fonts.cljs @@ -83,12 +83,13 @@ (defn update-text-layout [id] - (let [shape-id-buffer (uuid/get-u32 id)] - (h/call wasm/internal-module "_update_shape_text_layout_for" - (aget shape-id-buffer 0) - (aget shape-id-buffer 1) - (aget shape-id-buffer 2) - (aget shape-id-buffer 3)))) + (when wasm/context-initialized? + (let [shape-id-buffer (uuid/get-u32 id)] + (h/call wasm/internal-module "_update_shape_text_layout_for" + (aget shape-id-buffer 0) + (aget shape-id-buffer 1) + (aget shape-id-buffer 2) + (aget shape-id-buffer 3))))) ;; IMPORTANT: Only TTF fonts can be stored. (defn- store-font-buffer diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index 5871c8c58e..517d7895a4 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -13,6 +13,7 @@ [app.common.logging :as log] [app.common.types.color :as cc] [app.common.uri :as u] + [app.common.uuid :as uuid] [app.config :as cf] [app.main.fonts :as fonts] [app.main.render :as render] @@ -125,58 +126,75 @@ (def thumbnail-aspect-ratio (/ 2 3)) -(defmethod impl/handler :thumbnails/generate-for-file-wasm - [{:keys [file-id revn width] :as message} _] +(defn render-canvas-blob + [canvas width height background-color] + (-> (.convertToBlob canvas) + (p/then + (fn [blob] + (rds/renderToStaticMarkup + (mf/element + svg-wrapper + #js {:data-uri (blob->uri blob) + :width width + :height height + :background background-color})))))) +(defn process-wasm-thumbnail + [{:keys [id file-id revn width] :as message}] (->> (rx/from @init-wasm) (rx/mapcat #(request-data-for-thumbnail file-id revn false)) (rx/mapcat (fn [{:keys [page] :as file}] (rx/create (fn [subs] - (try - (let [background-color (or (:background page) cc/canvas) - height (* width thumbnail-aspect-ratio) - canvas (js/OffscreenCanvas. width height) - init? (wasm.api/init-canvas-context canvas)] - (if init? - (let [objects (:objects page) - frame (some->> page :thumbnail-frame-id (get objects)) - vbox (if frame - (-> (gsb/get-object-bounds objects frame) - (grc/fix-aspect-ratio thumbnail-aspect-ratio)) - (render/calculate-dimensions objects thumbnail-aspect-ratio)) - zoom (/ width (:width vbox))] + (let [background-color (or (:background page) cc/canvas) + height (* width thumbnail-aspect-ratio) + canvas (js/OffscreenCanvas. width height) + init? (wasm.api/init-canvas-context canvas)] + (if init? + (let [objects (:objects page) + frame (some->> page :thumbnail-frame-id (get objects)) + vbox (if frame + (-> (gsb/get-object-bounds objects frame) + (grc/fix-aspect-ratio thumbnail-aspect-ratio)) + (render/calculate-dimensions objects thumbnail-aspect-ratio)) + zoom (/ width (:width vbox))] - (wasm.api/initialize-viewport - objects zoom vbox background-color - (fn [] - (if frame - (wasm.api/render-sync-shape (:id frame)) - (wasm.api/render-sync)) + (wasm.api/initialize-viewport + objects zoom vbox background-color + (fn [] + (if frame + (wasm.api/render-sync-shape (:id frame)) + (wasm.api/render-sync)) - (-> (.convertToBlob canvas) - (p/then - (fn [blob] - (let [data - (rds/renderToStaticMarkup - (mf/element - svg-wrapper - #js {:data-uri (blob->uri blob) - :width width - :height height - :background background-color}))] - (rx/push! subs {:data data :file-id file-id :revn revn})))) - (p/catch #(do (.error js/console %) - (rx/error! subs %))) - (p/finally #(rx/end! subs)))))) + (-> (render-canvas-blob canvas width height background-color) + (p/then #(rx/push! subs {:id id :data % :file-id file-id :revn revn})) + (p/catch #(rx/error! subs %)) + (p/finally #(rx/end! subs)))))) - (do (rx/error! subs "Error loading webgl context") - (rx/end! subs))) + (rx/end! subs)) - nil) + nil))))))) - (catch :default err - (.error js/console err) - (rx/error! subs err) - (rx/end! subs))))))))) +(defonce thumbs-subject (rx/subject)) + +(defonce thumbs-stream + (->> thumbs-subject + (rx/mapcat process-wasm-thumbnail) + (rx/share))) + +(defmethod impl/handler :thumbnails/generate-for-file-wasm + [message _] + (rx/create + (fn [subs] + (let [id (uuid/next) + sid + (->> thumbs-stream + (rx/filter #(= id (:id %))) + (rx/subs! + #(do + (rx/push! subs %) + (rx/end! subs))))] + (rx/push! thumbs-subject (assoc message :id id)) + + #(rx/dispose! sid))))) diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index a45b03c9de..f4dcc12086 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -291,9 +291,10 @@ pub extern "C" fn set_shape_text_content() { let bytes = mem::bytes(); with_current_shape_mut!(state, |shape: &mut Shape| { let raw_text_data = RawParagraph::try_from(&bytes).unwrap(); - shape - .add_paragraph(raw_text_data.into()) - .expect("Failed to add paragraph"); + + if let Err(_) = shape.add_paragraph(raw_text_data.into()) { + println!("Error with set_shape_text_content on {:?}", shape.id); + } }); mem::free_bytes(); }