21344 Commits

Author SHA1 Message Date
Andrey Antukh
6fa440cf92 🎉 Add chunked upload API for large media and binary files
Introduce a purpose-agnostic three-step session-based upload API that
allows uploading large binary blobs (media files and .penpot imports)
without hitting multipart size limits.

Backend:
- Migration 0147: new `upload_session` table (profile_id, total_chunks,
  created_at) with indexes on profile_id and created_at.
- Three new RPC commands in media.clj:
    * `create-upload-session`  – allocates a session row; enforces
      `upload-sessions-per-profile` and `upload-chunks-per-session`
      quota limits (configurable in config.clj, defaults 5 / 20).
    * `upload-chunk`           – stores each slice as a storage object;
      validates chunk index bounds and profile ownership.
    * `assemble-file-media-object` – reassembles chunks via the shared
      `assemble-chunks!` helper and creates the final media object.
- `assemble-chunks!` is a public helper in media.clj shared by both
  `assemble-file-media-object` and `import-binfile`.
- `import-binfile` (binfile.clj): accepts an optional `upload-id` param;
  when provided, materialises the temp file from chunks instead of
  expecting an inline multipart body, removing the 200 MiB body limit
  on .penpot imports.  Schema updated with an `:and` validator requiring
  either `:file` or `:upload-id`.
- quotes.clj: new `upload-sessions-per-profile` quota check.
- Background GC task (`tasks/upload_session_gc.clj`): deletes stalled
  (never-completed) sessions older than 1 hour; scheduled daily at
  midnight via the cron system in main.clj.
- backend/AGENTS.md: document the background-task wiring pattern.

Frontend:
- New `app.main.data.uploads` namespace: generic `upload-blob-chunked`
  helper drives steps 1–2 (create session + upload all chunks with a
  concurrency cap of 2) and emits `{:session-id uuid}` for callers.
- `config.cljs`: expose `upload-chunk-size` (default 25 MiB, overridable
  via `penpotUploadChunkSize` global).
- `workspace/media.cljs`: blobs ≥ chunk-size go through the chunked path
  (`upload-blob-chunked` → `assemble-file-media-object`); smaller blobs
  use the existing direct `upload-file-media-object` path.
  `handle-media-error` simplified; `on-error` callback removed.
- `worker/import.cljs`: new `import-blob-via-upload` helper replaces the
  inline multipart approach for both binfile-v1 and binfile-v3 imports.
- `repo.cljs`: `:upload-chunk` derived as a `::multipart-upload`;
  `form-data?` removed from `import-binfile` (JSON params only).

Tests:
- Backend (rpc_media_test.clj): happy path, idempotency, permission
  isolation, invalid media type, missing chunks, session-not-found,
  chunk-index out-of-range, and quota-limit scenarios.
- Frontend (uploads_test.cljs): session creation and chunk-count
  correctness for `upload-blob-chunked`.
- Frontend (workspace_media_test.cljs): direct-upload path for small
  blobs, chunked path for large blobs, and chunk-count correctness for
  `process-blobs`.
- `helpers/http.cljs`: shared fetch-mock helpers (`install-fetch-mock!`,
  `make-json-response`, `make-transit-response`, `url->cmd`).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-16 19:43:57 +02:00
