Add a shared `schema:font-family` whitelist validator in
app.common.types.font that only allows letters, digits, spaces,
hyphens, underscores, and dots in font family names. Apply the schema
to create-font-variant and update-font RPC endpoints on the
backend, and add client-side validation in the dashboard fonts UI.
Include unit tests for the schema and integration tests for the RPC
handlers.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
The workspace Actions history panel previously showed the operation
icon and a one-line message for each undo entry with no indication
of when the action happened, any stable way to refer to it, or who
made the change. The reporter of issue #7660 (and @Takhoffman's
follow-up comment) asked for a git-like display: `<hash> · <time> by
<name>`.
This change stamps each undo entry with its creation timestamp and
author at the moment it lands on the undo stack and surfaces three
extra pieces of information in the history sidebar:
- A short 7-character identifier derived from the entry's existing
`:undo-group` UUID. Hovering shows the full UUID.
- A relative timestamp (e.g. `just now`, `5 minutes ago`, `2 hours
ago`, `3 days ago`) rendered via `app.common.time/timeago` so it
matches the formatting already used for comments and the dashboard.
- The display name of the profile that created the entry, rendered
as `by <Name>` in the same metadata row.
The undo stack is client-side per profile, so every entry is always
the current user; the author is stored on the entry anyway so the UI
does not need to reach into profile state while rendering and so the
data stays correct if the stack shape ever changes.
Changes at a glance:
- `data/workspace/undo.cljs`: extend `schema:undo-entry` with an
optional `:timestamp` and `:by`; new `profile-display-name` helper
that falls back from full name to email to nil; `stamp-entry` now
takes state and fills in both fields on entries that do not
already carry them. Pre-stamped entries (e.g. coming out of an
accumulated transaction) keep their original values.
- `ui/workspace/sidebar/history.cljs`: propagate `:timestamp`,
`:undo-group`, and `:by` through `parse-entries`; add `short-id`
helper; render the metadata row in `history-entry` using
`app.common.time/timeago` against `:timestamp`; skip the author
span entirely when `:by` is nil.
- `ui/workspace/sidebar/history.scss`: styling for the new metadata
row (monospace hash, muted separator, truncated time/author).
- `translations/en.po`: 1 new string for `by %s`.
Existing undo entries created before this change have neither
timestamp nor author; the UI is defensive about both, so old entries
simply render with whatever data they have (and often the plain
title on its own).
Github #7660
Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Signed-off-by: FairyPiggyDev <luislee3108@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 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>
* ✨ Implement asset re-uploading to wasm
* ✨ Show toast instead of error screen when webgl context is lost
* 🎉 Recover context after webgl context restored event
* 🎉 Set Read-only mode when the context has been lost
* ✨ Disable scroll & zoom when context loss
* ✨ Fix stale reload payload
* ✨ Use existing debounce util to take screenshots
* ✨ Implement design / ux specs
* ✨ Fix playwright test by looking for toast, not error page
When the Stripe checkout fails to start, the subscription page now
shows an inline error in the Business Nitrate card under the CTA
instead of a toast. When the post-payment activation fails, the toast
message is updated to point users to support@penpot.app.
The nitrate-form modal also passed a URI object to
build-nitrate-callback-urls while the underlying append-query-param
relied on lambdaisland's u/parse, which only accepts strings. Switched
to the local u/uri helper so both strings and URI records work, so
failures opened from the modal land on the subscription page.
* ✨ Show specific error messages for invitation token failures
Surface distinct error messages for the three invitation-token failure
modes that the backend already distinguishes: email mismatch, expired
token, and invalid/corrupted token. Replaces the single generic
could not accept invitation message with actionable text so the
user knows what went wrong and how to recover.
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
* 💄 Update CHANGE.md
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
* 💄 Address review feedback on invitation-error messages
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
---------
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Rename subscription.settings.sucess.dialog.{title,footer} to
subscription.settings.success.dialog.{title,footer} in en.po and
update the three callsites in subscription.cljs.
Closes#9203
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
When working with large asset groups, users asked for a one-click way
to remove every asset under a group path. Multi-select across hundreds
of items is impractical, and ungrouping first and then deleting leaves
the orphaned items in the flat list.
This change adds a "Delete group" option to the assets-panel
context-menu for three asset types that already carry group structure:
- Components (including variants — sibling variants sharing a variant
container are deduplicated, and the container is deleted once via
the same dispatch the per-item delete uses in file_library.cljs).
- Colors.
- Typographies.
A confirmation modal is shown before deletion, with the count of
assets to be removed, so the action is never silent. All deletes run
inside a single undo transaction, so one Cmd+Z restores the whole
group.
Changes
-------
- `assets/groups.cljs` — `asset-group-title*` accepts an optional
`on-delete-group` prop and conditionally adds the menu entry
between "Ungroup" and "Combine as variants". When the callback is
not supplied the option is hidden, so asset sections that do not
implement it stay unaffected.
- `assets/components.cljs` — threads `on-delete-group` through the
recursive `components-group*` and defines the section-level
handler, dispatching to `dwsh/delete-shapes` for variant containers
and `dwl/delete-component` for plain components.
- `assets/colors.cljs` — same threading + a simple `dwl/delete-color`
dispatch per color in the group.
- `assets/typographies.cljs` — same threading + a
`dwl/delete-typography` dispatch per typography in the group.
- `translations/en.po` — three new strings: the menu label
(`workspace.assets.delete-group`) and the modal title/message
(`modals.delete-asset-group.title`/`.message`, plural-aware).
Github #9141
Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Signed-off-by: FairyPiggyDev <luislee3108@gmail.com>
* ✨ Add read-only preview mode for saved versions (#7622)
* 🔧 Address review feedback on version preview (#7622)
* 🐛 Fix version preview for WASM renderer (#7622)
* 🐛 Fix stylelint color-named and color-function-notation in preview banner (#7622)
* 🐛 Fix invalid-arity call to initialize-workspace in exit-preview (#7622)
* 🐛 Fix unclosed defn paren in exit-preview (#7622)
* ♻️ Refactor version preview/restore flow
Separate enter-preview and enter-restore flows with dedicated dialogs
instead of a persistent banner. Removes preview-banner component in favor
of inline actions dialog. Uses backup/restore pattern for exit-preview
instead of full workspace reinitialization. Adds analytics events for
preview/restore actions.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ⚡ Extract on-name-input-focus as namespace-level private function
The callback had no dependencies on component-local state or props,
making it a pure function that can be hoisted to a defn-. This avoids
recreating the same callback identity on every render of version-entry*.
* ⚡ Extract extract-id-from-event helper to deduplicate snapshot callbacks
Three callbacks in snapshot-entry* shared the same DOM extraction logic
(get current target, read data-id, parse UUID). Extracted into a private
defn- to remove the duplication and simplify each callback.
* ⚡ Extract pure state-update callbacks from versions-toolbox* to namespace level
Eight callbacks that only emit fixed Potok events with no meaningful
deps were hoisted out of the component as defn- functions:
- on-create-version
- on-edit-version
- on-cancel-version-edition
- on-rename-version
- on-delete-version
- on-pin-version
- on-lock-version
- on-unlock-version
These no longer need mf/use-fn wrappers since namespace-level functions
have stable identity across renders, avoiding unnecessary callback
recreation on each render cycle.
* ✨ Rename filter parameter to filter-value in on-change-filter to avoid core shadowing
The parameter name 'filter' shadowed clojure.core/filter within the
function scope. Renamed to 'filter-value' for clarity and to prevent
potential bugs if core/filter were needed in future changes.
* 🔧 Fix linter warnings and errors across version-related namespaces
frontend/src/app/main/ui/workspace.cljs:
- Remove unused requires: app.common.data, app.main.data.notifications,
app.main.data.workspace.versions
frontend/src/app/main/data/workspace/versions.cljs:
- Remove unused require: app.common.uuid
- Fix duplicate reify type: enter-restore used ::restore-version
(same as the private restore-version fn), renamed to ::enter-restore
- Remove unused bindings: state in enter-restore, team-id in
exit-preview and restore-version-from-plugin
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Signed-off-by: wdeveloper16 <wdeveloer16@protonmail.com>
Co-authored-by: wdeveloper16 <wdeveloer16@protonmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* ✨ 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>