952 Commits

Author SHA1 Message Date
Andrey Antukh
9911ff7959 Merge remote-tracking branch 'origin/staging' into develop 2026-06-04 19:01:54 +02:00
Elena Torró
dfa88a28fd
🐛 Fix text editor swap when WebGL render is enabled/disabled 2026-06-04 08:39:33 +02:00
Andrey Antukh
4a8fb5af53 Merge remote-tracking branch 'origin/staging' into develop 2026-06-01 13:15:57 +02:00
Andrey Antukh
c5de4c27b0 Merge remote-tracking branch 'origin/main' into staging 2026-06-01 12:57:39 +02:00
John Eismeier
c156559f2c
📚 Fix several typos on code comments and messages (#9946)
Signed-off-by: John E <jeis4wpi@outlook.com>
2026-06-01 09:43:07 +02:00
Eva Marco
ba39600192 🐛 Fix show error on name-input 2026-05-29 12:28:10 +02:00
Yamila Moreno
ddba2ffa75
📎 Update Kaleidos Copyright (#9929) 2026-05-29 11:24:58 +02:00
Dexterity
01ac1529e1
♻️ Migrate perf/profiler to modern component syntax (#9429)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-29 09:41:21 +02:00
Alejandro Alonso
6663105f4b
🐛 Avoid restoring empty saved-font-size to prevent invalid text shapes on save (#9917) 2026-05-28 12:39:43 +02:00
Andrey Antukh
3858993a57 Merge remote-tracking branch 'origin/staging' into develop 2026-05-27 13:37:02 +02:00
Andrés Moya
65c6b7be7c 🐛 Fix changed font size when editing a text with no changes 2026-05-21 16:46:52 +02:00
Andrey Antukh
d620c86053 Merge remote-tracking branch 'origin/staging' into develop 2026-05-15 11:58:06 +02:00
Andrey Antukh
9021544c05 Merge remote-tracking branch 'origin/main' into staging 2026-05-14 15:24:29 +02:00
Alonso Torres
8afe8a5dfa
🐛 Fix plugins schema validation error (#9632) 2026-05-14 15:00:41 +02:00
Andrey Antukh
74ca40abd4 Merge remote-tracking branch 'origin/staging' into develop 2026-05-14 12:43:13 +02:00
Belén Albeza
78e3077a37
🔧 Use polyfilled helpers instead of raf (#9628) 2026-05-14 12:42:58 +02:00
Andrey Antukh
29f940fb7a
🐛 Sanitize comment content on rendering (#9605)
Add escape-html function that escapes HTML special characters and apply
it in the comment editor at four dom/set-html! call sites where
user-provided text is inserted as innerHTML, preventing stored XSS.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 11:20:11 +02:00
Andrey Antukh
06986e25a3 Merge remote-tracking branch 'origin/staging' into develop 2026-05-11 14:06:31 +02:00
Milos Milic
b54fa2f11c
🐛 Reject clipboard helpers gracefully on insecure origins (#9188)
* 🐛 Reject clipboard helpers gracefully on insecure origins

Closes #6514. Resolves the user-visible crash originally reported
in #4478.

`app.util.clipboard/to-clipboard` and `to-clipboard-promise` called
`(unchecked-get js/navigator "clipboard")` and then immediately
invoked `.writeText` / `.write` on the result, with no guard for the
case where `navigator.clipboard` is `undefined`. The W3C Clipboard
API spec requires a "secure context" (HTTPS or localhost), so a
Penpot instance served over plain HTTP - which the SSDP/LAN
self-hosted setup in #4478 was - throws

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

synchronously the moment the user clicks any copy button. The error
escapes the consuming function before any error-handling rx/of arm
runs, so the whole UI ends up on the error screen instead of just
the affected control showing a "could not copy" message.

A third helper (`to-clipboard-multi`) already guards `clipboard` and
`clipboard.write`, but if both are missing it silently returns nil
which is also surprising for callers expecting a Promise.

## Fix

Add a small `get-clipboard` accessor and an `unavailable-error`
factory that returns `Promise.reject(Error(...))` with a clear
message ("Clipboard API is unavailable. This usually happens when
the page is served over plain HTTP; serve Penpot over HTTPS to
enable copy-to-clipboard."). Wire all three helpers through the
same defensive contract:

- `to-clipboard` - return the rejected Promise when
  `navigator.clipboard.writeText` is missing.
- `to-clipboard-promise` - return the rejected Promise when
  `navigator.clipboard.write` is missing.
- `to-clipboard-multi` - convert the existing `if` into a `cond`
  with three branches: prefer `clipboard.write` for true multi-MIME
  output, fall through to `writeText` with the text/plain payload
  when only the legacy text path is available, and finally reject
  with the unavailable error when neither path exists. Previously
  the no-API case fell off the `when-let` and silently returned
  nil.

The contract is now consistent: every helper either resolves or
rejects a Promise, never throws synchronously, and never returns
nil. Callers (which are already structured around rx streams that
call `rx/from` on the helper's return value) can chain `.catch` /
`rx/catch` to surface a status-bar message instead of crashing.

The two stale `;; FIXME` comments on `to-clipboard` (rename to
`write-text`) and `to-clipboard-promise` (this API is very confuse)
are removed - the rename remains an open follow-up across 13+ call
sites and is intentionally out of scope, but the API is no longer
"confuse" once the contract is documented and uniform.

CHANGES.md entry added under the 2.17.0 Bugs-fixed section
describing the user-visible behaviour change.

* 🐛 Reject paste-from-navigator gracefully on insecure origins

Symmetric companion to the to-clipboard / to-clipboard-promise /
to-clipboard-multi guards added earlier in this PR. The paste path
went through fromNavigator (frontend/src/app/util/clipboard.js) which
called `navigator.clipboard.read()` with no nil-check; on insecure
origins (plain HTTP / non-localhost) this raised an opaque
`TypeError: Cannot read properties of undefined (reading 'read')` and
the workspace surfaced a generic 'Something wrong has happened' toast
instead of the descriptive 'serve Penpot over HTTPS …' message users
get for the copy direction.

Mirror the get-clipboard pattern from clipboard.cljs:
- Read `navigator.clipboard` once into a local.
- If it's missing the `.read` method, throw a descriptive Error that
  matches the wording the copy direction already uses (only the verb
  swaps: 'paste-from-clipboard' instead of 'copy-to-clipboard').
- Otherwise, dispatch through the local handle.

The existing app.util.clipboard/from-navigator (clipboard.cljs:32)
already wraps impl/fromNavigator in rx/from, so a rejected Promise
from the async function propagates as an rx error event. Existing
callers that subscribe with .catch / on-error see the structured
Error and surface the toast, identical to how to-clipboard's
unavailable-error already flows.

Repro (matches niwinz's reproduction in the PR comment):

  Object.defineProperty(navigator, 'clipboard', { value: undefined });
  // … then attempt a paste action in the workspace …

Before: TypeError in console + 'Something wrong has happened' toast.
After: descriptive Error caught by the rx subscription and rendered
through the existing unavailable-Clipboard-API surface.

Refs #6514, #4478

* 🐛 Show user-facing toast when clipboard API is unavailable

Niwinz's review on penpot#9188 caught that the rejected Promise from
to-clipboard / to-clipboard-promise / to-clipboard-multi /
fromNavigator now surfaces the correct error to the console, but the
workspace UI still falls through to the generic "Something wrong has
happened" toast because the on-clipboard-permission-error and the
paste error-handler in paste-from-clipboard only branched on
clipboard-permission-error?.

Apply the patch he suggested in the review:

- Add clipboard-unavailable-error? predicate that matches the
  Promise.reject(Error("Clipboard API is unavailable. ...")) thrown
  by the get-clipboard / unavailable-error helpers added earlier in
  this PR. Uses str/starts-with? on the message prefix so the
  predicate stays stable even if the trailing "serve Penpot over
  HTTPS ..." advice text is reworded later.
- Convert on-clipboard-permission-error from `if` to `cond` and add
  a third arm that fires errors.clipboard-api-unavailable as a
  warning toast.
- Add the same arm in the second cond block inside
  paste-from-clipboard, before the :not-implemented and :else arms.
- Add the matching errors.clipboard-api-unavailable entry to
  frontend/translations/en.po with the wording niwinz suggested:
  "Clipboard API is unavailable. Serve Penpot over HTTPS to enable
  clipboard access".

Refs penpot#9188 review.

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 13:52:36 +02:00
Andrey Antukh
15379f37f5 🐛 Fix maximum call stack size exceeded in SSE read-stream (#9484)
The recursive `read-items` function in `app.util.sse/read-stream`
caused a synchronous stack overflow when reading buffered stream
data. Each `rx/mapcat` call chained another recursive invocation
on the same call stack without yielding to the event loop.

Replace the recursive pattern with an `rx/create`-based async pump
that uses Promise `.then()` chaining, keeping the call stack depth
constant regardless of stream size.

Also add progress reporting with names and IDs during binfile
export and import, and bump `eventsource-parser` dependency.

Closes #9470

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 11:06:15 +02:00
Andrey Antukh
ec0d692856
🐛 Fix maximum call stack size exceeded in SSE read-stream (#9484)
The recursive `read-items` function in `app.util.sse/read-stream`
caused a synchronous stack overflow when reading buffered stream
data. Each `rx/mapcat` call chained another recursive invocation
on the same call stack without yielding to the event loop.

Replace the recursive pattern with an `rx/create`-based async pump
that uses Promise `.then()` chaining, keeping the call stack depth
constant regardless of stream size.

Also add progress reporting with names and IDs during binfile
export and import, and bump `eventsource-parser` dependency.

Closes #9470

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 10:59:35 +02:00
FairyPiggyDev
fa06efa84d
♻️ Migrate fo-text and html-text renderers to modern component syntax (#9385)
Step toward issue #9260 (incremental migration of legacy UI
components to the modern `*`-suffixed syntax, removing the per-render
JS-to-Clojure props conversion overhead).

Twin namespaces with parallel structure: each defines six components
that drive a recursive text rendering pass over the editor's content
tree (root -> paragraph-set -> paragraph -> node -> text). Both files
were uniformly legacy: every component carried `::mf/wrap-props
false` and read its props with `(obj/get props "key")`. None had
`::mf/register`, `unchecked-get` or `obj/merge!`, so they qualify as
clean Case-A migrations.

frontend/src/app/main/ui/shapes/text/fo_text.cljs (6 components)
----------------------------------------------------------------

- `render-text`           -> `render-text*`
- `render-root`           -> `render-root*`
- `render-paragraph-set`  -> `render-paragraph-set*`
- `render-paragraph`      -> `render-paragraph*`
- `render-node`           -> `render-node*`     (forward-props case,
                                                 see below)
- `text-shape`            -> `text-shape*`      (`::mf/forward-ref`
                                                 preserved)

The four leaf components switch from `[props]` + per-key
`(obj/get props "key")` to standard destructuring. `text-shape`
already used destructuring under `::mf/props :obj`; that legacy
metadata is dropped because the modern `*` form handles props
automatically. Its single `::mf/forward-ref true` is kept per the
prompt's "preserve forward-ref" rule.

`render-node` is the recursive driver. It needs to forward all of
its incoming props to the matched paragraph-* / text component and
then to a child `render-node*` after overriding `:node`, `:index`
and `:key`. The migrated form uses `::mf/props :obj` together with
`{:keys [node] :as props}` to keep the JS-object props symbol
available, and `(mf/spread-props props {…})` replaces the previous
`obj/clone` + `obj/set!` chain.

`app.util.object` is no longer required by this namespace and the
`(:require ... [app.util.object :as obj] ...)` line is removed.

frontend/src/app/main/ui/shapes/text/html_text.cljs (6 components)
-----------------------------------------------------------------

Identical six-component shape as `fo_text.cljs`, plus a `code?`
flag threaded through every component to switch the rendering path
between regular shapes and code-style shapes.

- `render-text`           -> `render-text*`
- `render-root`           -> `render-root*`
- `render-paragraph-set`  -> `render-paragraph-set*`
- `render-paragraph`      -> `render-paragraph*`
- `render-node`           -> `render-node*`     (same forward-props
                                                 treatment as above,
                                                 plus `is-code` in
                                                 the spread)
- `text-shape`            -> `text-shape*`      (`::mf/forward-ref`
                                                 preserved)

The `code?` boolean prop is renamed to `is-code` per the migration
prompt's "?-suffixed boolean -> `is-` prefix" rule. The rename is
applied at every read site (5 components) and at the `text-shape*`
internal call to `render-node*`, so the prop is consistent inside
the namespace.

`app.util.object` is no longer required by this namespace either
and the corresponding `:require` line is dropped.

External call sites (3 files, 4 sites)
--------------------------------------

- `frontend/src/app/main/ui/shapes/text.cljs` - the legacy
  text-shape wrapper (intentionally kept legacy in this PR because
  it dispatches to `svg/text-shape`, which is still being touched by
  the in-flight PR #9016) now calls `[:> fo/text-shape* props]`.
  The `props` symbol is the wrapper's incoming JS-object; modern
  destructured components accept JS-object props at the call site
  via `[:>` so this works unchanged.

- `frontend/src/app/util/code_gen/markup_html.cljs` -
  `(mf/element text/text-shape #js {:shape shape :code? true})`
  becomes
  `(mf/element text/text-shape* #js {:shape shape :is-code true})`
  (component renamed and the `code?` JS key updated to match the
  renamed prop).

- `frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs`
  - `[:& html/text-shape {…}]` -> `[:> html/text-shape* {…}]`.

Behavior preserved verbatim
---------------------------

Same render output, same forward-ref forwarding semantics, same
recursive children-by-index keying, same default `:dir "auto"` on
`render-paragraph*`. The visible-prop changes are only the `code?`
-> `is-code` rename, all driven from this namespace and its single
caller in `markup_html.cljs`.

Github #9260

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
2026-05-07 15:03:51 +02:00
Alejandro Alonso
7d923f8e1d Merge remote-tracking branch 'origin/staging' into develop 2026-05-05 06:52:38 +02:00
Alejandro Alonso
c794e0ed73
🎉 Events enhancements (#9310)
* 🎉 Coalesce viewport pointermove into one PointerEvent

* 🎉 Skip worker hover selection query during transform

* 🎉 Coalesce snap X/Y into one worker query-snap-xy
2026-05-04 17:57:00 +02:00
Clayton
4ce56e96fe
🐛 Fix MCP media uploads and SVG data URI image parsing (#9201)
* 🐛 Fix MCP media uploads and SVG data URI image parsing

Signed-off-by: Clayton <claytonlin1110@gmail.com>

* 🐛 Fix lint

Signed-off-by: Clayton <claytonlin1110@gmail.com>

* 🐛 Fix test

Signed-off-by: Clayton <claytonlin1110@gmail.com>

---------

Signed-off-by: Clayton <claytonlin1110@gmail.com>
2026-05-04 13:33:58 +02:00
Renzo
8821ada1bb
🐛 Suppress browser context menu on empty workspace sidebar space (#9196)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Signed-off-by: Renzo <170978465+RenzoMXD@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 17:58:38 +02:00
Renzo
8a8ebb7943
Preserve vector content when pasting from external tools (#9182)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-27 21:35:52 +02:00
Alejandro Alonso
984d292ab2 Merge remote-tracking branch 'origin/staging' into develop 2026-04-24 09:29:24 +02:00
Andrey Antukh
7135782e7d Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-24 08:19:47 +02:00
Eva Marco
0c60db56a2
🐛 Fix multiselection error with typography texts (#9071)
* 🐛 Ensure typography-ref attrs are always present and fix nil encoding

Add :typography-ref-file and :typography-ref-id (both defaulting to nil)
to default-text-attrs so these keys are always present in text node maps,
whether or not a typography is attached.

Skip nil values in attrs-to-styles (Draft.js style encoder) and in
attrs->styles (v2 CSS custom-property mapper) so nil typography-ref
entries are never serialised to CSS.

Replace when with if/acc in get-styles-from-style-declaration to prevent
the accumulator from being clobbered to nil when a mixed-value entry is
skipped during style decoding.

* 🎉 Add test

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-23 16:08:56 +02:00
Renzo
5bbb2c5cff
🐛 Fix Copy as SVG for multi-shape selection (#838) (#9066)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
2026-04-22 19:46:38 +02:00
Andrey Antukh
448b5d4786 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-22 09:58:20 +02:00
Andrey Antukh
98e8160875 ♻️ Remove worker URI from global templates and compute from public URI
- Remove penpotWorkerURI from index.mustache and rasterizer.mustache templates
- Remove worker_main entry from the build manifest
- Construct worker URI in config.cljs by joining public-uri with worker path
- Fix global variable casing for plugins-list-uri and templates-uri
- Fix alignment in worker.cljs let bindings
2026-04-22 09:52:44 +02:00
Eva Marco
f18670ed00
🐛 Fix errors from numeric input design review (#8993)
* 🐛 Fix blur input after enter value

* 🐛 Catch error on invalid maths

* 🐛 Fix race condition

* 🎉 Add tests that cover issues

* 🐛 Fix padding applying only to one side

* 🐛 Fix show broken pill when reference is on not active set
2026-04-21 14:39:06 +02:00
Alejandro Alonso
0d17debde7 Merge remote-tracking branch 'origin/staging' into develop 2026-04-21 08:24:29 +02:00
Andrey Antukh
3a39676969 Backport MCP from staging (part 1) 2026-04-20 19:37:02 +02:00
alonso.torres
dc5f222230 🐛 Improve change token set performance 2026-04-14 14:51:12 +02:00
Alejandro Alonso
27449139ad Merge remote-tracking branch 'origin/staging' into develop 2026-04-09 09:05:02 +02:00
Alonso Torres
6ce2aadfae
Improve message from schema errors in plugins (#8865) 2026-04-08 16:14:51 +02:00
Andrey Antukh
c1044ac522 Add protection for stale cache of js assets loading issues (#8638)
*  Use update-when for update dashboard state

This make updates more consistent and reduces possible eventual
consistency issues in out of order events execution.

* 🐛 Detect stale JS modules at boot and force reload

When the browser serves cached JS files from a previous deployment
alongside a fresh index.html, code-split modules reference keyword
constants that do not exist in the stale shared.js, causing TypeError
crashes.

This adds a compile-time version tag (via goog-define / closure-defines)
that is baked into the JS bundle. At boot, it is compared against the
runtime version tag from index.html (which is always fresh due to
no-cache headers). If they differ, the app forces a hard page reload
before initializing, ensuring all JS modules come from the same build.

* 📎 Ensure consistent version across builds on github e2e test workflow

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-30 19:46:51 +02:00
Andrey Antukh
87bb1b8e74 Merge remote-tracking branch 'origin/staging' into develop 2026-03-30 12:29:43 +02:00
Andrey Antukh
264cd0aaac Merge remote-tracking branch 'origin/main' into staging 2026-03-30 12:29:07 +02:00
Andrey Antukh
3767ee05bb Add retry mechanism for idenpotent get repo requests on frontend (#8792)
* ♻️ Handle fetch-error gracefully with toast instead of full-page error

Network-level failures (lost connectivity, DNS failure, etc.) on RPC
calls were propagating as :internal/:fetch-error to the global error
handler, which replaced the entire UI with a full-page error screen.

Now the :internal handler distinguishes :fetch-error from other internal
errors and shows a non-intrusive toast notification instead, allowing
the user to continue working.

*  Add automatic retry with backoff for idempotent RPC requests

Idempotent (GET) RPC requests are now automatically retried up to 3
times with exponential back-off (1s, 2s, 4s) when a transient error
occurs.  Retryable errors include: network-level failures
(:fetch-error), 502 Bad Gateway, 503 Service Unavailable, and browser
offline (status 0).

Mutation (POST) requests are never retried to avoid unintended
side-effects.  Non-transient errors (4xx client errors, auth errors,
validation errors) propagate immediately without retry.

* ♻️ Make retry helpers public with configurable parameters

Make retryable-error? and with-retry public functions, and replace
private constants with a default-retry-config map.  with-retry now
accepts an optional config map (:max-retries, :base-delay-ms) enabling
callers and tests to customize retry behavior.

*  Add tests for RPC retry mechanism

Comprehensive tests for the retry helpers in app.main.repo:
- retryable-error? predicate: covers all retryable types (fetch-error,
  bad-gateway, service-unavailable, offline) and non-retryable types
  (validation, authentication, authorization, plain errors)
- with-retry observable wrapper: verifies immediate success, recovery
  after transient failures, max-retries exhaustion, no retry for
  non-retryable errors, fetch-error retry, custom config, and mixed
  error scenarios

* ♻️ Introduce :network error type for fetch-level failures

Replace the awkward {:type :internal :code :fetch-error} combination
with a proper {:type :network} type in app.util.http/fetch.  This makes
the error taxonomy self-explanatory and removes the special-case branch
in the :internal handler.

Consequences:
- http.cljs: emit {:type :network} instead of {:type :internal :code :fetch-error}
- errors.cljs: add a dedicated ptk/handle-error :network method (toast);
  restore :internal handler to its original unconditional full-page error form
- repo.cljs: simplify retryable-types and retryable-error? — :network
  replaces the former :internal special-case, no code check needed
- repo_test.cljs: update tests to use {:type :network}

* 📚 Add comment explaining the use of bit-shift-left
2026-03-30 12:20:02 +02:00
Alejandro Alonso
508c67c930 Merge remote-tracking branch 'origin/staging' into develop 2026-03-27 12:21:30 +01:00
Andrey Antukh
d67c7f1c8e
Add retry mechanism for idenpotent get repo requests on frontend (#8792)
* ♻️ Handle fetch-error gracefully with toast instead of full-page error

Network-level failures (lost connectivity, DNS failure, etc.) on RPC
calls were propagating as :internal/:fetch-error to the global error
handler, which replaced the entire UI with a full-page error screen.

Now the :internal handler distinguishes :fetch-error from other internal
errors and shows a non-intrusive toast notification instead, allowing
the user to continue working.

*  Add automatic retry with backoff for idempotent RPC requests

Idempotent (GET) RPC requests are now automatically retried up to 3
times with exponential back-off (1s, 2s, 4s) when a transient error
occurs.  Retryable errors include: network-level failures
(:fetch-error), 502 Bad Gateway, 503 Service Unavailable, and browser
offline (status 0).

Mutation (POST) requests are never retried to avoid unintended
side-effects.  Non-transient errors (4xx client errors, auth errors,
validation errors) propagate immediately without retry.

* ♻️ Make retry helpers public with configurable parameters

Make retryable-error? and with-retry public functions, and replace
private constants with a default-retry-config map.  with-retry now
accepts an optional config map (:max-retries, :base-delay-ms) enabling
callers and tests to customize retry behavior.

*  Add tests for RPC retry mechanism

Comprehensive tests for the retry helpers in app.main.repo:
- retryable-error? predicate: covers all retryable types (fetch-error,
  bad-gateway, service-unavailable, offline) and non-retryable types
  (validation, authentication, authorization, plain errors)
- with-retry observable wrapper: verifies immediate success, recovery
  after transient failures, max-retries exhaustion, no retry for
  non-retryable errors, fetch-error retry, custom config, and mixed
  error scenarios

* ♻️ Introduce :network error type for fetch-level failures

Replace the awkward {:type :internal :code :fetch-error} combination
with a proper {:type :network} type in app.util.http/fetch.  This makes
the error taxonomy self-explanatory and removes the special-case branch
in the :internal handler.

Consequences:
- http.cljs: emit {:type :network} instead of {:type :internal :code :fetch-error}
- errors.cljs: add a dedicated ptk/handle-error :network method (toast);
  restore :internal handler to its original unconditional full-page error form
- repo.cljs: simplify retryable-types and retryable-error? — :network
  replaces the former :internal special-case, no code check needed
- repo_test.cljs: update tests to use {:type :network}

* 📚 Add comment explaining the use of bit-shift-left
2026-03-27 11:10:26 +01:00
Andrey Antukh
3eaf67a385
🐛 Fix fetch abort errors escaping the unhandled exception handler (#8801)
When AbortController.abort(reason) is called with a custom reason (a
ClojureScript ExceptionInfo), modern browsers (Chrome 98+, Firefox 97+)
reject the fetch promise with that reason object directly instead of with
the canonical DOMException{name:'AbortError'}.  The ExceptionInfo has
.name === 'Error', so both the p/catch guard and is-ignorable-exception?
failed to recognise it as an abort, letting it surface to users as an
error toast.

Fix by calling .abort() without a reason so the browser always produces
a native DOMException whose .name is 'AbortError', which is correctly
handled by all existing guards.

Also add a defense-in-depth check in is-ignorable-exception? that
filters errors whose message matches the 'fetch to \'' prefix, guarding
against any future re-introduction of a custom abort reason.

Co-authored-by: Penpot Dev <dev@penpot.app>
2026-03-26 14:13:38 +01:00
Marina López
cd67dc42c4
🐛 Fix dates to avoid show them in english when browser is in auto (#8775) 2026-03-26 09:33:13 +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
01284e2a00
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:54:05 +01:00
Andrey Antukh
8928e274fc Merge remote-tracking branch 'origin/main' into staging 2026-03-24 18:01:38 +01:00