Compare commits

...

10 Commits

Author SHA1 Message Date
Andrey Antukh
5c71c57dd9 Merge tag '2.12.1' 2025-12-30 15:37:30 +01:00
Alejandro Alonso
48e3f35bb3 🐛 Fix setting a portion of text as bold or underline messes things up 2025-12-30 11:34:24 +01:00
Yamila Moreno
d3ee50daf5 🔧 Add ci for branch staging-render 2025-12-30 11:13:00 +01:00
Andrey Antukh
de052b5161 📎 Update changelog 2025-12-29 11:10:04 +01:00
Andrey Antukh
8a3b33797f 🐛 Fix error handling on password change form
Fixes https://github.com/penpot/penpot/issues/7978
2025-12-29 10:27:27 +01:00
Andrey Antukh
13fd20f76f Backport form error management improvements from develop 2025-12-29 10:27:27 +01:00
Andrey Antukh
01ecde3bfa Add the ability to add relations on penpot sdk (#7987)
*  Add the ability to add relations on penpot sdk

* 📎 Remove debug console log
2025-12-22 20:55:31 +01:00
Alonso Torres
4000ec8762 🐛 Fix problem resizing auto size layouts (#7995) 2025-12-22 20:17:11 +01:00
Andrey Antukh
5abc1aafb4 Merge tag '2.12.0-RC3' 2025-12-12 12:19:29 +01:00
Andrey Antukh
935728aa39 🔧 Backport build-tag github workflow from develop 2025-12-05 10:26:01 +01:00
13 changed files with 178 additions and 50 deletions

View File

@@ -0,0 +1,14 @@
name: _STAGING RENDER
on:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging-render"
build_wasm: "yes"
build_storybook: "yes"

View File

@@ -1,6 +1,12 @@
# CHANGELOG
## 2.12.0 (Unreleased)
## 2.12.1
### :bug: Bugs fixed
- Fix setting a portion of text as bold or underline messes things up [Github #7980](https://github.com/penpot/penpot/issues/7980)
## 2.12.0
### :boom: Breaking changes & Deprecations

View File

@@ -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)

View File

@@ -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] "")

View File

@@ -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]

View File

@@ -106,9 +106,11 @@
:overflowWrap "initial"
:lineBreak "auto"
:whiteSpace "break-spaces"
:textRendering "geometricPrecision"
:display "inline-block"
:verticalAlign "top"}
:textRendering "geometricPrecision"}
base (cond-> base
(= (:line-height data) "0")
(-> (obj/set! "display" "inline-block")
(obj/set! "verticalAlign" "top")))
fills
(cond
;; DEPRECATED: still here for backward compatibility with

View File

@@ -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]

View File

@@ -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

View File

@@ -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",

View File

@@ -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);
});

View File

@@ -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)))

View File

@@ -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"

View File

@@ -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();