Commit Graph

12300 Commits

Author SHA1 Message Date
Andrey Antukh
5fca9457cf ♻️ Extract use-portal-container hook to reduce duplication (#8798)
The dedicated-container portal pattern was repeated across 6 components.
Extract it into a reusable use-portal-container hook under app.main.ui.hooks.
2026-03-26 12:45:42 +01:00
Andrey Antukh
a2672a598c 🐛 Fix TypeError when token error map lacks :error/fn key (#8767)
* 🐛 Fix TypeError when token error map lacks :error/fn key

Guard against missing :error/fn in token form control resolve streams.
When schema validation errors are produced they may not carry an
:error/fn key; calling nil as a function caused a TypeError crash.
Apply an if-let guard at all 7 affected sites across input.cljs,
color_input.cljs and fonts_combobox.cljs, falling back to :message
or returning the error map unchanged.

* ♻️ Extract token error helpers and add unit tests

Extract resolve-error-message and resolve-error-assoc-message helpers
into errors.cljs, replacing the seven duplicated inline lambdas in
input.cljs, color_input.cljs and fonts_combobox.cljs with named
function references.  Add frontend-tests.tokens.token-errors-test
covering both helpers for the normal path (:error/fn present) and the
fallback path (schema-validation errors that lack :error/fn).

Signed-off-by: Penpot Dev <dev@penpot.app>

---------

Signed-off-by: Penpot Dev <dev@penpot.app>
2026-03-25 12:12:18 +01:00
Andrey Antukh
0dfac801a4 Improve error handling and exception formatting (#8757)
*  Improve error handling and exception formatting

- Enhance exception formatting with visual separators and cause chaining
- Add new handler for :internal error type
- Refine error types: change assertion-related errors to :assertion type
- Improve error messages and hints consistency
- Clean up error handling in zip utilities and HTTP modules

* 🐛 Properly handle AbortError on fetch request unsubscription

When a fetch request in-flight is cancelled due to RxJS unsubscription
(e.g. navigating away from the workspace while thumbnail loads are
pending), the AbortController.abort() call triggers a catch handler
that previously relied solely on a @unsubscribed? flag to suppress the
error.

This was unreliable: nested observables spawned inside rx/mapcat (such
as datauri->blob-uri conversions within get-file-object-thumbnails)
could abort independently, with their own AbortController instances,
meaning the outer unsubscribed? flag was never set and the AbortError
propagated as an unhandled exception.

Add an explicit AbortError name check as a disjunctive condition so
that abort errors originating from any observable in the chain are
suppressed at the source, regardless of subscription state.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 19:55:23 +01:00
Andrey Antukh
cc03f3f884 📚 Add minor improvements to ai agents documentation 2026-03-24 18:00:39 +01:00
Andrey Antukh
750e8a9d51 🐛 Fix dissoc error when detaching stroke color from library (#8738)
* 🐛 Fix dissoc error when detaching stroke color from library

The detach-value function in color-row was only passing index to
on-detach, but the stroke's on-color-detach handler expects both
index and color arguments. This caused a protocol error when trying
to dissoc from a number instead of a map.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Fix crash when detaching color asset from stroke

The color_row detach-value callback calls on-detach with (index, color),
but stroke_row's local on-color-detach wrapper only took a single argument
(fn [color] ...), so it received index as color and passed it to
stroke.cljs which then called (dissoc index :ref-id :ref-file), crashing
with 'No protocol method IMap.-dissoc defined for type number'.

Fix the wrapper to accept (fn [_ color] ...) so it correctly ignores the
index passed by color_row (it already has index in the closure) and
forwards the actual color map to the parent handler.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 15:35:32 +01:00
Andrey Antukh
56f1fcdb53 🐛 Fix crash when pasting image into text editor
When pasting an image (with no text content) into the text editor,
Draft.js calls handlePastedText with null/empty text. The previous fix
guarded splitTextIntoTextBlocks against null, but insertText still
attempted to build a fragment from an empty block array, causing
Modifier.replaceWithFragment to crash with 'Cannot read properties of
undefined (reading getLength)'.

Fix insertText to return the original state unchanged when there are no
text blocks to insert. Also guard handle-pasted-text in the ClojureScript
editor to skip the insert-text call entirely when text is nil or empty.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 13:00:28 +00:00
Andrey Antukh
d863c7065f 🐛 Fix null text crash on paste in text editor
The splitTextIntoTextBlocks function in @penpot/draft-js called
.split() on the text parameter without a null check. When pasting
content without text data (e.g., images only), Draft.js passes null
to handlePastedText, causing a TypeError.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 13:49:28 +01:00
Alejandro Alonso
5d6eb3b3d6 Merge pull request #8739 from penpot/niwinz-main-bugfix-5
🐛 Fix error when get-parent-with-data encounters non-Element nodes
2026-03-24 12:50:48 +01:00
Andrey Antukh
d051a3ba45 🐛 Ensure path content is always PathData when saving
The save-path-content function only converted content to PathData when
there was a trailing :move-to command. When there was no trailing
:move-to, the content from get-path was stored as-is, which could be
a plain vector if the shape was already a :path type with non-PathData
content. This caused segment/get-points to fail with 'can't access
property "get", cache is undefined' when the with-cache macro tried
to access the cache field on a non-PathData object.

The fix ensures content is always converted to PathData via path/content
before being stored in the state.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 08:15:58 +01:00
Andrey Antukh
577f00dd24 🐛 Fix error when get-parent-with-data encounters non-Element nodes
The get-parent-with-data function traverses the DOM using parentElement
to find an ancestor with a specific data-* attribute. When the current
node is a non-Element DOM node (e.g. Document node reached from event
handlers on window), accessing .-dataset returns undefined, causing
obj/in? to throw "right-hand side of 'in' should be an object".

This adds a nodeType check to skip non-Element nodes during traversal
and continue up the parent chain.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-23 19:19:27 +00:00
Andrey Antukh
b484415a9f 🐛 Fix generic error shown on clipboard permission denial (#8666)
When the browser denies clipboard read permission (NotAllowedError),
the unhandled exception handler was showing a generic 'Something wrong
has happened' toast. This change adds proper error handling for
clipboard permission errors in paste operations and shows a
user-friendly warning message instead.

Changes:
- Add error handling in paste-from-clipboard for NotAllowedError
- Improve error handling in paste-selected-props to detect permission errors
- Mark clipboard NotAllowedError as ignorable in the uncaught error handler
  to prevent duplicate generic error toasts
- Add translation key for clipboard permission denied message

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-23 20:03:14 +01:00
Andrey Antukh
2d616cf9c0 📚 Add better organization for AGENTS.md file (#8675) 2026-03-18 14:59:38 +01:00
Andrey Antukh
1a59017e1c 🐛 Ignore posthog exceptions in unhandled exception handler (#8629)
PostHog recorder throws errors like 'Cannot assign to read only property
'assert' of object' which are unrelated to the application and should be
ignored to prevent noise in error reporting.
2026-03-17 18:41:06 +01:00
Andrey Antukh
e730e9ee64 🐛 Fix subscribe to undefined stream error in use-stream hook (#8633)
Add a nil guard before subscribing to the stream in the use-stream
hook. When a nil/undefined stream is passed (e.g., from a conditional
expression or timing edge case during React rendering), the subscribe
call on undefined causes a TypeError. The guard ensures we only
subscribe when the stream is defined.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-17 10:06:16 +01:00
Andrey Antukh
0779c9ca61 🐛 Fix TypeError in get-points when content is not PathData (#8634)
The with-cache macro in impl.cljc assumed the target was always a
PathData instance (which has a cache field). When content was a plain
vector, (.-cache content) returned undefined in JS, causing:

  TypeError: Cannot read properties of undefined (reading 'get')

Fix:
- path/get-points (app.common.types.path) is now the canonical safe
  entry point: converts non-PathData content via impl/path-data and
  handles nil safely before delegating to segment/get-points
- segment/get-points remains a low-level function that expects a
  PathData instance (no defensive logic at that level)
- streams.cljs: replace direct call to path.segm/get-points with
  path/get-points so the safe conversion path is always used
- with-cache macro: guards against nil/undefined cache, falling back
  to direct evaluation for non-PathData targets

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-17 09:31:10 +01:00
Andrey Antukh
33c5f82c43 🐛 Fix penpot.openPage() to navigate in same tab by default
- Change the default for the newWindow param from true to false, so
  openPage() navigates in the same tab instead of opening a new one
- Accept a UUID string as the page argument in addition to a Page object,
  avoiding the need to call penpot.getPage(uuid) first
- Add validation error when an invalid page argument is passed

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-13 09:19:50 +01:00
Alejandro Alonso
ebd17974a6 Merge pull request #8580 from penpot/niwinz-staging-fix-paste-issue
🐛 Fix crash when pasting non-map transit clipboard data
2026-03-13 08:33:19 +01:00
Andrey Antukh
eecb51ecc1 🐛 Fix clipboard getType error when no allowed types found (#8609)
When clipboard items have types that don't match the allowed types
list, the filtering results in an empty array. Calling getType with
undefined throws a NotFoundError. This change adds a check for null/undefined
types and filters them from the result.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-13 08:24:36 +01:00
Andrey Antukh
e7e6303184 🐛 Make ct/format-inst nil safe (#8612)
Prevent JS TypeError when date is nil in date formatting.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 19:55:51 +01:00
Andrey Antukh
8d5450391e 🐛 Fix crash when pasting non-map transit clipboard data
Guard against transit-decoded clipboard content that is not a map
before calling assoc, which caused a runtime crash ('No protocol
method IAssociative.-assoc defined for type number').

Also route :copied-props paste data to paste-transit-props instead
of incorrectly sending it to paste-transit-shapes.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 15:05:22 +01:00
Alejandro Alonso
be9b1158ed Merge pull request #8588 from penpot/niwinz-staging-abort-signal-fix
🐛 Fix unhandled AbortError in HTTP fetch requests
2026-03-12 13:53:18 +01:00
Eva Marco
8f5c38d476 🐛 Fix scroll on colorpicker (#8595) 2026-03-12 13:36:38 +01:00
Andrey Antukh
80d165ed5b 🐛 Fix unhandled AbortError in HTTP fetch requests
Identify and silence "signal is aborted without reason" errors by:
- Providing an explicit reason to AbortController when subscriptions are disposed.
- Updating the global error handler to ignore AbortError exceptions.
- Ensuring unhandled rejections use the ignorable exception filter.

The root cause was RxJS disposal calling .abort() without a reason, combined
with the on-unhandled-rejection handler missing the ignorable error filter.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 13:31:58 +01:00
Alejandro Alonso
f5cabac5f3 Merge pull request #8583 from penpot/niwinz-staging-ignore-extensions-exceptions
🐛 Ignore browser extension errors in unhandled exception handler
2026-03-12 13:20:11 +01:00
Andrey Antukh
b68e400cc1 🐛 Fix crash in select* when options vector is empty (#8578)
Guard get-option fallback with (when (seq options) ...) to avoid
"No item 0 in vector of length 0" when options is an empty vector.
Also guard the selected-option memo in select* to mirror the same
pattern already present in combobox*.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 13:06:25 +01:00
Alejandro Alonso
50c27aecc7 Merge pull request #8574 from penpot/niwinz-staging-unmount-fixes
🐛 Fix removeChild crash on portal-on-document* unmount
2026-03-12 13:06:00 +01:00
Alejandro Alonso
c254f88367 Merge pull request #8575 from penpot/niwinz-staging-fetch-exception
🐛 Wrap fetch TypeError into proper ex-info with :unable-to-fetch code
2026-03-12 12:48:48 +01:00
Andrey Antukh
82e3a5fa53 🐛 Fix 'not ISeqable' error when entering float values in layout/opacity inputs
Replace int? with number? in on-change handlers for layout item margins,
min/max sizes, and layer opacity. Using int? caused float values like 8.5
to fall into the design token branch, calling (first 8.5) and crashing.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 12:37:22 +01:00
Alejandro Alonso
1680be33ef Merge pull request #8568 from penpot/niwinz-staging-bugfix-1-path-get-points
🐛 Fix TypeError when path content is nil in get-points calls
2026-03-12 12:31:58 +01:00
Andrey Antukh
6ee8184821 🐛 Fix error when creating guides without frame (#8598)
* 🐛 Fix error when creating guides without frame

The error 'Cannot read properties of undefined (reading
$cljs$core$IFn$_invoke$arity$0$)' occurred when creating a new
guide. It is probably a race condition because it is not reproducible
from the user point of view.

The cause is mainly because of use incorrect jsx handler :& where :>
should be used. This caused that some props pased with incorrect casing
and the relevant callback props received as nil on the component and
on the use-guide hook.

The fix is simple: use correct jsx handler

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 💄 Add cosmetic changes to viewport guides components

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 12:23:09 +01:00
Eva Marco
c00ef7c128 🐛 Fix unnexpected warning (#8603) 2026-03-12 12:01:28 +01:00
Xaviju
9b3207b06c 🐛 Fix wrong value on property copy on inspect styles (#8605) 2026-03-12 11:45:16 +01:00
Alonso Torres
31a4a7f21f 💄 Removed forgotten print (#8594) 2026-03-11 16:04:33 +01:00
Andrey Antukh
7ec9261475 Add improvements to AGENTS.md (#8586) 2026-03-11 15:24:40 +01:00
Andrey Antukh
db9e9f4832 🐛 Ignore browser extension errors in unhandled exception handler
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 19:30:01 +01:00
Andrey Antukh
fed01fba73 🐛 Wrap fetch TypeError into proper ex-info with :unable-to-fetch code
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 15:59:14 +01:00
Andrey Antukh
4f0bceddae 🐛 Fix stale deferred DOM ops in dashboard navigation
Two related issues that could cause crashes during fast navigation
in the dashboard:

1. grid.cljs: On drag-start, a temporary counter element is appended
   to the file card node for the drag ghost image, then scheduled for
   removal via requestAnimationFrame. If the user navigates away before
   the RAF fires, React unmounts the section and removes the card node
   from the DOM. When the RAF fires, item-el.removeChild(counter-el)
   throws because counter-el is no longer a child. Fixed by guarding
   the removal with dom/child?.

2. sidebar.cljs: Keyboard navigation handlers used ts/schedule-on-idle
   (requestIdleCallback with a 30s timeout) to focus the newly rendered
   section title after navigation. This left a very wide window for the
   callback to fire against a stale DOM after a subsequent navigation.
   Additionally, the idle callbacks were incorrectly passed as arguments
   to st/emit! (which ignores non-event values), making the scheduling
   an accidental side effect. Fixed by replacing all occurrences with
   ts/schedule (setTimeout 0), which is sufficient to defer past the
   current render cycle, and moving the calls outside st/emit!.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 14:22:45 +00:00
Andrey Antukh
80b64c440c 🐛 Fix removeChild crash on portal-on-document* unmount
The previous implementation passed document.body directly as the
React portal containerInfo. During unmount, React's commit phase
(commitUnmountFiberChildrenRecursively, case 4) sets the current
container to containerInfo and then calls container.removeChild()
for every DOM node inside the portal tree.

When two concurrent state updates are processed — e.g. navigating
away from a dashboard section while a file-menu portal is open —
React could attempt document.body.removeChild(node) twice for the
same node, the second time throwing:

  NotFoundError: Failed to execute 'removeChild' on 'Node':
  The node to be removed is not a child of this node.

The fix allocates a dedicated <div> container per portal instance
via mf/use-memo. The container is appended to body on mount and
removed in the effect cleanup. React then owns an exclusive
containerInfo and its unmount path never races with another
portal or the modal container (which also targets document.body).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 14:22:34 +00:00
Andrey Antukh
9f66220caa 🐛 Fix flex layout container horizontalSizing/verticalSizing via plugin API (#8555)
Setting horizontalSizing/verticalSizing on a FlexLayoutProxy was
dispatching update-layout-child instead of update-layout, so the
frame's auto-sizing (hug content) was never triggered even though
the getter read back the value correctly.

Also restricts accepted values to #{:fix :auto} (matching shape.cljs)
since frames cannot use :fill, and fixes a copy-paste error that
reported :horizontalPadding instead of :horizontalSizing in error messages.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 15:01:23 +01:00
Aitor Moreno
3112b0d8cf 🐛 Fix grow options not verifying text-editor/v2 (#8571) 2026-03-10 15:01:23 +01:00
Andrey Antukh
0f47c30349 Merge branch 'main' into staging 2026-03-10 14:39:16 +01:00
Andrey Antukh
68fbacf8b3 Merge tag '2.14.0-RC2' 2026-03-10 14:38:58 +01:00
Andrey Antukh
7ab5f241da 🐛 Fix TypeError when path content is nil in get-points calls
Use nil-safe path/get-points wrapper (some-> based) instead of
direct path.segment/get-points calls in edition.cljs to prevent
'Cannot read properties of undefined (reading get)' crash.

Add nil-safety test to verify path/get-points returns nil without
throwing when content is nil.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 12:21:13 +00:00
Andrey Antukh
32cf95265a 📚 Add GitHub Copilot instructions (#8548) 2026-03-10 13:12:15 +01:00
Eva Marco
c59cc4dff4 🐛 Fix tooltip position on absolute positioned elements (#8509)
* 🐛 Fix tooltip position on absolute positioned elements

* 🐛 Fix tests
2026-03-09 12:11:39 +01:00
Eva Marco
cc3033735b 🐛 Fix showing warning when no shape is selected (#8515) 2026-03-04 10:58:36 +01:00
Andrey Antukh
c3f5117757 🐛 Fix unhandled exception on using decimals on stroke row (#8405) 2026-03-04 09:47:14 +01:00
Andrey Antukh
86e851f408 🐛 Fix incorrect version visibility on workspace (#8463)
* 🐛 Add missing order by clause to snapshot query

This fixes the incorrect snapshot visibility when file
has a lot of versions.

*  Reduce allocation on milestone-group* component

* 🐛 Fix milestone group timestamp formatting

* 📎 Update changelog

* 🐛 Fix scroll on history panel

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-03-04 09:27:51 +01:00
Andrey Antukh
a4351d133b Add minor improvements to error reporting (#8402) 2026-03-04 09:12:19 +01:00
Andrey Antukh
b704a7da0e 🐛 Fix inconsistency between plugins api doc and impl for shadows (#8454)
Related to offset-x and offset-y attributes.
2026-03-04 09:09:27 +01:00