Andrey Antukh
974beca12d Add 2h min-age threshold to storage/gc_touched task
Skip storage objects touched less than 2 hours ago, matching the pattern
used by upload-session-gc. Update all affected tests to advance the clock
past the threshold using ct/*clock* bindings.
2026-04-16 19:43:57 +02:00
Yamila Moreno
b38912f3cb 🔧 Add short tag to DocherHub release (#8864) 2026-04-16 18:22:22 +02:00
Yamila Moreno
697de53c16 🔧 Add short tag to DocherHub release (#8864) 2026-04-16 18:21:35 +02:00
alonso.torres
47abe09cfe 🐛 Fix problem with position data in Firefox 2026-04-16 18:08:34 +02:00
Elena Torró
b02e05e23d
Merge pull request #8919 from penpot/alotor-fix-change-font-grow-text
🐛 Fix problem when changing font and grow text
2026-04-16 16:38:32 +02:00
Andrey Antukh
b5922d32ca Merge remote-tracking branch 'origin/main' into staging 2026-04-16 10:59:36 +02:00
Andrey Antukh
69e505a6a2 📎 Update changelog 2026-04-16 10:21:15 +02:00
Andrey Antukh
390796f36e 📎 Update changelog 2.14.3 2026-04-16 10:20:05 +02:00
Andrey Antukh
de27ea904d
Add minor adjustments to the auth events (#9027) 2026-04-16 09:59:45 +02:00
Andrey Antukh
146219a439 Add tests for app.common.geom namespaces 2026-04-15 23:37:53 +02:00
Andrey Antukh
fa89790fd6 🐛 Fix grid layout case dispatch, divide-by-zero, and add set-auto-multi-span tests
Three critical fixes for app.common.geom.shapes.grid-layout.layout-data:

1. case dispatch on runtime booleans in get-cell-data (case→cond fix)
   In get-cell-data, column-gap and row-gap were computed with (case ...)
   using boolean locals auto-width? and auto-height? as dispatch values.
   In Clojure/ClojureScript, case compares against compile-time constants,
   so those branches never matched at runtime. Replaced both case forms
   with cond, using explicit equality tests for keyword branches.

2. divide-by-zero guards in fr/auto/span calc (JVM ArithmeticException fix)
   Guard against JVM ArithmeticException when all grid tracks are fixed
   (no flex or auto tracks):
   - (get allocated %1) → (get allocated %1 0) in set-auto-multi-span
   - (get allocate-fr-tracks %1) → (get allocate-fr-tracks %1 0) in set-flex-multi-span
   - (/ fr-column/row-space column/row-frs) guarded with (zero?) check
   - (/ auto-column/row-space column/row-autos) guarded with (zero?) check
   In JS, integer division by zero produces Infinity (caught by mth/finite),
   but on the JVM it throws before mth/finite can intercept.

3. Exhaustive tests for set-auto-multi-span behavior
   Cover all code paths and edge cases:
   - span=1 cells filtered out (unchanged track-list)
   - empty shape-cells no-op
   - even split across multiple auto tracks
   - gap deduction per extra span step
   - fixed track reducing budget; only auto tracks grow
   - smaller children not shrinking existing track sizes (max semantics)
   - flex tracks causing cell exclusion (handled by set-flex-multi-span)
   - non-spanned tracks preserved via (get allocated %1 0) default
   - :row type symmetry with :column type
   - row-gap correctly deducted in :row mode
   - documents that (sort-by span -) yields ascending order (smaller spans
     first), correcting the misleading code comment

All tests pass on both JS (Node.js) and JVM environments.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
71904c9ab6 🐛 Fix CLJS bounds-map deduplication and update intersect test
In the CLJS branch of resolve-modif-tree-ids, get-parent-seq returns
shape maps, but the js/Set was populated with UUIDs. As a result,
.has and .add were passing full shape maps instead of their :id
values, so parent deduplication never worked in ClojureScript.
Fixed both .has and .add calls to extract (:id %) from the shape map.

Also update the collinear-overlap test in geom-shapes-intersect-test
to expect true now that the ::coplanar keyword fix (commit 847bf51)
makes on-segment? collinear checks actually reachable.
2026-04-15 23:37:53 +02:00
Andrey Antukh
d13e464ed1 🐛 Fix three flex layout bugs in drop-area, positions and layout-data
drop_area.cljc: v-end? was guarded by row? instead of col?, making
vertical-end alignment check fire under horizontal layout conditions.
Aligned with v-center? which correctly uses col?.

positions.cljc: In get-base-line, the col? around? branch passed 2 as
a third argument to max instead of as a divisor in (/ free-width
num-lines 2). This made the offset clamp to at least 2 pixels rather
than computing half the per-line free space. Fixed parenthesization.

layout_data.cljc: The second cond branch (and col? space-evenly?
auto-height?) was permanently unreachable because the preceding branch
(and col? space-evenly?) is a strict superset. Removed the dead branch.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
7e9fac4f35 🐛 Fix constraint-modifier :default arity mismatch
All concrete constraint-modifier methods accept 6 arguments
(type, axis, child-before, parent-before, child-after, parent-after)
but the :default fallback only declared 5 parameters. Any unknown
constraint type would therefore receive 6 args and throw an arity
error at runtime. Added the missing sixth underscore parameter.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
80124657b8 🐛 Fix double rotation negation in adjust-shape-flips
When both dot-x and dot-y were negative (both axes flipped),
(update :rotation -) was applied twice which cancelled itself out,
leaving rotation unchanged. The intended behaviour is to negate
rotation once per flip, but flipping both axes simultaneously is
equivalent to a 180° rotation and should not alter the stored angle.

Replaced the two separate conditional rotation updates with a single
one gated on (not= (neg? dot-x) (neg? dot-y)) so the rotation is
negated only when exactly one axis is flipped.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
cf47d5e53e 🐛 Fix coplanar keyword mismatch in intersect-segments?
orientation returns the auto-qualified keyword ::coplanar
(app.common.geom.shapes.intersect/coplanar) but intersect-segments?
was comparing against the plain unqualified :coplanar keyword, which
never matches. This caused all collinear/on-segment edge cases to be
silently skipped, potentially missing valid segment intersections.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
adfe4c3945 🐛 Fix update-rect :size and unqualified abs in corners->rect
update-rect with :size type was only updating :x2 and :y2 but not
:x1 and :y1, leaving the Rect record in an inconsistent state (x1/y1
would not match x/y). Aligned its behaviour with update-rect! which
correctly updates all four corner fields.

corners->rect was calling unqualified abs which is not imported in
app.common.geom.rect namespace. Replaced with mth/abs which is
the proper namespaced version already available in the ns require.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
179bb51c76 🐛 Fix gpt/multiply docstring and gpt/abs Point record downcast
gpt/multiply had a copy-paste docstring from gpt/subtract claiming it
performs subtraction; corrected to accurately describe multiplication.

gpt/abs was using clojure.core/update on a Point record, which returns
a plain IPersistentMap instead of a Point instance, causing point?
checks on the result to return false. Replaced with a direct pos->Point
constructor call using mth/abs on each coordinate.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
3d4c914daa 🐛 Fix trailing comma in matrix->str and remove duplicate dispatch
matrix->str was producing malformed strings like '1,0,0,1,0,0,'
instead of '1,0,0,1,0,0', breaking string serialization of matrix
values used in transit and print-dup handlers.

Also remove the first pp/simple-dispatch registration for Matrix at
line 362 which was dead code shadowed by the identical registration
further down in the file.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:53 +02:00
Andrey Antukh
f5271dabee
🐛 Fix error handling issues (#8962)
* 🚑 Fix RangeError from re-entrant error handling in errors.cljs

Two complementary changes to prevent 'RangeError: Maximum call stack
size exceeded' when an error fires while the potok store error pipeline
is still on the call stack:

1. Re-entrancy guard on on-error: a volatile flag (handling-error?)
   is set true for the duration of each on-error invocation. Any
   nested call (e.g. from a notification emit that itself throws) is
   suppressed with a console.error instead of recursing indefinitely.

2. Async notification in flash: the st/emit!(ntf/show ...) call is
   now wrapped in ts/schedule (setTimeout 0) so the notification event
   is pushed to the store on the next event-loop tick, outside the
   error-handler call stack. This matches the pattern already used by
   the :worker-error, :svg-parser and :comment-error handlers.

* 🐛 Add unit tests for app.main.errors

Test coverage for the error-handling module:

- stale-asset-error?: 6 cases covering keyword-constant and
  protocol-dispatch mismatch signatures, plus negative cases
- exception->error-data: plain JS Error, ex-info with/without :hint
- on-error dispatch: map errors routed via ptk/handle-error, JS
  exceptions wrapped into error-data before dispatch
- Re-entrancy guard: verifies that a second on-error call issued
  from within a handle-error method is suppressed (exactly one
  handler invocation)

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:04 +02:00
Andrey Antukh
a7e362dbfe 📎 Add commented helpers on backend _env for testing nexus 2026-04-15 18:03:11 +02:00
Andrey Antukh
f8f7a0828e Add missing indexes on audit_log table 2026-04-15 18:02:13 +02:00
Elena Torró
e186a27174
Merge pull request #9019 from penpot/azazeln28-fix-pointer-selection
🐛 Fix text editor v3 pointer selection
2026-04-15 17:15:20 +02:00
Aitor Moreno
1477758656 🐛 Fix pointer selection 2026-04-15 16:44:24 +02:00
Elena Torro
41bc8c9b9d 🐛 Fix masked shapes causing render cuts at tile boundaries 2026-04-15 16:36:16 +02:00
Elena Torró
b442ca2209
Merge pull request #8988 from penpot/alotor-improve-token-set-change
🐛 Improve change token set performance
2026-04-15 16:23:55 +02:00
alonso.torres
4d2d559383 🐛 Fix problem with finish render callback 2026-04-15 16:09:38 +02:00
alonso.torres
e3bafab529 🐛 Fix problem with resizes in plugins 2026-04-15 16:09:38 +02:00
alonso.torres
3f5226485b 🐛 Fix problem when changing font and grow text 2026-04-15 16:09:38 +02:00
Aitor Moreno
424b689dca 🐛 Fix mixed fills issues 2026-04-15 14:32:32 +02:00
Aitor Moreno
77b4d07d1f 🐛 Fix v3 text styles not being applied when inc/dec value 2026-04-15 14:32:32 +02:00
Aitor Moreno
6fd264051a 🐛 Fix v2/v3 wrong styling 2026-04-15 14:32:32 +02:00
Andrey Antukh
8f30a95ca0 🐛 Guard against nil variant-data in typography-item
When d/seek finds no matching font variant (e.g. the variant-id stored
on the typography no longer exists in the font's variants list),
variant-data is nil and (:name nil) produces nil, resulting in a
malformed label like "14px | ". Fall back to "--" in that case.
2026-04-15 12:27:18 +02:00
Andrey Antukh
e8547ab6dd 🐛 Pass on-finish-drag to harmony-selector in colorpicker
Without this, dragging the value/opacity sliders in the harmony tab
never called on-finish-drag, so the undo transaction started by
on-start-drag was never committed.
2026-04-15 12:27:18 +02:00
Andrey Antukh
628ce604c5 ♻️ Convert colorpicker and its sub-components to modern rumext * format
slider-selector (slider_selector.cljs):
- Rename to slider-selector*
- Rename prop vertical? to is-vertical
- Remove prop reverse? entirely: it was never passed by any callsite,
  so the related reversal logic in calculate-pos and handler positioning
  is also removed as dead code

value-saturation-selector (ramp.cljs):
- Rename to value-saturation-selector*
- Update internal call site to [:> value-saturation-selector* ...]
- Update slider-selector call sites to [:> slider-selector* ...]

harmony-selector (harmony.cljs):
- Rename to harmony-selector*
- Update slider-selector call sites to [:> slider-selector* ...] with
  renamed is-vertical prop
- Remove stale duplicate :vertical true prop
- Fix spurious extra wrapping vector around the opacity slider in the
  when branch

hsva-selector (hsva.cljs):
- Rename to hsva-selector*
- Update all four slider-selector call sites to [:> slider-selector* ...]
- Remove no-op :reverse? false prop from the value slider

color-inputs (color_inputs.cljs):
- Rename to color-inputs*

colorpicker.cljs:
- Update :refer imports for color-inputs*, harmony-selector*,
  hsva-selector* and libraries*
- Update all corresponding call sites from [:& ...] to [:> ...]
2026-04-15 12:27:18 +02:00
Andrey Antukh
90d052464f ♻️ Convert text-palette components to modern * format
Convert typography-item, palette and text-palette to typography-item*,
palette* and text-palette* using {:keys [...]} destructuring. Rename
prop name-only? to is-name-only in typography-item*. Update internal
call sites to [:> ...] and update the :refer import in palette.cljs.
2026-04-15 12:27:18 +02:00
Andrey Antukh
fbee875d75 ♻️ Convert active-sessions to modern * component format
Convert active-sessions to active-sessions* (zero-prop component).
Update call site in right_header.cljs to use [:> ...] and update the
:refer import accordingly.
2026-04-15 12:27:18 +02:00
Andrey Antukh
bf7c12ae75 ♻️ Convert coordinates to modern * component format
Convert coordinates to coordinates* using {:keys [...]} destructuring
and rename prop colorpalette? to is-colorpalette. Update call site in
workspace.cljs to use [:> ...] with new prop name.
2026-04-15 12:27:18 +02:00
Andrey Antukh
175f122a0f ♻️ Convert viewport-scrollbars to modern * component format
Convert viewport-scrollbars to viewport-scrollbars* using {:keys [...]}
destructuring and update call sites in viewport.cljs and viewport_wasm.cljs
to use [:> ...].
2026-04-15 12:27:18 +02:00
Andrey Antukh
b2f4e90a79 ♻️ Convert shape-distance-segment to modern * component format
Convert shape-distance-segment to shape-distance-segment* using {:keys [...]}
destructuring and update its internal call site in shape-distance to use [:> ...].
2026-04-15 12:27:18 +02:00
Andrey Antukh
b4ec0a6d55 🐛 Add missing zoom and page-id dep on snap-feedback use-effect 2026-04-15 12:27:18 +02:00
Elena Torró
9cd1542dd9
Merge pull request #9000 from penpot/niwinz-main-bugfixes
🐛 Add several fixes for app.common.types namespace
2026-04-15 11:51:14 +02:00
Andrey Antukh
2e97f01838 🐛 Fix safe-subvec 2-arity rejecting start=0
The guard used (> start 0) instead of (>= start 0), so
(safe-subvec v 0) returned nil instead of the full vector.
2026-04-15 11:42:49 +02:00
Andrey Antukh
176edadb6f 🐛 Fix nan? returning false for ##NaN on JVM
Clojure's = uses .equals on doubles, and Double.equals(Double.NaN)
returns true, so (not= v v) was always false for NaN. Use
Double/isNaN with a number? guard instead.
2026-04-15 11:42:49 +02:00
Andrey Antukh
b26ef158ef 📚 Fix typos in vec2, zip-all, and map-perm docstrings 2026-04-15 11:42:49 +02:00
Andrey Antukh
95d4d42c91 🐛 Add missing string? guard to num-string? on JVM
The CLJS branch of num-string? checked (string? v) first, but the
JVM branch did not. Passing non-string values (nil, keywords, etc.)
would rely on exception handling inside parse-double for control
flow. Add the string? check for consistency and to avoid using
exceptions for normal control flow.
2026-04-15 11:42:49 +02:00
Andrey Antukh
bba3610b7b ♻️ Rename shadowed 'fn' parameter to 'pred' in removev
The removev function used 'fn' as its predicate parameter name,
which shadows clojure.core/fn. Rename to 'pred' for clarity and
to follow the naming convention used elsewhere in the namespace.
2026-04-15 11:42:49 +02:00
Andrey Antukh
83da487b24 🐛 Fix append-class producing leading space for empty class
When called with an empty string as the base class, append-class
was producing " bar" (with a leading space) because (some? "")
returns true. Use (seq class) instead to treat both nil and empty
string as absent, avoiding invalid CSS class strings with leading
whitespace.
2026-04-15 11:42:49 +02:00
Andrey Antukh
da8e44147c Remove redundant str call in format-number
format-precision already returns a string, so wrapping its result
in an additional (str ...) call was unnecessary.
2026-04-15 11:42:49 +02:00