* 🐛 Fix area selection aborted by select-shapes interrupt
Only emit :interrupt from select-shapes when edition mode is active.
Unconditional :interrupt (from #10798) made drag-stopper cancel the
marquee mid-drag.
* 🔧 Fix text editor v2 fill e2e test on develop
Each event entry in `last-events` is now wrapped as
`{:name <event-type> :t (app.common.time/now)}` so every event carries a
wall-clock timestamp. A new helper `format-last-events` renders the
buffer as a multi-line string with ISO time and delta-since-previous-
event in ms, replacing the previous pprint dump in error reports.
This lets support/devs tell whether the events leading up to a crash
were spaced out (user action) or jammed together (runaway loop).
AI-assisted-by: minimax-m3
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Non-export zip files were tagged as :legacy-zip with the body attached,
causing downstream parsing to crash on unrecognized zip content. Now
they are marked :unknown, matching how other unrecognized formats are
handled, so the import fails gracefully.
AI-assisted-by: deepseek-v4-flash
Holding an arrow key on a selection with a fast OS key-repeat rate
crashed the workspace with React error #185 (Maximum update depth
exceeded): each OS key-repeat event was converted 1:1 into a
`set-modifiers`/`set-wasm-modifiers` store write inside
`nudge-selected-shapes` with no throttle, starving the renderer.
The sibling mouse-driven resize/rotate/move paths got an `rx/sample`
throttle in PR #10560; the keyboard-nudge path was the only transform
stream left un-throttled. This change applies the same `rx/sample`
throttle to the nudge stream, mirroring the drag-path structure, and
adds a regression test guarding the final committed position
invariant under a burst of 20 `move-selected` events for both the WASM
and legacy (non-WASM) branches.
Closes#10726
AI-assisted-by: glm-5.2
The dashboard route can be reached without a `:team-id` query parameter
(e.g. `/#/dashboard/recent`). When that happened, `team-container*` was
emitting `dtm/initialize-team` with a `nil` team-id, which set
`:current-team-id` to `nil` in the application state. The dashboard
and workspace initialize events then built `df/fetch-fonts` with a
`nil` team-id, producing a `:get-font-variants` RPC with empty params
`{}` that the backend rejected with HTTP 400.
Guard `team-container*` so it does not emit `initialize-team` /
`finalize-team` and does not render the children when `team-id` is
not a uuid. The `with-effect` body and the render are guarded
independently; the cleanup closure captures the same `team-id` as the
setup, so the finalize still fires correctly when transitioning between
valid teams.
AI-assisted-by: minimax-m3
The workspace route can be reached without a `:file-id` query parameter
(e.g. `/#/workspace?team-id=...`). When that happened, `workspace*` was
emitting `dw/initialize-workspace` with a nil file-id, which stored nil
in `:current-file-id`. The `fetch-profiles` event then read nil from
state and called `:get-profiles-for-file-comments` with `{:file-id
nil}`, producing a 400 response.
Move the `use-equal-memo` calls for `file-id` and `page-id` from
`workspace*` up to `workspace-page*`, and guard the render with
`(when (uuid? file-id) ...)` so `workspace*` only mounts when `file-id`
is a valid uuid. Since `workspace*` never mounts with a nil `file-id`,
`initialize-workspace` is never emitted with nil, and the 400 is
prevented at the source.
AI-assisted-by: minimax-m3
Position-data entries for text shapes could omit the :fills key when the
element had no explicit fills, causing backend malli validation failures.
- Use get-default-text-attrs as base in WASM calculate-position-data
- Default fills when CSS --fills property is absent in DOM calc path
AI-assisted-by: deepseek-v4-pro
When ImageMagick fails to process an uploaded image (e.g., corrupted PNG
with invalid IHDR data), the backend was raising :type :internal with
:code :imagemagick-error, which mapped to HTTP 500. The frontend treated
this as a server error and displayed the full error page.
Changed exec-magick! to raise :type :validation with :code :invalid-image
instead. This flows through the existing :invalid-image handler in
errors.clj which returns HTTP 400. The frontend's handle-media-error and
process-error now catch this code and show a notification banner.
AI-assisted-by: qwen3.7-plus
The webhook creation form used a hardcoded AWS instance metadata
endpoint (http://169.254.169.254/...) as the default :uri value for
new webhooks. This leaked an internal cloud credential endpoint into
the UI defaults and could expose it to users.
Remove the default :uri so new webhooks start with an empty URI
instead of a sensitive hardcoded value.
Event targets can be DOM text nodes (nodeType 3) which lack the
.closest() method. Add a get-element helper that normalizes text
nodes to their parent Element before calling .closest(), matching
the existing pattern in dom/get-parent-with-data.
Fixes#10640
AI-assisted-by: mimo-v2.5-pro
* 🐛 Fix too much recursion when clicking shape in comments mode
Remove `deselect-all` from `handle-interrupt` in comments mode. The
`select-shape` event emits `:interrupt` which the comments stream
watcher routes to `handle-interrupt`. In comments mode, calling
`deselect-all` cleared the selection and emitted a competing
`rt/nav`, creating a synchronous recursion cycle in the potok store
that overflowed the JS call stack.
Fixes#10620
AI-assisted-by: opencode
* 🐛 Add unit tests for comments handle-interrupt
Make `handle-interrupt` public (defn- → defn) and add 4 tests covering
each branch: draft thread, open thread, comments mode, and noop.
AI-assisted-by: opencode
Guard remaining text pipeline locations that accessed :selrect directly
against nil/zero-dimension selrects by using safe-size-rect, which
provides a 4-level fallback chain (selrect -> points -> shape fields ->
empty 0.01x0.01 rect).
Fixes:
- fix-position in viewport_texts_html.cljs: replaced dm/get-prop
:selrect with ctm/safe-size-rect for both old and new shape
- assoc-position-data in modifiers.cljs: replaced (:selrect ...)
with ctm/safe-size-rect for delta computation
- change-orientation-modifiers in modifiers.cljc: replaced raw
:selrect access with safe-size-rect for scale and origin computation
Closes#10617
AI-assisted-by: mimo-v2.5-pro
Use `get` instead of `nth` to avoid index-out-of-bounds when a selected
component copy has fewer variant properties than the first component in
the selection.
Closes#10615
AI-assisted-by: deepseek-v4-pro
The InteractionProxy `delay` setter rejected `0` via `(not (pos? value))`,
so an immediate after-delay interaction (`interaction.delay = 0`) was
refused. The model allows it: `set-delay`
(common/.../shape/interactions.cljc) only asserts `check-safe-int`, with
no positivity constraint, and `safe-int` permits 0.
Replace the guard with `(or (not (sm/valid-safe-int? value)) (neg? value))`:
it allows 0 and positive integers, rejects negatives, and rejects
non-integers cleanly (the previous `number?` check let fractional values
through, which then failed the model's `check-safe-int` assertion
downstream).
Adds a regression test (delay round-trips a positive value, then 0).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
The ShapeProxy `borderRadius` setters (`borderRadius` and the four
per-corner variants) validated with `sm/valid-safe-int?`, so a fractional
radius (e.g. `shape.borderRadius = 7.5`) was rejected as invalid. The data
model types `:r1`-`:r4` as `::sm/safe-number` (shape.cljc), the radius
sidebar input only constrains `min 0` (not integer), and `set-radius-*`
store the value verbatim — so the plugin API was stricter than the model.
Same class of defect as the merged #9780.
Switch those 5 guards to `sm/valid-safe-number?`, keeping the existing
non-negative guard on the all-corners setter. Integer-only setters in the
file (`getRange` bounds, `setParentIndex`) keep `valid-safe-int?`.
Adds a regression test asserting fractional radius values are accepted
(with throwValidationErrors enabled).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
The flex and grid layout proxies validated `rowGap`, `columnGap` and the
four padding setters with `sm/valid-safe-int?`, so a fractional value
(e.g. `flex.rowGap = 10.5`) was rejected as invalid. The data model types
`:row-gap`/`:column-gap` and `:p1`-`:p4` as `::sm/safe-number`
(layout.cljc), and the sidebar accepts decimals, so the plugin API was
stricter than the model — the same class of defect as the merged #9780.
Switch those 16 gap/padding guards (8 in flex.cljs, 8 in grid.cljs) to
`sm/valid-safe-number?`, matching the model and the predicate already used
by the flex-element setters in the same file. Integer-only setters
(`zIndex`, grid track indices/counts and cell positions/spans) keep
`valid-safe-int?`. Also fixes a `:righPadding` typo in two grid
rightPadding error branches.
Adds a regression test asserting fractional gap/padding values are
accepted (with throwValidationErrors enabled) for both flex and grid.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Setting or clearing `board.guides` from a plugin threw a malli
`:malli.core/invalid-schema` for every value, including `[]`. Causes:
- `shape.cljs` validated against `[:vector ::ctg/grid]`, but the
`:app.common.types.grid/grid` reference is no longer registered
(direct schemas replaced the namespaced-keyword registrations). Use
the direct var `ctg/schema:grid`, matching every other setter.
- `parser.cljs` `parse-frame-guide` returned the `parse-frame-guide-column`
/ `parse-frame-guide-row` fns instead of calling them with the guide,
so column/row guides parsed to a vector containing a function.
- `parse-frame-guide-square` used `parse-frame-guide-column-params`
instead of the dedicated `parse-frame-guide-square-params`.
Adds a regression test parsing column/square guides and validating the
result (and an empty vector) against `ctg/schema:grid`.
Fixes#9773
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
The plugin proxy `:on-error` handler funneled every throwable through
`handle-error`, which only produced a message for malli ExceptionInfo
carrying `::sm/explain`. Two paths lost the real cause and rendered the
unhelpful "[PENPOT PLUGIN] Value not valid. Code: :error":
- A plain JS error (TypeError, etc.) is not an ExceptionInfo, so
`(ex-data cause)` is nil and the message bound to nil; the real
`.-message` only reached the host-page console, invisible to the
plugin sandbox.
- A malli explain whose errors flatten to nothing made `error-messages`
return "", which rendered as "Value not valid: . Code: :error".
`error-messages` now returns nil (not "") when there is nothing to
render, and `handle-error` falls back to the raw explain, then to the
throwable's `ex-message`/`str`, so a useful message always surfaces.
Working cases (well-formed explain) are unchanged.
Adds regression tests for the plain-JS-error path, the empty-explain
path, and the `error-messages` nil contract.
Fixes#9692
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>