mirror of
https://github.com/penpot/penpot.git
synced 2026-09-09 12:19:58 -04:00
Compare commits
10
Commits
issue-11573
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac84557740 | ||
|
|
43f7e49aa0 | ||
|
|
eca1d81692 | ||
|
|
d263c23a58 | ||
|
|
8a3540336b | ||
|
|
6f4a68100d | ||
|
|
62fdc02c0f | ||
|
|
011feeaf71 | ||
|
|
e96a75d366 | ||
|
|
3b1290c6a2 |
No files matched your search
+2
-1
@@ -34,7 +34,7 @@ JSON, REPL access, and so on.
|
||||
|---|---|---|
|
||||
| [`make-a-plan`](skills/make-a-plan/SKILL.md) | Researches the task, writes an implementation plan, asks you the open questions in plain language, and saves the plan to `.agents/plans/`. | "make a plan for the token refresh bug" |
|
||||
| [`review-plan`](skills/review-plan/SKILL.md) | Evaluates a plan before anyone writes code: completeness, ordering, risks. Approves it or asks for changes. | "review this plan before we start" |
|
||||
| [`implement-plan`](skills/implement-plan/SKILL.md) | Shows you the full flow first — the issue and branch it will create (or the branch it continues on), the execution style, and the task checklist — and, after your go-ahead, executes a ready plan. Default: every task, one commit. On request ("step by step"): one task, one commit, your confirmation between tasks. | "implement the plan" · "step by step, one commit per task" |
|
||||
| [`implement-plan`](skills/implement-plan/SKILL.md) | Shows you the full flow first — the issue and branch it will create (or the branch it continues on), the execution style, and the task checklist — and, after your go-ahead, executes a ready plan. Default: every task, one commit. On request ("step by step"): one task, one commit, your confirmation between tasks. On request ("direct"): no issue and no branch, commits on the current branch. | "implement the plan" · "step by step, one commit per task" · "direct, no branch" |
|
||||
| [`review-code`](skills/review-code/SKILL.md) | Reviews a diff, branch, or PR and returns findings ranked by impact. | "review my changes before I push" |
|
||||
| [`create-pr`](skills/create-pr/SKILL.md) | Opens a pull request for the current branch — with checks on base branch, commits, issue, and push state — or updates an existing PR's title and description. | "open a PR for this branch" |
|
||||
| [`resolve-git-conflicts`](skills/resolve-git-conflicts/SKILL.md) | Untangles merge or rebase conflicts: explains both sides, proposes a resolution, applies it after you approve. Never runs `git rebase --continue`. | "resolve these conflicts" |
|
||||
@@ -65,6 +65,7 @@ JSON, REPL access, and so on.
|
||||
| [`nrepl-eval`](skills/nrepl-eval/SKILL.md) | Run Clojure or ClojureScript code in the live REPL sessions (backend and frontend). |
|
||||
| [`taiga`](skills/taiga/SKILL.md) | Look up Penpot issues, user stories, and tasks in Taiga. |
|
||||
| [`testing`](skills/testing/SKILL.md) | The repo's testing rules and TDD workflow, loaded before writing tests. |
|
||||
| [`local-ci`](skills/local-ci/SKILL.md) | Run CI-style lint, test, and format checks for the modules you touched with `scripts/ci`, and read the logs when they fail. |
|
||||
| [`security-and-hardening`](skills/security-and-hardening/SKILL.md) | Security checks for code that handles user input, auth, or external services. |
|
||||
| [`ste`](skills/ste/SKILL.md) | Rewrites prose in Simplified Technical English. Loads only when you name it. |
|
||||
| [`refine-prompt`](skills/refine-prompt/SKILL.md) | Rewrites a rough prompt into a clearer one. Never runs the prompt. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: implement-plan
|
||||
description: Implementation flow — execute a ready plan from the session context: read the plan, detect the flow, then present the full picture (issue and branch to create or the branch to continue on, execution style, task checklist) and wait for confirmation. Default is every task with one final commit; on request ("step by step"), one task and one commit at a time with a pause after each. Use it when the user asks to implement or execute a plan, in any phrasing.
|
||||
description: Implementation flow — execute a ready plan from the session context: read the plan, detect the flow, then present the full picture (issue and branch to create or the branch to continue on, execution style, task checklist) and wait for confirmation. Default is every task with one final commit; on request ("step by step"), one task and one commit at a time with a pause after each; on request ("direct"), no issue and no branch — the commit lands on the current branch. Use it when the user asks to implement or execute a plan, in any phrasing.
|
||||
---
|
||||
|
||||
# Implement Plan
|
||||
@@ -45,9 +45,14 @@ read-only: nothing is created until the user confirms (step 3).
|
||||
No issue or branch is created. The branch name provides the issue
|
||||
reference when it follows the `issue-NNNN` pattern.
|
||||
|
||||
Arguments override detection: `standalone`, `continue`,
|
||||
`no issue` / `without issue`, or an explicit base such as
|
||||
`from origin/develop`.
|
||||
Arguments override detection: `standalone`, `continue`, `direct`
|
||||
(`no branch` / `direct commit`), `no issue` / `without issue`, or an
|
||||
explicit base such as `from origin/develop`.
|
||||
|
||||
**Direct mode** (`direct`, `no branch`, `direct commit`): no issue and
|
||||
no branch — the implementation and the commit land on the current branch
|
||||
as it is, even when it is a base branch. Best for small or tooling-only
|
||||
changes the user wants committed in place.
|
||||
|
||||
**Standalone while already on a feature branch:** stop and explain that this
|
||||
would stack branches. Ask the user to re-run with an explicit base, for
|
||||
@@ -59,8 +64,10 @@ Before touching the repository, show the user the full picture:
|
||||
|
||||
- **The flow**: whether the GitHub issue and the branch will be created
|
||||
(standalone mode — give the planned branch name, `issue-NNNN` or
|
||||
`plan-<slug>`), or whether you continue on the current branch
|
||||
(continue mode — name it).
|
||||
`plan-<slug>`), whether you continue on the current branch
|
||||
(continue mode — name it), or whether everything lands on the current
|
||||
branch as it is (direct mode — name it, and say so when it is a base
|
||||
branch).
|
||||
- **The execution style**: batch or step-by-step (see *Execution modes*).
|
||||
- A checklist (todolist) of the plan's tasks, in order.
|
||||
|
||||
@@ -84,6 +91,11 @@ If the arguments say `no issue` / `without issue`, skip the issue and
|
||||
create a branch named `plan-<slug>` instead, where `<slug>` is the plan
|
||||
title, lowercase and hyphen-separated.
|
||||
|
||||
If the arguments say `direct` / `no branch` / `direct commit`, skip the
|
||||
issue and the branch: implement and commit on the current branch as it
|
||||
is. If it is a base branch, the checklist presentation already said so —
|
||||
no further confirmation is needed.
|
||||
|
||||
### Batch mode (default)
|
||||
|
||||
Implement every task in one go. Work methodically, keeping changes
|
||||
@@ -126,7 +138,7 @@ instruction from me overrides them):
|
||||
|
||||
Extra context in the user's invocation (the message that triggered this
|
||||
skill) plays the role command arguments play elsewhere: `standalone`,
|
||||
`continue`, `no issue` / `without issue`, an explicit base such as
|
||||
`from origin/develop`, or `step by step` / `one commit per task` for the
|
||||
step-by-step execution mode. Modes combine freely, for example
|
||||
"standalone step by step".
|
||||
`continue`, `direct` (`no branch` / `direct commit`), `no issue` /
|
||||
`without issue`, an explicit base such as `from origin/develop`, or
|
||||
`step by step` / `one commit per task` for the step-by-step execution
|
||||
mode. Modes combine freely, for example "standalone step by step".
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: local-ci
|
||||
description: Run local CI-style checks with ./scripts/ci (lint, tests, format) per monorepo module. Use when verifying changes before declaring work done, running lint or tests locally, fixing formatting, or repairing Clojure delimiter errors.
|
||||
---
|
||||
|
||||
# Local CI
|
||||
|
||||
Run the same checks CI runs, locally, for the modules you touched, with
|
||||
`scripts/ci`. Each task writes a log file; the final summary says what
|
||||
passed and what failed.
|
||||
|
||||
Full details: `mem:scripts/ci` (file: `.serena/memories/scripts/ci.md`)
|
||||
|
||||
## When to use
|
||||
|
||||
- After implementing or fixing code — verify every module you touched
|
||||
before declaring the work done.
|
||||
- When the user asks to run CI, lint, tests, or format checks locally.
|
||||
- When you changed `common/` — validate its consumers too.
|
||||
|
||||
**Skip:** while exploring, planning, or reading code.
|
||||
|
||||
## Command reference
|
||||
|
||||
Run from the repo root:
|
||||
|
||||
```bash
|
||||
./scripts/ci [OPTIONS] [MODULES...]
|
||||
```
|
||||
|
||||
Modules: `frontend` `backend` `common` `render-wasm` `exporter` `mcp`
|
||||
`plugins` `library`, or `--all` for every module.
|
||||
|
||||
With no task flags it runs three tasks per module, in order: **lint**,
|
||||
**test**, **fmt** (format check; `--fix` formats files instead).
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `--all` | Run every module |
|
||||
| `--exclude MOD` | Skip one module (repeatable) |
|
||||
| `--lint` / `--no-lint` | Run only lint / drop lint |
|
||||
| `--test` / `--no-test` | Run only tests / drop tests |
|
||||
| `--fmt` / `--no-fmt` | Run only format check / drop it |
|
||||
| `--fix` | Format files instead of checking (other tasks unaffected) |
|
||||
| `--paren-repair` | Fix delimiter errors in Clojure/CLJS files |
|
||||
| `--fail-fast` | Stop at the first failure |
|
||||
| `--quiet` | Suppress failure output |
|
||||
| `--dry-run` | Show what would run, execute nothing |
|
||||
| `--clean` | Delete the `.ci-logs/` directory |
|
||||
|
||||
## Reading failures
|
||||
|
||||
Every task writes its full output to `.ci-logs/<module>-<task>.log`. On
|
||||
failure the script prints only the last 30 lines. To diagnose a failure,
|
||||
**read the log file** — never re-run the command piped through filters
|
||||
(repo rule: redirect to a file first, then read it). The exit code is 1
|
||||
when any task failed; the summary lists each failed `module:task` and its
|
||||
log path.
|
||||
|
||||
## Typical workflows
|
||||
|
||||
```bash
|
||||
# Verify a module you changed: lint + tests + format check
|
||||
./scripts/ci frontend
|
||||
|
||||
# Fast pass while iterating: lint only
|
||||
./scripts/ci --lint frontend
|
||||
|
||||
# Lint + format check, skip the long test suite
|
||||
./scripts/ci --no-test frontend
|
||||
|
||||
# Format the module without running the test suite
|
||||
./scripts/ci --fix --no-test frontend
|
||||
|
||||
# Broke delimiters in Clojure/CLJS files: repair first, then lint
|
||||
./scripts/ci --paren-repair frontend
|
||||
./scripts/ci --lint frontend
|
||||
|
||||
# Changed common/ — validate its consumers too
|
||||
./scripts/ci frontend backend exporter
|
||||
|
||||
# Preview what would run, without running it
|
||||
./scripts/ci --dry-run --all
|
||||
```
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Run from the repo root.
|
||||
- Test tasks are long-running (backend runs `clojure -M:dev:test`); give
|
||||
the bash call a generous timeout (10–20 minutes) instead of letting it
|
||||
time out mid-run.
|
||||
- `mcp` has no lint task — it shows as skipped, not failed.
|
||||
- `--paren-repair` only fixes delimiters; run lint afterwards to catch
|
||||
what remains. See `mem:scripts/paren-repair`.
|
||||
- What to run and how to read test results: `mem:testing`.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Execute a ready plan — task checklist, your confirmation, then all tasks with one commit (default) or step by step with a commit and a pause per task; creates issue + branch when on a base branch — loads and follows the implement-plan skill
|
||||
description: Execute a ready plan — task checklist, your confirmation, then all tasks with one commit (default) or step by step with a commit and a pause per task; creates issue + branch when on a base branch, or commits on the current branch with "direct" — loads and follows the implement-plan skill
|
||||
agent: build
|
||||
---
|
||||
|
||||
|
||||
@@ -78,6 +78,10 @@ module. You can read it from `mem:<MODULE>/core`
|
||||
workspaces (root, modules, member packages). Keeps the shared pnpm store
|
||||
at `<repo>/.pnpm-store` unless `--store`; ignores `external/` and
|
||||
`.opencode/`. Usage and reinstall steps: `mem:workflow/updating-pnpm`.
|
||||
- `scripts/ci` — CI orchestration script: runs lint, tests, and format
|
||||
checks per module (`frontend backend common render-wasm exporter mcp
|
||||
plugins library`). Logs go to `.ci-logs/`; read the log file on failure.
|
||||
See `mem:scripts/ci`.
|
||||
|
||||
# Dependency graph
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# CI (scripts/ci)
|
||||
|
||||
`scripts/ci` runs CI-style checks — lint, tests, format — for one or more
|
||||
monorepo modules and prints a per-task summary. It is the local equivalent
|
||||
of CI; use it to verify changes before declaring work done.
|
||||
|
||||
## When to use
|
||||
|
||||
- After implementing or fixing code in a module: run its checks before
|
||||
finishing (AGENTS.md: run the applicable lint and format checks).
|
||||
- When `common/` changed: validate its consumers too (frontend, backend,
|
||||
exporter; see the dependency graph in `mem:critical-info`).
|
||||
- To fix formatting across a module (`--fix`) or repair delimiters
|
||||
(`--paren-repair`) before linting.
|
||||
|
||||
## How to use (CLI)
|
||||
|
||||
Run from the repo root:
|
||||
|
||||
```bash
|
||||
./scripts/ci MODULE... # lint + test + fmt per module
|
||||
./scripts/ci --all --no-test # lint + fmt on all modules
|
||||
./scripts/ci --lint frontend # lint only
|
||||
./scripts/ci --fix --no-test frontend # format files, skip tests
|
||||
./scripts/ci --paren-repair --all # fix delimiters in all Clojure modules
|
||||
./scripts/ci --dry-run --all # preview what would run
|
||||
```
|
||||
|
||||
Modules: `frontend backend common render-wasm exporter mcp plugins library`.
|
||||
|
||||
Flags:
|
||||
|
||||
- Default tasks: `lint`, `test`, `fmt` (format check; `--fix` formats
|
||||
instead).
|
||||
- `--lint` / `--test` / `--fmt` run one task only; `--no-lint` /
|
||||
`--no-test` / `--no-fmt` drop one task from the default set.
|
||||
- `--paren-repair` runs only the delimiter repair — it wraps
|
||||
`scripts/paren-repair` over each module's Clojure/CLJS sources; see
|
||||
`mem:scripts/paren-repair`.
|
||||
- `--all` selects every module; `--exclude MOD` drops one (repeatable).
|
||||
- `--fail-fast` stops at the first failure; `--quiet` suppresses failure
|
||||
output; `--dry-run` prints commands without running; `--clean` removes
|
||||
the log directory.
|
||||
|
||||
## Logs and exit codes
|
||||
|
||||
- Full output of every task: `.ci-logs/<module>-<task>.log`.
|
||||
- On failure the script prints the last 30 lines; the final summary lists
|
||||
every failed `module:task` with its log path.
|
||||
- Exit code 0 when all selected tasks passed, 1 otherwise.
|
||||
- Diagnose failures by reading the log file — never pipe test output
|
||||
through filters (AGENTS.md hard rule).
|
||||
|
||||
## Notes
|
||||
|
||||
- `mcp` has no lint task (shows as skipped). `render-wasm` uses `./lint`,
|
||||
`./test`, and `cargo fmt`.
|
||||
- Test tasks are long-running (backend: `clojure -M:dev:test`); use a
|
||||
generous timeout when calling it from an agent shell.
|
||||
- Skill entry point: `.agents/skills/local-ci/SKILL.md`.
|
||||
- Testing principles and output discipline: `mem:testing`.
|
||||
@@ -52,10 +52,12 @@ file (never pipe tool output through filters).
|
||||
then re-run `corepack use pnpm@<tag>` in that directory.
|
||||
- A workspace may fail with `ERR_PNPM_IGNORED_BUILDS`, and pnpm then writes
|
||||
a placeholder scaffold into its `pnpm-workspace.yaml`:
|
||||
`allowBuilds: esbuild: set this to true or false` plus
|
||||
`ignoredBuiltDependencies`. Repo convention is `allowBuilds: esbuild: true`.
|
||||
Replace the placeholder and drop the `ignoredBuiltDependencies` entry,
|
||||
then re-run.
|
||||
`allowBuilds: esbuild: set this to true or false`. Current pnpm writes
|
||||
only the `allowBuilds` placeholder; any legacy key still present
|
||||
(`ignoredBuiltDependencies`, `onlyBuiltDependencies`,
|
||||
`neverBuiltDependencies`) is ignored since pnpm 11. Repo convention is
|
||||
`allowBuilds: esbuild: true`. Replace the placeholder and drop the
|
||||
legacy entry, then re-run.
|
||||
- `plugins/apps/composable-test-suite` once had its own
|
||||
`pnpm-workspace.yaml` and acted as a nested workspace root. That state is
|
||||
gone on purpose: pnpm picks the nearest `pnpm-workspace.yaml` walking up,
|
||||
|
||||
@@ -145,6 +145,6 @@ precision while maintaining a strong focus on maintainability and performance.
|
||||
- `scripts/nrepl-eval.mjs` — Evaluate Clojure code via nREPL (backend + frontend).
|
||||
- `scripts/check-commit` — Validate commit messages against Penpot's commit guidelines.
|
||||
- `scripts/check-fmt-clj` — Check Clojure formatting without modifying files.
|
||||
- `scripts/ci` — CI orchestration script for running lint, tests, and format checks across modules. See `scripts/ci --help`.
|
||||
- `scripts/ci` — CI orchestration script for running lint, tests, and format checks across modules. See `mem:scripts/ci`.
|
||||
- `scripts/gh.py` — Multi-purpose GitHub CLI helper. Subcommands: `issues` (list issues in a milestone), `prs` (fetch PR details), `advisories` (list/inspect security advisories). See `python3 scripts/gh.py --help`.
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## 2.18.0 (Unreleased)
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- Group toolbar drawing tools into shape and free-draw flyouts [#9316](https://github.com/penpot/penpot/issues/9316) (PR: [#9480](https://github.com/penpot/penpot/pull/9480), [#10354](https://github.com/penpot/penpot/pull/10354))
|
||||
- Add dedicated Line and Arrow drawing tools (by @davidv399) [#9145](https://github.com/penpot/penpot/issues/9145) (PR: [#9146](https://github.com/penpot/penpot/pull/9146))
|
||||
- Show and manage comments while designing in the workspace [#10239](https://github.com/penpot/penpot/issues/10239) (PR: [#10275](https://github.com/penpot/penpot/pull/10275))
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix MCP integration hanging when the Penpot tab is backgrounded or frozen by the browser [#10323](https://github.com/penpot/penpot/issues/10323) (PR: [#10392](https://github.com/penpot/penpot/pull/10392))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.util.ssrf :as ssrf]
|
||||
[app.worker :as-alias wrk]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[java-http-clj.core :as http])
|
||||
@@ -23,6 +24,8 @@
|
||||
java.net.URI))
|
||||
|
||||
(def default-max-redirects 5)
|
||||
(def default-connect-timeout 30000)
|
||||
(def default-request-timeout 30000)
|
||||
|
||||
(defn client?
|
||||
[o]
|
||||
@@ -33,15 +36,17 @@
|
||||
:pred client?})
|
||||
|
||||
(defmethod ig/init-key ::client
|
||||
[_ _]
|
||||
(http/build-client {:connect-timeout 30000
|
||||
[_ {:keys [::wrk/executor]}]
|
||||
(http/build-client {:connect-timeout default-connect-timeout
|
||||
:executor executor
|
||||
:follow-redirects :never}))
|
||||
|
||||
(defn send!
|
||||
([client req] (send! client req {}))
|
||||
([client req {:keys [response-type] :or {response-type :string}}]
|
||||
(assert (client? client) "expected valid http client")
|
||||
(http/send req {:client client :as response-type})))
|
||||
(http/send (merge {:timeout default-request-timeout} req)
|
||||
{:client client :as response-type})))
|
||||
|
||||
(defn- resolve-client
|
||||
[params]
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
::http.client/client
|
||||
{}
|
||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
::session/manager
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
@@ -75,10 +75,10 @@
|
||||
{:method method
|
||||
:uri uri
|
||||
:body body
|
||||
:headers headers}
|
||||
:headers headers
|
||||
:timeout timeout}
|
||||
{:response-type :input-stream
|
||||
:skip-ssrf-check? true
|
||||
:timeout timeout})
|
||||
:skip-ssrf-check? true})
|
||||
status (:status resp)]
|
||||
(when (not (<= 200 status 299))
|
||||
(let [body (:body resp)]
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
[app.rpc.notifications :as notifications]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.ssrf :as ssrf]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -960,13 +961,18 @@ RETURNING id, deleted_at;")
|
||||
(sv/defmethod ::check-organization-sso
|
||||
"Validate an organization SSO configuration by generating a login redirect URL.
|
||||
Nitrate calls this while configuring SSO to verify client credentials and OIDC
|
||||
discovery before saving the settings."
|
||||
discovery before saving the settings. The issuer URL is nitrate-supplied
|
||||
(customer-configured), so it is checked against the SSRF blocklist before
|
||||
any outbound request is attempted."
|
||||
{::doc/added "2.18"
|
||||
::sm/params cto/schema:nitrate-sso
|
||||
::sm/result schema:check-organization-sso-result
|
||||
::rpc/auth false}
|
||||
[cfg params]
|
||||
{:valid (oidc/is-organization-sso-config-valid? cfg params)})
|
||||
(let [issuer (oidc/organization-sso-discovery-uri params)]
|
||||
{:valid (boolean (and issuer
|
||||
(ssrf/safe-url? issuer)
|
||||
(oidc/is-organization-sso-config-valid? cfg params)))}))
|
||||
|
||||
;; ---- API: notify-organization-sso-change
|
||||
(sv/defmethod ::notify-organization-sso-change
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
[app.http.client :as http]
|
||||
[app.main :as-alias main]
|
||||
[app.setup :as-alias setup]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.json :as json]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]))
|
||||
@@ -248,20 +247,16 @@
|
||||
:props (or (some-> props db/decode-transit-pgobject) {})
|
||||
:context (or (some-> context db/decode-transit-pgobject) {})}))
|
||||
|
||||
(defn- encode-batch
|
||||
"Encode a sequence of event maps into a fressian+zstd base64 string
|
||||
suitable for JSON transport."
|
||||
^String [events]
|
||||
(blob/encode-str events {:version 4}))
|
||||
|
||||
(defn send-event-batch
|
||||
"Send a single batch of events to the telemetry endpoint. Returns
|
||||
true on success."
|
||||
true on success. The events are sent as a plain vector of event
|
||||
maps; the JSON encoder handles UUID and temporal types natively and
|
||||
the receiver coerces them back to proper types."
|
||||
[{:keys [::setup/props] :as cfg} batch]
|
||||
(let [payload {:type :telemetry-events
|
||||
:version (:full cf/version)
|
||||
:instance-id (:instance-id props)
|
||||
:events (encode-batch batch)}
|
||||
:events (vec batch)}
|
||||
request {:method :post
|
||||
:uri (cf/get :telemetry-uri)
|
||||
:headers {"content-type" "application/json"}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.http-client-test
|
||||
(:require
|
||||
[app.http.client :as http]
|
||||
[clojure.test :as t]
|
||||
[java-http-clj.core :as jhttp]
|
||||
[mockery.core :refer [with-mocks]]))
|
||||
|
||||
(t/deftest send-injects-default-timeout-when-absent
|
||||
(with-mocks [mock {:target 'java-http-clj.core/send
|
||||
:return {:status 200 :body ""}}]
|
||||
(let [client (jhttp/build-client {})]
|
||||
(http/send! client {:method :get :uri "https://example.com/"})
|
||||
(let [[req _opts] (:call-args @mock)]
|
||||
(t/is (= http/default-request-timeout (:timeout req)))))))
|
||||
|
||||
(t/deftest send-preserves-caller-supplied-timeout
|
||||
(with-mocks [mock {:target 'java-http-clj.core/send
|
||||
:return {:status 200 :body ""}}]
|
||||
(let [client (jhttp/build-client {})]
|
||||
(http/send! client {:method :get
|
||||
:uri "https://example.com/"
|
||||
:timeout 5000})
|
||||
(let [[req _opts] (:call-args @mock)]
|
||||
(t/is (= 5000 (:timeout req)))))))
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.config :as cf]
|
||||
[app.http.client :as http]
|
||||
[app.media.remote :as media.remote]
|
||||
[app.setup :as-alias setup]
|
||||
[app.util.json :as json]
|
||||
@@ -500,6 +501,22 @@
|
||||
:headers {}})]
|
||||
(t/is (= 200 (:status resp))))))))
|
||||
|
||||
(t/deftest service-request-puts-configured-timeout-in-request
|
||||
(t/testing "service-request puts media-processing-service-timeout on the http request"
|
||||
(let [captured (atom nil)]
|
||||
(with-redefs [cf/get (th/config-get-mock config-mock)
|
||||
http/req (fn [_client request _opts]
|
||||
(reset! captured request)
|
||||
{:status 200
|
||||
:body (json-stream {:width 100 :height 100})})]
|
||||
(media.remote/service-request
|
||||
(mk-system)
|
||||
{:method :post
|
||||
:uri "http://localhost:6065/api/image/info"
|
||||
:body nil
|
||||
:headers {}})
|
||||
(t/is (= 5000 (:timeout @captured)))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Shared key
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.msgbus :as mbus]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.util.ssrf :as ssrf]
|
||||
[app.worker :as wrk]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.set :as set]
|
||||
@@ -1806,13 +1807,14 @@
|
||||
|
||||
(t/deftest check-organization-sso-returns-valid-true
|
||||
(let [organization-id (uuid/random)
|
||||
out (with-redefs [oidc/is-organization-sso-config-valid? (constantly true)]
|
||||
(th/management-command!
|
||||
{::th/type :check-organization-sso
|
||||
:organization-id organization-id
|
||||
:client-id "test-client"
|
||||
:client-secret "test-secret"
|
||||
:issuer "https://idp.example.com"}))]
|
||||
out (with-redefs [ssrf/safe-url? (constantly true)
|
||||
oidc/is-organization-sso-config-valid? (constantly true)]
|
||||
(th/management-command!
|
||||
{::th/type :check-organization-sso
|
||||
:organization-id organization-id
|
||||
:client-id "test-client"
|
||||
:client-secret "test-secret"
|
||||
:issuer "https://idp.example.com"}))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (true? (-> out :result :valid)))))
|
||||
|
||||
@@ -1827,19 +1829,36 @@
|
||||
|
||||
(t/deftest check-organization-sso-passes-issuer-to-validation
|
||||
(let [organization-id (uuid/random)
|
||||
out (with-redefs [oidc/is-organization-sso-config-valid?
|
||||
(fn [_cfg sso]
|
||||
(and (= "test-client" (:client-id sso))
|
||||
(= "https://idp.example.com/" (:issuer sso))))]
|
||||
(th/management-command!
|
||||
{::th/type :check-organization-sso
|
||||
:organization-id organization-id
|
||||
:client-id "test-client"
|
||||
:client-secret "test-secret"
|
||||
:issuer "https://idp.example.com/"}))]
|
||||
out (with-redefs [ssrf/safe-url? (constantly true)
|
||||
oidc/is-organization-sso-config-valid?
|
||||
(fn [_cfg sso]
|
||||
(and (= "test-client" (:client-id sso))
|
||||
(= "https://idp.example.com/" (:issuer sso))))]
|
||||
(th/management-command!
|
||||
{::th/type :check-organization-sso
|
||||
:organization-id organization-id
|
||||
:client-id "test-client"
|
||||
:client-secret "test-secret"
|
||||
:issuer "https://idp.example.com/"}))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (true? (-> out :result :valid)))))
|
||||
|
||||
(t/deftest check-organization-sso-returns-valid-false-on-ssrf-blocked-issuer
|
||||
(t/testing "an SSRF-blocked issuer must not reach the OIDC validation flow"
|
||||
(let [called? (atom false)
|
||||
out (with-redefs [oidc/is-organization-sso-config-valid?
|
||||
(fn [_cfg _sso] (reset! called? true) true)]
|
||||
(th/management-command!
|
||||
{::th/type :check-organization-sso
|
||||
:organization-id (uuid/random)
|
||||
:client-id "test-client"
|
||||
:client-secret "test-secret"
|
||||
:issuer "http://127.0.0.1/idp"}))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (false? (-> out :result :valid)))
|
||||
(t/is (false? @called?)
|
||||
"OIDC validation should not run when the issuer is SSRF-blocked"))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PUSH AUDIT EVENTS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.tasks.telemetry :as telemetry]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.json :as json]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
@@ -59,11 +58,6 @@
|
||||
:cnt
|
||||
long))
|
||||
|
||||
(defn- decode-event-batch
|
||||
"Decode the base64+fressian+zstd event-batch sent to the mock."
|
||||
[b64-str]
|
||||
(blob/decode-str b64-str))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; STATS / REPORT STRUCTURE TESTS (existing behaviour, extended)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -245,21 +239,19 @@
|
||||
(t/is (not (contains? ev :ip-addr)))))))))
|
||||
|
||||
(t/deftest test-batch-encoding-is-decodable
|
||||
;; Verify that encode-batch produces a blob that round-trips back
|
||||
;; through blob/decode to the original data.
|
||||
;; Events are sent as a plain vector of raw event maps (no blob
|
||||
;; encoding): every batch must JSON round-trip unchanged, because
|
||||
;; the receiver coerces types from the plain JSON representation.
|
||||
(let [events [{:name "navigate" :type "action" :source "telemetry"
|
||||
:tracked-at (ct/now)}
|
||||
{:name "create-file" :type "action" :source "telemetry"
|
||||
:tracked-at (ct/now)}]
|
||||
;; Call the private fn through the ns-mapped var
|
||||
encode (ns-resolve 'app.tasks.telemetry 'encode-batch)
|
||||
encoded (encode events)
|
||||
decoded (decode-event-batch encoded)]
|
||||
(t/is (string? encoded))
|
||||
(t/is (seq decoded))
|
||||
(t/is (= (count events) (count decoded)))
|
||||
(t/is (= "navigate" (:name (first decoded))))
|
||||
(t/is (= "create-file" (:name (second decoded))))))
|
||||
encoded (json/encode-str {:events (vec events)})
|
||||
decoded (json/decode encoded)]
|
||||
(t/is (vector? (:events decoded)))
|
||||
(t/is (= (count events) (count (:events decoded))))
|
||||
(t/is (= "navigate" (:name (first (:events decoded)))))
|
||||
(t/is (= "create-file" (:name (second (:events decoded)))))))
|
||||
|
||||
(t/deftest test-multiple-batches-when-many-events
|
||||
;; Lower batch-size to 1 so that 3 events produce 3 separate
|
||||
@@ -787,9 +779,13 @@
|
||||
(t/is (= "telemetry-events" (name (:type body))))
|
||||
(t/is (string? (:version body)))
|
||||
(t/is (some? (:instance-id body)))
|
||||
;; :events is a base64-encoded blob
|
||||
(t/is (string? (:events body)))
|
||||
(t/is (pos? (count (:events body))))))))))
|
||||
;; :events is a plain vector of raw event maps
|
||||
(t/is (vector? (:events body)))
|
||||
(t/is (pos? (count (:events body))))
|
||||
(doseq [ev (:events body)]
|
||||
(t/is (string? (:name ev)))
|
||||
(t/is (string? (:source ev)))
|
||||
(t/is (string? (:tracked-at ev))))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TASK BRANCH COVERAGE
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Comments
|
||||
order: 4
|
||||
order: 6
|
||||
desc: Learn how to import and export files in Penpot, the free, open-source design tool. Discover file formats, backups, sharing, and library management.
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Enterprise plan
|
||||
order: 2
|
||||
desc: Learn how the Enterprise plan works in Penpot. Discover its features and how to use it within your organization.
|
||||
---
|
||||
|
||||
<h1 id="enterprise">Enterprise plan</h1>
|
||||
<p class="main-paragraph">Penpot Enterprise gives organizations the tools to govern how design work happens across their teams: from creating a structured org and managing members, to applying fine-grained permissions and configuration through the Admin Console.</p>
|
||||
|
||||
<h3 id="what-is-penpot-enterprise">What is Penpot Enterprise?
|
||||
</h3>
|
||||
<p>Penpot Enterprise is the plan that unlocks organizational governance features. While Penpot remains free and unlimited as an open-source platform, Enterprise adds a layer of control on top: the ability to create Organizations, manage teams under it, and apply configuration settings that define what members can and cannot do.</p>
|
||||
<p>The organization owner is the user who creates the organization. They have exclusive access to the Admin Console and are responsible for configuring Modules.</p>
|
||||
<p>The key concepts you'll work with:</p>
|
||||
<ul>
|
||||
<li><strong><a href="/user-guide/account-teams/organizations/">Organization</a>:</strong> the top-level structure that groups one or more Teams under a shared governance layer.</li>
|
||||
<li><strong><a href="/user-guide/account-teams/organizations/#the-admin-console">Admin Console</a>:</strong> the back-office interface where the org owner manages settings, teams and members.</li>
|
||||
<li><strong><a href="/user-guide/account-teams/organizations/#modules-and-controls">Modules</a>:</strong> the paid, configurable capabilities applied to an organization. Each Module consists of individual Controls (specific settings or restrictions).</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="subscribing-to-enterprise">Subscribing to Enterprise
|
||||
<a class="direct-link" href="#subscribing-to-enterprise">#</a>
|
||||
</h3>
|
||||
<p>To create an organization, you first need to upgrade to the <a href="https://penpot.app/pricing" target="_blank">Enterprise plan</a>. Click "Create Organization" to begin. Once the subscription process is complete, you'll be redirected to the Admin Console to finish creating your organization.</p>
|
||||
|
||||
<h3 id="frequently-asked-questions"> Frequently asked questions
|
||||
<a class="direct-link" href="#frequently-asked-questions">#</a>
|
||||
</h3>
|
||||
|
||||
<h4>Can I have multiple organizations?</h4>
|
||||
<p>Yes. You can create more than one organization under a single Enterprise subscription and manage them from the Admin Console.</p>
|
||||
|
||||
<h4>What happens if I cancel my Enterprise subscription?</h4>
|
||||
<p>Your organizations are deleted but not their teams. Governance settings no longer apply to any of them.</p>
|
||||
|
||||
@@ -13,7 +13,19 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<p>Access your account settings and manage personal access tokens</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/enterprise-plan/">
|
||||
<h2>Enterprise plan →</h2>
|
||||
<p>Learn how the Enterprise plan works and what it includes</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/organizations/">
|
||||
<h2>Organizations →</h2>
|
||||
<p>Create and manage organizations, the Admin Console, and SSO</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/teams">
|
||||
<h2>Teams →</h2>
|
||||
<p>Create and manage your teams</p>
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: Organizations
|
||||
order: 3
|
||||
desc: Learn how Organizations work in Penpot Enterprise, creating one, the Admin Console, membership, Modules and Controls, and Single Sign-On (SSO).
|
||||
---
|
||||
|
||||
<h1 id="organizations">Organizations</h1>
|
||||
<p class="main-paragraph">An organization is the governance layer Penpot Enterprise adds on top of your teams. This section covers how to create and manage one: the Admin Console, membership, and the Modules and Controls used to configure it, including Single Sign-On (SSO).</p>
|
||||
|
||||
<h3 id="creating-an-organization">Creating an organization
|
||||
<a class="direct-link" href="#creating-an-organization">#</a>
|
||||
</h3>
|
||||
<p>Creating an organization is as easy as giving it a name.</p>
|
||||
<p>Once the organization is created, you'll be taken to the Admin Console. At this point, the organization has one member: you, the owner.</p>
|
||||
|
||||
<h3 id="the-admin-console">The Admin Console
|
||||
<a class="direct-link" href="#the-admin-console">#</a>
|
||||
</h3>
|
||||
<p>The Admin Console is the admin interface for your organization. Only the organization owner can access it.</p>
|
||||
<p>You can reach the Admin Console directly at <code>/admin-console</code>, or from any team dashboard in Penpot by opening the organization navigation menu and clicking the Admin Console link. If you own more than one organization, you can switch between them from within the Admin Console.</p>
|
||||
|
||||
<figure>
|
||||
<a href="/img/enterprise/enterprise-access-admin-console.webp" target="_blank">
|
||||
<img alt="Accessing the Admin Console" src="/img/enterprise/enterprise-access-admin-console.webp"/>
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
<p>The Admin Console includes:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Switch organization menu: </strong> To navigate between the organizations you own.</li>
|
||||
<li><strong>Organization settings: </strong> To change basic settings, such as renaming it or deleting it.</li>
|
||||
<li><strong>A "Go to files" button </strong> that returns you to a team dashboard.</li>
|
||||
<li><strong>Your user avatar</strong>, that expands into a full user menu.</li>
|
||||
<li><strong>Module: </strong> The control itself to configure.</li>
|
||||
</ol>
|
||||
|
||||
<figure>
|
||||
<a href="/img/enterprise/enterprise-admin-console.webp" target="_blank">
|
||||
<img alt="Admin Console" src="/img/enterprise/enterprise-admin-console.webp"/>
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
<h3 id="organizations-and-teams">Organizations and teams
|
||||
<a class="direct-link" href="#organizations-and-teams">#</a>
|
||||
</h3>
|
||||
<p>An organization groups one or more Teams under a shared governance structure. Teams continue to work just as they do in standard Penpot, with the added layer that the org owner can apply configuration that affects all members across the organization's teams.</p>
|
||||
<h4>How teams relate to organizations</h4>
|
||||
|
||||
<p>Teams inside an organization inherit the governance settings applied at the org level via the Admin Console. The structure is:</p>
|
||||
|
||||
<figure>
|
||||
<img alt="organization-hierarchy" src="/img/enterprise/enterprise-organization-hierarchy-w.webp"/>
|
||||
</figure>
|
||||
|
||||
<p>Team members work within projects and files as usual. What changes under Enterprise is the org owner's ability to restrict or govern that work from the Admin Console.</p>
|
||||
<p>Within each team, the <a href="https://help.penpot.app/user-guide/account-teams/teams/#teams-members" target="_blank">standard Penpot roles</a> apply.</p>
|
||||
|
||||
<h3 id="managing-organization-membership">Managing organization membership
|
||||
<a class="direct-link" href="#managing-organization-membership">#</a>
|
||||
</h3>
|
||||
<p>At launch, an organization has a single member: the owner. Additional members are brought in by being part of a team added to the organization, being invited to teams within the organization, or being directly invited to the organization by the owner.</p>
|
||||
<p>The Admin Console provides a unified view of all members across teams within the organization.</p>
|
||||
|
||||
<figure>
|
||||
<a href="/img/enterprise/enterprise-membership-w.webp" target="_blank">
|
||||
<img alt="Admin-Console Members page" src="/img/enterprise/enterprise-membership-w.webp"/>
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
<h3 id="modules-and-controls">Modules and Controls
|
||||
<a class="direct-link" href="#modules-and-controls">#</a>
|
||||
</h3>
|
||||
|
||||
<p>Modules are the configurable governance capabilities available to Enterprise organizations. Each Module is made up of one or more Controls: the individual settings that define who can do what, and where.</p>
|
||||
<p>Modules are configured from the Admin Console and apply organization-wide. </p>
|
||||
|
||||
<table cellspacing="0" cellpadding="1" border="1" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th valign="top" class="cellrowborder">Modules</th>
|
||||
<th valign="top" class="cellrowborder" style="text-align: center;">What it does</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Single Sign-On (SSO)</td>
|
||||
<td>Requires all org members to authenticate through your corporate identity provider before accessing the organization's teams and files.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Advanced permissions</td>
|
||||
<td>Defines who can create, view, edit, administer, or share teams, projects, and files. Also controls who can invite new members to teams.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 id="module-advanced-permissions">Module: Advanced permissions
|
||||
<a class="direct-link" href="#module-advanced-permissions">#</a>
|
||||
</h3>
|
||||
|
||||
<p>Advanced Permissions gives the organization owner fine-grained control over what members can do across all teams in the organization. Rather than relying on the default Penpot team roles alone, this module lets you restrict or open up specific actions at the organization level.</p>
|
||||
|
||||
<h4>What it controls</h4>
|
||||
|
||||
<p>The Advanced Permissions module is made up of individual Controls. Each Control governs a specific action, and each has a set of options to choose from. The selected option becomes the rule for the entire organization.</p>
|
||||
|
||||
<h4>How to configure it</h4>
|
||||
|
||||
<p>Advanced Permissions is configured from the Admin Console. Changes apply to all teams within the organization immediately.</p>
|
||||
<ol>
|
||||
<li>Open the Admin Console.</li>
|
||||
<li>Select Advanced Permissions from the left sidebar.</li>
|
||||
<li>For each Control, select the option that fits your governance policy.</li>
|
||||
<li>Changes take effect right away. There is no publish or save step.</li>
|
||||
</ol>
|
||||
|
||||
<h4>How it relates to team roles</h4>
|
||||
|
||||
<p>Advanced Permissions works on top of the standard Penpot team roles (Viewer, Editor, Admin, Owner). It does not replace them. Think of it as a ceiling: even if a member's team role would normally allow an action, an Advanced Permissions Control can prevent it organization-wide.</p>
|
||||
<p>For example, if “New team members” is set to "Organization members only," a team owner who would normally be able to invite anyone will find that option restricted to people who are already part of the organization.</p>
|
||||
|
||||
<h4>Default behavior</h4>
|
||||
|
||||
<p>When an organization is first created, all controls are configured with the most permissive setting; the same setting is used by all teams that are not part of any organization. No behavior changes until you actively configure a Control.</p>
|
||||
|
||||
<h3 id="module-single-sign-on">Module: Single Sign-On (SSO)
|
||||
<a class="direct-link" href="#module-single-sign-on">#</a>
|
||||
</h3>
|
||||
|
||||
<p>Single Sign-On lets you require all members of your organization to authenticate through your corporate identity provider (IdP) before accessing any of the organization's teams and files.</p>
|
||||
<p>SSO applies to teams and files only. The Admin Console is always accessible without SSO, so you can always reach your configuration to adjust or deactivate it, even if your own directory entry changes.</p>
|
||||
|
||||
<h4>Configuring your identity provider</h4>
|
||||
<p>Before setting up SSO in Penpot, you need to register Penpot as an application in your identity provider. The steps vary by provider, but you will always need to set the following <strong>callback URL</strong> in your IdP configuration:</p>
|
||||
<p><strong><code>https://<your-penpot-domain>/api/auth/oidc/callback</code></strong></p>
|
||||
<p>Your IdP will then give you a <strong>Client ID</strong> and <strong>Client Secret</strong> to use in Penpot. Once you have those:</p>
|
||||
<ol>
|
||||
<li>Open the Admin Console.</li>
|
||||
<li>Select <strong>SSO Config</strong>.</li>
|
||||
<li>Choose your provider and fill in the fields.</li>
|
||||
</ol>
|
||||
|
||||
<figure>
|
||||
<a href="/img/enterprise/enterprise-module-sso.webp" target="_blank">
|
||||
<img alt="SSO configuration" src="/img/enterprise/enterprise-module-sso.webp"/>
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
<h5>Generic authentication (OpenID Connect)</h5>
|
||||
<p>Use this option for any identity provider that supports the OIDC protocol, such as Okta, Keycloak, or Auth0.</p>
|
||||
<ul>
|
||||
<li><strong>Issuer / Authority URL:</strong> base URL of your OIDC provider, used to autodiscover endpoints</li>
|
||||
<li><strong>Client ID:</strong> client identifier assigned by your provider</li>
|
||||
<li><strong>Client Secret:</strong> client secret assigned by your provider</li>
|
||||
</ul>
|
||||
|
||||
<h5>Azure Active Directory (OpenID Connect)</h5>
|
||||
<ul>
|
||||
<li><strong>Issuer / Authority URL:</strong> <code>https://login.microsoftonline.com/<your-tenant-id>/v2.0/</code></li>
|
||||
<li><strong>Client ID:</strong> Application (client) ID from your Azure app registration</li>
|
||||
<li><strong>Client Secret:</strong> client secret value from your Azure app registration</li>
|
||||
</ul>
|
||||
|
||||
<h5>Google (OAuth)</h5>
|
||||
<ul>
|
||||
<li><strong>Client ID:</strong> client identifier from your Google Cloud OAuth 2.0 credentials</li>
|
||||
<li><strong>Client Secret:</strong> client secret from your Google Cloud OAuth 2.0 credentials</li>
|
||||
</ul>
|
||||
|
||||
<p>When all fields are filled, click <strong>Activate SSO</strong>. Penpot will run a test connection against your IdP. If the connection fails, your draft is kept and no changes are applied.</p>
|
||||
<p>If the test passes, a confirmation dialog will appear. It will warn you that members not in your directory will lose access to the organization's teams once SSO is active. Review your member list if needed, then confirm. SSO becomes active immediately.</p>
|
||||
|
||||
<h4>What happens to existing sessions</h4>
|
||||
<p>When SSO is activated, any member who is currently inside one of the organization's teams is cut off immediately and sent through the SSO login. This does not log them out of Penpot entirely. They can still reach teams that do not belong to your organization without re-authenticating.</p>
|
||||
<p>SSO sessions last 4 hours. When a session expires, members are routed through the SSO login again. If they are still in the directory, they are signed back in immediately.</p>
|
||||
|
||||
<h4>Editing an active configuration</h4>
|
||||
<p>While SSO is active, you can edit any field. As soon as you make a change, <strong>Apply changes</strong> and <strong>Discard changes</strong> buttons appear. Discarding restores every field to the current live configuration. Applying runs the same test connection as the initial setup, without a confirmation dialog. If the connection fails, your live configuration is not touched. Changes may take up to 5 minutes to apply for members who are currently working in a file.</p>
|
||||
|
||||
<h4>Deactivating SSO</h4>
|
||||
<p>Click <strong>Deactivate SSO</strong> and confirm. The configuration is preserved as a draft so you can reactivate it later without re-entering your credentials. Members are notified by email the first time SSO is activated. If you deactivate and reactivate within 24 hours, the notification is not re-sent.</p>
|
||||
|
||||
<h4>For your members</h4>
|
||||
<p>Members do not need to do anything to prepare. When they next try to access the organization's teams, they will be asked to authenticate through your IdP. If they are already signed in through that provider, the step is skipped automatically.</p>
|
||||
<p>Org membership still requires an invitation from you. Being in the directory alone does not grant access to Penpot or to your organization.</p>
|
||||
<p>If a member is not in your directory, they remain an org member but cannot enter the organization's teams until they are added. A single email is sent to all current members and pending invitees when SSO is first activated, explaining what changed and who to contact if they cannot get in.</p>
|
||||
|
||||
<h3 id="frequently-asked-questions"> Frequently asked questions
|
||||
<a class="direct-link" href="#frequently-asked-questions">#</a>
|
||||
</h3>
|
||||
|
||||
<h4>Can a team belong to more than one organization?</h4>
|
||||
<p>No. A team belongs to a single organization.</p>
|
||||
|
||||
<h4>Can non-owners access the Admin Console?</h4>
|
||||
<p>No. Access to the Admin Console is currently exclusive to the organization owner.</p>
|
||||
|
||||
<h4>What is the organization name used for?</h4>
|
||||
<p>It's the human-readable name used to identify your organization in the UI, in emails, and in URLs. It is not your official billing name. You can change it at any time without affecting navigation or functionality.</p>
|
||||
|
||||
<h4>Do Advanced Permissions replace the standard Penpot team roles?</h4>
|
||||
<p>No. Advanced Permissions work on top of the existing roles (Viewer, Editor, Admin, Owner). They add an organization-wide ceiling on what any role can do, but they do not change how roles work within a team.</p>
|
||||
|
||||
<h4>What is the default behavior when I first create an organization?</h4>
|
||||
<p>All Controls are set to their most permissive option. Nothing changes until you actively configure a Control.</p>
|
||||
|
||||
<h4>What happens to existing team admins if I change a Control that restricts their permissions?</h4>
|
||||
<p>The restriction applies immediately. An admin who could previously perform an action will no longer be able to do so as soon as the Control is changed, with no grace period.</p>
|
||||
|
||||
<h4>Is this the same as the authentication providers in Penpot's self-hosting configuration?</h4>
|
||||
<p>No. Penpot's self-hosted configuration lets server administrators enable login methods (Google, GitHub, GitLab, OIDC) at the instance level, so users can sign in to Penpot itself with those providers. That is a server-level setting managed by whoever runs the infrastructure.</p>
|
||||
<p>The SSO module in Enterprise is different in scope and purpose. It is configured by you, the organization owner, from the Admin Console, and it governs access to your organization's teams and files specifically. It does not change how users log in to Penpot as a platform, only whether they need to pass through your corporate identity provider to reach your organization's content.</p>
|
||||
|
||||
<h4>Does SSO affect the Admin Console?</h4>
|
||||
<p>No. The Admin Console is always accessible without SSO, regardless of your configuration. This ensures you can always reach your settings to adjust or deactivate SSO, even if something changes on the directory side.</p>
|
||||
|
||||
<h4>What happens if my identity provider goes down while SSO is active?</h4>
|
||||
<p>Current sessions continue until they expire. The next time a member tries to authenticate through your IdP and the provider is unreachable, the login will fail. There is no automatic bypass. Since the Admin Console is outside SSO, you can still reach your configuration to deactivate SSO if needed.</p>
|
||||
|
||||
<h4>Can I use the same identity provider for more than one organization?</h4>
|
||||
<p>Yes. Two different organizations, and the Penpot instance itself, can share the same IdP. A successful SSO login never grants org membership on its own, so there is no risk of cross-organization access. Membership always requires an explicit invitation.</p>
|
||||
|
||||
<h4>What if a member is not in my directory?</h4>
|
||||
<p>They remain an org member and keep their Penpot account, but they cannot enter the organization's teams until they are added to the directory. We send them an email when SSO is first activated explaining the situation and telling them to contact you.</p>
|
||||
|
||||
<h4>Does accepting an invitation automatically give someone access to my org's teams?</h4>
|
||||
<p>Only if they are also in your directory. An invitee can register and accept the invitation, but if they are not in the directory, they become an org member without being able to enter the teams. Acceptance is never blocked on that basis.</p>
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Projects and Files
|
||||
order: 3
|
||||
order: 5
|
||||
desc: Learn how to organize your work in Penpot. Create, manage and organize projects and files, work with drafts, and handle deleted items.
|
||||
---
|
||||
|
||||
@@ -56,6 +56,10 @@ desc: Learn how to organize your work in Penpot. Create, manage and organize pro
|
||||
</figure>
|
||||
<p>When creating a file, you'll be asked to give it a name. The file will open in the workspace where you can start designing immediately.</p>
|
||||
|
||||
<h3 id="file-view">Grid and list view</h3>
|
||||
<p>In the dashboard header you can switch how files are shown: a thumbnail <strong>Grid view</strong> or a compact <strong>List view</strong>. The choice is kept in the browser and applies both to the team files view and inside a project.</p>
|
||||
<p>List view shows the file name, whether the file is a shared library, the last modification time, and the same options menu as the file cards.</p>
|
||||
|
||||
<h3 id="edit-file">Edit a file</h3>
|
||||
<p>To rename a file, right-click on the file card in the dashboard and select <strong>Rename</strong>, or click on the three-dot menu on the file card. Enter the new name and confirm the change. You can also access file settings and other options from the file's context menu.</p>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Teams
|
||||
order: 2
|
||||
order: 4
|
||||
desc: Manage teams and roles with Penpot's collaboration features! Learn how to manage teams, roles (Viewer, Editor, Admin, Owner), send invites and use webhooks.
|
||||
---
|
||||
|
||||
|
||||
@@ -122,8 +122,14 @@ desc: Style your designs with Penpot's options! Learn about color fills, gradien
|
||||
<img alt="Multiple strokes" src="/img/styling/stroke-multiple.webp"/>
|
||||
</figure>
|
||||
|
||||
<h2 id="stroke-to-path">Stroke to path</h2>
|
||||
<p>A path is the underlying geometry: a sequence of points and segments (straight lines or Bezier curves). A stroke is the visible line drawn along that path, with properties such as width, color, opacity, dashes, caps, and joins.</p>
|
||||
<p><strong>Stroke to path</strong> turns the visible outline of a stroke into a separate, editable path. You can then reshape or style that outline independently of the original path. The new shape is usually closed. That matters for SVG, boolean operations, and editing the outline as geometry.</p>
|
||||
<p>To convert a stroke, select a layer that has one, open the layer menu, and choose <strong>Stroke to path</strong>. Penpot creates a new path from each stroke (named with " (stroke)") and removes the strokes from the original layer.</p>
|
||||
<p class="advice">Stroke to path is currently available when <a href="/user-guide/first-steps/troubleshooting-webgl/">WebGL rendering</a> is enabled.</p>
|
||||
|
||||
<h2 id="stroke-caps">Stroke Caps</h2>
|
||||
<p>Ever needed an arrow to point something? You can style the ends of any open paths selecting different styles for each end of an open path.</p>
|
||||
<p>Ever needed an arrow to point to something? You can style the ends of any open paths selecting different styles for each end of an open path. You can also start from the <a href="/user-guide/designing/layers/#lines-arrows">Arrow tool</a>, which already applies a triangle cap.</p>
|
||||
<figure>
|
||||
<video title="Stroke cap" muted="" playsinline="" controls="" width="100%" poster="/img/styling/stroke-cap.webp" height="auto">
|
||||
<source src="/img/styling/stroke-cap.mp4" type="video/mp4">
|
||||
|
||||
@@ -140,7 +140,7 @@ Penpot allows you to decide if the fill of an artboard will be shown in exports,
|
||||
<h3 id="rectangles-ellipses">Rectangles and ellipses</h3>
|
||||
<p>Rectangle and ellipses are two basic “primitive” geometric shapes that are useful when starting
|
||||
a design.</p>
|
||||
<p>The shortcut keys are <kbd>E</kbd> for ellipses and <kbd>R</kbd> for rectangles.</p>
|
||||
<p>The shortcut keys are <kbd>E</kbd> for ellipses and <kbd>R</kbd> for rectangles. Both tools live in the shapes flyout of the toolbar, together with line and arrow.</p>
|
||||
<p>To find out more about how to edit and modify these shapes go to <a href="/user-guide/designing/layers/#layer-actions">Layer basics</a>.</p>
|
||||
<figure>
|
||||
<video title="Rectangles and ellipses" muted="" playsinline="" controls="" width="auto" poster="/img/objects/rectangles-ellipses.webp" height="auto">
|
||||
@@ -151,13 +151,24 @@ a design.</p>
|
||||
<h3 id="text">Text</h3>
|
||||
<p>Text layers are how you add copy to your designs in Penpot. If you want to go deeper into fonts, typography and advanced text options, check the dedicated <a href="/user-guide/designing/text-typo/">Text & Typography</a> section.</p>
|
||||
|
||||
<h3 id="lines-arrows">Lines and arrows</h3>
|
||||
<p>Use the Line and Arrow tools to draw a two-point path in one drag. They live in the shapes flyout of the toolbar, together with rectangle and ellipse.</p>
|
||||
<ul>
|
||||
<li><strong>Line</strong> (<kbd>L</kbd>): click and drag. Hold <kbd>Shift/⇧</kbd> to snap the angle in 15 degree increments.</li>
|
||||
<li><strong>Arrow</strong>: the same gesture, with a triangle arrow cap on the end. Pick it from the shapes flyout. You can change or remove the cap from the <a href="/user-guide/designing/color-stroke/#stroke-caps">stroke caps</a>.</li>
|
||||
</ul>
|
||||
<p>The result is a regular path, so you can edit its nodes and stroke like any other path.</p>
|
||||
<figure>
|
||||
<img src="/img/objects/line-arrow-tools.webp" alt="Line and Arrow tools in the shapes flyout of the toolbar" />
|
||||
</figure>
|
||||
|
||||
<h3 id="curves">Curves (freehand)</h3>
|
||||
<p>The curve tool allows a path to be created directly in a freehand mode.
|
||||
Select the curve tool by clicking on the icon at the toolbar or pressing <kbd>Shift/⇧</kbd> + <kbd>c</kbd>.
|
||||
Select the curve tool from the free-draw flyout in the toolbar or press <kbd>Shift/⇧</kbd> + <kbd>C</kbd>.</p>
|
||||
<p>The path created will contain a lot of points, but it is edited the same way as any other curve.</p>
|
||||
|
||||
<h3 id="paths">Paths (bezier)</h3>
|
||||
<p>A path is composed of two or more nodes and the line segments between them, which may also be curved. To draw a new path you have to select the path tool by clicking on the icon at the toolbar or pressing <kbd>P</kbd>. Then you have two ways to create the path:</p>
|
||||
<p>A path is composed of two or more nodes and the line segments between them, which may also be curved. To draw a new path, select the path tool from the free-draw flyout in the toolbar or press <kbd>P</kbd>. Then you have two ways to create the path:</p>
|
||||
<ol>
|
||||
<li><strong>Click</strong> to create a new corner node.</li>
|
||||
<li><strong>Click and drag</strong> to create a curved node.</li>
|
||||
@@ -211,7 +222,7 @@ You can choose to edit individual nodes or create new ones. Press <kbd>Esc</kbd>
|
||||
<h2 id="layer-actions">Layer actions</h2>
|
||||
|
||||
<h3 id="creating-layers">Create</h3>
|
||||
<p>To create a layer you have to select the type of layer by clicking the selected tool (board, rectangle, ellipse, text, image, path or curve) at the toolbar. Then you usually have to click and drag your mouse on the viewport. </p>
|
||||
<p>To create a layer, pick a tool from the toolbar. Board, text, and image stay on the toolbar. Rectangle, ellipse, line, and arrow are in the shapes flyout. Path and curve are in the free-draw flyout. Then you usually click and drag on the viewport.</p>
|
||||
<p>Hold <kbd>Shift/⇧</kbd> while creating an ellipse or a rectangle to maintain equal width and height.</p>
|
||||
<figure>
|
||||
<video title="Layers create" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-create.webp" height="auto">
|
||||
|
||||
@@ -32,7 +32,7 @@ desc: Penpot's guide on custom fonts! Upload, manage, and use custom fonts in Pe
|
||||
<img src="/img/objects/text-options.webp" alt="text options">
|
||||
</figure>
|
||||
<ol>
|
||||
<li><strong>Font family.</strong> Penpot includes by default the <a href="https://fonts.google.com/" target=”_blank”>Google Fonts</a> cataloge. You can also <a href="/user-guide/designing/text-typo/#custom-fonts">install your own fonts</a>.</li>
|
||||
<li><strong>Font family.</strong> Penpot includes by default the <a href="https://fonts.google.com/" target=”_blank”>Google Fonts</a> cataloge. You can also <a href="/user-guide/designing/text-typo/#custom-fonts">install your own fonts</a>. In the font selector, each family name is shown in its own typeface so you can compare fonts before applying one. The name is the preview. While a font loads, you may briefly see the interface font instead. The preview uses a default variant, not every weight or style of the family.</li>
|
||||
<li><strong>Font size.</strong></li>
|
||||
<li><strong>Font type.</strong></li>
|
||||
<li><strong>Line height</strong> (in pixels).</li>
|
||||
|
||||
@@ -43,6 +43,15 @@ desc: Master Penpot's workspace basics! Learn interface navigation, zoom tools,
|
||||
</video>
|
||||
</figure>
|
||||
|
||||
<h4 id="select-delete-pages">Select and delete several pages</h4>
|
||||
<p>In the Pages panel you can select more than one page and delete them in a single action.</p>
|
||||
<ul>
|
||||
<li>Click a page to open it. This selects only that page.</li>
|
||||
<li><kbd>Shift/⇧</kbd> + click to select a range of pages. This does not change the page you are viewing.</li>
|
||||
<li><kbd>Ctrl/⌘</kbd> + click to add or remove one page from the selection. This does not navigate.</li>
|
||||
</ul>
|
||||
<p>Right-click a selected page and choose <strong>Delete pages</strong>. Confirm to remove all selected pages in one step. A file always keeps at least one page. Page separators cannot be included in the selection.</p>
|
||||
|
||||
<h4 id="page-separators">Page separators</h4>
|
||||
<p>You can add visual dividers in the page list to group pages without extra structure. Create an empty page, then rename it to <code>---</code>. The page appears as a horizontal line in the list.</p>
|
||||
<p>A page only becomes a separator if its name is <code>---</code> and the page has no content on the canvas. A page with content that is named <code>---</code> stays a normal page. Separators cannot be opened or selected; you can reorder or delete them like other pages.</p>
|
||||
|
||||
@@ -681,8 +681,8 @@ desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Curve</td>
|
||||
<td style="text-align: center;"><kbd>Ctrl</kbd><kbd>C</kbd></td>
|
||||
<td style="text-align: center;"><kbd>⌘</kbd><kbd>C</kbd></td>
|
||||
<td style="text-align: center;"><kbd>Shift</kbd><kbd>C</kbd></td>
|
||||
<td style="text-align: center;"><kbd>⇧</kbd><kbd>C</kbd></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ellipse</td>
|
||||
@@ -694,6 +694,11 @@ desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface
|
||||
<td style="text-align: center;"><kbd>Shift</kbd><kbd>K</kbd></td>
|
||||
<td style="text-align: center;"><kbd>⇧</kbd><kbd>K</kbd></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Line</td>
|
||||
<td style="text-align: center;"><kbd>L</kbd></td>
|
||||
<td style="text-align: center;"><kbd>L</kbd></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Path</td>
|
||||
<td style="text-align: center;"><kbd>P</kbd></td>
|
||||
|
||||
@@ -41,9 +41,9 @@ desc: Discover Penpot's free user guide! Learn the interface, workspace basics,
|
||||
|
||||
<ol>
|
||||
<li><strong>Viewport:</strong> An infinite canvas where you can design without limits.</li>
|
||||
<li><strong>Toolbar: </strong> This is where you’ll find all the tools to quickly and easily create different types of layers: board, rectangle, ellipse, text, graphic, path, and free drawing. <a href="/user-guide/designing/layers/">Learn more about layers.</a></li>
|
||||
<li><strong>Toolbar: </strong> Tools to create layers. Board, text, and image stay on the toolbar. Shapes (rectangle, ellipse, line, and arrow) and free-draw tools (path and curve) are grouped in flyouts. Open a flyout to pick a tool, or use its shortcut. <a href="/user-guide/designing/layers/">Learn more about layers.</a></li>
|
||||
<li><strong>Main menu:</strong> From the main menu, you can customize your workspace. Manage the visibility of grids, rulers, and panels. Enable or disable snapping and dynamic alignment. Add or remove the file as a Shared Library. You’ll also find help resources here.</li>
|
||||
<li><strong>Pages:</strong> A file can contain as many pages as you need. Each page has its own viewport (the almost infinite area where you design) and its own layers. You can create, delete, or reorder pages as needed.</li>
|
||||
<li><strong>Pages:</strong> A file can contain as many pages as you need. Each page has its own viewport (the almost infinite area where you design) and its own layers. You can create, delete, or reorder pages, and select several pages at once to delete them. <a href="/user-guide/designing/workspace-basics/#layer-basics">More about pages.</a></li>
|
||||
<li><strong>Layers:</strong> Layers are the different objects you can place in the design viewport. <a href="/user-guide/designing/workspace-basics/#layer-basics">More about Layers panel.</a></li>
|
||||
<li><strong>Rulers:</strong> Rulers provide coordinates to help you design. You can also drag guides from them.</li>
|
||||
<li><strong>Color palette:</strong> The color palette gives you quick access to a visible library of colors. Use the menu to easily switch between libraries. <a href="/user-guide/designing/color-stroke/#color-palette">Learn more about the color palette.</a>.</li>
|
||||
@@ -137,7 +137,7 @@ desc: Discover Penpot's free user guide! Learn the interface, workspace basics,
|
||||
<li><strong>User area:</strong> This must be you! Access your <a href="/user-guide/account-teams/your-account"> profile settings</a>, Penpot tutorials, the Penpot Community and more. You can also find here a way to leave us feedback. We’d love to read your thoughts :). </li>
|
||||
<li><strong>Comments notifications:</strong> Here you will be able to see if you have unread comments inside the files of the team. There's also a button to mark all notifications as read.</li>
|
||||
<li><strong>Create project:</strong> Create as many projects as you need to organize your designs.</li>
|
||||
<li><strong>File card:</strong> Basic information about a file at plain sight. A preview, update info or if it’s added as a Shared Library. From there you can perform several actions over the file (rename, duplicate, move, download, delete).</li>
|
||||
<li><strong>File card:</strong> Basic information about a file at plain sight. A preview, update info or if it’s added as a Shared Library. From there you can perform several actions over the file (rename, duplicate, move, download, delete). You can also switch the files area between a thumbnail grid and a compact list. <a href="/user-guide/account-teams/projects-files/#file-view">More about grid and list view.</a></li>
|
||||
<li><strong>Libraries & Templates module:</strong> A curated selection of Libraries & Templates files ready to import.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.thumbnails :as thc]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.helpers :as dsh]
|
||||
@@ -23,6 +24,7 @@
|
||||
[app.main.render :as render]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.queue :as q]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
@@ -289,13 +291,60 @@
|
||||
(mapcat get-frame-ids-cached))
|
||||
changes))))
|
||||
|
||||
;; Board thumbnails used to render text shapes without position-data as
|
||||
;; nothing instead of falling back to the foreignObject renderer (see
|
||||
;; frame-imposter in app.main.render), so any board thumbnail cached before
|
||||
;; that fix may be missing its text. The backend doesn't tell the client
|
||||
;; when a fetched thumbnail was generated, so we can't tell stale apart from
|
||||
;; fresh by inspecting it; instead each board thumbnail with text content is
|
||||
;; regenerated at most once per browser, tracked via local-storage so repeat
|
||||
;; visits (once healed) don't keep re-rendering it.
|
||||
(def ^:private healed-storage-key ::healed-text-thumbnails)
|
||||
|
||||
(defn- frame-has-text?
|
||||
[objects frame-id]
|
||||
(->> (cfh/get-children-with-self objects frame-id)
|
||||
(some cfh/text-shape?)
|
||||
(some?)))
|
||||
|
||||
(defn- unhealed-text-thumbnail?
|
||||
[state object-id]
|
||||
(and (some? (dm/get-in state [:thumbnails object-id :uri]))
|
||||
(not (contains? (get @storage/global healed-storage-key) object-id))))
|
||||
|
||||
(defn- mark-thumbnail-healed!
|
||||
[object-id]
|
||||
(swap! storage/global update healed-storage-key (fnil conj #{}) object-id))
|
||||
|
||||
(defn- heal-stale-text-thumbnails
|
||||
"Emits an `update-thumbnail` for every board on the page that has text
|
||||
content and hasn't already been healed (see `healed-storage-key`) in this
|
||||
browser."
|
||||
[state file-id page-id]
|
||||
(let [objects (-> (dsh/lookup-file-data state file-id)
|
||||
(dsh/get-page page-id)
|
||||
:objects)
|
||||
frame-ids (ctt/get-root-frames-ids objects)
|
||||
xf (comp
|
||||
(filter #(frame-has-text? objects %))
|
||||
(keep
|
||||
(fn [frame-id]
|
||||
(let [object-id (thc/fmt-object-id file-id page-id frame-id "frame")]
|
||||
(when (unhealed-text-thumbnail? state object-id)
|
||||
[frame-id object-id])))))]
|
||||
(->> (rx/from (eduction xf frame-ids))
|
||||
(rx/map
|
||||
(fn [[frame-id object-id]]
|
||||
(mark-thumbnail-healed! object-id)
|
||||
(update-thumbnail file-id page-id frame-id "frame" "heal-stale-text-thumbnails"))))))
|
||||
|
||||
(defn watch-state-changes
|
||||
"Watch the state for changes inside frames. If a change is detected will force a rendering
|
||||
of the frame data so the thumbnail can be updated."
|
||||
[file-id page-id]
|
||||
(ptk/reify ::watch-state-changes
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(watch [_ state stream]
|
||||
(let [stopper-s (rx/filter
|
||||
(fn [event]
|
||||
(as-> (ptk/type event) type
|
||||
@@ -330,6 +379,10 @@
|
||||
(rx/tap #(l/trc :hint "buffer initialized")))]
|
||||
|
||||
(->> (rx/merge
|
||||
;; Heal boards with text whose cached thumbnail may predate the
|
||||
;; text-position fix (see heal-stale-text-thumbnails).
|
||||
(heal-stale-text-thumbnails state file-id page-id)
|
||||
|
||||
;; Perform instant thumbnail cleaning of affected frames
|
||||
;; and interrupt any ongoing update-thumbnail process
|
||||
;; related to current frame-id
|
||||
|
||||
@@ -266,16 +266,17 @@
|
||||
[{:keys [objects frame vbox x y width height background]}]
|
||||
(let [shape-wrapper (shape-wrapper-factory objects)]
|
||||
[:& (mf/provider muc/render-thumbnails) {:value false}
|
||||
[:svg {:view-box vbox
|
||||
:width (ust/format-precision width viewbox-decimal-precision)
|
||||
:height (ust/format-precision height viewbox-decimal-precision)
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:fill "none"}
|
||||
(when (some? background)
|
||||
[:rect {:x x :y y :width width :height height :fill background}])
|
||||
[:& shape-wrapper {:shape frame}]]]))
|
||||
[:& (mf/provider muc/is-render?) {:value true}
|
||||
[:svg {:view-box vbox
|
||||
:width (ust/format-precision width viewbox-decimal-precision)
|
||||
:height (ust/format-precision height viewbox-decimal-precision)
|
||||
:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:fill "none"}
|
||||
(when (some? background)
|
||||
[:rect {:x x :y y :width width :height height :fill background}])
|
||||
[:& shape-wrapper {:shape frame}]]]]))
|
||||
|
||||
;; Component that serves for render frame thumbnails, mainly used in
|
||||
;; the viewer and inspector
|
||||
|
||||
@@ -658,7 +658,9 @@
|
||||
|
||||
(defn use-shape
|
||||
[id]
|
||||
(when (initialized?)
|
||||
;; Use `wasm/live?` (not `initialized?`) so context-restore reload can
|
||||
;; select shapes while `reloading?` still blocks external app callers.
|
||||
(when (wasm/live?)
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
(h/call wasm/internal-module "_use_shape"
|
||||
(aget buffer 0)
|
||||
@@ -668,7 +670,7 @@
|
||||
|
||||
(defn has-shape
|
||||
[id]
|
||||
(when (initialized?)
|
||||
(when (wasm/live?)
|
||||
(let [buffer (uuid/get-u32 id)
|
||||
|
||||
result
|
||||
@@ -686,9 +688,11 @@
|
||||
;; Cache content for text editor sync
|
||||
(text-editor/cache-shape-text-content! shape-id content)
|
||||
|
||||
;; The WASM design state may not be ready (e.g. while switching renderer
|
||||
;; with a text shape being edited). Skip the WASM layout calls in that case.
|
||||
(when (initialized?)
|
||||
;; Skip when the GL/WASM context is not usable. Use `wasm/live?` (not
|
||||
;; `initialized?`) so context-restore reload can re-upload text while
|
||||
;; `reloading?` still blocks external app callers. Geometry shapes already
|
||||
;; use `live?` via `set-shape-base-props`; text must match that path.
|
||||
(when (wasm/live?)
|
||||
(h/call wasm/internal-module "_clear_shape_text")
|
||||
|
||||
(set-shape-vertical-align (get content :vertical-align))
|
||||
|
||||
@@ -2,5 +2,3 @@ storeDir: ../.pnpm-store
|
||||
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
Reference in new issue
Block a user