Let users pick the pixel grid color from a standard color picker.
The grid color was previously hardcoded, making it invisible on
mid-tone canvases. Choice is stored on the file so it persists
across sessions. Defaults preserve the current appearance when
unset.
Closes#7750
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Closes#7736.
The Layers sidebar offered no way to expand every nested level of a
single subtree at once. Unfolding a layer that wraps a deep tree
required clicking each disclosure indicator one level at a time -
O(siblings * depth) clicks. The asymmetry was particularly visible
next to the existing Shift+click gesture, which collapses every
layer in the panel in a single action via `dwc/collapse-all`, with
no expand counterpart for either a single subtree or the whole
tree.
Add a new `dwc/expand-subtree` event in
`app.main.data.workspace.collapse` that uses
`cfh/get-children-ids-with-self` to gather the shape's id together
with every descendant id, then merges `{descendant-id true}` entries
into `[:workspace-local :expanded]` so the entire subtree opens in
one update. Existing expansion state on unrelated branches is left
untouched (`merge`, not `assoc`), matching the per-key shape used by
`toggle-collapse` and `expand-collapse`.
Wire the gesture into `layer_item.cljs` `toggle-collapse` callback as
a third branch:
- Shift+click while expanded - collapse every layer (existing).
- Alt+click while collapsed - expand the entire subtree (new).
- Otherwise - toggle this single level (existing).
Alt is chosen instead of Shift to avoid the ambiguity the issue
author flagged: "for a layer of middle depth it is unclear whether
[Shift+click] should fold all (up to the topmost parent) or expand
all (only the current subtree)". Alt is a common platform
convention for "do this recursively" (Finder, file managers,
several IDEs), so the asymmetric mapping matches user expectations.
The callback's `mf/deps` vector is extended with `id` and `objects`
so the closure refreshes when the shape tree changes.
CHANGES.md entry added under the 2.17.0 New features section.
`library.connectLibrary()` declared its permission check **outside** the
`js/Promise.` wrapper, so when a plugin without `library:write` permission
called `await library.connectLibrary(id)` the method did not return a
`Promise` at all:
- With the default `throwValidationErrors` flag off → `u/not-valid`
logs to console and returns `nil`. `await nil` resolves to `nil`, so
the plugin sees a "successful" result and crashes later when it tries
to use methods on what it thinks is a `LibraryProxy`.
- With `throwValidationErrors` on → `u/not-valid` throws synchronously,
so the caller gets a thrown exception instead of a rejected promise —
inconsistent with every other `library:*` / `content:*` method which
always returns a Promise that rejects via `reject-not-valid`.
Additionally, the in-Promise `(not (string? library-id))` branch used
`(reject nil)` — the plugin got a rejected Promise but with no error
message.
Move the permission check inside the Promise constructor and replace
both validation errors with `u/reject-not-valid`, matching the pattern
used by the sibling methods `restore`, `remove`, `pin`, `saveVersion`,
`findVersions` in `frontend/src/app/plugins/file.cljs` and every other
promise-returning plugin method. No new imports.
Also add a CHANGES.md entry under the 2.17.0 Unreleased bugs-fixed section.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Closes#9092.
`restore-version-from-plugin` accepted `_reject` as a dead parameter and
its stream had no `rx/catch`, so errors raised during the restore flow
(failed `rp/cmd! :restore-file-snapshot`, persistence timeouts, or
exceptions inside the watch body) silently swallowed instead of
rejecting the plugin-facing promise at `file.cljs:81`. Plugin code
that did `await version.restore()` would hang indefinitely on any
failure.
Wire `reject` through and wrap the emission with the same `rx/catch`
pattern already used by `create-version-from-plugins` in this file.
- Rename `_reject` to `reject` in the function signature
- Wrap the `rx/concat` body with `rx/catch` that calls `(reject error)`
and returns `rx/empty` on error, mirroring `create-version-from-plugins`
- Add a CHANGES.md entry under the 2.17.0 Unreleased bugs-fixed section
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
The malli schema for the LDAP provider params (`schema:params` in
`backend/src/app/auth/ldap.clj`) declared the bind-password slot as
`:bind-passwor` (missing trailing `d`). The runtime code in the same
file uses `:bind-password` everywhere — `prepare-params` reads
`(:bind-password cfg)` on line 21 and `try-connectivity` reads
`(:bind-password cfg)` on line 89. Effects of the typo:
1. The schema slot for `:bind-password` is missing, so a wrong type
(e.g. a number or vector instead of a string) for the actual key
slips through `check-params` unvalidated. Malli `[:map ...]` is
open by default, so the genuine `:bind-password` key is silently
accepted as an unknown extra key.
2. Anyone reading the schema (operator, future contributor, or
tooling generating docs) sees a non-existent `:bind-passwor`
parameter and could legitimately set that key — schema would
accept it, runtime would never read it, LDAP bind would silently
fail with a confusing "no password" error.
Cross-checked against the pre-malli `clojure.spec` shape removed in
commit 88fb5e7ab (2024-10-29, "♻️ Update integrant to latest
version", which carried the spec→malli migration). The deleted spec
defined `(s/def ::bind-password ::us/string)` correctly — the typo
was introduced when re-typing the keys into the new malli vector-of-
tuples form.
Add a CHANGES.md entry under the 2.17.0 Unreleased 🐛 Bugs fixed
section.
One-character fix.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
login-with-ldap raised a :restriction exception with the message
"ldap auth provider is not initialized" stored under :hide instead
of :hint. ex/raise (common/src/app/common/exceptions.cljc:33-34)
uses :hint as the ExceptionInfo message and the downstream error
formatters only read :hint (line 250, 312) — :hide is unread
anywhere in the codebase (0 other occurrences vs 447 for :hint).
Effect: when LDAP is misconfigured, operators saw the generic
"restriction" error message instead of the diagnostic string. The
typo has been present since the LDAP command was first introduced
by commit 14d1cb90bd (2022-06-30, "Refactor auth code") and was
carried forward through 6cdf696fc (2023-01-05, "Fix issues on ldap
provider and rpc method") without ever surfacing as a code-review
comment.
One-character fix: :hide -> :hint. Add a CHANGES.md entry under
the 2.17.0 Unreleased 🐛 Bugs fixed section.
The "Update Settings" button in Your Account > Settings and Notifications
was always enabled, even when the form had no changes, and clicking it
emitted a success notification despite no data being modified.
Disable the submit button when the current form data equals its initial
state, so it activates only when there are actual changes to persist.
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
The submenu opened by hovering Help & Learning in the user account
menu rendered with a vertical offset, making it appear visually
disconnected from its parent row and aligned instead with the
Community
Signed-off-by: Juan Flores <112629487+juan-flores077@users.noreply.github.com>
Closes#9108.
The `case` expression in `get-info` (`backend/src/app/auth/oidc.clj`)
dispatched on `:token` and `:userinfo` keywords, but the provider map's
`:user-info-source` value is a string — both from config (the malli
schema in `app.config` pins it to one of `"token"`, `"userinfo"`,
`"auto"`) and from the hard-coded Google / GitHub provider maps (which
already write `"userinfo"`). Strings never equal keywords in Clojure
`case`, so every call fell through to the auto-fallback that prefers
ID-token claims and only hits the UserInfo endpoint when claims are
empty. The net effect: setting `PENPOT_OIDC_USER_INFO_SOURCE=userinfo`
did nothing, and OIDC flows whose IdP requires the UserInfo endpoint
(so claims come back empty/partial) failed with "incomplete user info".
- Extract a pure helper `select-user-info-source` that maps the raw
config string to a dispatch keyword (`:token`, `:userinfo`, `:auto`),
falling back to `:auto` for unknown / missing / accidentally-keyword
values
- Rewrite `get-info`'s `case` to dispatch on the helper's output so
the arms unambiguously match the normalised keyword
- Add vitest-style deftests in `auth_oidc_test.clj` pinning the three
valid strings, the nil / "auto" / unknown fallback, and the reverse
regression (a keyword input must not slip through as if it were the
matching string)
- Add a CHANGES.md entry under the 2.17.0 Unreleased `🐛 Bugs fixed`
section linking back to #9108
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* ✨ Remove the need to navigate to page for deletion operation
* 🐛 Fix multiple selection with applied-tokens on stroke-color
* 🐛 Fix button position on page header
---------
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
The plugin parser's parse-point returned a plain `{:x … :y …}` map,
but shape interaction schemas (for example schema:open-overlay-interaction)
require the attribute to be a `::gpt/point` record. `(instance? Point {:x 0 :y 0})`
is false, so validation silently rejected plugin `addInteraction` calls
that passed `manualPositionLocation`; only a console warning was produced.
Change parse-point to return a `gpt/point` record via `gpt/point`.
All three call sites (parser.cljs:open-overlay, plugins/page.cljs,
plugins/comments.cljs) continue to work because Point records support
the same `:x`/`:y` access plain maps do.
Add a unit test that covers nil input and verifies the returned value
satisfies `gpt/point?`.
Github #8409
Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
When a text element has a line-height coming from a design token, the value
may be a number (e.g. 1.5) and fails frontend data validation expecting a
string. Normalize line-height before creating the typography style so the
operation succeeds without throwing an assertion error.
Signed-off-by: juan-flores077 <toptalent399@gmail.com>
The viewer-side `obfuscate-email` helper used by `anonymize-member` when
building share-link bundles called `clojure.string/split` on the raw
email input and then on the extracted domain. Two failure modes:
1. When the stored email had no `@` (legacy data, LDAP-sourced UIDs, direct
DB inserts, or fixtures that bypassed `::sm/email`), destructuring
left `domain` bound to `nil` and the follow-up `(str/split nil "." 2)`
raised `NullPointerException`. Because `obfuscate-email` runs inside
`get-view-only-bundle`, the exception aborted the whole RPC response
for share-link viewers, not just the field.
2. When the stored email used a single-label domain (`alice@localhost`),
`(str/split "localhost" "." 2)` returned `["localhost"]`; destructuring
bound `rest` to `nil` and the final `(str name "@****." rest)` produced
a dangling-dot output `"****@****."` (nil coerces to empty in `str`).
Guard both split calls with `(or x "")` so the chain is nil-safe, and
emit the trailing `.<tld>` segment only when `rest` is present. Add three
`deftest` groups covering the happy path, dotless domains, and malformed
inputs (nil / empty / no-`@`), plus a CHANGES.md entry under the 2.17.0
Unreleased bugs-fixed section.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix text export with custom fonts across SVG, PNG and JPG
Text layers using custom or non-standard fonts were rendered incorrectly
on export regardless of the target format. The exporter was not resolving
the font face correctly before rasterization/serialization, causing the
output to fall back to a default glyph set and producing broken or
misaligned text. This fix ensures font data is resolved and embedded
consistently in the export pipeline for all output formats.
Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>
* 📚 Add entry to CHANGES.md under 2.17.0
Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>
---------
Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>
Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>
* ✨ Add loader feedback while importing and exporting files
Show a loader icon with a status label ("Importing files…" /
"Exporting files…") in the import and export dialog footers while the
operation is running, so users get clear in-progress feedback and
cannot retrigger the action by mistake.
Closes#9020
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
* ✨ Address import/export loader feedback PR review
- Show the loader beside file names in the import dialog while files
are being imported (previously queued entries kept showing the
Penpot logo until each one moved into :import-progress).
- Drop the loader from the "Importing files…" / "Exporting files…"
footer status, leaving just the text styled with the modal title
color, per the design proposal.
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
* ✨ Match design proposal for import/export progress feedback
- Move the in-progress label from the modal footer into the modal
body, under the file rows, styled italic with the modal title
color.
- Rename the labels to match the design wording: "Uploading file…"
for import and "Downloading file…" for export.
- Restore the disabled "Accept" button in the import footer during
the import-progress phase, mirroring the disabled "Close" button
used by export.
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
* 🐛 Rename deprecated bodySmallTypography mixin to body-small-typography
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
---------
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Fixes#9067. Adds a delete button that appears on hover over an
uploaded profile photo; clicking it opens a confirm modal and, on
accept, clears the stored photo so the generated fallback avatar is
shown again. A new :delete-profile-photo RPC schedules the old
storage object for garbage collection and sets photo-id to null.
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Paste clipboard contents in place of the currently selected shape,
inheriting its position, parent, and z-index. The replaced shape
is deleted in the same transaction for a single undo step.
Signed-off-by: eureka0928 <meobius123@gmail.com>
* ✨ Add search bar to color palette
Fixes#7653
Signed-off-by: eureka0928 <meobius123@gmail.com>
* ♻️ Use search icon toggle for color palette search
Address UX feedback: replace always-visible search input with a
search icon that toggles the input on click. Hide the search
functionality when no colors exist. Move CHANGES.md entry to
2.16.0 section.
Signed-off-by: eureka0928 <meobius123@gmail.com>
---------
Signed-off-by: eureka0928 <meobius123@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>