From bd580ab1594d114cd3a88fbe9f79a275521e6a1d Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 22 Dec 2025 17:14:37 +0100 Subject: [PATCH 1/5] :bug: Fix problem when changing colors with multiple fonts --- frontend/src/app/main/data/workspace/texts.cljs | 2 +- .../ui/workspace/sidebar/options/menus/typography.cljs | 2 +- frontend/src/app/util/text/content/styles.cljs | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index eb7312744c..f2fdf75aa4 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -554,7 +554,7 @@ (when (features/active-feature? state "text-editor/v2") (let [instance (:workspace-editor state) styles (some-> (editor.v2/getCurrentStyle instance) - (styles/get-styles-from-style-declaration) + (styles/get-styles-from-style-declaration :removed-mixed true) ((comp update-node-fn migrate-node)) (styles/attrs->styles))] (editor.v2/applyStylesToSelection instance styles))))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 51e74d6f49..79d745421a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -307,7 +307,7 @@ :title (tr "inspect.attributes.typography.font-family") :on-click #(reset! open-selector? true)} (cond - (= :multiple font-id) + (or (= :multiple font-id) (= "mixed" font-id)) "--" (some? font) diff --git a/frontend/src/app/util/text/content/styles.cljs b/frontend/src/app/util/text/content/styles.cljs index f37938525c..4b801ee864 100644 --- a/frontend/src/app/util/text/content/styles.cljs +++ b/frontend/src/app/util/text/content/styles.cljs @@ -187,19 +187,23 @@ style-value (normalize-style-value style-name v)] (assoc acc style-name style-value)))) {} style-defaults))) +(def mixed-values #{:mixed :multiple "mixed" "multiple"}) + (defn get-styles-from-style-declaration "Returns a ClojureScript object compatible with text nodes" - [style-declaration] + [style-declaration & {:keys [removed-mixed] :or {removed-mixed false}}] (reduce (fn [acc k] (if (contains? mapping k) (let [style-name (get-style-name-as-css-variable k) [_ style-decode] (get mapping k) style-value (.getPropertyValue style-declaration style-name)] - (assoc acc k (style-decode style-value))) + (when (or (not removed-mixed) (not (contains? mixed-values style-value))) + (assoc acc k (style-decode style-value)))) (let [style-name (get-style-name k) style-value (normalize-attr-value k (.getPropertyValue style-declaration style-name))] - (assoc acc k style-value)))) {} txt/text-style-attrs)) + (when (or (not removed-mixed) (not (contains? mixed-values style-value))) + (assoc acc k style-value))))) {} txt/text-style-attrs)) (defn get-styles-from-event "Returns a ClojureScript object compatible with text nodes" From 4000ec8762a01adac3f6e8806f9e7ecbede19c45 Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Mon, 22 Dec 2025 20:17:11 +0100 Subject: [PATCH 2/5] :bug: Fix problem resizing auto size layouts (#7995) --- frontend/src/app/main/data/workspace/transforms.cljs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 7583f3c4da..dd427e6bd7 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -238,12 +238,12 @@ :always (ctm/resize scalev resize-origin shape-transform shape-transform-inverse) - (and (ctl/any-layout-immediate-child? objects shape) + (and (or (ctl/any-layout-immediate-child? objects shape) (ctl/any-layout? shape)) (not= (:layout-item-h-sizing shape) :fix) ^boolean change-width?) (ctm/change-property :layout-item-h-sizing :fix) - (and (ctl/any-layout-immediate-child? objects shape) + (and (or (ctl/any-layout-immediate-child? objects shape) (ctl/any-layout? shape)) (not= (:layout-item-v-sizing shape) :fix) ^boolean change-height?) (ctm/change-property :layout-item-v-sizing :fix) From 01ecde3bfa7bc9d3ba674bcdaf053077c7d60a25 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 22 Dec 2025 20:55:31 +0100 Subject: [PATCH 3/5] :sparkles: Add the ability to add relations on penpot sdk (#7987) * :sparkles: Add the ability to add relations on penpot sdk * :paperclip: Remove debug console log --- library/CHANGES.md | 6 ++++++ library/package.json | 4 ++-- library/playground/sample-relations.js | 30 ++++++++++++++++++++++++++ library/src/lib/builder.cljs | 13 ++++++++++- library/src/lib/export.cljs | 3 ++- library/test/builder.test.js | 27 +++++++++++++++++++++++ 6 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 library/playground/sample-relations.js diff --git a/library/CHANGES.md b/library/CHANGES.md index e136c7759c..ac3e4ca0bd 100644 --- a/library/CHANGES.md +++ b/library/CHANGES.md @@ -1,5 +1,11 @@ # CHANGELOG + +## 1.2.0-RC1 + +- Add the ability to add relations (with `addRelation` method) + + ## 1.1.0 - Same as 1.1.0-RC2 diff --git a/library/package.json b/library/package.json index 3eb2bac236..ab32814487 100644 --- a/library/package.json +++ b/library/package.json @@ -1,9 +1,9 @@ { "name": "@penpot/library", - "version": "1.1.0", + "version": "1.2.0-RC1", "license": "MPL-2.0", "author": "Kaleidos INC", - "packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54", + "packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8", "type": "module", "repository": { "type": "git", diff --git a/library/playground/sample-relations.js b/library/playground/sample-relations.js new file mode 100644 index 0000000000..ab2350c812 --- /dev/null +++ b/library/playground/sample-relations.js @@ -0,0 +1,30 @@ +import * as penpot from "#self"; +import { writeFile, readFile } from "fs/promises"; + +(async function () { + const context = penpot.createBuildContext(); + + { + const file1 = context.addFile({ name: "Test File 1" }); + const file2 = context.addFile({ name: "Test File 1" }); + + context.addRelation(file1, file2); + } + + { + let result = await penpot.exportAsBytes(context); + await writeFile("sample-relations.zip", result); + } +})() + .catch((cause) => { + console.error(cause); + + const innerCause = cause.cause; + if (innerCause) { + console.error("Inner cause:", innerCause); + } + process.exit(-1); + }) + .finally(() => { + process.exit(0); + }); diff --git a/library/src/lib/builder.cljs b/library/src/lib/builder.cljs index 316631d8d4..4cc8cc97e0 100644 --- a/library/src/lib/builder.cljs +++ b/library/src/lib/builder.cljs @@ -87,7 +87,8 @@ (try (let [params (-> params decode-params fb/decode-file)] (-> (swap! state fb/add-file params) - (get ::fb/current-file-id))) + (get ::fb/current-file-id) + (dm/str))) (catch :default cause (handle-exception cause)))) @@ -273,6 +274,16 @@ (catch :default cause (handle-exception cause)))) + :addRelation + (fn [file-id library-id] + (let [file-id (uuid/parse file-id) + library-id (uuid/parse library-id)] + (if (and file-id library-id) + (do + (swap! state update :relations assoc file-id library-id) + true) + false))) + :genId (fn [] (dm/str (uuid/next))) diff --git a/library/src/lib/export.cljs b/library/src/lib/export.cljs index 72c779c24e..1c0e883d0e 100644 --- a/library/src/lib/export.cljs +++ b/library/src/lib/export.cljs @@ -194,7 +194,8 @@ :generated-by "penpot-library/%version%" :referer (get opts :referer) :files files - :relations []} + :relations (->> (:relations state) + (mapv vec))} params (d/without-nils params)] ["manifest.json" diff --git a/library/test/builder.test.js b/library/test/builder.test.js index 207f863e35..e4c7bf8a37 100644 --- a/library/test/builder.test.js +++ b/library/test/builder.test.js @@ -54,6 +54,33 @@ test("create context with two file", () => { assert.equal(file.data.pages.length, 0) }); +test("create context with two file and relation between", () => { + const context = penpot.createBuildContext(); + + const fileId_1 = context.addFile({name: "sample 1"}); + const fileId_2 = context.addFile({name: "sample 2"}); + + context.addRelation(fileId_1, fileId_2); + + const internalState = context.getInternalState(); + + assert.ok(internalState.files[fileId_1]); + assert.ok(internalState.files[fileId_2]); + assert.equal(internalState.files[fileId_1].name, "sample 1"); + assert.equal(internalState.files[fileId_2].name, "sample 2"); + + assert.ok(internalState.relations[fileId_1]); + assert.equal(internalState.relations[fileId_1], fileId_2); + + const file = internalState.files[fileId_2]; + + assert.ok(file.data); + assert.ok(file.data.pages); + assert.ok(file.data.pagesIndex); + assert.equal(file.data.pages.length, 0) +}); + + test("create context with file and page", () => { const context = penpot.createBuildContext(); From 13fd20f76f5520fcff64f9a787b732d53de8b48d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 19 Dec 2025 10:08:22 +0100 Subject: [PATCH 4/5] :rewind: Backport form error management improvements from develop --- .../src/app/main/ui/components/forms.cljs | 3 +- frontend/src/app/util/forms.cljs | 88 ++++++++++++------- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 45419d56ca..0e6c55882a 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -50,7 +50,8 @@ touched? (and (contains? (:data @form) input-name) (get-in @form [:touched input-name])) - error (get-in @form [:errors input-name]) + error (or (get-in @form [:errors input-name]) + (get-in @form [:extra-errors input-name])) value (get-in @form [:data input-name] "") diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs index 968c319e7f..a48fae2310 100644 --- a/frontend/src/app/util/forms.cljs +++ b/frontend/src/app/util/forms.cljs @@ -48,7 +48,11 @@ (let [props (m/properties schema) tprops (m/type-properties schema) field (or (first in) - (:error/field props))] + (:error/field props)) + + field (if (vector? field) + field + [field])] (if (contains? acc field) acc @@ -58,30 +62,30 @@ (or (= type :malli.core/missing-key) (nil? value)) - (assoc acc field {:message (tr "errors.field-missing")}) + (assoc-in acc field {:message (tr "errors.field-missing")}) ;; --- CHECK on schema props (contains? props :error/fn) - (assoc acc field (handle-error-fn props problem)) + (assoc-in acc field (handle-error-fn props problem)) (contains? props :error/message) - (assoc acc field (handle-error-message props)) + (assoc-in acc field (handle-error-message props)) (contains? props :error/code) - (assoc acc field (handle-error-code props)) + (assoc-in acc field (handle-error-code props)) ;; --- CHECK on type props (contains? tprops :error/fn) - (assoc acc field (handle-error-fn tprops problem)) + (assoc-in acc field (handle-error-fn tprops problem)) (contains? tprops :error/message) - (assoc acc field (handle-error-message tprops)) + (assoc-in acc field (handle-error-message tprops)) (contains? tprops :error/code) - (assoc acc field (handle-error-code tprops)) + (assoc-in acc field (handle-error-code tprops)) :else - (assoc acc field {:message (tr "errors.invalid-data")}))))) + (assoc-in acc field {:message (tr "errors.invalid-data")}))))) (defn- use-rerender-fn [] @@ -114,20 +118,35 @@ [f {:keys [schema validators]}] (fn [& args] (let [state (apply f args) - cleaned (sm/decode schema (:data state) sm/string-transformer) + cleaned (sm/decode schema (:data state) sm/json-transformer) valid? (sm/validate schema cleaned) - errors (when-not valid? - (collect-schema-errors schema validators state))] + + errors + (when-not valid? + (collect-schema-errors schema validators state)) + + extra-errors + (not-empty (:extra-errors state))] (assoc state :errors errors :clean-data (when valid? cleaned) - :valid (and (not errors) valid?))))) + :valid (and (not errors) + (not extra-errors) + valid?))))) + + +(defn- make-initial-state + [initial-data] + (let [initial (if (fn? initial-data) (initial-data) initial-data) + initial (d/nilv initial {})] + {:initial initial + :data initial + :errors {} + :touched {}})) (defn- create-form-mutator - [internal-state rerender-fn wrap-update-fn initial opts] - (mf/set-ref-val! internal-state initial) - + [internal-state rerender-fn wrap-update-fn opts] (reify IDeref (-deref [_] @@ -136,7 +155,10 @@ IReset (-reset! [_ new-value] (if (nil? new-value) - (mf/set-ref-val! internal-state (if (fn? initial) (initial) initial)) + (let [initial (-> (mf/ref-val internal-state) + (get :initial) + (make-initial-state))] + (mf/set-ref-val! internal-state initial)) (mf/set-ref-val! internal-state new-value)) (rerender-fn)) @@ -162,24 +184,25 @@ (rerender-fn))))) (defn use-form - [& {:keys [initial] :as opts}] + [& {:keys [initial schema validators] :as opts}] (let [rerender-fn (use-rerender-fn) initial (mf/with-memo [initial] - {:data (if (fn? initial) (initial) initial) - :errors {} - :touched {}}) + (make-initial-state initial)) internal-state - (mf/use-ref nil) + (mf/use-ref initial) form-mutator - (mf/with-memo [initial] - (create-form-mutator internal-state rerender-fn wrap-update-schema-fn initial opts))] + (mf/with-memo [schema validators] + (let [mutator (create-form-mutator internal-state rerender-fn wrap-update-schema-fn + (select-keys opts [:schema :validators]))] + (swap! mutator identity) + mutator))] ;; Initialize internal state once - (mf/with-layout-effect [] + (mf/with-effect [] (mf/set-ref-val! internal-state initial)) (mf/with-effect [initial] @@ -191,11 +214,16 @@ ([form field value] (on-input-change form field value false)) ([form field value trim?] - (swap! form (fn [state] - (-> state - (assoc-in [:touched field] true) - (assoc-in [:data field] (if trim? (str/trim value) value)) - (update :errors dissoc field)))))) + (letfn [(clean-errors [errors] + (-> errors + (dissoc field) + (not-empty)))] + (swap! form (fn [state] + (-> state + (assoc-in [:touched field] true) + (assoc-in [:data field] (if trim? (str/trim value) value)) + (update :errors clean-errors) + (update :extra-errors clean-errors))))))) (defn update-input-value! [form field value] From 8a3b33797fe7b68f334cfff136ed13a0c3295255 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 19 Dec 2025 10:08:48 +0100 Subject: [PATCH 5/5] :bug: Fix error handling on password change form Fixes https://github.com/penpot/penpot/issues/7978 --- .../src/app/main/ui/settings/password.cljs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs index 5de1e2796b..a5a2dd2b93 100644 --- a/frontend/src/app/main/ui/settings/password.cljs +++ b/frontend/src/app/main/ui/settings/password.cljs @@ -18,16 +18,18 @@ (defn- on-error [form error] - (case (:code (ex-data error)) - :old-password-not-match - (swap! form assoc-in [:errors :password-old] - {:message (tr "errors.wrong-old-password")}) - :email-as-password - (swap! form assoc-in [:errors :password-1] - {:message (tr "errors.email-as-password")}) + (let [data (ex-data error)] + (case (:code data) + :old-password-not-match + (swap! form assoc-in [:extra-errors :password-old] + {:message (tr "errors.wrong-old-password")}) - (let [msg (tr "generic.error")] - (st/emit! (ntf/error msg))))) + :email-as-password + (swap! form assoc-in [:extra-errors :password-1] + {:message (tr "errors.email-as-password")}) + + (let [msg (tr "generic.error")] + (st/emit! (ntf/error msg)))))) (defn- on-success [form]