mirror of
https://github.com/penpot/penpot.git
synced 2026-07-22 14:08:03 +00:00
Merge branch 'develop' into niwinz-performance-tests
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
commit
8e739e01e1
10
.github/workflows/auto-label.yml
vendored
10
.github/workflows/auto-label.yml
vendored
@ -10,10 +10,18 @@ jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: triage-app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.TRIAGE_APP_ID }}
|
||||
private-key: ${{ secrets.TRIAGE_APP_PRIVATE_KEY }}
|
||||
owner: penpot
|
||||
|
||||
- name: Process Issue or PR
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.PROJECT_TOKEN }}
|
||||
github-token: ${{ steps.triage-app-token.outputs.token }}
|
||||
script: |
|
||||
// === 1. CONFIGURATION ===
|
||||
const PROJECT_NUMBER = 8; // <--- Replace with your project board number
|
||||
|
||||
133
.github/workflows/tests-plugin-api-suite.yml
vendored
Normal file
133
.github/workflows/tests-plugin-api-suite.yml
vendored
Normal file
@ -0,0 +1,133 @@
|
||||
name: "CI: Plugin API Test Suite"
|
||||
|
||||
# Runs the Plugin API Test Suite (it exercises the real Penpot Plugin API, so it
|
||||
# needs a running frontend + the plugin runtime). Two jobs:
|
||||
#
|
||||
# - api-test-suite-mocked (pull_request / push): the per-PR gate. Serves the
|
||||
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
|
||||
# (MOCK_BACKEND=1). No backend / no login. Validates the frontend Plugin API
|
||||
# binding + in-memory store; backend-result-dependent tests are skipped via the
|
||||
# `skipIfMocked` tag. See plugins/apps/plugin-api-test-suite/README.md.
|
||||
#
|
||||
# - api-test-suite-live (workflow_dispatch): true end-to-end against a LIVE
|
||||
# instance. Point PENPOT_BASE_URL at a reachable instance and provide login
|
||||
# credentials via repo secrets. Manual because the CI runner has no Docker to
|
||||
# stand up a full stack.
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_url:
|
||||
description: "Penpot base URL (e.g. https://localhost:3449)"
|
||||
required: false
|
||||
default: "https://localhost:3449"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'frontend/src/app/plugins/**'
|
||||
- 'common/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
api-test-suite-mocked:
|
||||
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
|
||||
name: "Run Plugin API Test Suite (mocked)"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# Mocked mode serves the prebuilt bundle from frontend/resources/public.
|
||||
- name: Build frontend bundle
|
||||
working-directory: ./frontend
|
||||
run: ./scripts/build
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
working-directory: ./plugins
|
||||
run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
|
||||
|
||||
- name: Generate API surface
|
||||
working-directory: ./plugins
|
||||
run: pnpm --filter plugin-api-test-suite run gen:api
|
||||
|
||||
- name: Run API test suite (mocked)
|
||||
working-directory: ./plugins
|
||||
env:
|
||||
MOCK_BACKEND: "1"
|
||||
run: pnpm --filter plugin-api-test-suite run test:ci
|
||||
|
||||
## The following job will launch the whole suite of tests but we need
|
||||
## to have a full environment in the CI for this to work.
|
||||
|
||||
# api-test-suite-live:
|
||||
# if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
# name: Run Plugin API Test Suite (live)
|
||||
# runs-on: penpot-runner-02
|
||||
# container:
|
||||
# image: penpotapp/devenv:latest
|
||||
#
|
||||
# env:
|
||||
# PENPOT_BASE_URL: ${{ github.event.inputs.base_url }}
|
||||
# E2E_LOGIN_EMAIL: ${{ secrets.E2E_LOGIN_EMAIL }}
|
||||
# E2E_LOGIN_PASSWORD: ${{ secrets.E2E_LOGIN_PASSWORD }}
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v6
|
||||
#
|
||||
# - name: Setup Node
|
||||
# uses: actions/setup-node@v6
|
||||
# with:
|
||||
# node-version-file: .nvmrc
|
||||
#
|
||||
# - name: Install deps
|
||||
# working-directory: ./plugins
|
||||
# run: |
|
||||
# corepack enable;
|
||||
# corepack install;
|
||||
# pnpm install;
|
||||
#
|
||||
# - name: Install Playwright Chromium
|
||||
# working-directory: ./plugins
|
||||
# run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
|
||||
#
|
||||
# - name: Generate API surface
|
||||
# working-directory: ./plugins
|
||||
# run: pnpm --filter plugin-api-test-suite run gen:api
|
||||
#
|
||||
# # Note: requires a running Penpot instance reachable at PENPOT_BASE_URL.
|
||||
# - name: Run API test suite
|
||||
# working-directory: ./plugins
|
||||
# run: pnpm --filter plugin-api-test-suite run test:ci
|
||||
8
.github/workflows/tests-plugins.yml
vendored
8
.github/workflows/tests-plugins.yml
vendored
@ -40,19 +40,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm install -r;
|
||||
|
||||
- name: Run Lint
|
||||
working-directory: ./plugins
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -13,6 +13,8 @@
|
||||
.nyc_output
|
||||
.rebel_readline_history
|
||||
.repl
|
||||
opencode.json
|
||||
.opencode/package-lock.json
|
||||
/*.jpg
|
||||
/*.md
|
||||
!CHANGES.md
|
||||
@ -30,7 +32,6 @@
|
||||
/.clj-kondo/.cache
|
||||
/_dump
|
||||
/notes
|
||||
/.opencode/package-lock.json
|
||||
/plans
|
||||
/prompts
|
||||
/playground/
|
||||
|
||||
@ -1,28 +1,145 @@
|
||||
---
|
||||
name: commiter
|
||||
description: Git commit assistant
|
||||
mode: all
|
||||
mode: subagent
|
||||
permission:
|
||||
read: allow
|
||||
glob: allow
|
||||
grep: allow
|
||||
list: allow
|
||||
edit: deny
|
||||
webfetch: deny
|
||||
websearch: deny
|
||||
task: deny
|
||||
skill: deny
|
||||
lsp: deny
|
||||
todowrite: deny
|
||||
question: allow
|
||||
external_directory: deny
|
||||
bash:
|
||||
# Broad read-side: any non-write git query
|
||||
"git status*": allow
|
||||
"git log*": allow
|
||||
"git diff*": allow
|
||||
"git show*": allow
|
||||
"git rev-parse*": allow
|
||||
"git branch*": allow
|
||||
"git remote -v*": allow
|
||||
"git config --get*": allow
|
||||
|
||||
# Commit flow: staged, explicit paths only. `git commit*` (no space)
|
||||
# also covers `git commit --amend`. `git add -*` overrides the allow
|
||||
# below to block flag-driven bulk adds (`-A`, `--all`, `-u`, ...).
|
||||
"git add *": allow
|
||||
"git commit*": allow
|
||||
"git add -*": deny
|
||||
|
||||
# Read-only filesystem helpers used in the commit flow
|
||||
"cat *": allow
|
||||
"head *": allow
|
||||
"tail *": allow
|
||||
"wc *": allow
|
||||
"date *": allow
|
||||
|
||||
# Dangerous: deny outright
|
||||
"rm *": deny
|
||||
"rmdir *": deny
|
||||
"mv *": deny
|
||||
"cp *": deny
|
||||
"dd *": deny
|
||||
"chmod *": deny
|
||||
"chown *": deny
|
||||
"sudo *": deny
|
||||
"git push*": deny
|
||||
"git clean*": deny
|
||||
"git reset*": deny
|
||||
"git checkout*": deny
|
||||
"git restore*": deny
|
||||
"git config --global*": deny
|
||||
"curl *": deny
|
||||
"wget *": deny
|
||||
"ssh *": deny
|
||||
"scp *": deny
|
||||
"eval *": deny
|
||||
|
||||
# Risky-but-sometimes-needed: ask the user
|
||||
"git stash*": ask
|
||||
"git rebase*": ask
|
||||
"git merge*": ask
|
||||
"git tag*": ask
|
||||
"git fetch*": ask
|
||||
"git pull*": ask
|
||||
# Note: `git config <anything-other-than-(--get|--global)>` falls
|
||||
# through to the `*` catch-all below and is asked.
|
||||
|
||||
# Safety net
|
||||
"*": ask
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are responsible for creating git commits for Penpot and must
|
||||
follow the repository commit-format rules exactly. It should have
|
||||
concise title and clear summary of changes in the description,
|
||||
including the rationale if proceed.
|
||||
You are the Penpot commit assistant. You produce git commits that follow the
|
||||
repository's commit conventions exactly: an emoji-prefixed imperative
|
||||
subject, a body that explains the why, and the required trailers. You do
|
||||
not implement features, review code, or push branches — you commit.
|
||||
|
||||
## Requirements
|
||||
## Required Reading
|
||||
|
||||
* Override your internal commit rules when the user explicitly requests
|
||||
something that conflicts with them.
|
||||
* Read `.serena/memories/workflows/creating-commits.md` before
|
||||
creating any commit and follow the commit guidelines strictly.
|
||||
* Keep the description (commit body) with maximum line length of 80
|
||||
characters. Use manual line breaks to wrap text before it exceeds
|
||||
this limit.
|
||||
* Use `git commit -s` so the commit includes the required
|
||||
`Signed-off-by` line.
|
||||
* Do not guess or hallucinate git author information (Name or
|
||||
Email). Never include the `--author` flag in git commands unless
|
||||
specifically instructed by the user for a unique case; assume the
|
||||
local environment is already configured.
|
||||
Before drafting any commit, read `.serena/memories/workflow/creating-commits.md`
|
||||
end-to-end. It is the canonical source for the emoji menu, subject/body
|
||||
limits, and trailer format. The summary in this file does not replace it.
|
||||
|
||||
## Pre-commit Workflow
|
||||
|
||||
1. Run `git status` to inspect the working tree. If there are unstaged or
|
||||
untracked changes that are unrelated to the user's request, STOP and ask
|
||||
the user how to handle them. Do not silently include unrelated work in
|
||||
the commit.
|
||||
2. Run `git diff --staged` (or `git diff` for unstaged changes) and review
|
||||
the content. If you see secrets (API keys, tokens, passwords, private
|
||||
keys, `.env` values), debug prints, or anything that does not match the
|
||||
user's stated intent, STOP and tell the user before committing.
|
||||
3. Pick the commit emoji from the menu in
|
||||
`mem:workflow/creating-commits`. If none of the listed emojis fit, use
|
||||
`:paperclip:` (other) and explain in the body why.
|
||||
4. Draft the commit message (see format below), then run
|
||||
`git commit -s -m "<subject>" -m "<body>"` (or pass the message via
|
||||
`git commit -s -F -` if the body has unusual characters).
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
```
|
||||
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
|
||||
|
||||
Body explaining what changed and why. Wrap at 80 chars. Use manual
|
||||
line breaks; do not rely on the terminal to wrap.
|
||||
|
||||
Co-authored-by: <model-name> <model-name@penpot.app>
|
||||
```
|
||||
|
||||
- Subject: imperative mood, capitalized, no trailing period, max 70 chars.
|
||||
- Body: wraps at 80 chars. Explain the *why*, not just the *what* — what
|
||||
was wrong before, what this change does about it, and any non-obvious
|
||||
trade-offs.
|
||||
- `Co-authored-by` trailer is mandatory. Replace `<model-name>` with your
|
||||
own model identifier (e.g. `claude-sonnet-4-6`).
|
||||
- `Signed-off-by` is added automatically by `git commit -s`, using the
|
||||
local `git config user.name` / `user.email`.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not push. Pushing is a separate workflow handled by the user (the
|
||||
agent's permission set also denies `git push*`).
|
||||
- Do not run `git reset*`, `git checkout*`, `git restore*`, `git clean*`,
|
||||
or `rm*` — the permission set denies these outright. If staged work
|
||||
needs to be discarded, ask the user to do it.
|
||||
- Do not pass `--author`. Author identity comes from the local git
|
||||
config. Never guess or hallucinate a name or email.
|
||||
- Do not amend a commit you did not create in this session, unless the
|
||||
user explicitly asks. `git commit --amend` rewrites history and is
|
||||
irreversible once pushed.
|
||||
- Do not bypass pre-commit hooks (`--no-verify`) unless the user
|
||||
explicitly asks, and call out the deviation in your response.
|
||||
- If the user asks for something that conflicts with these rules, follow
|
||||
the user's request and explain the deviation in your response. Do not
|
||||
silently override the format.
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
---
|
||||
name: Engineer
|
||||
description: Senior Full-Stack Software Engineer
|
||||
mode: primary
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are a high-autonomy Senior Full-Stack Software Engineer working on Penpot, an
|
||||
open-source design tool. You have full permission to navigate the codebase, modify files,
|
||||
and execute commands to fulfill your tasks. Your goal is to solve complex technical tasks
|
||||
with high precision while maintaining a strong focus on maintainability and performance.
|
||||
|
||||
## Before Start
|
||||
|
||||
**Read `AGENTS.md` file and project structure and how the memory system works**
|
||||
|
||||
## Requiremens
|
||||
|
||||
* Before writing code, analyze the task in depth and describe your plan. If the task is
|
||||
complex, break it down into atomic steps.
|
||||
* Do **not** touch unrelated modules unless the task explicitly requires it.
|
||||
* Only reference functions, namespaces, or APIs that actually exist in the
|
||||
codebase. Verify their existence before citing them. If unsure, search first.
|
||||
* Be concise and autonomous — avoid unnecessary explanations.
|
||||
@ -1,60 +0,0 @@
|
||||
---
|
||||
name: Planner
|
||||
description: Software architect for planning and analysis only
|
||||
mode: primary
|
||||
permission:
|
||||
edit: ask
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are a Senior Software Architect working on Penpot, an open-source design
|
||||
tool. Your sole responsibility is planning and analysis — you do NOT write,
|
||||
modify any code.
|
||||
|
||||
You help users understand the codebase, design solutions, and create detailed
|
||||
implementation plans that other agents or developers can execute. Document
|
||||
everything they need to know: which files to touch for each task, code, testing,
|
||||
docs they might need to check, how to test it. Give them the whole plan as
|
||||
bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
|
||||
|
||||
Do **not** suggest commit messages or commit names anywhere in your plans or
|
||||
responses — committing is the developer's responsibility.
|
||||
|
||||
Assume they are a skilled developer, but know almost nothing about our toolset
|
||||
or problem domain. Assume they don't know good test design very well.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Analyze the codebase architecture and identify affected modules.
|
||||
* Read `AGENTS.md` file and project structure and how the memory system works and how to
|
||||
navigate and read relevant information conventions.
|
||||
* Break down complex features or bugs into atomic, actionable steps.
|
||||
* Propose solutions with clear rationale, trade-offs, and sequencing.
|
||||
* Identify risks, edge cases, and testing considerations.
|
||||
|
||||
Save plans to: plans/YYYY-MM-DD-<plan-one-line-title>.md
|
||||
|
||||
## Constraints
|
||||
|
||||
* You are **read-only** — never create, edit, or delete files.
|
||||
* You do **not** run builds, tests, linters, or any commands that modify state.
|
||||
* You do **not** create git commits or interact with version control.
|
||||
* You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
|
||||
`find`, `cat`).
|
||||
* Your output is a structured plan or analysis, ready for handoff to an
|
||||
engineer agent or developer.
|
||||
|
||||
## Output format
|
||||
|
||||
When producing a plan, structure it as:
|
||||
|
||||
1. **Context** — What is the problem or feature request?
|
||||
2. **Affected modules** — Which parts of the codebase are involved?
|
||||
3. **Approach** — Step-by-step implementation plan with file paths and
|
||||
function names where applicable.
|
||||
4. **Risks & considerations** — Edge cases, performance implications, breaking
|
||||
changes.
|
||||
5. **Testing strategy** — How to verify the implementation works correctly.
|
||||
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
---
|
||||
name: Prompt Assistant
|
||||
description: Refines and improves prompts for maximum clarity and effectiveness
|
||||
mode: all
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are an expert Prompt Engineer with strong knowledge of
|
||||
penpot. Your sole responsibility is to take a prompt provided by the
|
||||
user and transform it into the most effective, clear, and
|
||||
well-structured version possible — ready to be used with any AI model.
|
||||
|
||||
## Requirements
|
||||
|
||||
* You do NOT execute tasks. You do NOT write code. You only design and refine prompts
|
||||
* Read `AGENTS.md` file and project structure and how the memory system works and how to
|
||||
navigate and read relevant information conventions.
|
||||
* Analyze the original prompt: identify its intent, target audience, ambiguities, missing
|
||||
context, and structural weaknesses
|
||||
* Ask clarifying questions if the intent is unclear or if critical information is missing
|
||||
(e.g. target model, expected output format, tone, constraints). Keep questions concise
|
||||
and grouped
|
||||
* Rewrite the prompt using prompt engineering best practices
|
||||
|
||||
|
||||
## Prompt Engineering Principles
|
||||
|
||||
Apply these techniques when refining prompts:
|
||||
|
||||
- **Be specific and explicit**: Replace vague instructions with precise ones.
|
||||
- **Set the context**: Include background information the model needs to
|
||||
perform well.
|
||||
- **Specify the output format**: State the desired structure, length, tone,
|
||||
or format (e.g. bullet list, JSON, step-by-step).
|
||||
- **Add constraints**: Include what the model should avoid or not do.
|
||||
- **Use examples** (few-shot): When applicable, suggest adding examples to
|
||||
anchor the model's behaviour.
|
||||
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
|
||||
- **Avoid ambiguity**: Remove pronouns and references that could be
|
||||
misinterpreted.
|
||||
- **Chain of thought**: For reasoning tasks, include "Think step by step."
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do NOT execute the prompt yourself.
|
||||
- Do NOT answer the question inside the prompt.
|
||||
- Do NOT add unnecessary verbosity — prompts should be as short as they can
|
||||
be while remaining complete.
|
||||
- Always preserve the user's original intent.
|
||||
|
||||
## Output
|
||||
|
||||
Refined Prompt: The improved, ready-to-use prompt. Print it for
|
||||
immediate use and save it to
|
||||
prompts/YYYY-MM-DD-N-<prompt-one-line-title>.md for future use.
|
||||
@ -1,124 +1,39 @@
|
||||
---
|
||||
name: create-pr
|
||||
description: Create or update a GitHub PR following Penpot conventions, with a concise engineer-focused description
|
||||
description: Create or update a GitHub PR following Penpot conventions.
|
||||
---
|
||||
|
||||
# Pull Request (Create or Update)
|
||||
# Skill: create-pr
|
||||
|
||||
Create or update a GitHub PR with proper title format and a concise description that explains reasoning, not implementation details.
|
||||
Create or update a GitHub PR. Read and follow:
|
||||
- `mem:workflow/creating-prs` — title format, description structure, writing principles
|
||||
- `mem:workflow/creating-commits` — commit type emojis
|
||||
|
||||
## When to Use
|
||||
|
||||
- Opening a new pull request
|
||||
- Updating an existing PR's title or description
|
||||
- The user asks to create or update a PR
|
||||
- Code changes are ready and committed
|
||||
- Creating a new PR from a feature branch
|
||||
- Updating an existing PR's title or description to match conventions
|
||||
|
||||
## Conventions
|
||||
## Prerequisites
|
||||
|
||||
All PR conventions (title format, body structure, writing principles, what NOT to include) are documented in `mem:workflow/creating-prs`. This skill covers the procedure only.
|
||||
- `gh` CLI authenticated (`gh auth status`)
|
||||
|
||||
## Workflow A: Creating a New PR
|
||||
## Commands
|
||||
|
||||
### A1. Verify Prerequisites
|
||||
**Create:**
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git log --oneline main..HEAD
|
||||
gh pr create --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
|
||||
```
|
||||
|
||||
### A2. Check if Branch is Pushed
|
||||
**Update:**
|
||||
|
||||
```bash
|
||||
BRANCH=$(git branch --show-current)
|
||||
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
|
||||
echo "Branch is pushed, proceeding with PR creation"
|
||||
else
|
||||
echo "ERROR: Branch '$BRANCH' is not pushed to remote. Please push the branch first."
|
||||
exit 1
|
||||
fi
|
||||
gh pr edit <NUMBER> --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
|
||||
```
|
||||
|
||||
**If the branch is not pushed, STOP here and ask the user to push it. The LLM does not have push permissions.**
|
||||
|
||||
### A3. Create PR Body
|
||||
|
||||
Write the body to `/tmp/pr-body.md` using the template from `mem:workflow/creating-prs` (Description Body section):
|
||||
**Verify:**
|
||||
|
||||
```bash
|
||||
cat > /tmp/pr-body.md << 'EOF'
|
||||
**Note:** This PR was created with AI assistance as part of the Penpot MCP self-improvement initiative.
|
||||
|
||||
## What
|
||||
|
||||
<one paragraph: the problem or feature, user-facing impact>
|
||||
|
||||
## Why
|
||||
|
||||
<root cause or motivation, why this change was necessary>
|
||||
|
||||
## How
|
||||
|
||||
<high-level approach, key technical decisions>
|
||||
EOF
|
||||
gh pr view <NUMBER> --repo penpot/penpot --json title,body
|
||||
```
|
||||
|
||||
### A4. Create the PR
|
||||
|
||||
```bash
|
||||
gh pr create --base main --project "Main" --title "<title>" --body-file /tmp/pr-body.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow B: Updating an Existing PR
|
||||
|
||||
Use this when a PR already exists and you need to change its title or description (or both).
|
||||
|
||||
### B1. Find the PR Number
|
||||
|
||||
```bash
|
||||
BRANCH=$(git branch --show-current)
|
||||
gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number'
|
||||
```
|
||||
|
||||
If the result is empty, there is no open PR for this branch — use Workflow A instead.
|
||||
If multiple PRs match, pick the first one (typically there should only be one).
|
||||
|
||||
### B2. Determine What to Update
|
||||
|
||||
- **Title only:** use `--title "<new title>"`
|
||||
- **Description (body) only:** write to `/tmp/pr-body.md` and use `--body-file /tmp/pr-body.md`
|
||||
- **Both title and description:** supply both flags
|
||||
|
||||
### B3. Prepare Description (if updating)
|
||||
|
||||
Use the same template from `mem:workflow/creating-prs`:
|
||||
|
||||
```bash
|
||||
cat > /tmp/pr-body.md << 'EOF'
|
||||
**Note:** This PR was created with AI assistance as part of the Penpot MCP self-improvement initiative.
|
||||
|
||||
## What
|
||||
|
||||
<one paragraph: the problem or feature, user-facing impact>
|
||||
|
||||
## Why
|
||||
|
||||
<root cause or motivation, why this change was necessary>
|
||||
|
||||
## How
|
||||
|
||||
<high-level approach, key technical decisions>
|
||||
EOF
|
||||
```
|
||||
|
||||
### B4. Update the PR
|
||||
|
||||
```bash
|
||||
gh pr edit <NUMBER> --title "<title>" --body-file /tmp/pr-body.md
|
||||
```
|
||||
|
||||
Omit either flag if only one field is being updated.
|
||||
|
||||
**The LLM has permissions to edit PRs. No push required.**
|
||||
|
||||
135
.opencode/skills/planner/SKILL.md
Normal file
135
.opencode/skills/planner/SKILL.md
Normal file
@ -0,0 +1,135 @@
|
||||
---
|
||||
name: planner
|
||||
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to plans/YYYY-MM-DD-<title>.md only when the calling agent has write permission.
|
||||
---
|
||||
|
||||
# Planner
|
||||
|
||||
Read-only senior software architect role for Penpot. Produces structured
|
||||
implementation plans that engineers or other agents can execute. Never writes
|
||||
or modifies code.
|
||||
|
||||
## When to Use
|
||||
|
||||
- The user asks for a plan, design, or analysis of a feature or bug.
|
||||
- The user wants to understand which parts of the codebase a task will touch.
|
||||
- The user needs a step-by-step implementation plan with file paths, function
|
||||
names, and test strategy.
|
||||
- The user asks "how would I implement X?" or "what's involved in fixing Y?".
|
||||
- The user is about to start non-trivial work and wants a bite-sized task
|
||||
breakdown (DRY, YAGNI, TDD, frequent commits).
|
||||
|
||||
Do **not** use this skill to actually implement anything — it is read-only.
|
||||
|
||||
## Role
|
||||
|
||||
You are a Senior Software Architect working on Penpot, an open-source design
|
||||
tool. Your sole responsibility is planning and analysis — you do NOT write or
|
||||
modify code.
|
||||
|
||||
You help users understand the codebase, design solutions, and create detailed
|
||||
implementation plans that other agents or developers can execute. Document
|
||||
everything they need to know: which files to touch for each task, code, tests,
|
||||
docs they might need to check, and how to verify it. Give them the whole plan
|
||||
as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
|
||||
|
||||
Assume the implementer is a skilled developer, but knows almost nothing about
|
||||
our toolset or problem domain. Assume they don't know good test design very
|
||||
well.
|
||||
|
||||
Do **not** suggest commit messages or commit names anywhere in your plans or
|
||||
responses — committing is the developer's responsibility.
|
||||
|
||||
## Required Reading Before Planning
|
||||
|
||||
Before drafting any plan, work through the project's own guidance:
|
||||
|
||||
1. Read `AGENTS.md` (root) for the project-level rules.
|
||||
2. Read `.serena/memories/critical-info.md` (or the equivalent entry point) to
|
||||
identify which modules are affected.
|
||||
3. Read each affected module's core memory, e.g. `mem:frontend/core`,
|
||||
`mem:backend/core`, `mem:common/core`, `mem:exporter/core`,
|
||||
`mem:render-wasm/core`. Follow `mem:` references deeper as needed.
|
||||
4. For frontend/backend work, check the relevant section's notes on lint,
|
||||
format, and test commands so the plan can include them.
|
||||
|
||||
Skipping this step is the #1 cause of incorrect or incomplete plans.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Analyze the codebase architecture and identify affected modules.
|
||||
- Read `AGENTS.md` and the memory system conventions before drafting.
|
||||
- Break down complex features or bugs into atomic, actionable steps.
|
||||
- Propose solutions with clear rationale, trade-offs, and sequencing.
|
||||
- Identify risks, edge cases, performance implications, and breaking changes.
|
||||
- Apply DRY and KISS principles to the proposed implementation.
|
||||
- Define a testing strategy aligned with each affected module's tooling.
|
||||
|
||||
## Constraints
|
||||
|
||||
- You are **analysis-only** — never create, edit, or delete source code.
|
||||
- The only file write you may attempt is the plan itself, and only when the
|
||||
calling agent has write permission (see "Plan Output"). If the write is
|
||||
denied, deliver the plan in the response and move on.
|
||||
- You do **not** run builds, tests, linters, or any commands that modify state.
|
||||
- You do **not** create git commits or interact with version control.
|
||||
- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
|
||||
`find`, `cat`, `bat`).
|
||||
- Your output is a structured plan or analysis, ready for handoff to an
|
||||
engineer agent or developer.
|
||||
|
||||
## Plan Output
|
||||
|
||||
The plan is always delivered in the response so the user sees it regardless
|
||||
of which agent is running the skill.
|
||||
|
||||
Persistence is a **separate, best-effort step** that only runs when the
|
||||
calling agent has `edit` write permission:
|
||||
|
||||
- **Has write permission** (e.g. `build`, `general`, `engineer`): in addition
|
||||
to the in-response plan, save the plan to:
|
||||
|
||||
```
|
||||
plans/YYYY-MM-DD-<plan-one-line-title>.md
|
||||
```
|
||||
|
||||
Use today's date in the user's local timezone. The `<plan-one-line-title>`
|
||||
slug is lowercase, hyphen-separated, and a short summary of the task
|
||||
(e.g. `add-batch-get-profiles-for-file-comments`). Create the `plans/`
|
||||
directory if it does not exist.
|
||||
|
||||
- **No write permission** (e.g. the built-in `plan` agent, which denies
|
||||
`edit`): do not attempt to write the file — the write tool will be
|
||||
rejected. Just deliver the plan in the response. The user can copy it into
|
||||
`plans/...` manually if they want it persisted.
|
||||
|
||||
If the user explicitly provides a target file path, use that path instead of
|
||||
the default `plans/YYYY-MM-DD-<slug>.md` (still subject to write permission).
|
||||
|
||||
How to detect write permission: try the write. If it is denied, treat the
|
||||
plan as response-only and proceed — do not retry, do not ask the user, and do
|
||||
not mention the failed write in the response.
|
||||
|
||||
## Output Format
|
||||
|
||||
Structure the plan as:
|
||||
|
||||
1. **Context** — What is the problem or feature request? Why is it needed?
|
||||
2. **Affected modules** — Which parts of the codebase are involved? Reference
|
||||
module paths and any `mem:` memories that were consulted.
|
||||
3. **Approach** — Step-by-step implementation plan with file paths, function
|
||||
names, and code shape where applicable. Group steps into atomic, ordered
|
||||
tasks.
|
||||
4. **Risks & considerations** — Edge cases, performance implications,
|
||||
breaking changes, migration concerns, security implications.
|
||||
5. **Testing strategy** — How to verify the implementation works correctly:
|
||||
which test commands to run per module, what cases to cover, manual
|
||||
verification steps, lint/format checks.
|
||||
|
||||
Each step in **Approach** should be small enough to be reviewed and committed
|
||||
independently. Cite exact file paths (`path/to/file.ext:line` when useful) so
|
||||
the implementer can navigate directly.
|
||||
|
||||
When the plan is purely analytical (e.g. a code review or feasibility study
|
||||
with no implementation), skip the **Approach** section and lead with
|
||||
**Findings** instead, keeping the rest of the structure.
|
||||
114
.opencode/skills/refine-prompt/SKILL.md
Normal file
114
.opencode/skills/refine-prompt/SKILL.md
Normal file
@ -0,0 +1,114 @@
|
||||
---
|
||||
name: refine-prompt
|
||||
description: Refine and improve a user-supplied prompt for maximum clarity and effectiveness using prompt-engineering best practices and Penpot project context. Outputs a rewritten prompt (and brief rationale); never executes the prompt.
|
||||
---
|
||||
|
||||
# Refine Prompt
|
||||
|
||||
Expert prompt-engineering pass on a user-supplied prompt. Takes a draft prompt
|
||||
and returns a clearer, more effective, well-structured version — ready to be
|
||||
used with any AI model. Never executes the prompt itself.
|
||||
|
||||
## When to Use
|
||||
|
||||
- The user shares a prompt and asks to improve, refine, polish, or rewrite it.
|
||||
- The user asks "make this prompt better" or "can you clean this up?".
|
||||
- The user wants to add structure, constraints, examples, or output format to
|
||||
a vague prompt.
|
||||
- The user wants a prompt adapted for a specific target model, audience, or
|
||||
task type.
|
||||
|
||||
Do **not** use this skill to actually answer the prompt or do the task — it
|
||||
only rewrites the prompt.
|
||||
|
||||
## Role
|
||||
|
||||
You are an expert Prompt Engineer with strong knowledge of Penpot. Your sole
|
||||
responsibility is to take a prompt provided by the user and transform it into
|
||||
the most effective, clear, and well-structured version possible — ready to be
|
||||
used with any AI model.
|
||||
|
||||
You do **not** execute tasks. You do **not** write code. You only design and
|
||||
refine prompts.
|
||||
|
||||
## Required Reading Before Refining
|
||||
|
||||
Before rewriting, internalize the project context the prompt will likely run
|
||||
against:
|
||||
|
||||
1. Read `AGENTS.md` (root) for the project-level rules and conventions.
|
||||
2. Read `.serena/memories/critical-info.md` (or the equivalent entry point) to
|
||||
understand the module layout (`frontend`, `backend`, `common`,
|
||||
`render-wasm`, `exporter`, `mcp`, `plugins`, `library`).
|
||||
3. Skim the relevant module's core memory (`mem:frontend/core`,
|
||||
`mem:backend/core`, etc.) when the prompt targets a specific module — this
|
||||
lets you inject precise vocabulary, file conventions, and test commands
|
||||
into the refined prompt.
|
||||
|
||||
This step matters most when the user is preparing a prompt *about* the
|
||||
Penpot codebase. For generic prompts, focus on prompt-engineering principles
|
||||
and only weave in Penpot context when it is clearly relevant.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Analyze the original prompt: identify its intent, target audience,
|
||||
ambiguities, missing context, and structural weaknesses.
|
||||
- Ask clarifying questions if the intent is unclear or if critical information
|
||||
is missing (e.g. target model, expected output format, tone, constraints).
|
||||
Keep questions concise and grouped. Prefer to ask 1–4 questions at once
|
||||
rather than one at a time.
|
||||
- Rewrite the prompt using prompt-engineering best practices (see below).
|
||||
- Preserve the user's original intent — do not change the underlying task.
|
||||
- When the user provides Penpot project context, weave in the relevant
|
||||
conventions, module paths, and tooling.
|
||||
|
||||
## Prompt Engineering Principles
|
||||
|
||||
Apply these techniques when refining prompts:
|
||||
|
||||
- **Be specific and explicit**: Replace vague instructions with precise ones.
|
||||
- **Set the context**: Include background information the model needs to
|
||||
perform well.
|
||||
- **Specify the output format**: State the desired structure, length, tone,
|
||||
or format (e.g. bullet list, JSON, step-by-step).
|
||||
- **Add constraints**: Include what the model should avoid or not do.
|
||||
- **Use examples** (few-shot): When applicable, suggest adding examples to
|
||||
anchor the model's behaviour.
|
||||
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
|
||||
- **Avoid ambiguity**: Remove pronouns and references that could be
|
||||
misinterpreted.
|
||||
- **Chain of thought**: For reasoning tasks, include "Think step by step."
|
||||
- **Role framing**: When helpful, give the model a clear role
|
||||
("You are a senior backend engineer...").
|
||||
- **Tool awareness**: When the prompt targets an agentic model, mention
|
||||
relevant tools (`grep`, `glob`, `read`, `bash`, etc.) so the model uses the
|
||||
right surface.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do **not** execute the prompt yourself.
|
||||
- Do **not** answer the question inside the prompt.
|
||||
- Do **not** add unnecessary verbosity — prompts should be as short as they
|
||||
can be while remaining complete.
|
||||
- Always preserve the user's original intent.
|
||||
- If the user provides Penpot project context, prefer Penpot-specific
|
||||
vocabulary over generic terms (e.g. name actual modules and `mem:`
|
||||
references instead of "the codebase").
|
||||
|
||||
## Output Format
|
||||
|
||||
Deliver the result in the response as two clearly separated blocks:
|
||||
|
||||
1. **Refined prompt** — a single fenced code block (markdown ```) containing
|
||||
the rewritten prompt, ready to copy and use.
|
||||
2. **What changed (brief)** — a short bulleted list of the most important
|
||||
changes you made and why (3–7 bullets max). Skip the rationale if the
|
||||
changes are trivial.
|
||||
|
||||
If you asked clarifying questions, list them in a separate **Clarifying
|
||||
questions** section above the refined prompt and stop — do not produce a
|
||||
refined prompt until the user answers. If the user explicitly told you to
|
||||
proceed without questions (e.g. "just rewrite it"), make reasonable
|
||||
assumptions and note them under **Assumptions made** in the rationale block.
|
||||
|
||||
No file persistence — the refined prompt lives entirely in the response.
|
||||
@ -92,6 +92,10 @@ IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
|
||||
* **Linting:** `pnpm run lint` from the repository root.
|
||||
* **Formatting:** `pnpm run check-fmt`. Use `pnpm run fmt` to fix. Avoid unrelated whitespace diffs.
|
||||
|
||||
**Before linting:** if delimiter errors are suspected (after LLM edits), run
|
||||
`tools/paren-repair.bb` on the affected files first. Delimiter errors produce
|
||||
misleading linter/compiler output. See `mem:tools/paren-repair`.
|
||||
|
||||
## Testing
|
||||
|
||||
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
|
||||
|
||||
@ -48,7 +48,7 @@ Components, variants, and debugging:
|
||||
|
||||
Text and tests:
|
||||
- Shared text data conversion, DraftJS compatibility, modern text content, and derived position data: `mem:common/text-subtleties`.
|
||||
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/test-setup`.
|
||||
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/testing`.
|
||||
|
||||
## Areas without focused memories
|
||||
|
||||
|
||||
@ -1,24 +1,27 @@
|
||||
# Common Module Test Setup
|
||||
# Common Testing and Verification
|
||||
|
||||
`common/` is CLJC shared code. Tests should cover the relevant runtime(s): JVM for backend/common logic and JS for frontend/exporter behavior. For geometry, component, and file-model changes, JVM tests are common and fast, but JS/browser behavior can differ when WASM modifier math or CLJS-specific state is involved.
|
||||
|
||||
## Running tests
|
||||
## Unit tests
|
||||
|
||||
Common tests live under `common/test/common_tests/` and use `clojure.test`.
|
||||
They are CLJC and run on both JVM and JS.
|
||||
|
||||
From `common/`:
|
||||
- Full JVM test run: `clojure -M:dev:test`
|
||||
- Full JS test run: `pnpm run test:quiet`
|
||||
- Focus a JVM test namespace: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test`
|
||||
- Focus a JVM test var: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch`
|
||||
- Focus a JS test namespace: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test`
|
||||
- Focus a JS test var: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute`
|
||||
- Quiet logging during a JS run: append `--log-level warn` (or `trace|debug|info|warn|error`)
|
||||
- Build JS test target only: `pnpm run build:test`
|
||||
- After `pnpm run build:test`, direct compiled runner: `node target/tests/test.js --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn`
|
||||
- Watch tests: `pnpm run watch:test`
|
||||
|
||||
```bash
|
||||
pnpm run test:jvm
|
||||
clojure -M:dev:test
|
||||
pnpm run test:jvm --focus common-tests.logic.variants-switch-test
|
||||
clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch
|
||||
pnpm run test:js
|
||||
pnpm run test:quiet
|
||||
pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test
|
||||
pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn
|
||||
pnpm run watch:test
|
||||
```
|
||||
|
||||
Use `test:quiet` for non-interactive JS runs; it buffers `build:test` output and forwards runner args. Common JS runner args support `--focus <namespace-or-var>` and `--log-level trace|debug|info|warn|error`. After `pnpm run build:test`, direct compiled runner focus is faster: `node target/tests/test.js --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn`. New common JS test namespaces must be required/listed in `common_tests/runner.cljc`; new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags compose as a union.
|
||||
New common JS test namespaces must be required/listed in `common_tests/runner.cljc`;
|
||||
new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags
|
||||
compose as a union.
|
||||
|
||||
## Test helpers
|
||||
|
||||
@ -45,4 +48,4 @@ For geometry-sensitive tests, read `mem:common/geometry-invariants` before posit
|
||||
|
||||
## Debugging
|
||||
|
||||
Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes.
|
||||
Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes.
|
||||
@ -14,6 +14,11 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
|
||||
- Issues are also managed on Taiga. Read issues using the `read_taiga_issue` tool.
|
||||
- Before writing code, analyze the task in depth and describe your plan. If the task is complex, break it down into atomic steps.
|
||||
*After making changes, run the applicable lint and format checks for the affected module before considering the work done (per example `mem:backend/core` or `mem:frontend/core`).
|
||||
- Align `let` binding values: when a `let` form has multiple bindings spanning
|
||||
several lines, align the value forms to the same column with spaces.
|
||||
- If you introduce delimiter errors (mismatched parens/brackets) in Clojure/CLJS files,
|
||||
fix them with `tools/paren-repair.bb` BEFORE running lint/format checks.
|
||||
See `mem:tools/paren-repair` for usage.
|
||||
- Never run anything that destroys data without explicit permission, including `drop-devenv`, `docker compose down -v`, `docker volume rm ...`. The user's real work lives in the volumes of the shared infra.
|
||||
|
||||
# Project modules
|
||||
@ -39,6 +44,9 @@ module. You can read it from `mem:<MODULE>/core`
|
||||
When working on devenv startup, compose layout, instance config (`defaults.env`),
|
||||
tmux session lifecycle, MinIO provisioning, or anything in `manage.sh`'s
|
||||
`*-devenv` commands, read `mem:devenv/core`.
|
||||
- `tools/` contains standalone dev utilities: `nrepl-eval.mjs` (backend REPL eval),
|
||||
`paren-repair.bb` (delimiter-error fixer, see `mem:tools/paren-repair`), and
|
||||
`taiga.py` / `gh.py` (issue management helpers).
|
||||
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
|
||||
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.
|
||||
|
||||
|
||||
@ -26,6 +26,11 @@ From `frontend/`:
|
||||
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`.
|
||||
- Translation formatting after i18n edits: `pnpm run translations`.
|
||||
|
||||
**Before linting:** if delimiter errors are suspected (after LLM edits, or
|
||||
lint/compiler reports syntax errors), run `tools/paren-repair.bb` on the
|
||||
affected files first. Delimiter errors produce misleading linter output.
|
||||
See `mem:tools/paren-repair`.
|
||||
|
||||
## Focused memory routing
|
||||
|
||||
UI and packages:
|
||||
|
||||
@ -10,6 +10,12 @@ You have access to two tools for finding errors in Clojure source code (which yo
|
||||
The latter is needed because syntax errors in parentheses give an uninformative compiler error, and the second
|
||||
tool can often find the exact location of such errors.
|
||||
|
||||
When delimiter errors are detected (typically from lint or compiler output),
|
||||
fix the affected files with `tools/paren-repair.bb`. The `clj_check_parentheses`
|
||||
MCP tool can also pinpoint the error location when available, but it is not
|
||||
required — standard build errors are usually enough.
|
||||
See `mem:tools/paren-repair`.
|
||||
|
||||
## Runtime patching with `set!`
|
||||
|
||||
Some frontend vars are deliberately mutable escape hatches for runtime instrumentation or circular-dependency patching.
|
||||
|
||||
29
.serena/memories/tools/paren-repair.md
Normal file
29
.serena/memories/tools/paren-repair.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Paren-Repair
|
||||
|
||||
`tools/paren-repair.bb` fixes mismatched parentheses, brackets, and braces in
|
||||
Clojure/ClojureScript files, then reformats them with cljfmt.
|
||||
|
||||
## When to use
|
||||
|
||||
- After LLM edits introduce broken delimiters — proactively run it on files
|
||||
you just touched.
|
||||
- When lint (clj-kondo), the Clojure compiler, or shadow-cljs report syntax
|
||||
errors mentioning mismatched/unclosed delimiters, reader errors, or
|
||||
unexpected EOF.
|
||||
- Before running lint/format checks — delimiter errors make linter output
|
||||
misleading. Fix them first, then lint.
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
# File mode (in-place fix + format)
|
||||
bb tools/paren-repair.bb path/to/file.clj
|
||||
|
||||
# Pipe mode (stdin → fixed code to stdout)
|
||||
echo '(def x 1' | bb tools/paren-repair.bb
|
||||
|
||||
# Help
|
||||
bb tools/paren-repair.bb --help
|
||||
```
|
||||
|
||||
`bb` must be invoked from the repo root so the path `tools/paren-repair.bb` resolves.
|
||||
@ -15,7 +15,7 @@ automatically pull the identity from the local git config `user.name` and `user.
|
||||
|
||||
Body explaining what changed and why.
|
||||
|
||||
Co-authored-by: model-name <model-name@penpot.app>
|
||||
AI-assisted-by: model-name
|
||||
```
|
||||
|
||||
## Commit Type Emojis
|
||||
|
||||
60
CHANGES.md
60
CHANGES.md
@ -1,12 +1,35 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.18.0
|
||||
|
||||
### :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))
|
||||
- Fix synced component copy not reflowing children after spacing token update [#9892](https://github.com/penpot/penpot/issues/9892)
|
||||
- Fix spacebar activating pan mode while typing a comment (by @Krishcode264) [#10285](https://github.com/penpot/penpot/issues/10285) (PR: [#10287](https://github.com/penpot/penpot/pull/10287))
|
||||
- Fix plugin API rejecting negative letterSpacing values (by @filipsajdak) [#9780](https://github.com/penpot/penpot/issues/9780) (PR: [#10257](https://github.com/penpot/penpot/pull/10257))
|
||||
- Fix plugin API addTheme calls failing with the signature shown in the high-level overview [#10074](https://github.com/penpot/penpot/issues/10074) (PR: [#10359](https://github.com/penpot/penpot/pull/10359))
|
||||
- Fix empty text shape not being deleted on editor exit [#10540](https://github.com/penpot/penpot/issues/10540) (PR: [#10541](https://github.com/penpot/penpot/pull/10541))
|
||||
- Fix broken token pills showing wrong default state when not selected [#10524](https://github.com/penpot/penpot/issues/10524) (PR: [#10535](https://github.com/penpot/penpot/pull/10535))
|
||||
- Replace hyphens with bullets in subscription benefits list [#10547](https://github.com/penpot/penpot/issues/10547) (PR: [#10523](https://github.com/penpot/penpot/pull/10523))
|
||||
- Fix Chinese (zh-CN) translation showing wrong label for Intersection in board path menu (by @sawirricardo) [#10346](https://github.com/penpot/penpot/issues/10346) (PR: [#10381](https://github.com/penpot/penpot/pull/10381))
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- 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 outline stroke to Paths [#9961](https://github.com/penpot/penpot/issues/9961) (PR: [#8677](https://github.com/penpot/penpot/pull/8677))
|
||||
- Make throwValidationErrors default to true for v2 manifest plugins [#10401](https://github.com/penpot/penpot/issues/10401) (PR: [#10433](https://github.com/penpot/penpot/pull/10433))
|
||||
- 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))
|
||||
- Refactor wasm rulers and UI state [#10116](https://github.com/penpot/penpot/issues/10116) (PR: [#10461](https://github.com/penpot/penpot/pull/10461))
|
||||
- Improve team invitations modal in the dashboard [#10484](https://github.com/penpot/penpot/issues/10484) (PR: [#10459](https://github.com/penpot/penpot/pull/10459))
|
||||
|
||||
## 2.17.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- Render prototype viewer with WASM (Skia) engine instead of SVG [#10037](https://github.com/penpot/penpot/issues/10037) (PR: [#10038](https://github.com/penpot/penpot/pull/10038), [#10314](https://github.com/penpot/penpot/pull/10314))
|
||||
- Render prototype viewer with WASM (Skia) engine instead of SVG [#10037](https://github.com/penpot/penpot/issues/10037) (PR: [#10038](https://github.com/penpot/penpot/pull/10038))
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
@ -40,6 +63,7 @@
|
||||
- Add configurable resource limits to ImageMagick image processing [#10223](https://github.com/penpot/penpot/issues/10223) (PR: [#10240](https://github.com/penpot/penpot/pull/10240))
|
||||
- Add resource limits to font processing child processes [#10234](https://github.com/penpot/penpot/issues/10234) (PR: [#10274](https://github.com/penpot/penpot/pull/10274))
|
||||
- Add color variants and positioning to selection size badge (by @bittoby) [#10258](https://github.com/penpot/penpot/issues/10258) (PR: [#9210](https://github.com/penpot/penpot/pull/9210))
|
||||
- Use hard reload for render engine switching in the workspace menu [#10441](https://github.com/penpot/penpot/issues/10441) (PR: [#10444](https://github.com/penpot/penpot/pull/10444))
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@ -108,7 +132,6 @@
|
||||
- Fix export presets not being saved in view mode inspect panel [#9971](https://github.com/penpot/penpot/issues/9971) (PR: [#9972](https://github.com/penpot/penpot/pull/9972))
|
||||
- Fix cropped outer stroke of rotated board in view mode [#9978](https://github.com/penpot/penpot/issues/9978) (PR: [#9979](https://github.com/penpot/penpot/pull/9979))
|
||||
- Fix re-registration with same email showing verification prompt when email verification is disabled [#9998](https://github.com/penpot/penpot/issues/9998) (PR: [#9999](https://github.com/penpot/penpot/pull/9999))
|
||||
- Fix mask with children not applying blur correctly [#10004](https://github.com/penpot/penpot/issues/10004) (PR: [#10028](https://github.com/penpot/penpot/pull/10028))
|
||||
- Fix toggle color library visibility from color picker shortcut button [#10036](https://github.com/penpot/penpot/issues/10036) (PR: [#10129](https://github.com/penpot/penpot/pull/10129))
|
||||
- Improve image rendering performance by making snapshots async [#10045](https://github.com/penpot/penpot/issues/10045) (PR: [#10150](https://github.com/penpot/penpot/pull/10150))
|
||||
- Fix text layers wrapping to two lines after copy-pasting between files until edited [#10052](https://github.com/penpot/penpot/issues/10052) (PR: [#10081](https://github.com/penpot/penpot/pull/10081))
|
||||
@ -123,9 +146,38 @@
|
||||
- Fix guide hover feedback sticking or stopping after interaction [#10321](https://github.com/penpot/penpot/issues/10321) (PR: [#10333](https://github.com/penpot/penpot/pull/10333))
|
||||
- Fix rulers visible in thumbnail rendering [#10328](https://github.com/penpot/penpot/issues/10328)
|
||||
- Fix page switch blur disappearing early [#10332](https://github.com/penpot/penpot/issues/10332) (PR: [#10337](https://github.com/penpot/penpot/pull/10337))
|
||||
- Fix WebGL italic font mismatch when custom font variants are mapped [#10060](https://github.com/penpot/penpot/issues/10060) (PR: [#10122](https://github.com/penpot/penpot/pull/10122))
|
||||
- Fix open overlay board mispositioned with repeated image artifacts in viewer WebGL [#10180](https://github.com/penpot/penpot/issues/10180) (PR: [#10191](https://github.com/penpot/penpot/pull/10191))
|
||||
- Fix color picker preview when browser has non-100% zoom level [#10265](https://github.com/penpot/penpot/issues/10265) (PR: [#10305](https://github.com/penpot/penpot/pull/10305))
|
||||
- Fix negative margins no longer working [#10001](https://github.com/penpot/penpot/issues/10001) (PR: [#10353](https://github.com/penpot/penpot/pull/10353))
|
||||
- Fix undo in properties panel reversing each drag step instead of the entire drag when adjusting numeric inputs [#10066](https://github.com/penpot/penpot/issues/10066) (PR: [#10193](https://github.com/penpot/penpot/pull/10193))
|
||||
- Fix Plugin API reference token creation and token application failing when the target token set is inactive [#10070](https://github.com/penpot/penpot/issues/10070), [#10071](https://github.com/penpot/penpot/issues/10071) (PR: [#10297](https://github.com/penpot/penpot/pull/10297))
|
||||
- Fix Plugin API rejecting numeric token values with a generic validation error [#10073](https://github.com/penpot/penpot/issues/10073) (PR: [#10270](https://github.com/penpot/penpot/pull/10270))
|
||||
- Fix Plugin API whole-page export failing with a generic HTTP error [#10076](https://github.com/penpot/penpot/issues/10076) (PR: [#10148](https://github.com/penpot/penpot/pull/10148))
|
||||
- Fix Plugin API applyToken failing on padding properties [#10077](https://github.com/penpot/penpot/issues/10077) (PR: [#10087](https://github.com/penpot/penpot/pull/10087))
|
||||
- Fix Plugin API crash when instancing or detaching variants [#10099](https://github.com/penpot/penpot/issues/10099) (PR: [#10140](https://github.com/penpot/penpot/pull/10140))
|
||||
- Fix black square appearing on first view mode launch [#10188](https://github.com/penpot/penpot/issues/10188) (PR: [#10210](https://github.com/penpot/penpot/pull/10210))
|
||||
- Fix Plugin API openPage() not switching active page within the same execution [#10195](https://github.com/penpot/penpot/issues/10195) (PR: [#10085](https://github.com/penpot/penpot/pull/10085))
|
||||
- Fix Plugin API appending shapes to the active page instead of the specified .root [#10196](https://github.com/penpot/penpot/issues/10196) (PR: [#10271](https://github.com/penpot/penpot/pull/10271))
|
||||
- Fix Plugin API createPage() not making the new page active synchronously [#10197](https://github.com/penpot/penpot/issues/10197) (PR: [#10085](https://github.com/penpot/penpot/pull/10085))
|
||||
- Fix Plugin API Flow.startingBoard returning null after createFlow [#10203](https://github.com/penpot/penpot/issues/10203) (PR: [#10244](https://github.com/penpot/penpot/pull/10244))
|
||||
- Fix Plugin API page.flows returning null instead of empty array when page has no flows [#10204](https://github.com/penpot/penpot/issues/10204) (PR: [#10246](https://github.com/penpot/penpot/pull/10246))
|
||||
- Fix Plugin API auto-creating a stray 'Flow 1' when wiring interactions [#10205](https://github.com/penpot/penpot/issues/10205) (PR: [#10231](https://github.com/penpot/penpot/pull/10231))
|
||||
- Fix Plugin API createText throwing error when called with an empty string [#10206](https://github.com/penpot/penpot/issues/10206) (PR: [#10219](https://github.com/penpot/penpot/pull/10219))
|
||||
- Fix export rendering auto-width text with a substituted fallback font [#10208](https://github.com/penpot/penpot/issues/10208) (PR: [#10238](https://github.com/penpot/penpot/pull/10238))
|
||||
- Expose 'Fix when scrolling' (sticky) constraint in the Plugin API [#10209](https://github.com/penpot/penpot/issues/10209) (PR: [#10218](https://github.com/penpot/penpot/pull/10218))
|
||||
- Fix long typography token name in remap modal [#10301](https://github.com/penpot/penpot/issues/10301) (PR: [#10356](https://github.com/penpot/penpot/pull/10356))
|
||||
- Fix 'Go to your Penpot' link on error page redirecting to the same team instead of user home [#10324](https://github.com/penpot/penpot/issues/10324) (PR: [#10322](https://github.com/penpot/penpot/pull/10322))
|
||||
- Fix premature WASM view-interaction end during pan/zoom pause [#10424](https://github.com/penpot/penpot/issues/10424) (PR: [#10425](https://github.com/penpot/penpot/pull/10425))
|
||||
- Fix emojis without fill but with outer stroke not being rendered [#10473](https://github.com/penpot/penpot/issues/10473) (PR: [#10519](https://github.com/penpot/penpot/pull/10519))
|
||||
- Fix texts with lots of emojis not being rendered across multiple tiles [#10474](https://github.com/penpot/penpot/issues/10474) (PR: [#10504](https://github.com/penpot/penpot/pull/10504))
|
||||
- Fix artifacts in texts with inner strokes [#10476](https://github.com/penpot/penpot/issues/10476) (PR: [#10509](https://github.com/penpot/penpot/pull/10509))
|
||||
|
||||
## 2.16.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix error 500 when submitting the contact form [#10178](https://github.com/penpot/penpot/issues/10178) (PR: [#10419](https://github.com/penpot/penpot/pull/10419))
|
||||
- Fix text editor modifying content and detaching applied typography tokens [#10389](https://github.com/penpot/penpot/issues/10389) (PR: [#10402](https://github.com/penpot/penpot/pull/10402))
|
||||
|
||||
## 2.16.1
|
||||
|
||||
@ -283,6 +335,8 @@
|
||||
- Fix publishing or unpublishing file as library failing with unexpected state found error [#10094](https://github.com/penpot/penpot/issues/10094) (PR: [#10093](https://github.com/penpot/penpot/pull/10093))
|
||||
- Fix team invitation failing when email address contains consecutive dots in domain [#10097](https://github.com/penpot/penpot/issues/10097) (PR: [#10096](https://github.com/penpot/penpot/pull/10096))
|
||||
- Add detailed error messages for unspecified import errors [#9759](https://github.com/penpot/penpot/issues/9759) (PR: [#9886](https://github.com/penpot/penpot/pull/9886))
|
||||
- Fix mask with children not applying blur correctly [#10004](https://github.com/penpot/penpot/issues/10004) (PR: [#10028](https://github.com/penpot/penpot/pull/10028))
|
||||
- Fix WebGL italic font mismatch when custom font variants are mapped [#10060](https://github.com/penpot/penpot/issues/10060) (PR: [#10122](https://github.com/penpot/penpot/pull/10122))
|
||||
|
||||
|
||||
## 2.15.4
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
org.clojure/clojure {:mvn/version "1.12.5"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.1"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-10"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-11"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
||||
@ -39,7 +39,7 @@
|
||||
metosin/reitit-core {:mvn/version "0.10.1"}
|
||||
nrepl/nrepl {:mvn/version "1.7.0"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.11"}
|
||||
org.postgresql/postgresql {:mvn/version "42.7.12"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
|
||||
@ -63,14 +63,16 @@
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.46.7"}
|
||||
software.amazon.awssdk/sts {:mvn/version "2.46.7"}}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.46.18"}
|
||||
software.amazon.awssdk/sts {:mvn/version "2.46.18"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
{:dev
|
||||
{:extra-deps
|
||||
{com.bhauman/rebel-readline {:mvn/version "0.1.7"}
|
||||
{:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
:extra-deps
|
||||
{com.bhauman/rebel-readline {:mvn/version "0.1.11"}
|
||||
clojure-humanize/clojure-humanize {:mvn/version "0.2.2"}
|
||||
org.clojure/data.csv {:mvn/version "1.1.1"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"}
|
||||
@ -84,9 +86,7 @@
|
||||
|
||||
:test
|
||||
{:main-opts ["-m" "kaocha.runner"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
|
||||
"--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
|
||||
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
|
||||
|
||||
:outdated
|
||||
|
||||
@ -4,19 +4,19 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
},
|
||||
"dependencies": {
|
||||
"luxon": "^3.4.4",
|
||||
"sax": "^1.4.1"
|
||||
"sax": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.2",
|
||||
"nodemon": "^3.1.14",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.17.0"
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "clj-kondo --parallel --lint ../common/src src/",
|
||||
|
||||
84
backend/pnpm-lock.yaml
generated
84
backend/pnpm-lock.yaml
generated
@ -12,18 +12,18 @@ importers:
|
||||
specifier: ^3.4.4
|
||||
version: 3.7.2
|
||||
sax:
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.3
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
devDependencies:
|
||||
nodemon:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.11
|
||||
specifier: ^3.1.14
|
||||
version: 3.1.14
|
||||
source-map-support:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
ws:
|
||||
specifier: ^8.17.0
|
||||
version: 8.18.3
|
||||
specifier: ^8.21.0
|
||||
version: 8.21.0
|
||||
|
||||
packages:
|
||||
|
||||
@ -31,15 +31,17 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
balanced-match@4.0.4:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
brace-expansion@5.0.7:
|
||||
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
@ -52,9 +54,6 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@ -104,14 +103,15 @@ packages:
|
||||
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
minimatch@10.2.5:
|
||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
nodemon@3.1.11:
|
||||
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
|
||||
nodemon@3.1.14:
|
||||
resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@ -119,8 +119,8 @@ packages:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
picomatch@2.3.2:
|
||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
pstree.remy@1.1.8:
|
||||
@ -130,11 +130,12 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
sax@1.4.3:
|
||||
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
|
||||
sax@1.6.0:
|
||||
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
||||
engines: {node: '>=11.0.0'}
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@ -164,8 +165,8 @@ packages:
|
||||
undefsafe@2.0.5:
|
||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||
|
||||
ws@8.18.3:
|
||||
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
|
||||
ws@8.21.0:
|
||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@ -181,16 +182,15 @@ snapshots:
|
||||
anymatch@3.1.3:
|
||||
dependencies:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
picomatch: 2.3.2
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
balanced-match@4.0.4: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
brace-expansion@5.0.7:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
balanced-match: 4.0.4
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
@ -210,8 +210,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
debug@4.4.3(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@ -247,20 +245,20 @@ snapshots:
|
||||
|
||||
luxon@3.7.2: {}
|
||||
|
||||
minimatch@3.1.2:
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
brace-expansion: 5.0.7
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nodemon@3.1.11:
|
||||
nodemon@3.1.14:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
ignore-by-default: 1.0.1
|
||||
minimatch: 3.1.2
|
||||
minimatch: 10.2.5
|
||||
pstree.remy: 1.1.8
|
||||
semver: 7.7.3
|
||||
semver: 7.8.5
|
||||
simple-update-notifier: 2.0.0
|
||||
supports-color: 5.5.0
|
||||
touch: 3.1.1
|
||||
@ -268,21 +266,21 @@ snapshots:
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
picomatch@2.3.2: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
picomatch: 2.3.2
|
||||
|
||||
sax@1.4.3: {}
|
||||
sax@1.6.0: {}
|
||||
|
||||
semver@7.7.3: {}
|
||||
semver@7.8.5: {}
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
semver: 7.8.5
|
||||
|
||||
source-map-support@0.5.21:
|
||||
dependencies:
|
||||
@ -303,4 +301,4 @@ snapshots:
|
||||
|
||||
undefsafe@2.0.5: {}
|
||||
|
||||
ws@8.18.3: {}
|
||||
ws@8.21.0: {}
|
||||
|
||||
@ -195,21 +195,39 @@
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20" style="display:inline-block;vertical-align:middle;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20"
|
||||
style="display:inline-block;vertical-align:middle;">
|
||||
<tr>
|
||||
<td width="20" height="20" align="center" valign="middle"
|
||||
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
|
||||
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
|
||||
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
|
||||
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
|
||||
{% if organization.initials %}{{organization.initials}}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<span style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
|
||||
{{ organization.name|abbreviate:50 }}
|
||||
<span
|
||||
style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
|
||||
{{ organization.name|abbreviate:50 }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% if organization.sso-active %}
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
|
||||
teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't
|
||||
in the directory yet.
|
||||
To get access, contact the organization owner.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
Hello!
|
||||
|
||||
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:25 }}”.
|
||||
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:50 }}”.
|
||||
|
||||
{% if organization.sso-active %}
|
||||
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||
{% endif %}
|
||||
|
||||
Accept invitation using this link:
|
||||
|
||||
|
||||
@ -186,10 +186,26 @@
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if organization %}
|
||||
part of the organization “{{ organization|abbreviate:25 }}”{% endif %}.</div>
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if
|
||||
organization %}
|
||||
part of the organization “{{ organization.name|abbreviate:50 }}”{% endif %}.</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% if organization.sso-active %}
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to
|
||||
its
|
||||
teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't
|
||||
in the directory yet.
|
||||
To get access, contact the organization owner.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
@ -241,4 +257,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@ -1,6 +1,11 @@
|
||||
Hello!
|
||||
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization|abbreviate:25 }}"{% endif %}.
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization.name|abbreviate:50 }}"{% endif %}.
|
||||
|
||||
{% if organization.sso-active %}
|
||||
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||
{% endif %}
|
||||
|
||||
Accept invitation using this link:
|
||||
|
||||
|
||||
223
backend/resources/app/email/organization-setup-sso/en.html
Normal file
223
backend/resources/app/email/organization-setup-sso/en.html
Normal file
@ -0,0 +1,223 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Hi{% if user-name %} {{ user-name|abbreviate:25 }}{% endif %},
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
"{{ organization-name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
|
||||
teams and files goes through your organization's identity provider. If you can't get in, your
|
||||
account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1 @@
|
||||
“{{ organization-name|abbreviate:25 }}” has set up single sign-on (SSO) in Penpot
|
||||
@ -0,0 +1,7 @@
|
||||
Hello!
|
||||
|
||||
"{{ organization-name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||
|
||||
|
||||
The Penpot team.
|
||||
@ -761,6 +761,16 @@
|
||||
;; ORG SSO HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- non-blank-uri
|
||||
[value]
|
||||
(when-not (str/blank? value) value))
|
||||
|
||||
(defn org-sso-discovery-uri
|
||||
"Return the OIDC discovery URI from an org SSO config, preferring :issuer."
|
||||
[sso]
|
||||
(or (non-blank-uri (:issuer sso))
|
||||
(non-blank-uri (:base-url sso))))
|
||||
|
||||
(defn prepare-org-sso-provider
|
||||
"Build an OIDC provider map dynamically from the Nitrate org SSO config.
|
||||
Uses OIDC discovery via :base-url (or :issuer as fallback) when
|
||||
@ -770,12 +780,96 @@
|
||||
{:type "oidc"
|
||||
:client-id client-id
|
||||
:client-secret client-secret
|
||||
:base-uri (some-> (or base-url issuer)
|
||||
:base-uri (some-> (or (non-blank-uri base-url)
|
||||
(non-blank-uri issuer))
|
||||
(str/rtrim "/")
|
||||
(str "/"))
|
||||
:scopes (into default-oidc-scopes (or scopes #{}))
|
||||
:skip-ssrf-check? true}))
|
||||
|
||||
(defn build-org-sso-auth-redirect-uri
|
||||
"Build the OIDC authorization redirect URI for an organization SSO config.
|
||||
Raises if the config is incomplete or OIDC discovery fails."
|
||||
[cfg sso & {:keys [dest-url organization-id provider]}]
|
||||
(let [organization-id (or organization-id (:organization-id sso))
|
||||
issuer (org-sso-discovery-uri sso)
|
||||
dest-url (or dest-url (str (cf/get :public-uri)))]
|
||||
(when-not issuer
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-sso-config
|
||||
:hint "missing issuer or base-url"))
|
||||
(let [oidc-provider (or provider (prepare-org-sso-provider cfg sso))
|
||||
state-token (tokens/generate cfg {:iss "oidc"
|
||||
:dest-url dest-url
|
||||
:organization-id organization-id
|
||||
:issuer issuer
|
||||
:exp (ct/in-future "4h")})]
|
||||
(build-auth-redirect-uri oidc-provider state-token))))
|
||||
|
||||
(def ^:private probe-auth-code "penpot-sso-config-probe")
|
||||
|
||||
(defn- decode-token-error-response
|
||||
[body]
|
||||
(when (and (string? body) (pos? (count body)))
|
||||
(try
|
||||
(json/decode body)
|
||||
(catch Throwable _ nil))))
|
||||
|
||||
(defn- token-endpoint-error
|
||||
[response]
|
||||
(some-> response :body decode-token-error-response :error d/name))
|
||||
|
||||
(defn- token-endpoint-error-description
|
||||
[response]
|
||||
(some-> response :body decode-token-error-response :error-description))
|
||||
|
||||
(defn- token-endpoint-valid-client-error?
|
||||
"Token endpoint rejected the dummy auth code but accepted the client credentials."
|
||||
[response]
|
||||
(= "invalid_grant" (token-endpoint-error response)))
|
||||
|
||||
(defn- token-endpoint-invalid-client-error?
|
||||
"Token endpoint rejected the client credentials."
|
||||
[{:keys [status] :as response}]
|
||||
(let [error (token-endpoint-error response)
|
||||
description (str/lower (or (token-endpoint-error-description response) ""))]
|
||||
(or (= status 401)
|
||||
(#{"invalid_client" "unauthorized_client"} error)
|
||||
(and (= error "access_denied")
|
||||
(str/includes? description "unauthorized")))))
|
||||
|
||||
(defn- probe-org-sso-client-credentials
|
||||
"Probe the token endpoint with a dummy authorization code.
|
||||
Valid client credentials are expected to answer with `invalid_grant`."
|
||||
[cfg provider]
|
||||
(let [params {:client_id (:client-id provider)
|
||||
:client_secret (:client-secret provider)
|
||||
:code probe-auth-code
|
||||
:grant_type "authorization_code"
|
||||
:redirect_uri (build-redirect-uri)}
|
||||
req {:method :post
|
||||
:headers {"content-type" "application/x-www-form-urlencoded"
|
||||
"accept" "application/json"}
|
||||
:uri (:token-uri provider)
|
||||
:body (u/map->query-string params)}
|
||||
response (http/req cfg req {:skip-ssrf-check? (:skip-ssrf-check? provider)})]
|
||||
(cond
|
||||
(token-endpoint-valid-client-error? response) true
|
||||
(token-endpoint-invalid-client-error? response) false
|
||||
:else false)))
|
||||
|
||||
(defn is-organization-sso-config-valid?
|
||||
"Return true when the SSO config can be discovered, can build a login URL,
|
||||
and the client credentials are accepted by the token endpoint."
|
||||
[cfg sso]
|
||||
(try
|
||||
(if (org-sso-discovery-uri sso)
|
||||
(let [provider (prepare-org-sso-provider cfg sso)]
|
||||
(and (build-org-sso-auth-redirect-uri cfg sso :provider provider)
|
||||
(probe-org-sso-client-credentials cfg provider)))
|
||||
false)
|
||||
(catch Throwable _ false)))
|
||||
|
||||
(defn- auth-handler
|
||||
[cfg {:keys [params] :as request}]
|
||||
(let [provider (resolve-provider cfg params)
|
||||
|
||||
@ -419,10 +419,19 @@
|
||||
:id ::change-email
|
||||
:schema schema:change-email))
|
||||
|
||||
(def ^:private schema:organization-data
|
||||
[:map
|
||||
[:name ::sm/text]
|
||||
[:initials {:optional true} [:maybe :string]]
|
||||
[:logo {:optional true} [:maybe ::sm/uri]]
|
||||
[:avatar-bg-url {:optional true} [:maybe ::sm/uri]]
|
||||
[:sso-active {:optional true} [:maybe ::sm/boolean]]])
|
||||
|
||||
(def ^:private schema:invite-to-team
|
||||
[:map
|
||||
[:invited-by ::sm/text]
|
||||
[:team ::sm/text]
|
||||
[:organization {:optional true} [:maybe schema:organization-data]]
|
||||
[:token ::sm/text]])
|
||||
|
||||
(def invite-to-team
|
||||
@ -431,13 +440,6 @@
|
||||
:id ::invite-to-team
|
||||
:schema schema:invite-to-team))
|
||||
|
||||
(def ^:private schema:organization-data
|
||||
[:map
|
||||
[:name ::sm/text]
|
||||
[:initials [:maybe :string]]
|
||||
[:logo [:maybe ::sm/uri]]
|
||||
[:avatar-bg-url [:maybe ::sm/uri]]])
|
||||
|
||||
(def ^:private schema:invite-to-org
|
||||
[:map
|
||||
[:invited-by ::sm/text]
|
||||
@ -451,7 +453,16 @@
|
||||
:id ::invite-to-org
|
||||
:schema schema:invite-to-org))
|
||||
|
||||
(def ^:private schema:organization-setup-sso
|
||||
[:map
|
||||
[:user-name {:optional true} [:maybe ::sm/text]]
|
||||
[:organization-name ::sm/text]])
|
||||
|
||||
(def organization-setup-sso
|
||||
"Email when an organization set up SSO"
|
||||
(template-factory
|
||||
:id ::organization-setup-sso
|
||||
:schema schema:organization-setup-sso))
|
||||
|
||||
(def ^:private schema:renewal-notice
|
||||
[:map
|
||||
|
||||
@ -88,7 +88,8 @@
|
||||
#{:session-id
|
||||
:password
|
||||
:old-password
|
||||
:token})
|
||||
:token
|
||||
:client-secret})
|
||||
|
||||
(defn extract-utm-params
|
||||
"Extracts additional data from params and namespace them under
|
||||
|
||||
@ -14,23 +14,36 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.organization :as cto]
|
||||
[app.common.types.organization :as cto
|
||||
:refer [schema:nitrate-sso]]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.http.client :as http]
|
||||
[app.http.session :as session]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.util.cache :as cache]
|
||||
[clojure.core :as c]
|
||||
[clojure.string :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- join-path-segments
|
||||
"Build a single relative path from Nitrate URI segments, normalizing slashes."
|
||||
[segments]
|
||||
(let [path (->> segments (map str) (str/join "/"))]
|
||||
(->> (str/split path #"/")
|
||||
(remove str/blank?)
|
||||
(str/join "/"))))
|
||||
|
||||
(defn- join-base-uri
|
||||
"Join path segments to a base URI."
|
||||
[base-uri & segments]
|
||||
(apply u/join (u/ensure-path-slash base-uri) segments))
|
||||
(u/join (u/ensure-path-slash base-uri)
|
||||
(join-path-segments segments)))
|
||||
|
||||
(defn- generate-nitrate-uri
|
||||
"Joins relative path segments to the Nitrate backend URI.
|
||||
@ -266,7 +279,6 @@
|
||||
profile-id)
|
||||
schema:profile-org params))
|
||||
|
||||
|
||||
(defn- get-org-summary-api
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
@ -418,17 +430,6 @@
|
||||
[:permissions [:map-of :keyword :string]]]
|
||||
params))
|
||||
|
||||
(def ^:private schema:nitrate-sso
|
||||
[:map
|
||||
[:organization-id ::sm/uuid]
|
||||
[:active [:maybe :boolean]]
|
||||
[:provider [:maybe :string]]
|
||||
[:client-id [:maybe :string]]
|
||||
[:base-url [:maybe :string]]
|
||||
[:client-secret [:maybe :string]]
|
||||
[:issuer [:maybe :string]]
|
||||
[:scopes [:maybe [::sm/set ::sm/text]]]])
|
||||
|
||||
(defn- get-org-sso-api
|
||||
"Fetches the SSO configuration for an organization from Nitrate."
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
@ -496,6 +497,47 @@
|
||||
;; UTILS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defonce ^:private team-org-owner-cache
|
||||
;; Short TTL: permission checks run on the read path, so we avoid an
|
||||
;; HTTP call to nitrate per check. The org owner of a team rarely
|
||||
;; changes, and stale entries only grant read access for a few seconds.
|
||||
(cache/create :expire "30s" :max-size 2048))
|
||||
|
||||
(defn- nitrate-client?
|
||||
"True when `cfg` is a config map carrying the nitrate client (i.e. not
|
||||
a raw db connection/pool passed by an internal caller)."
|
||||
[cfg]
|
||||
(and (map? cfg) (some? (get cfg ::client))))
|
||||
|
||||
(def ^:private cache-miss ::no-org-owner)
|
||||
|
||||
(defn- get-team-org-owner-id
|
||||
"Returns the organization owner-id for `team-id`, or nil. Cached
|
||||
briefly, including negative results (teams with no organization) so
|
||||
repeated unauthorized probes don't each hit nitrate."
|
||||
[cfg team-id]
|
||||
(let [owner-id (cache/get team-org-owner-cache team-id
|
||||
(fn [team-id]
|
||||
(let [team-with-org (call cfg :get-team-org {:team-id team-id})]
|
||||
(or (get-in team-with-org [:organization :owner-id])
|
||||
cache-miss))))]
|
||||
(when-not (= owner-id cache-miss)
|
||||
owner-id)))
|
||||
|
||||
(defn organization-owner-of-team?
|
||||
"True if `profile-id` is the owner of the organization that owns
|
||||
`team-id`. Used to grant non-member org owners read-only access to the
|
||||
teams of their organizations. `cfg` must be a config map with the
|
||||
nitrate client; raw db connections/pools yield false so internal
|
||||
callers are unaffected. Returns false when the :nitrate flag is off."
|
||||
[cfg profile-id team-id]
|
||||
(boolean
|
||||
(when (and (contains? cf/flags :nitrate)
|
||||
(nitrate-client? cfg)
|
||||
(some? team-id)
|
||||
(some? profile-id))
|
||||
(= profile-id (get-team-org-owner-id cfg team-id)))))
|
||||
|
||||
(defn sso-session-authorized?
|
||||
"Fetches the org-SSO config for the given organization or team and checks
|
||||
whether the HTTP request has a valid session entry for it. Returns a map
|
||||
|
||||
@ -74,8 +74,8 @@
|
||||
::doc/changes [["2.12" "Remove version parameter, only one version is supported"]]
|
||||
::webhooks/event? true
|
||||
::sm/params schema:export-binfile}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(files/check-read-permissions! pool profile-id file-id)
|
||||
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(files/check-read-permissions! cfg profile-id file-id)
|
||||
(sse/response (partial export-binfile cfg params)))
|
||||
|
||||
;; --- Command: import-binfile
|
||||
|
||||
@ -230,8 +230,8 @@
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:get-comment-threads}
|
||||
[cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(get-comment-threads conn profile-id file-id))))
|
||||
|
||||
(defn- get-comment-threads-sql
|
||||
@ -328,8 +328,8 @@
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:get-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(some-> (db/exec-one! conn [sql:get-comment-thread profile-id file-id id])
|
||||
(decode-row)))))
|
||||
|
||||
@ -347,9 +347,9 @@
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:get-comments}
|
||||
[cfg {:keys [::rpc/profile-id thread-id share-id]}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [{:keys [file-id]} (get-comment-thread conn thread-id)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(get-comments conn thread-id)))))
|
||||
|
||||
(def sql:get-comments
|
||||
@ -406,8 +406,8 @@
|
||||
::doc/changes ["1.15" "Imported from queries and renamed."]
|
||||
::sm/params schema:get-profiles-for-file-comments}
|
||||
[cfg {:keys [::rpc/profile-id file-id share-id]}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(get-file-comments-users conn file-id profile-id))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@ -534,9 +534,9 @@
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread-status
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id share-id]}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id share-id]}]
|
||||
(let [{:keys [file-id]} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(upsert-comment-thread-status! conn profile-id id)))
|
||||
|
||||
;; --- COMMAND: Update Comment Thread
|
||||
@ -552,9 +552,9 @@
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id is-resolved share-id]}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id]}]
|
||||
(let [{:keys [file-id]} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:is-resolved is-resolved}
|
||||
{:id id})
|
||||
@ -582,7 +582,7 @@
|
||||
{:keys [team-id project-id] :as file}
|
||||
(get-file cfg file-id page-id)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
@ -653,7 +653,7 @@
|
||||
{:keys [file-id page-id] :as thread}
|
||||
(get-comment-thread conn thread-id ::sql/for-update true)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
|
||||
;; Don't allow edit comments to not owners
|
||||
(when-not (= owner-id profile-id)
|
||||
@ -690,9 +690,9 @@
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:delete-comment-thread
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id share-id]}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id share-id]}]
|
||||
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
@ -713,14 +713,14 @@
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:delete-comment
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id share-id]}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id share-id]}]
|
||||
(let [{:keys [owner-id thread-id] :as comment}
|
||||
(get-comment conn id ::sql/for-update true)
|
||||
|
||||
{:keys [file-id]}
|
||||
(get-comment-thread conn thread-id)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(when-not (= owner-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed))
|
||||
@ -743,9 +743,9 @@
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread-position
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id position frame-id share-id]}]
|
||||
(let [{:keys [file-id]} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:position (db/pgpoint position)
|
||||
@ -767,9 +767,9 @@
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:update-comment-thread-frame
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id frame-id share-id]}]
|
||||
(let [{:keys [file-id]} (get-comment-thread conn id ::sql/for-update true)]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(db/update! conn :comment-thread
|
||||
{:modified-at request-at
|
||||
:frame-id frame-id}
|
||||
|
||||
@ -54,7 +54,6 @@
|
||||
|
||||
(eml/send! {::eml/conn pool
|
||||
::eml/factory eml/user-feedback
|
||||
:from (cf/get :smtp-default-from)
|
||||
:to destination
|
||||
:reply-to (:email profile)
|
||||
:email (:email profile)
|
||||
|
||||
@ -84,10 +84,10 @@
|
||||
(perms/make-edition-predicate-fn bfc/get-file-permissions))
|
||||
|
||||
(def has-read-permissions?
|
||||
(perms/make-read-predicate-fn bfc/get-file-permissions))
|
||||
(perms/make-read-predicate-fn perms/get-file-read-permissions))
|
||||
|
||||
(def has-comment-permissions?
|
||||
(perms/make-comment-predicate-fn bfc/get-file-permissions))
|
||||
(perms/make-comment-predicate-fn perms/get-file-read-permissions))
|
||||
|
||||
(def check-edition-permissions!
|
||||
(perms/make-check-fn has-edit-permissions?))
|
||||
@ -99,8 +99,8 @@
|
||||
;; explicit comment permissions through the share-id
|
||||
|
||||
(defn check-comment-permissions!
|
||||
[conn profile-id file-id share-id]
|
||||
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
|
||||
[cfg profile-id file-id share-id]
|
||||
(let [perms (perms/get-file-read-permissions cfg profile-id file-id share-id)
|
||||
can-read (has-read-permissions? perms)
|
||||
can-comment (has-comment-permissions? perms)]
|
||||
(when-not (or can-read can-comment)
|
||||
@ -152,7 +152,7 @@
|
||||
(defn- get-minimal-file-with-perms
|
||||
[cfg {:keys [:id ::rpc/profile-id]}]
|
||||
(let [mfile (get-minimal-file cfg id)
|
||||
perms (bfc/get-file-permissions cfg profile-id id)]
|
||||
perms (perms/get-file-read-permissions cfg profile-id id)]
|
||||
(assoc mfile :permissions perms)))
|
||||
|
||||
(defn get-file-etag
|
||||
@ -171,7 +171,7 @@
|
||||
::sm/params schema:get-file
|
||||
::sm/result schema:file-with-permissions
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id project-id] :as params}]
|
||||
[cfg {:keys [::rpc/profile-id id project-id] :as params}]
|
||||
;; The COND middleware makes initial request for a file and
|
||||
;; permissions when the incoming request comes with an
|
||||
;; ETAG. When ETAG does not matches, the request is resolved
|
||||
@ -179,10 +179,10 @@
|
||||
;; will be already prefetched and we just reuse them instead
|
||||
;; of making an additional database queries.
|
||||
(let [perms (or (:permissions (::cond/object params))
|
||||
(bfc/get-file-permissions conn profile-id id))]
|
||||
(perms/get-file-read-permissions cfg profile-id id))]
|
||||
(check-read-permissions! perms)
|
||||
|
||||
(let [team (teams/get-team conn
|
||||
(let [team (teams/get-team cfg
|
||||
:profile-id profile-id
|
||||
:project-id project-id
|
||||
:file-id id)
|
||||
@ -242,7 +242,7 @@
|
||||
::sm/result schema:file-fragment}
|
||||
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
|
||||
(db/run! cfg (fn [cfg]
|
||||
(let [perms (bfc/get-file-permissions cfg profile-id file-id share-id)]
|
||||
(let [perms (perms/get-file-read-permissions cfg profile-id file-id share-id)]
|
||||
(check-read-permissions! perms)
|
||||
(-> (get-file-fragment cfg file-id fragment-id)
|
||||
(rph/with-http-cache long-cache-duration))))))
|
||||
@ -286,7 +286,7 @@
|
||||
::sm/params schema:get-project-files
|
||||
::sm/result schema:files}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
|
||||
(projects/check-read-permissions! pool profile-id project-id)
|
||||
(projects/check-read-permissions! cfg profile-id project-id)
|
||||
(get-project-files pool project-id))
|
||||
|
||||
;; --- COMMAND QUERY: has-file-libraries
|
||||
@ -304,7 +304,7 @@
|
||||
::sm/result ::sm/boolean}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(check-read-permissions! cfg profile-id file-id)
|
||||
(get-has-file-libraries conn file-id)))
|
||||
|
||||
(def ^:private sql:has-file-libraries
|
||||
@ -337,7 +337,7 @@
|
||||
::sm/result ::sm/int}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(check-read-permissions! cfg profile-id file-id)
|
||||
(get-library-usage conn file-id)))
|
||||
|
||||
(def ^:private sql:get-library-usage
|
||||
@ -387,7 +387,7 @@
|
||||
:code :params-validation
|
||||
:hint "page-id is required when object-id is provided"))
|
||||
|
||||
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
|
||||
(let [perms (perms/get-file-read-permissions cfg profile-id file-id share-id)
|
||||
file (bfc/get-file cfg file-id :read-only? true)
|
||||
|
||||
proj (db/get conn :project {:id (:project-id file)})
|
||||
@ -438,8 +438,8 @@
|
||||
::sm/params schema:get-page}
|
||||
[cfg {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(check-read-permissions! conn profile-id file-id share-id)
|
||||
(fn [cfg]
|
||||
(check-read-permissions! cfg profile-id file-id share-id)
|
||||
(get-page cfg (assoc params :profile-id profile-id)))))
|
||||
|
||||
;; --- COMMAND QUERY: get-team-shared-files
|
||||
@ -562,7 +562,7 @@
|
||||
|
||||
(defn- get-team-shared-files
|
||||
[{:keys [::db/conn] :as cfg} {:keys [team-id profile-id]}]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(teams/check-read-permissions! cfg profile-id team-id)
|
||||
|
||||
(let [process-row
|
||||
(fn [{:keys [id library-file-ids]}]
|
||||
@ -675,8 +675,8 @@
|
||||
::sm/params schema:get-file-stats
|
||||
::sm/result schema:get-file-stats-result
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
[cfg {:keys [::rpc/profile-id id]}]
|
||||
(check-read-permissions! cfg profile-id id)
|
||||
(get-file-stats cfg id))
|
||||
|
||||
|
||||
@ -719,7 +719,7 @@
|
||||
::sm/params schema:get-library-file-references}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
(check-read-permissions! cfg profile-id file-id)
|
||||
(get-library-file-references conn file-id)))
|
||||
|
||||
;; --- COMMAND QUERY: get-team-recent-files
|
||||
@ -763,7 +763,7 @@
|
||||
::sm/params schema:get-team-recent-files}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(teams/check-read-permissions! cfg profile-id team-id)
|
||||
(get-team-recent-files conn team-id)))
|
||||
|
||||
|
||||
@ -808,8 +808,8 @@
|
||||
{::doc/added "2.12"
|
||||
::sm/params schema:get-team-deleted-files}
|
||||
[cfg {:keys [::rpc/profile-id team-id]}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(teams/check-read-permissions! cfg profile-id team-id)
|
||||
(get-team-deleted-files conn team-id))))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-info
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
@ -33,8 +34,8 @@
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:get-file-snapshots}
|
||||
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-read-permissions! cfg profile-id file-id)
|
||||
(fsnap/get-visible-snapshots conn file-id))))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-snapshot
|
||||
@ -52,8 +53,8 @@
|
||||
::sm/params schema:get-file-snapshot
|
||||
::sm/result files/schema:file-with-permissions
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
|
||||
(let [perms (bfc/get-file-permissions conn profile-id file-id)]
|
||||
[cfg {:keys [::rpc/profile-id file-id id] :as params}]
|
||||
(let [perms (perms/get-file-read-permissions cfg profile-id file-id)]
|
||||
(files/check-read-permissions! perms)
|
||||
(let [snapshot (fsnap/get-snapshot cfg file-id id)]
|
||||
(when-not snapshot
|
||||
|
||||
@ -85,7 +85,7 @@
|
||||
::sm/result [:map-of [:string {:max 250}] [:string {:max 250}]]}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id tag] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(files/check-read-permissions! cfg profile-id file-id)
|
||||
(if tag
|
||||
(get-object-thumbnails-by-tag conn file-id tag)
|
||||
(get-object-thumbnails conn file-id))))
|
||||
@ -197,9 +197,9 @@
|
||||
::sm/params schema:get-file-data-for-thumbnail
|
||||
::sm/result schema:partial-file}
|
||||
[cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(let [team (teams/get-team conn
|
||||
(db/run! cfg (fn [cfg]
|
||||
(files/check-read-permissions! cfg profile-id file-id)
|
||||
(let [team (teams/get-team cfg
|
||||
:profile-id profile-id
|
||||
:file-id file-id)
|
||||
file (bfc/get-file cfg file-id
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.rpc.commands.fonts
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
@ -30,6 +29,7 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
@ -71,14 +71,14 @@
|
||||
(cond
|
||||
(uuid? team-id)
|
||||
(do
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(teams/check-read-permissions! cfg profile-id team-id)
|
||||
(db/query conn :team-font-variant
|
||||
{:team-id team-id
|
||||
:deleted-at nil}))
|
||||
|
||||
(uuid? project-id)
|
||||
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
|
||||
(projects/check-read-permissions! conn profile-id project-id)
|
||||
(projects/check-read-permissions! cfg profile-id project-id)
|
||||
(db/query conn :team-font-variant
|
||||
{:team-id (:team-id project)
|
||||
:deleted-at nil}))
|
||||
@ -86,7 +86,7 @@
|
||||
(uuid? file-id)
|
||||
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
|
||||
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})
|
||||
perms (bfc/get-file-permissions conn profile-id file-id share-id)]
|
||||
perms (perms/get-file-read-permissions cfg profile-id file-id share-id)]
|
||||
(files/check-read-permissions! perms)
|
||||
(db/query conn :team-font-variant
|
||||
{:team-id (:team-id project)
|
||||
@ -400,7 +400,7 @@
|
||||
::sm/params schema:download-font}
|
||||
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(let [variant (db/get pool :team-font-variant {:id id})]
|
||||
(teams/check-read-permissions! pool profile-id (:team-id variant))
|
||||
(teams/check-read-permissions! cfg profile-id (:team-id variant))
|
||||
|
||||
;; Try to get the best available font format (prefer TTF for broader compatibility).
|
||||
(let [media-id (or (:ttf-file-id variant)
|
||||
@ -432,7 +432,7 @@
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found))
|
||||
|
||||
(teams/check-read-permissions! pool profile-id (:team-id (first variants)))
|
||||
(teams/check-read-permissions! cfg profile-id (:team-id (first variants)))
|
||||
|
||||
(let [tempfile (tmp/tempfile :suffix ".zip")
|
||||
ffamily (-> variants first :font-family)]
|
||||
|
||||
@ -176,7 +176,7 @@
|
||||
;; profile-id is present; it can be ommited if this function is
|
||||
;; called from SREPL helpers where no profile is available
|
||||
(when (uuid? profile-id)
|
||||
(teams/check-read-permissions! conn profile-id team-id))
|
||||
(teams/check-read-permissions! cfg profile-id team-id))
|
||||
|
||||
(binding [bfc/*state* (volatile! {:index {team-id (uuid/next)}})]
|
||||
(let [projs (bfc/get-team-projects cfg team-id)
|
||||
|
||||
@ -21,8 +21,9 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.nitrate.emails-helper :as neh]
|
||||
[app.rpc.nitrate.organization-helper :as noh]
|
||||
[app.rpc.notifications :as notifications]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
|
||||
@ -394,12 +395,6 @@
|
||||
(notifications/notify-team-change cfg {:id team-id :organization {:name organization-name}} "dashboard.team-no-longer-belong-org")
|
||||
nil)
|
||||
|
||||
(def ^:private sql:get-team-invitation-emails
|
||||
"SELECT email_to
|
||||
FROM team_invitation
|
||||
WHERE team_id = ?
|
||||
AND valid_until > now()")
|
||||
|
||||
(def ^:private sql:delete-team-external-invitations
|
||||
"DELETE FROM team_invitation
|
||||
WHERE team_id = ?
|
||||
@ -421,8 +416,7 @@
|
||||
allows-anybody (nitrate-perms/allowed? :add-anybody-to-team {:org-perms org-perms})]
|
||||
(if allows-anybody
|
||||
{:allows-anybody true :external-emails []}
|
||||
(let [invitation-emails (db/exec! conn [sql:get-team-invitation-emails team-id])
|
||||
emails (map :email-to invitation-emails)]
|
||||
(let [emails (map :email (noh/get-team-invitation-emails conn team-id))]
|
||||
(if (empty? emails)
|
||||
{:allows-anybody false :external-emails []}
|
||||
(let [emails-array (db/create-array conn "text" (vec emails))
|
||||
@ -451,7 +445,8 @@
|
||||
(assert-membership cfg profile-id organization-id)
|
||||
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [team-with-org (nitrate/call cfg :get-team-org {:team-id team-id})
|
||||
(let [org-member-ids-before (into #{} (nitrate/call cfg :get-org-members {:organization-id organization-id}))
|
||||
team-with-org (nitrate/call cfg :get-team-org {:team-id team-id})
|
||||
source-org-id (get-in team-with-org [:organization :id])
|
||||
source-org-perms (when source-org-id
|
||||
(nitrate/call cfg :get-org-permissions
|
||||
@ -487,26 +482,32 @@
|
||||
:profile-id profile-id})
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
:hint "You are not allowed to add teams in this organization")))
|
||||
:hint "You are not allowed to add teams in this organization"))
|
||||
|
||||
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})]
|
||||
;; Add teammates to the org if needed
|
||||
(doseq [{member-id :profile-id} team-members
|
||||
:when (not= member-id profile-id)]
|
||||
(teams/initialize-user-in-nitrate-org cfg member-id organization-id)))
|
||||
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})
|
||||
new-member-ids (->> team-members
|
||||
(map :profile-id)
|
||||
(remove #{profile-id})
|
||||
(remove org-member-ids-before))]
|
||||
(doseq [member-id new-member-ids]
|
||||
(teams/initialize-user-in-nitrate-org cfg member-id organization-id)))
|
||||
|
||||
;; Api call to nitrate
|
||||
(let [team (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false})]
|
||||
;; Api call to nitrate
|
||||
(let [team (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false})]
|
||||
;; Notify connected users
|
||||
(notifications/notify-team-change cfg team "dashboard.team-belong-org"))
|
||||
|
||||
;; Notify connected users
|
||||
(notifications/notify-team-change cfg team "dashboard.team-belong-org"))
|
||||
;; Delete pending invitations for users who are not members of the target organization
|
||||
(let [{:keys [allows-anybody external-emails]} (get-external-invitation-info cfg team-id organization-id)]
|
||||
(when (and (not allows-anybody) (seq external-emails))
|
||||
(let [conn (::db/conn cfg)
|
||||
emails-array (db/create-array conn "text" external-emails)]
|
||||
(db/exec! conn [sql:delete-team-external-invitations team-id emails-array]))))
|
||||
|
||||
;; Delete pending invitations for users who are not members of the target organization
|
||||
(let [{:keys [allows-anybody external-emails]} (get-external-invitation-info cfg team-id organization-id)]
|
||||
(when (and (not allows-anybody) (seq external-emails))
|
||||
(let [conn (::db/conn cfg)
|
||||
emails-array (db/create-array conn "text" external-emails)]
|
||||
(db/exec! conn [sql:delete-team-external-invitations team-id emails-array])))))
|
||||
;; Send warnings via email if the org has sso
|
||||
(neh/send-organization-setup-sso-emails-for-team!
|
||||
cfg organization-id team-id org-member-ids-before)))
|
||||
|
||||
nil)
|
||||
|
||||
@ -645,17 +646,11 @@
|
||||
{:keys [authorized sso]} (nitrate/sso-session-authorized? cfg organization-id team-id request)]
|
||||
(if authorized
|
||||
{:authorized true}
|
||||
(if-let [issuer (or (:issuer sso) (:base-url sso))]
|
||||
(let [oidc-provider (oidc/prepare-org-sso-provider cfg sso)
|
||||
org-id (or organization-id (:organization-id sso))
|
||||
state-token (tokens/generate cfg {:iss "oidc"
|
||||
:dest-url url
|
||||
:organization-id org-id
|
||||
:issuer issuer
|
||||
:exp (ct/in-future "4h")})
|
||||
redirect-uri (oidc/build-auth-redirect-uri oidc-provider state-token)]
|
||||
{:authorized false
|
||||
:redirect-uri redirect-uri})
|
||||
(if (oidc/org-sso-discovery-uri sso)
|
||||
{:authorized false
|
||||
:redirect-uri (oidc/build-org-sso-auth-redirect-uri cfg sso
|
||||
:dest-url url
|
||||
:organization-id organization-id)}
|
||||
{:authorized false
|
||||
:redirect-uri nil})))
|
||||
{:authorized true}))
|
||||
|
||||
@ -56,11 +56,16 @@
|
||||
:can-edit (or is-owner is-admin can-edit)
|
||||
:can-read true})))
|
||||
|
||||
(defn- get-read-permissions
|
||||
[cfg profile-id project-id]
|
||||
(or (get-permissions cfg profile-id project-id)
|
||||
(perms/get-organization-owner-permissions cfg profile-id :project-id project-id)))
|
||||
|
||||
(def has-edit-permissions?
|
||||
(perms/make-edition-predicate-fn get-permissions))
|
||||
|
||||
(def has-read-permissions?
|
||||
(perms/make-read-predicate-fn get-permissions))
|
||||
(perms/make-read-predicate-fn get-read-permissions))
|
||||
|
||||
(def check-edition-permissions!
|
||||
(perms/make-check-fn has-edit-permissions?))
|
||||
@ -159,10 +164,10 @@
|
||||
{::doc/added "1.18"
|
||||
::rpc/id-type :project
|
||||
::sm/params schema:get-project}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(let [project (db/get-by-id conn :project id)]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
(check-read-permissions! cfg profile-id id)
|
||||
project)))
|
||||
|
||||
|
||||
@ -230,8 +235,8 @@
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id team-id is-pinned] :as params}]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}]
|
||||
(check-read-permissions! cfg profile-id id)
|
||||
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
||||
nil)
|
||||
|
||||
|
||||
@ -60,6 +60,11 @@
|
||||
:can-edit (or is-owner is-admin can-edit)
|
||||
:can-read true})))
|
||||
|
||||
(defn get-read-permissions
|
||||
[cfg profile-id team-id]
|
||||
(or (get-permissions cfg profile-id team-id)
|
||||
(perms/get-organization-owner-permissions cfg profile-id :team-id team-id)))
|
||||
|
||||
(def has-admin-permissions?
|
||||
(perms/make-admin-predicate-fn get-permissions))
|
||||
|
||||
@ -67,7 +72,7 @@
|
||||
(perms/make-edition-predicate-fn get-permissions))
|
||||
|
||||
(def has-read-permissions?
|
||||
(perms/make-read-predicate-fn get-permissions))
|
||||
(perms/make-read-predicate-fn get-read-permissions))
|
||||
|
||||
(def check-admin-permissions!
|
||||
(perms/make-check-fn has-admin-permissions?))
|
||||
@ -180,7 +185,6 @@
|
||||
sql (if (contains? cf/flags :subscriptions)
|
||||
sql:get-teams-with-permissions-and-subscription
|
||||
sql:get-teams-with-permissions)]
|
||||
|
||||
(->> (db/exec! conn [sql (:default-team-id profile) profile-id])
|
||||
(into [] xform:process-teams))))
|
||||
|
||||
@ -238,19 +242,34 @@
|
||||
{::doc/added "1.17"
|
||||
::rpc/id-type :team
|
||||
::sm/params schema:get-team}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id file-id]}]
|
||||
(get-team pool :profile-id profile-id :team-id id :file-id file-id))
|
||||
[cfg {:keys [::rpc/profile-id id file-id] :as params}]
|
||||
(let [team (get-team cfg :profile-id profile-id :team-id id :file-id file-id)]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(nitrate/add-org-info-to-team cfg team params)
|
||||
team)))
|
||||
|
||||
(defn- get-org-owner-viewer-team
|
||||
"When `profile-id` is a non-member owner of the organization that owns
|
||||
the requested team, returns the team shaped with viewer permissions;
|
||||
otherwise nil. `cfg` must carry the nitrate client."
|
||||
[cfg profile-id default-team-id params]
|
||||
(when-let [team-id (perms/resolve-team-id cfg params)]
|
||||
(when (nitrate/organization-owner-of-team? cfg profile-id team-id)
|
||||
(when-let [team (db/get* cfg :team {:id team-id})]
|
||||
(when-not (db/is-row-deleted? team)
|
||||
(-> team
|
||||
(decode-row)
|
||||
(merge perms/viewer-role-flags)
|
||||
(assoc :is-default (= team-id default-team-id))
|
||||
(process-permissions)))))))
|
||||
|
||||
(defn get-team
|
||||
[conn & {:keys [profile-id team-id project-id file-id] :as params}]
|
||||
[cfg & {:keys [profile-id team-id project-id file-id] :as params}]
|
||||
|
||||
(assert (uuid? profile-id) "profile-id is mandatory")
|
||||
(assert (or (db/connection? conn)
|
||||
(db/pool? conn))
|
||||
"connection or pool is mandatory")
|
||||
|
||||
(let [{:keys [default-team-id] :as profile}
|
||||
(profile/get-profile conn profile-id)
|
||||
(profile/get-profile cfg profile-id)
|
||||
|
||||
sql
|
||||
(if (contains? cf/flags :subscriptions)
|
||||
@ -262,14 +281,14 @@
|
||||
(some? team-id)
|
||||
(let [sql (str "WITH teams AS (" sql ") "
|
||||
"SELECT * FROM teams WHERE id=?")]
|
||||
(db/exec-one! conn [sql default-team-id profile-id team-id]))
|
||||
(db/exec-one! cfg [sql default-team-id profile-id team-id]))
|
||||
|
||||
(some? project-id)
|
||||
(let [sql (str "WITH teams AS (" sql ") "
|
||||
"SELECT t.* FROM teams AS t "
|
||||
" JOIN project AS p ON (p.team_id = t.id) "
|
||||
" WHERE p.id=?")]
|
||||
(db/exec-one! conn [sql default-team-id profile-id project-id]))
|
||||
(db/exec-one! cfg [sql default-team-id profile-id project-id]))
|
||||
|
||||
(some? file-id)
|
||||
(let [sql (str "WITH teams AS (" sql ") "
|
||||
@ -277,17 +296,18 @@
|
||||
" JOIN project AS p ON (p.team_id = t.id) "
|
||||
" JOIN file AS f ON (f.project_id = p.id) "
|
||||
" WHERE f.id=?")]
|
||||
(db/exec-one! conn [sql default-team-id profile-id file-id]))
|
||||
(db/exec-one! cfg [sql default-team-id profile-id file-id]))
|
||||
|
||||
:else
|
||||
(throw (IllegalArgumentException. "invalid arguments")))]
|
||||
|
||||
(when-not result
|
||||
(ex/raise :type :not-found
|
||||
:code :team-does-not-exist))
|
||||
(-> result
|
||||
(decode-row)
|
||||
(process-permissions))))
|
||||
(if result
|
||||
(-> result
|
||||
(decode-row)
|
||||
(process-permissions))
|
||||
(or (get-org-owner-viewer-team cfg profile-id default-team-id params)
|
||||
(ex/raise :type :not-found
|
||||
:code :team-does-not-exist)))))
|
||||
|
||||
;; --- Query: Team Members
|
||||
|
||||
@ -316,7 +336,7 @@
|
||||
::sm/params schema:get-team-memebrs}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(check-read-permissions! cfg profile-id team-id)
|
||||
(get-team-members conn team-id)))
|
||||
|
||||
;; --- Query: Team Users
|
||||
@ -342,10 +362,10 @@
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(if team-id
|
||||
(do
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(check-read-permissions! cfg profile-id team-id)
|
||||
(get-users conn team-id))
|
||||
(let [{team-id :id} (get-team-for-file conn file-id)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(check-read-permissions! cfg profile-id team-id)
|
||||
(get-users conn team-id)))))
|
||||
|
||||
;; This is a similar query to team members but can contain more data
|
||||
@ -432,7 +452,7 @@
|
||||
::sm/params schema:get-team-stats}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(check-read-permissions! cfg profile-id team-id)
|
||||
(get-team-stats conn team-id)))
|
||||
|
||||
(def sql:team-stats
|
||||
@ -468,7 +488,7 @@
|
||||
::sm/params schema:get-team-invitations}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(check-read-permissions! cfg profile-id team-id)
|
||||
(get-team-invitations conn team-id)))
|
||||
|
||||
|
||||
|
||||
@ -94,7 +94,9 @@
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:initials [:maybe :string]]
|
||||
[:logo ::sm/uri]]]
|
||||
[:logo ::sm/uri]
|
||||
[:avatar-bg-url [:maybe ::sm/uri]]
|
||||
[:sso-active [:maybe ::sm/boolean]]]]
|
||||
[:profile
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
@ -246,7 +248,7 @@
|
||||
:to email
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:organization (dm/get-in team [:organization :name])
|
||||
:organization (:organization team)
|
||||
:token itoken
|
||||
:extra-data ptoken})))
|
||||
|
||||
@ -539,7 +541,7 @@
|
||||
::doc/module :teams
|
||||
::sm/params schema:get-team-invitation-token}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
|
||||
(teams/check-read-permissions! pool profile-id team-id)
|
||||
(teams/check-read-permissions! cfg profile-id team-id)
|
||||
(let [email (profile/clean-email email)
|
||||
invit (-> (db/get pool :team-invitation
|
||||
{:team-id team-id
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.cond :as-alias cond]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.util.services :as sv]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@ -125,8 +126,8 @@
|
||||
::sm/params schema:get-view-only-bundle}
|
||||
[system {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||
(db/run! system
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
|
||||
(fn [system]
|
||||
(let [perms (perms/get-file-read-permissions system profile-id file-id share-id)
|
||||
params (-> params
|
||||
(assoc ::perms perms)
|
||||
(assoc :profile-id profile-id))]
|
||||
@ -139,5 +140,3 @@
|
||||
:hint "object not found"))
|
||||
|
||||
(get-view-only-bundle system params)))))
|
||||
|
||||
|
||||
|
||||
@ -172,6 +172,6 @@
|
||||
::sm/params schema:get-webhooks}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(check-read-permissions! cfg profile-id team-id)
|
||||
(->> (db/exec! conn [sql:get-webhooks team-id])
|
||||
(mapv decode-row))))
|
||||
|
||||
@ -8,11 +8,13 @@
|
||||
"Internal Nitrate HTTP RPC API. Provides authenticated access to
|
||||
organization management and token validation endpoints."
|
||||
(:require
|
||||
[app.auth :as aauth]
|
||||
[app.auth.oidc :as oidc]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.organization :refer [schema:team-with-organization schema:organization-with-avatar]]
|
||||
[app.common.types.organization :refer [schema:team-with-organization schema:organization-with-avatar schema:nitrate-sso]]
|
||||
[app.common.types.profile :refer [schema:profile, schema:basic-profile]]
|
||||
[app.common.types.team :refer [schema:team]]
|
||||
[app.config :as cf]
|
||||
@ -23,12 +25,15 @@
|
||||
[app.media :as media]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as rpc]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.nitrate :as cnit]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.commands.teams-invitations :as ti]
|
||||
[app.rpc.doc :as doc]
|
||||
[app.rpc.nitrate.emails-helper :as neh]
|
||||
[app.rpc.nitrate.organization-helper :as noh]
|
||||
[app.rpc.notifications :as notifications]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
@ -485,23 +490,6 @@ RETURNING id, deleted_at;")
|
||||
|
||||
;; API: get-org-invitations
|
||||
|
||||
(def ^:private sql:get-org-invitations
|
||||
"SELECT DISTINCT ON (email_to)
|
||||
ti.id,
|
||||
ti.org_id AS organization_id,
|
||||
ti.email_to AS email,
|
||||
ti.created_at AS sent_at,
|
||||
p.fullname AS name,
|
||||
p.id AS profile_id,
|
||||
p.photo_id
|
||||
FROM team_invitation AS ti
|
||||
LEFT JOIN profile AS p
|
||||
ON p.email = ti.email_to
|
||||
AND p.deleted_at IS NULL
|
||||
WHERE ti.valid_until >= now()
|
||||
AND (ti.org_id = ? OR ti.team_id = ANY(?))
|
||||
ORDER BY ti.email_to, ti.valid_until DESC, ti.created_at DESC;")
|
||||
|
||||
(def ^:private schema:get-org-invitations-params
|
||||
[:map
|
||||
[:organization-id ::sm/uuid]])
|
||||
@ -521,20 +509,16 @@ LEFT JOIN profile AS p
|
||||
"Get valid invitations for an organization, returning at most one invitation per email."
|
||||
{::doc/added "2.16"
|
||||
::sm/params schema:get-org-invitations-params
|
||||
::sm/result schema:get-org-invitations-result}
|
||||
::sm/result schema:get-org-invitations-result
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
team-ids (->> (:teams org-summary)
|
||||
(map :id)
|
||||
(filter uuid?)
|
||||
(into []))]
|
||||
(let [team-ids (noh/get-org-team-ids cfg organization-id)]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(->> (db/exec! conn [sql:get-org-invitations organization-id ids-array])
|
||||
(mapv (fn [{:keys [photo-id] :as invitation}]
|
||||
(cond-> (dissoc invitation :photo-id)
|
||||
photo-id
|
||||
(assoc :photo-url (files/resolve-public-uri photo-id)))))))))))
|
||||
(->> (noh/get-org-invitations conn organization-id team-ids)
|
||||
(mapv (fn [{:keys [photo-id] :as invitation}]
|
||||
(cond-> (dissoc invitation :photo-id)
|
||||
photo-id
|
||||
(assoc :photo-url (files/resolve-public-uri photo-id))))))))))
|
||||
|
||||
|
||||
;; API: delete-org-invitations
|
||||
@ -552,14 +536,11 @@ LEFT JOIN profile AS p
|
||||
(sv/defmethod ::delete-org-invitations
|
||||
"Delete all invitations for one email in an organization scope (org + org teams)."
|
||||
{::doc/added "2.16"
|
||||
::sm/params schema:delete-org-invitations-params}
|
||||
::sm/params schema:delete-org-invitations-params
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [organization-id email]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
clean-email (profile/clean-email email)
|
||||
team-ids (->> (:teams org-summary)
|
||||
(map :id)
|
||||
(filter uuid?)
|
||||
(into []))]
|
||||
(let [clean-email (profile/clean-email email)
|
||||
team-ids (noh/get-org-team-ids cfg organization-id)]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(db/exec! conn [sql:delete-org-invitations clean-email organization-id ids-array]))))
|
||||
@ -585,9 +566,7 @@ LEFT JOIN profile AS p
|
||||
::sm/params schema:delete-all-org-invitations-params
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
team-ids (->> (:teams org-summary)
|
||||
(map :id))]
|
||||
(let [team-ids (noh/get-org-team-ids cfg organization-id)]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(db/exec! conn [sql:delete-all-org-invitations organization-id ids-array]))))
|
||||
@ -825,18 +804,170 @@ LEFT JOIN profile AS p
|
||||
nil))
|
||||
|
||||
|
||||
;; ---- API: notify-org-sso-change
|
||||
;; ---- API: get-teams-detail
|
||||
|
||||
(def ^:private sql:get-teams-detail
|
||||
"SELECT
|
||||
t.id,
|
||||
t.name,
|
||||
t.photo_id,
|
||||
t.created_at,
|
||||
(SELECT MAX(p2.modified_at)
|
||||
FROM project AS p2
|
||||
WHERE p2.team_id = t.id
|
||||
AND p2.deleted_at IS NULL
|
||||
AND p2.is_default IS FALSE) AS last_activity_at,
|
||||
owner_tpr.profile_id AS owner_profile_id,
|
||||
owner_p.fullname AS owner_name,
|
||||
owner_p.photo_id AS owner_photo_id,
|
||||
(SELECT COUNT(*)
|
||||
FROM project AS p3
|
||||
WHERE p3.team_id = t.id
|
||||
AND p3.deleted_at IS NULL
|
||||
AND p3.is_default IS FALSE) AS num_projects,
|
||||
(SELECT COUNT(*)
|
||||
FROM file AS f
|
||||
JOIN project AS p4 ON p4.id = f.project_id
|
||||
WHERE p4.team_id = t.id
|
||||
AND f.deleted_at IS NULL
|
||||
AND p4.deleted_at IS NULL) AS num_files,
|
||||
(SELECT COUNT(*)
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.team_id = t.id) AS num_members
|
||||
FROM team AS t
|
||||
LEFT JOIN team_profile_rel AS owner_tpr
|
||||
ON owner_tpr.team_id = t.id AND owner_tpr.is_owner IS TRUE
|
||||
LEFT JOIN profile AS owner_p
|
||||
ON owner_p.id = owner_tpr.profile_id
|
||||
WHERE t.id = ANY(?)
|
||||
AND t.deleted_at IS NULL
|
||||
AND t.is_default IS FALSE
|
||||
ORDER BY last_activity_at DESC NULLS LAST")
|
||||
|
||||
(def ^:private schema:get-teams-detail-params
|
||||
[:map
|
||||
[:organization-id ::sm/uuid]])
|
||||
|
||||
(def ^:private schema:get-teams-detail-result
|
||||
[:vector
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name ::sm/text]
|
||||
[:photo-url {:optional true} ::sm/uri]
|
||||
[:created-at ::sm/inst]
|
||||
[:last-activity-at {:optional true} [:maybe ::sm/inst]]
|
||||
[:owner-profile-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:owner-name {:optional true} [:maybe ::sm/text]]
|
||||
[:owner-photo-url {:optional true} ::sm/uri]
|
||||
[:num-projects ::sm/int]
|
||||
[:num-files ::sm/int]
|
||||
[:num-members ::sm/int]]])
|
||||
|
||||
(sv/defmethod ::get-teams-detail
|
||||
"Get detailed information for all non-deleted teams in an organization,
|
||||
including owner info and project/file/member counts."
|
||||
{::doc/added "2.20"
|
||||
::sm/params schema:get-teams-detail-params
|
||||
::sm/result schema:get-teams-detail-result
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
team-ids (into [] (comp d/xf:map-id (filter uuid?)) (:teams org-summary))]
|
||||
(if (empty? team-ids)
|
||||
[]
|
||||
(db/run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(->> (db/exec! conn [sql:get-teams-detail ids-array])
|
||||
(mapv (fn [{:keys [photo-id owner-photo-id] :as row}]
|
||||
(cond-> (dissoc row :photo-id :owner-photo-id)
|
||||
photo-id (assoc :photo-url (files/resolve-public-uri photo-id))
|
||||
owner-photo-id (assoc :owner-photo-url (files/resolve-public-uri owner-photo-id))))))))))))
|
||||
|
||||
;; ---- API: check-organization-sso
|
||||
|
||||
(def ^:private schema:check-organization-sso-result
|
||||
[:map
|
||||
[:valid ::sm/boolean]])
|
||||
|
||||
(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."
|
||||
{::doc/added "2.20"
|
||||
::sm/params schema:nitrate-sso
|
||||
::sm/result schema:check-organization-sso-result
|
||||
::rpc/auth false}
|
||||
[cfg params]
|
||||
{:valid (oidc/is-organization-sso-config-valid? cfg params)})
|
||||
|
||||
;; ---- API: notify-org-sso-change
|
||||
(sv/defmethod ::notify-org-sso-change
|
||||
"Nitrate notifies that an organization sso values have changed"
|
||||
{::doc/added "2.19"
|
||||
::sm/params [:map
|
||||
[:organization-id ::sm/uuid]
|
||||
[:updated-props ::sm/boolean]]
|
||||
[:updated-props ::sm/boolean]
|
||||
[:became-active ::sm/boolean]]
|
||||
::rpc/auth false}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [organization-id updated-props]}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [organization-id updated-props became-active]}]
|
||||
(when updated-props
|
||||
(rpc/invalidate-org-sso-cache-by-org! organization-id)
|
||||
(session/clear-org-sso-sessions! pool organization-id))
|
||||
(notifications/notify-organization-change-sso cfg organization-id)
|
||||
(when became-active
|
||||
(neh/send-organization-setup-sso-emails! cfg organization-id))
|
||||
nil)
|
||||
|
||||
;; ---- API: bulk-create-profiles
|
||||
|
||||
(def ^:private schema:bulk-create-profiles-params
|
||||
[:map
|
||||
[:password [::sm/word-string {:max 500}]]
|
||||
[:emails [:vector ::sm/email]]])
|
||||
|
||||
(def ^:private schema:bulk-create-profiles-result
|
||||
[:map
|
||||
[:created [:vector ::sm/email]]
|
||||
[:skipped [:vector ::sm/email]]])
|
||||
|
||||
(defn- create-active-profile!
|
||||
"Create a single already-active profile (email pre-verified, onboarding
|
||||
skipped) plus its default team. Returns nil; existence checks happen in the
|
||||
caller so duplicates are skipped instead of aborting the whole batch."
|
||||
[cfg email password]
|
||||
(let [fullname (-> (str/split email "@") first)]
|
||||
(->> {:email email
|
||||
:fullname fullname
|
||||
:password password
|
||||
:is-active true
|
||||
:props {:onboarding-viewed true}}
|
||||
(auth/create-profile cfg)
|
||||
(auth/create-profile-rels cfg))
|
||||
nil))
|
||||
|
||||
(sv/defmethod ::bulk-create-profiles
|
||||
"Create multiple already-active profiles that share a single password. The
|
||||
created users skip email verification and onboarding. Emails that already
|
||||
belong to an existing profile are skipped. Intended for the Nitrate admin
|
||||
bulk-creation screen; access is gated by the shared key and, in Nitrate, an
|
||||
email allow-list."
|
||||
{::doc/added "2.19"
|
||||
::sm/params schema:bulk-create-profiles-params
|
||||
::sm/result schema:bulk-create-profiles-result
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [password emails]}]
|
||||
(let [derived (aauth/derive-password password)]
|
||||
(db/tx-run!
|
||||
cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(reduce
|
||||
(fn [acc email]
|
||||
(let [email (eml/clean email)]
|
||||
(if (profile/get-profile-by-email conn email)
|
||||
(update acc :skipped conj email)
|
||||
(do
|
||||
(create-active-profile! cfg email derived)
|
||||
(update acc :created conj email)))))
|
||||
{:created [] :skipped []}
|
||||
emails)))))
|
||||
|
||||
106
backend/src/app/rpc/nitrate/emails_helper.clj
Normal file
106
backend/src/app/rpc/nitrate/emails_helper.clj
Normal file
@ -0,0 +1,106 @@
|
||||
;; 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
|
||||
|
||||
(ns app.rpc.nitrate.emails-helper
|
||||
"Helpers for organization SSO notification emails triggered by Nitrate integration."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.email :as eml]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.nitrate.organization-helper :as neh]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def ^:private sql:get-profile-emails-by-ids
|
||||
"SELECT email
|
||||
FROM profile
|
||||
WHERE id = ANY(?)
|
||||
AND deleted_at IS NULL")
|
||||
|
||||
(def ^:private sql:get-profiles-by-emails
|
||||
"SELECT id, email, fullname, is_muted
|
||||
FROM profile
|
||||
WHERE email = ANY(?)
|
||||
AND deleted_at IS NULL")
|
||||
|
||||
(defn- org-sso-active?
|
||||
"Return whether SSO is enabled for the organization."
|
||||
[cfg organization-id]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(true? (:active (nitrate/call cfg :get-org-sso {:organization-id organization-id})))))
|
||||
|
||||
(def ^:private xf:map-email (map :email))
|
||||
|
||||
(defn- recipients-by-emails
|
||||
"Build `{:email :user-name :profile}` maps for a deduplicated email list."
|
||||
[conn emails]
|
||||
(let [profiles (if (seq emails)
|
||||
(let [emails-array (db/create-array conn "text" emails)]
|
||||
(db/exec! conn [sql:get-profiles-by-emails emails-array]))
|
||||
[])
|
||||
profile-by-email (d/index-by (comp str/lower :email) profiles)]
|
||||
(map (fn [email]
|
||||
(let [profile (get profile-by-email (str/lower email))]
|
||||
{:email email
|
||||
:user-name (:fullname profile)
|
||||
:profile profile}))
|
||||
emails)))
|
||||
|
||||
(defn- send-organization-setup-sso-email!
|
||||
"Send the organization SSO setup email to a single recipient, when allowed."
|
||||
[conn organization-name {:keys [email user-name profile]}]
|
||||
(when (or (nil? profile)
|
||||
(eml/allow-send-emails? conn profile))
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/organization-setup-sso
|
||||
:public-uri (cf/get :public-uri)
|
||||
:to email
|
||||
:user-name user-name
|
||||
:organization-name organization-name})))
|
||||
|
||||
(defn- get-org-sso-notify-recipients
|
||||
"Unique org members and pending org/team invitees for SSO activation emails."
|
||||
[conn cfg organization-id org-summary]
|
||||
(let [member-ids (nitrate/call cfg :get-org-members {:organization-id organization-id})
|
||||
team-ids (neh/get-org-team-ids org-summary)
|
||||
member-emails (if (seq member-ids)
|
||||
(let [ids-array (db/create-array conn "uuid" member-ids)]
|
||||
(into #{} (map :email (db/exec! conn [sql:get-profile-emails-by-ids ids-array]))))
|
||||
#{})
|
||||
invite-emails (into #{} (map :email
|
||||
(neh/get-org-invitations conn organization-id team-ids)))
|
||||
emails (into #{} (concat member-emails invite-emails))]
|
||||
(recipients-by-emails conn emails)))
|
||||
|
||||
(defn- get-team-sso-notify-recipients
|
||||
"Team members who are not in `org-member-ids`, plus pending team invitations."
|
||||
[conn team-id org-member-ids]
|
||||
(let [team-members (->> (teams/get-team-members conn team-id)
|
||||
(remove #(contains? org-member-ids (:id %))))
|
||||
invitations (neh/get-team-invitation-emails conn team-id)]
|
||||
(->> (sequence xf:map-email (concat team-members invitations))
|
||||
(recipients-by-emails conn))))
|
||||
|
||||
(defn send-organization-setup-sso-emails!
|
||||
"Notify all org members and pending org/team invitees that SSO is active."
|
||||
[cfg organization-id]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(doseq [recipient (get-org-sso-notify-recipients conn cfg organization-id org-summary)]
|
||||
(send-organization-setup-sso-email! conn (:name org-summary) recipient))))))
|
||||
|
||||
(defn send-organization-setup-sso-emails-for-team!
|
||||
"Notify team members who are not in `org-member-ids-before` and pending team invitees."
|
||||
[cfg organization-id team-id org-member-ids-before]
|
||||
(when (org-sso-active? cfg organization-id)
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(doseq [recipient (get-team-sso-notify-recipients conn team-id org-member-ids-before)]
|
||||
(send-organization-setup-sso-email! conn (:name org-summary) recipient)))))))
|
||||
60
backend/src/app/rpc/nitrate/organization_helper.clj
Normal file
60
backend/src/app/rpc/nitrate/organization_helper.clj
Normal file
@ -0,0 +1,60 @@
|
||||
;; 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
|
||||
|
||||
(ns app.rpc.nitrate.organization-helper
|
||||
"Shared Nitrate organization query helpers."
|
||||
(:require
|
||||
[app.db :as db]
|
||||
[app.nitrate :as nitrate]))
|
||||
|
||||
(def ^:private sql:get-org-invitations
|
||||
"SELECT DISTINCT ON (email_to)
|
||||
ti.id,
|
||||
ti.org_id AS organization_id,
|
||||
ti.email_to AS email,
|
||||
ti.created_at AS sent_at,
|
||||
p.fullname AS name,
|
||||
p.id AS profile_id,
|
||||
p.photo_id
|
||||
FROM team_invitation AS ti
|
||||
LEFT JOIN profile AS p
|
||||
ON p.email = ti.email_to
|
||||
AND p.deleted_at IS NULL
|
||||
WHERE ti.valid_until >= now()
|
||||
AND (ti.org_id = ? OR ti.team_id = ANY(?))
|
||||
ORDER BY ti.email_to, ti.valid_until DESC, ti.created_at DESC;")
|
||||
|
||||
(def ^:private sql:get-team-invitation-emails
|
||||
"SELECT DISTINCT ON (email_to)
|
||||
ti.email_to AS email
|
||||
FROM team_invitation AS ti
|
||||
WHERE ti.team_id = ?
|
||||
AND ti.valid_until >= now()
|
||||
ORDER BY ti.email_to, ti.valid_until DESC, ti.created_at DESC;")
|
||||
|
||||
(defn get-org-team-ids
|
||||
"Return team ids for an organization.
|
||||
|
||||
Accepts either `cfg` and `organization-id` (fetches the org summary from
|
||||
Nitrate) or an already-resolved org summary map."
|
||||
([cfg organization-id]
|
||||
(get-org-team-ids (nitrate/call cfg :get-org-summary {:organization-id organization-id})))
|
||||
([org-summary]
|
||||
(->> (:teams org-summary)
|
||||
(map :id)
|
||||
(filter uuid?)
|
||||
(vec))))
|
||||
|
||||
(defn get-org-invitations
|
||||
"Fetch valid org-level and team-level invitations for an organization."
|
||||
[conn organization-id team-ids]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(db/exec! conn [sql:get-org-invitations organization-id ids-array])))
|
||||
|
||||
(defn get-team-invitation-emails
|
||||
"Return distinct valid team invitation recipient emails."
|
||||
[conn team-id]
|
||||
(db/exec! conn [sql:get-team-invitation-emails team-id]))
|
||||
@ -7,8 +7,11 @@
|
||||
(ns app.rpc.permissions
|
||||
"A permission checking helper factories."
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]))
|
||||
[app.common.schema :as sm]
|
||||
[app.db :as db]
|
||||
[app.nitrate :as nitrate]))
|
||||
|
||||
(def schema:permissions
|
||||
[:map {:title "Permissions"}
|
||||
@ -89,3 +92,64 @@
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "not found"))))
|
||||
|
||||
;; --- Organization owner (Nitrate) viewer access
|
||||
;;
|
||||
;; Read-permission helpers that augment normal Penpot membership with
|
||||
;; Nitrate organization-owner viewer access. Edit/admin permission
|
||||
;; providers intentionally stay membership-only.
|
||||
|
||||
(def viewer-role-flags
|
||||
"Role flags granted to a non-member organization owner: read-only.
|
||||
Shared so callers that build full team/file rows shape permissions the
|
||||
same way the permission lookups do."
|
||||
{:is-owner false
|
||||
:is-admin false
|
||||
:can-edit false})
|
||||
|
||||
(def ^:private sql:get-team-id-for-project
|
||||
"SELECT team_id FROM project WHERE id = ?")
|
||||
|
||||
(def ^:private sql:get-team-id-for-file
|
||||
"SELECT p.team_id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?")
|
||||
|
||||
(defn get-team-id-for-project
|
||||
[cfg project-id]
|
||||
(some-> (db/exec-one! cfg [sql:get-team-id-for-project project-id])
|
||||
(:team-id)))
|
||||
|
||||
(defn get-team-id-for-file
|
||||
[cfg file-id]
|
||||
(some-> (db/exec-one! cfg [sql:get-team-id-for-file file-id])
|
||||
(:team-id)))
|
||||
|
||||
(defn resolve-team-id
|
||||
[cfg {:keys [team-id project-id file-id]}]
|
||||
(cond
|
||||
(some? team-id) team-id
|
||||
(some? project-id) (get-team-id-for-project cfg project-id)
|
||||
(some? file-id) (get-team-id-for-file cfg file-id)))
|
||||
|
||||
(defn get-organization-owner-permissions
|
||||
"When `profile-id` is a non-member owner of the organization that owns
|
||||
the team/project/file referenced by `params`, returns read-only viewer
|
||||
permissions; otherwise nil."
|
||||
[cfg profile-id & {:as params}]
|
||||
(when-let [team-id (resolve-team-id cfg params)]
|
||||
(when (nitrate/organization-owner-of-team? cfg profile-id team-id)
|
||||
(assoc viewer-role-flags
|
||||
:can-read true
|
||||
:type :membership
|
||||
:is-logged (some? profile-id)))))
|
||||
|
||||
(defn get-file-read-permissions
|
||||
([cfg profile-id file-id]
|
||||
(or (bfc/get-file-permissions cfg profile-id file-id)
|
||||
(get-organization-owner-permissions cfg profile-id :file-id file-id)))
|
||||
|
||||
([cfg profile-id file-id share-id]
|
||||
(or (bfc/get-file-permissions cfg profile-id file-id share-id)
|
||||
(get-organization-owner-permissions cfg profile-id :file-id file-id))))
|
||||
|
||||
@ -53,3 +53,20 @@
|
||||
;; not silently slip through as if it were the matching string.
|
||||
(t/is (= :auto (#'oidc/select-user-info-source :token)))
|
||||
(t/is (= :auto (#'oidc/select-user-info-source :userinfo)))))
|
||||
|
||||
(t/deftest token-endpoint-errors-detect-valid-client-credentials
|
||||
(let [response {:status 403
|
||||
:body "{\"error\":\"invalid_grant\",\"error_description\":\"Invalid authorization code\"}"}]
|
||||
(t/is (#'oidc/token-endpoint-valid-client-error? response))
|
||||
(t/is (not (#'oidc/token-endpoint-invalid-client-error? response)))))
|
||||
|
||||
(t/deftest token-endpoint-errors-detect-invalid-client-credentials
|
||||
(t/is (#'oidc/token-endpoint-invalid-client-error?
|
||||
{:status 401
|
||||
:body "{\"error\":\"access_denied\",\"error_description\":\"Unauthorized\"}"}))
|
||||
(t/is (#'oidc/token-endpoint-invalid-client-error?
|
||||
{:status 400
|
||||
:body "{\"error\":\"invalid_client\"}"}))
|
||||
(t/is (not (#'oidc/token-endpoint-valid-client-error?
|
||||
{:status 400
|
||||
:body "{\"error\":\"invalid_client\"}"}))))
|
||||
|
||||
@ -6,10 +6,12 @@
|
||||
|
||||
(ns backend-tests.email-sending-test
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.email :as emails]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
@ -23,3 +25,67 @@
|
||||
(t/is (contains? result :to))
|
||||
#_(t/is (contains? result :reply-to))
|
||||
(t/is (map? (:body result)))))
|
||||
|
||||
(def ^:private sso-notice-snippet
|
||||
"has set up single sign-on (SSO) in Penpot")
|
||||
|
||||
(defn- email-text-body
|
||||
[result]
|
||||
(get-in result [:body "text/plain"]))
|
||||
|
||||
(defn- invite-email-params
|
||||
[organization]
|
||||
{:to "invitee@example.com"
|
||||
:public-uri (cf/get :public-uri)
|
||||
:invited-by "Owner User"
|
||||
:user-name "Invitee User"
|
||||
:token "test-token"
|
||||
:organization organization})
|
||||
|
||||
(t/deftest invite-to-org-includes-sso-notice-when-active
|
||||
(let [result (emails/render emails/invite-to-org
|
||||
(invite-email-params {:name "Acme Inc"
|
||||
:sso-active true}))]
|
||||
(t/is (str/includes? (email-text-body result) sso-notice-snippet))
|
||||
(t/is (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet))))
|
||||
|
||||
(t/deftest invite-to-org-omits-sso-notice-when-inactive
|
||||
(let [result (emails/render emails/invite-to-org
|
||||
(invite-email-params {:name "Acme Inc"
|
||||
:sso-active false}))]
|
||||
(t/is (not (str/includes? (email-text-body result) sso-notice-snippet)))
|
||||
(t/is (not (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet)))))
|
||||
|
||||
(t/deftest invite-to-team-includes-sso-notice-when-active
|
||||
(let [result (emails/render emails/invite-to-team
|
||||
{:to "invitee@example.com"
|
||||
:public-uri (cf/get :public-uri)
|
||||
:invited-by "Owner User"
|
||||
:team "Design Team"
|
||||
:token "test-token"
|
||||
:organization {:name "Acme Inc"
|
||||
:sso-active true}})]
|
||||
(t/is (str/includes? (email-text-body result) sso-notice-snippet))
|
||||
(t/is (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet))))
|
||||
|
||||
(t/deftest invite-to-team-omits-sso-notice-when-inactive
|
||||
(let [result (emails/render emails/invite-to-team
|
||||
{:to "invitee@example.com"
|
||||
:public-uri (cf/get :public-uri)
|
||||
:invited-by "Owner User"
|
||||
:team "Design Team"
|
||||
:token "test-token"
|
||||
:organization {:name "Acme Inc"
|
||||
:sso-active false}})]
|
||||
(t/is (not (str/includes? (email-text-body result) sso-notice-snippet)))
|
||||
(t/is (not (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet)))))
|
||||
|
||||
(t/deftest invite-to-team-omits-sso-notice-without-organization
|
||||
(let [result (emails/render emails/invite-to-team
|
||||
{:to "invitee@example.com"
|
||||
:public-uri (cf/get :public-uri)
|
||||
:invited-by "Owner User"
|
||||
:team "Design Team"
|
||||
:token "test-token"})]
|
||||
(t/is (not (str/includes? (email-text-body result) sso-notice-snippet)))
|
||||
(t/is (not (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet)))))
|
||||
|
||||
@ -6,11 +6,13 @@
|
||||
|
||||
(ns backend-tests.rpc-management-nitrate-test
|
||||
(:require
|
||||
[app.auth.oidc :as oidc]
|
||||
[app.common.data :as d]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
[app.email :as eml]
|
||||
[app.msgbus :as mbus]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
@ -1198,3 +1200,118 @@
|
||||
(t/is (some? rel1)))
|
||||
(let [rel2 (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id user)})]
|
||||
(t/is (some? rel2)))))
|
||||
|
||||
(t/deftest notify-org-sso-change-sends-setup-sso-email-once-per-recipient
|
||||
(let [owner (th/create-profile* 1 {:is-active true :fullname "Owner"})
|
||||
member (th/create-profile* 2 {:is-active true
|
||||
:fullname "Member"
|
||||
:email "member@example.com"})
|
||||
invited (th/create-profile* 3 {:is-active true
|
||||
:fullname "Invited User"
|
||||
:email "invited@example.com"})
|
||||
org-id (uuid/random)
|
||||
org-name "Acme Inc"
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})
|
||||
org-summary {:id org-id
|
||||
:name org-name
|
||||
:teams [{:id (:id team)}]}
|
||||
sent (atom [])
|
||||
params {::th/type :notify-org-sso-change
|
||||
:organization-id org-id
|
||||
:updated-props false
|
||||
:became-active true}]
|
||||
|
||||
;; Member also has a pending invitation: should still receive only one email.
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:org-id org-id
|
||||
:team-id nil
|
||||
:email-to (:email member)
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "24h")})
|
||||
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:team-id (:id team)
|
||||
:org-id nil
|
||||
:email-to (:email invited)
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "48h")})
|
||||
|
||||
;; Invite without an existing profile.
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:team-id (:id team)
|
||||
:org-id nil
|
||||
:email-to "external@example.com"
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "72h")})
|
||||
|
||||
(with-redefs [nitrate/call (fn [_cfg method _params]
|
||||
(case method
|
||||
:get-org-members [(:id owner) (:id member)]
|
||||
:get-org-summary org-summary
|
||||
nil))
|
||||
eml/send! (fn [params] (swap! sent conj params))]
|
||||
(management-command-with-nitrate! params))
|
||||
|
||||
(let [emails (->> @sent (map :to) set)]
|
||||
(t/is (= 4 (count @sent)))
|
||||
(t/is (= #{"member@example.com"
|
||||
(:email owner)
|
||||
"invited@example.com"
|
||||
"external@example.com"}
|
||||
emails))
|
||||
(doseq [email-params @sent]
|
||||
(t/is (= org-name (:organization-name email-params)))
|
||||
(t/is (= eml/organization-setup-sso (::eml/factory email-params)))))))
|
||||
|
||||
(t/deftest notify-org-sso-change-skips-email-when-not-active
|
||||
(let [sent (atom [])
|
||||
params {::th/type :notify-org-sso-change
|
||||
:organization-id (uuid/random)
|
||||
:updated-props false
|
||||
:became-active false}]
|
||||
(with-redefs [eml/send! (fn [params] (swap! sent conj params))]
|
||||
(management-command-with-nitrate! params))
|
||||
(t/is (empty? @sent))))
|
||||
|
||||
(t/deftest check-organization-sso-returns-valid-true
|
||||
(let [org-id (uuid/random)
|
||||
out (with-redefs [oidc/is-organization-sso-config-valid? (constantly true)]
|
||||
(management-command-with-nitrate!
|
||||
{::th/type :check-organization-sso
|
||||
:organization-id org-id
|
||||
:client-id "test-client"
|
||||
:client-secret "test-secret"
|
||||
:base-url "https://idp.example.com"}))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (true? (-> out :result :valid)))))
|
||||
|
||||
(t/deftest check-organization-sso-returns-valid-false-on-invalid-config
|
||||
(let [out (management-command-with-nitrate!
|
||||
{::th/type :check-organization-sso
|
||||
:organization-id (uuid/random)
|
||||
:client-id "test-client"
|
||||
:client-secret "test-secret"})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (false? (-> out :result :valid)))))
|
||||
|
||||
(t/deftest check-organization-sso-uses-issuer-when-base-url-is-blank
|
||||
(let [org-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))))]
|
||||
(management-command-with-nitrate!
|
||||
{::th/type :check-organization-sso
|
||||
:organization-id org-id
|
||||
:client-id "test-client"
|
||||
:client-secret "test-secret"
|
||||
:base-url ""
|
||||
:issuer "https://idp.example.com/"}))]
|
||||
(t/is (th/success? out))
|
||||
(t/is (true? (-> out :result :valid)))))
|
||||
|
||||
@ -6,12 +6,15 @@
|
||||
|
||||
(ns backend-tests.rpc-nitrate-test
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
[app.email :as eml]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.nitrate]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[cuerdas.core :as str]))
|
||||
@ -815,3 +818,118 @@
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :validation (th/ex-type (:error out))))
|
||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
||||
|
||||
(defn- add-team-to-org-nitrate-mock
|
||||
[{:keys [org-id org-summary org-perms owner-id team-id sso-active?]}]
|
||||
(fn [_cfg method params]
|
||||
(case method
|
||||
:get-org-membership (if (= (:profile-id params) owner-id)
|
||||
{:is-member true :organization-id org-id}
|
||||
{:is-member false :organization-id org-id})
|
||||
:get-org-members [owner-id]
|
||||
:get-team-org {:organization nil}
|
||||
:get-org-permissions org-perms
|
||||
:set-team-org {:id team-id}
|
||||
:get-org-sso {:active sso-active?}
|
||||
:get-org-summary (assoc org-summary :teams [{:id team-id}])
|
||||
:add-profile-to-org {:is-member true}
|
||||
nil)))
|
||||
|
||||
(t/deftest add-team-to-organization-sends-sso-emails-to-new-members-and-invitees
|
||||
(let [owner (th/create-profile* 301 {:is-active true
|
||||
:fullname "Owner"
|
||||
:email "owner301@example.com"})
|
||||
member (th/create-profile* 302 {:is-active true
|
||||
:fullname "Member"
|
||||
:email "member302@example.com"})
|
||||
team (th/create-team* 301 {:profile-id (:id owner)})
|
||||
_ (th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id member)
|
||||
:role :editor})
|
||||
org-id (uuid/random)
|
||||
org-name "SSO Org"
|
||||
org-summary {:id org-id
|
||||
:name org-name
|
||||
:owner-id (:id owner)
|
||||
:teams []}
|
||||
org-perms {:owner-id (:id owner)
|
||||
:permissions {:create-teams "any"
|
||||
:move-teams "always"
|
||||
:new-team-members "members"}}
|
||||
sent (atom [])]
|
||||
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:team-id (:id team)
|
||||
:org-id nil
|
||||
:email-to "external301@example.com"
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "48h")})
|
||||
|
||||
(with-redefs [cf/flags (conj cf/flags :nitrate)
|
||||
nitrate/call (add-team-to-org-nitrate-mock
|
||||
{:org-id org-id
|
||||
:org-summary org-summary
|
||||
:org-perms org-perms
|
||||
:owner-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:sso-active? true})
|
||||
teams/initialize-user-in-nitrate-org (fn [& _] nil)
|
||||
eml/send! (fn [params] (swap! sent conj params))]
|
||||
(let [out (th/command! {::th/type :add-team-to-organization
|
||||
::rpc/profile-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:organization-id org-id})]
|
||||
(t/is (th/success? out))))
|
||||
|
||||
(let [emails (->> @sent (map :to) set)]
|
||||
(t/is (= 2 (count @sent)))
|
||||
(t/is (= #{"member302@example.com" "external301@example.com"} emails))
|
||||
(doseq [email-params @sent]
|
||||
(t/is (= org-name (:organization-name email-params)))
|
||||
(t/is (= eml/organization-setup-sso (::eml/factory email-params)))))))
|
||||
|
||||
(t/deftest add-team-to-organization-skips-sso-emails-when-sso-inactive
|
||||
(let [owner (th/create-profile* 303 {:is-active true :email "owner303@example.com"})
|
||||
member (th/create-profile* 304 {:is-active true :email "member304@example.com"})
|
||||
team (th/create-team* 303 {:profile-id (:id owner)})
|
||||
_ (th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id member)
|
||||
:role :editor})
|
||||
org-id (uuid/random)
|
||||
org-summary {:id org-id
|
||||
:name "No SSO Org"
|
||||
:owner-id (:id owner)
|
||||
:teams []}
|
||||
org-perms {:owner-id (:id owner)
|
||||
:permissions {:create-teams "any"
|
||||
:move-teams "always"
|
||||
:new-team-members "members"}}
|
||||
sent (atom [])]
|
||||
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:team-id (:id team)
|
||||
:org-id nil
|
||||
:email-to "external303@example.com"
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "48h")})
|
||||
|
||||
(with-redefs [cf/flags (conj cf/flags :nitrate)
|
||||
nitrate/call (add-team-to-org-nitrate-mock
|
||||
{:org-id org-id
|
||||
:org-summary org-summary
|
||||
:org-perms org-perms
|
||||
:owner-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:sso-active? false})
|
||||
teams/initialize-user-in-nitrate-org (fn [& _] nil)
|
||||
eml/send! (fn [params] (swap! sent conj params))]
|
||||
(let [out (th/command! {::th/type :add-team-to-organization
|
||||
::rpc/profile-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:organization-id org-id})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (empty? @sent))))))
|
||||
|
||||
150
backend/test/backend_tests/rpc_org_owner_permissions_test.clj
Normal file
150
backend/test/backend_tests/rpc_org_owner_permissions_test.clj
Normal file
@ -0,0 +1,150 @@
|
||||
;; 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.rpc-org-owner-permissions-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(defn- org-data
|
||||
[org-id owner-id]
|
||||
{:id org-id
|
||||
:name "Acme"
|
||||
:slug "acme"
|
||||
:owner-id owner-id
|
||||
:avatar-bg-url "http://example.com/avatar.png"
|
||||
:permissions {}})
|
||||
|
||||
(defn- with-org-owner-access
|
||||
[{:keys [org-owner-id org-id team-id]} f]
|
||||
(with-redefs [cf/flags (conj cf/flags :nitrate)
|
||||
nitrate/organization-owner-of-team?
|
||||
(fn [_cfg profile-id candidate-team-id]
|
||||
(and (= org-owner-id profile-id)
|
||||
(= team-id candidate-team-id)))
|
||||
|
||||
nitrate/call
|
||||
(fn [_cfg method params]
|
||||
(case method
|
||||
:get-owned-orgs
|
||||
[{:id org-id
|
||||
:name "Acme"
|
||||
:owner-id org-owner-id
|
||||
:teams [{:id team-id :is-your-penpot false}]}]
|
||||
|
||||
:get-team-org
|
||||
(if (= team-id (:team-id params))
|
||||
{:id team-id
|
||||
:is-your-penpot false
|
||||
:organization (org-data org-id org-owner-id)}
|
||||
{:id (:team-id params)
|
||||
:is-your-penpot false
|
||||
:organization nil})))]
|
||||
(f)))
|
||||
|
||||
(t/deftest org-owner-access-disabled-without-nitrate-flag
|
||||
(let [team-owner (th/create-profile* 1)
|
||||
org-owner (th/create-profile* 2)
|
||||
target-team (th/create-team* 1 {:profile-id (:id team-owner)})]
|
||||
|
||||
(let [out (th/command! {::th/type :get-projects
|
||||
::rpc/profile-id (:id org-owner)
|
||||
:team-id (:id target-team)})
|
||||
error (:error out)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :not-found)))))
|
||||
|
||||
(t/deftest non-member-org-owner-gets-viewer-access-to-org-team
|
||||
(let [team-owner (th/create-profile* 1)
|
||||
org-owner (th/create-profile* 2)
|
||||
target-team (th/create-team* 1 {:profile-id (:id team-owner)})
|
||||
project (th/create-project* 1 {:profile-id (:id team-owner)
|
||||
:team-id (:id target-team)})
|
||||
file (th/create-file* 1 {:profile-id (:id team-owner)
|
||||
:project-id (:id project)})
|
||||
org-id (uuid/next)]
|
||||
|
||||
(with-org-owner-access {:org-owner-id (:id org-owner)
|
||||
:org-id org-id
|
||||
:team-id (:id target-team)}
|
||||
(fn []
|
||||
;; The team is not listed for a non-member, even though the org
|
||||
;; owner can access it directly.
|
||||
(let [out (th/command! {::th/type :get-teams
|
||||
::rpc/profile-id (:id org-owner)})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (not-any? #(= (:id target-team) (:id %)) (:result out))))
|
||||
|
||||
(let [out (th/command! {::th/type :get-team
|
||||
::rpc/profile-id (:id org-owner)
|
||||
:id (:id target-team)})
|
||||
team (:result out)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= (:id target-team) (:id team)))
|
||||
(t/is (false? (get-in team [:permissions :is-owner])))
|
||||
(t/is (false? (get-in team [:permissions :is-admin])))
|
||||
(t/is (false? (get-in team [:permissions :can-edit])))
|
||||
(t/is (= org-id (get-in team [:organization :id])))
|
||||
(t/is (= "Acme" (get-in team [:organization :name]))))
|
||||
|
||||
(let [out (th/command! {::th/type :get-team-members
|
||||
::rpc/profile-id (:id org-owner)
|
||||
:team-id (:id target-team)})
|
||||
members (:result out)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some #(= (:id team-owner) (:id %)) members))
|
||||
(t/is (not-any? #(= (:id org-owner) (:id %)) members)))
|
||||
|
||||
(let [out (th/command! {::th/type :get-projects
|
||||
::rpc/profile-id (:id org-owner)
|
||||
:team-id (:id target-team)})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (some #(= (:id project) (:id %)) (:result out))))
|
||||
|
||||
(let [out (th/command! {::th/type :get-file
|
||||
::rpc/profile-id (:id org-owner)
|
||||
:id (:id file)})]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= (:id file) (get-in out [:result :id])))
|
||||
(t/is (false? (get-in out [:result :permissions :can-edit]))))
|
||||
|
||||
(let [out (th/command! {::th/type :rename-project
|
||||
::rpc/profile-id (:id org-owner)
|
||||
:id (:id project)
|
||||
:name "Nope"})
|
||||
error (:error out)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :not-found)))))))
|
||||
|
||||
(t/deftest org-owner-member-keeps-team-role
|
||||
(let [team-owner (th/create-profile* 1)
|
||||
org-owner (th/create-profile* 2)
|
||||
target-team (th/create-team* 1 {:profile-id (:id team-owner)})
|
||||
org-id (uuid/next)]
|
||||
|
||||
(th/create-team-role* {:team-id (:id target-team)
|
||||
:profile-id (:id org-owner)
|
||||
:role :editor})
|
||||
|
||||
(with-org-owner-access {:org-owner-id (:id org-owner)
|
||||
:org-id org-id
|
||||
:team-id (:id target-team)}
|
||||
(fn []
|
||||
(let [out (th/command! {::th/type :get-team
|
||||
::rpc/profile-id (:id org-owner)
|
||||
:id (:id target-team)})
|
||||
team (:result out)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (false? (get-in team [:permissions :is-owner])))
|
||||
(t/is (false? (get-in team [:permissions :is-admin])))
|
||||
(t/is (true? (get-in team [:permissions :can-edit]))))))))
|
||||
@ -20,7 +20,13 @@
|
||||
selmer/selmer {:mvn/version "1.13.4"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
|
||||
metosin/jsonista {:mvn/version "1.0.0"}
|
||||
metosin/jsonista {:mvn/version "1.0.0"
|
||||
:exclusions [com.fasterxml.jackson.core/jackson-core
|
||||
com.fasterxml.jackson.core/jackson-databind]}
|
||||
com.fasterxml.jackson.core/jackson-core {:mvn/version "2.22.0"}
|
||||
com.fasterxml.jackson.core/jackson-databind {:mvn/version "2.22.0"}
|
||||
|
||||
|
||||
metosin/malli {:mvn/version "0.19.1"}
|
||||
|
||||
expound/expound {:mvn/version "0.9.0"}
|
||||
@ -58,7 +64,7 @@
|
||||
{org.clojure/tools.namespace {:mvn/version "1.5.1"}
|
||||
thheller/shadow-cljs {:mvn/version "3.2.0"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"}
|
||||
com.bhauman/rebel-readline {:mvn/version "0.1.5"}
|
||||
com.bhauman/rebel-readline {:mvn/version "0.1.11"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
mockery/mockery {:mvn/version "0.1.4"}}
|
||||
:extra-paths ["test" "dev"]}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
"devDependencies": {
|
||||
"concurrently": "^10.0.3",
|
||||
"nodemon": "^3.1.14",
|
||||
"prettier": "3.8.4",
|
||||
"prettier": "3.9.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
|
||||
10
common/pnpm-lock.yaml
generated
10
common/pnpm-lock.yaml
generated
@ -19,8 +19,8 @@ importers:
|
||||
specifier: ^3.1.14
|
||||
version: 3.1.14
|
||||
prettier:
|
||||
specifier: 3.8.4
|
||||
version: 3.8.4
|
||||
specifier: 3.9.4
|
||||
version: 3.9.4
|
||||
source-map-support:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
@ -161,8 +161,8 @@ packages:
|
||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
prettier@3.8.4:
|
||||
resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==}
|
||||
prettier@3.9.4:
|
||||
resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
@ -378,7 +378,7 @@ snapshots:
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
|
||||
prettier@3.8.4: {}
|
||||
prettier@3.9.4: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
|
||||
@ -355,11 +355,15 @@
|
||||
|
||||
(defn has-point?
|
||||
[shape point]
|
||||
(if (or ^boolean (cfh/path-shape? shape)
|
||||
^boolean (cfh/bool-shape? shape)
|
||||
^boolean (cfh/circle-shape? shape))
|
||||
(slow-has-point? shape point)
|
||||
(fast-has-point? shape point)))
|
||||
(let [rotation (dm/get-prop shape :rotation)]
|
||||
;; Rotated shapes don't match their axis-aligned box, so use the polygon test.
|
||||
(if (or ^boolean (cfh/path-shape? shape)
|
||||
^boolean (cfh/bool-shape? shape)
|
||||
^boolean (cfh/circle-shape? shape)
|
||||
(and (some? rotation)
|
||||
(not ^boolean (mth/almost-zero? rotation))))
|
||||
(slow-has-point? shape point)
|
||||
(fast-has-point? shape point))))
|
||||
|
||||
(defn rect-contains-shape?
|
||||
[rect shape]
|
||||
|
||||
@ -249,7 +249,7 @@
|
||||
(defn is-variant-container?
|
||||
"Check if this shape is a variant container"
|
||||
[shape]
|
||||
(:is-variant-container shape))
|
||||
(boolean (:is-variant-container shape)))
|
||||
|
||||
(defn set-touched-group
|
||||
[touched group]
|
||||
|
||||
@ -61,4 +61,16 @@
|
||||
[:name ::sm/text]
|
||||
[:initials [:maybe :string]]
|
||||
[:logo [:maybe ::sm/uri]]
|
||||
[:avatar-bg-url [:maybe ::sm/uri]]])
|
||||
[:avatar-bg-url [:maybe ::sm/uri]]
|
||||
[:sso-active {:optional true} [:maybe :boolean]]])
|
||||
|
||||
(def schema:nitrate-sso
|
||||
[:map {:title "NitrateOrganizationSso"}
|
||||
[:organization-id ::sm/uuid]
|
||||
[:active {:optional true} [:maybe :boolean]]
|
||||
[:provider {:optional true} [:maybe :string]]
|
||||
[:client-id {:optional true} [:maybe :string]]
|
||||
[:base-url {:optional true} [:maybe :string]]
|
||||
[:client-secret {:optional true} [:maybe :string]]
|
||||
[:issuer {:optional true} [:maybe :string]]
|
||||
[:scopes {:optional true} [:maybe [::sm/set ::sm/text]]]])
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
(def schema:registry-entry
|
||||
[:map
|
||||
[:plugin-id :string]
|
||||
[:version {:optional true} :int]
|
||||
[:name :string]
|
||||
[:description {:optional true} :string]
|
||||
[:host :string]
|
||||
|
||||
@ -139,8 +139,8 @@
|
||||
[:map {:title "OpenOverlayInteraction"}
|
||||
[:action-type [:= :open-overlay]]
|
||||
[:event-type [::sm/one-of event-types]]
|
||||
[:overlay-position ::gpt/point]
|
||||
[:overlay-pos-type [::sm/one-of overlay-positioning-types]]
|
||||
[:overlay-position {:optional true} ::gpt/point]
|
||||
[:overlay-pos-type {:optional true} [::sm/one-of overlay-positioning-types]]
|
||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||
[:close-click-outside {:optional true} :boolean]
|
||||
[:background-overlay {:optional true} :boolean]
|
||||
@ -151,8 +151,8 @@
|
||||
[:map {:title "ToggleOverlayInteraction"}
|
||||
[:action-type [:= :toggle-overlay]]
|
||||
[:event-type [::sm/one-of event-types]]
|
||||
[:overlay-position ::gpt/point]
|
||||
[:overlay-pos-type [::sm/one-of overlay-positioning-types]]
|
||||
[:overlay-position {:optional true} ::gpt/point]
|
||||
[:overlay-pos-type {:optional true} [::sm/one-of overlay-positioning-types]]
|
||||
[:destination {:optional true} [:maybe ::sm/uuid]]
|
||||
[:close-click-outside {:optional true} :boolean]
|
||||
[:background-overlay {:optional true} :boolean]
|
||||
|
||||
@ -78,6 +78,11 @@
|
||||
text-transform-attrs
|
||||
text-fills))
|
||||
|
||||
(def text-span-attrs
|
||||
"Inline text span attrs. Line-height is paragraph-level in the DOM editor;
|
||||
it may still be stored redundantly on span nodes."
|
||||
(vec (remove #{:line-height} text-node-attrs)))
|
||||
|
||||
(defn text-node-attr?
|
||||
[attr]
|
||||
(d/index-of text-node-attrs attr))
|
||||
@ -317,9 +322,16 @@
|
||||
"Given two content text structures, conformed by maps and vectors,
|
||||
compare them, and returns a set with the attributes that have changed.
|
||||
This is independent of the text structure, so if the structure changes
|
||||
but the attributes are the same, it will return an empty set."
|
||||
but the attributes are the same, it will return an empty set.
|
||||
|
||||
Line-height on text nodes is ignored: it is a paragraph-level attribute
|
||||
and may be stored redundantly on spans (e.g. after token apply)."
|
||||
[a b]
|
||||
(let [diff-attrs (compare-text-content a b
|
||||
(let [strip-span-line-height
|
||||
#(transform-nodes is-text-node? (fn [node] (dissoc node :line-height)) %)
|
||||
a (strip-span-line-height a)
|
||||
b (strip-span-line-height b)
|
||||
diff-attrs (compare-text-content a b
|
||||
{:text-cb identity
|
||||
:attribute-cb (fn [acc attr] (conj acc attr))})]
|
||||
(if-not (contains? diff-attrs :text-content-structure)
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
(:require
|
||||
[app.common.features :as ffeat]
|
||||
[app.common.files.changes :as ch]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
@ -736,6 +738,55 @@
|
||||
|
||||
{:num 1000})))
|
||||
|
||||
(t/deftest set-comment-thread-position
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
page-id (uuid/custom 1 1)
|
||||
thread-id (uuid/custom 3 1)
|
||||
frame-id (uuid/custom 4 1)
|
||||
data (make-file-data file-id page-id)]
|
||||
|
||||
(t/testing "stores position and frame-id"
|
||||
(let [change {:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id thread-id
|
||||
:frame-id frame-id
|
||||
:position (gpt/point 10 20)}
|
||||
res (ch/process-changes data [change])]
|
||||
(t/is (= {:frame-id frame-id :position (gpt/point 10 20)}
|
||||
(get-in res [:pages-index page-id :comment-thread-positions thread-id])))))
|
||||
|
||||
(t/testing "removes the position when frame-id and position are nil"
|
||||
(let [data (ch/process-changes data [{:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id thread-id
|
||||
:frame-id frame-id
|
||||
:position (gpt/point 10 20)}])
|
||||
res (ch/process-changes data [{:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id thread-id
|
||||
:frame-id nil
|
||||
:position nil}])]
|
||||
(t/is (nil? (get-in res [:pages-index page-id :comment-thread-positions thread-id])))))
|
||||
|
||||
(t/testing "builder round-trips the position through undo and redo"
|
||||
(let [data (ch/process-changes data [{:type :set-comment-thread-position
|
||||
:page-id page-id
|
||||
:comment-thread-id thread-id
|
||||
:frame-id frame-id
|
||||
:position (gpt/point 10 20)}])
|
||||
page (get-in data [:pages-index page-id])
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-page page)
|
||||
(pcb/set-comment-thread-position {:id thread-id
|
||||
:frame-id frame-id
|
||||
:position (gpt/point 100 200)}))
|
||||
redone (ch/process-changes data (:redo-changes changes))
|
||||
undone (ch/process-changes redone (:undo-changes changes))]
|
||||
(t/is (= (gpt/point 100 200)
|
||||
(get-in redone [:pages-index page-id :comment-thread-positions thread-id :position])))
|
||||
(t/is (= (gpt/point 10 20)
|
||||
(get-in undone [:pages-index page-id :comment-thread-positions thread-id :position])))))))
|
||||
|
||||
(t/deftest set-plugin-data-json-encode-decode
|
||||
(let [schema ch/schema:set-plugin-data-change
|
||||
encode (sm/encoder schema (sm/json-transformer))
|
||||
|
||||
@ -254,3 +254,19 @@
|
||||
shape {:points points}]
|
||||
(t/is (true? (gint/slow-has-point? shape (pt 50 25))))
|
||||
(t/is (false? (gint/slow-has-point? shape (pt 150 25)))))))
|
||||
|
||||
(t/deftest has-point-rotated-test
|
||||
;; Diamond (a square rotated 45º); its axis-aligned x/y/width/height box does
|
||||
;; not match the rotated polygon.
|
||||
(let [points [(pt 50 0) (pt 100 50) (pt 50 100) (pt 0 50)]
|
||||
shape {:x 20 :y 20 :width 60 :height 60 :rotation 45 :points points}]
|
||||
(t/testing "point inside the polygon but outside the box is contained"
|
||||
(t/is (true? (gint/has-point? shape (pt 50 5)))))
|
||||
(t/testing "point inside the box but outside the polygon is not contained"
|
||||
(t/is (false? (gint/has-point? shape (pt 22 22)))))))
|
||||
|
||||
(t/deftest has-point-axis-aligned-test
|
||||
(let [shape {:x 10 :y 20 :width 100 :height 50 :rotation 0}]
|
||||
(t/testing "unrotated shape uses the axis-aligned box"
|
||||
(t/is (true? (gint/has-point? shape (pt 50 40))))
|
||||
(t/is (false? (gint/has-point? shape (pt 200 40)))))))
|
||||
|
||||
@ -76,8 +76,31 @@
|
||||
(assoc-in content-base [:children 0 :children 0 :children 0 :font-family] "Arial"))
|
||||
|
||||
(def content-changed-line-height
|
||||
(assoc-in content-base [:children 0 :children 0 :line-height] "1.5"))
|
||||
|
||||
(def content-redundant-span-line-height
|
||||
(assoc-in content-base [:children 0 :children 0 :children 0 :line-height] "1.5"))
|
||||
|
||||
;; Token apply may store line-height on paragraph and spans; after a DOM
|
||||
;; round-trip spans no longer carry it (paragraph-level in the editor).
|
||||
(def content-token-like-line-height
|
||||
(-> content-base
|
||||
(assoc-in [:children 0 :children 0 :line-height] 1.4)
|
||||
(assoc-in [:children 0 :children 0 :children 0 :line-height] 1.4)))
|
||||
|
||||
(def content-after-editor-roundtrip
|
||||
(update-in content-token-like-line-height
|
||||
[:children 0 :children 0 :children 0]
|
||||
dissoc :line-height))
|
||||
|
||||
;; from_dom used to merge default nil typography refs on import.
|
||||
(def content-explicit-nil-typography-refs
|
||||
(-> content-base
|
||||
(assoc-in [:children 0 :children 0 :typography-ref-id] nil)
|
||||
(assoc-in [:children 0 :children 0 :typography-ref-file] nil)
|
||||
(assoc-in [:children 0 :children 0 :children 0 :typography-ref-id] nil)
|
||||
(assoc-in [:children 0 :children 0 :children 0 :typography-ref-file] nil)))
|
||||
|
||||
(def content-changed-letter-spacing
|
||||
(assoc-in content-base [:children 0 :children 0 :children 0 :letter-spacing] "2"))
|
||||
|
||||
@ -185,6 +208,10 @@
|
||||
;; Other text-node-attr categories
|
||||
attrs-font-family (cttx/get-diff-attrs content-base content-changed-font-family)
|
||||
attrs-line-height (cttx/get-diff-attrs content-base content-changed-line-height)
|
||||
attrs-span-line-height (cttx/get-diff-attrs content-base content-redundant-span-line-height)
|
||||
attrs-roundtrip-line-height (cttx/get-diff-attrs content-token-like-line-height
|
||||
content-after-editor-roundtrip)
|
||||
attrs-nil-typography-refs (cttx/get-diff-attrs content-base content-explicit-nil-typography-refs)
|
||||
attrs-letter-spacing (cttx/get-diff-attrs content-base content-changed-letter-spacing)
|
||||
attrs-text-decoration (cttx/get-diff-attrs content-base content-changed-text-decoration)
|
||||
attrs-text-transform (cttx/get-diff-attrs content-base content-changed-text-transform)
|
||||
@ -215,6 +242,9 @@
|
||||
;; Each text-node-attr category reports correct attr key
|
||||
(t/is (= #{:font-family} attrs-font-family))
|
||||
(t/is (= #{:line-height} attrs-line-height))
|
||||
(t/is (= #{} attrs-span-line-height))
|
||||
(t/is (= #{} attrs-roundtrip-line-height))
|
||||
(t/is (= #{} attrs-nil-typography-refs))
|
||||
(t/is (= #{:letter-spacing} attrs-letter-spacing))
|
||||
(t/is (= #{:text-decoration} attrs-text-decoration))
|
||||
(t/is (= #{:text-transform} attrs-text-transform))
|
||||
|
||||
@ -32,7 +32,7 @@ RUN set -ex; \
|
||||
|
||||
FROM base AS setup-node
|
||||
|
||||
ENV NODE_VERSION=v24.16.0 \
|
||||
ENV NODE_VERSION=v24.18.0 \
|
||||
PATH=/opt/node/bin:$PATH
|
||||
|
||||
RUN set -eux; \
|
||||
@ -60,6 +60,33 @@ RUN set -eux; \
|
||||
corepack enable; \
|
||||
rm -rf /tmp/nodejs.tar.gz;
|
||||
|
||||
################################################################################
|
||||
## OPENCODE SETUP
|
||||
################################################################################
|
||||
|
||||
FROM base AS setup-opencode
|
||||
|
||||
ENV OPENCODE_VERSION=1.17.13
|
||||
|
||||
RUN set -ex; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
case "${ARCH}" in \
|
||||
aarch64|arm64) \
|
||||
BINARY_URL="https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-arm64.tar.gz"; \
|
||||
;; \
|
||||
amd64|x86_64) \
|
||||
BINARY_URL="https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz"; \
|
||||
;; \
|
||||
*) \
|
||||
echo "Unsupported arch: ${ARCH}"; exit 1; \
|
||||
;; \
|
||||
esac; \
|
||||
curl -fsSL ${BINARY_URL} -o /tmp/opencode.tar.gz; \
|
||||
mkdir -p /tmp/opencode; \
|
||||
tar -xzf /tmp/opencode.tar.gz -C /tmp/opencode; \
|
||||
chmod +x /tmp/opencode/opencode; \
|
||||
rm -f /tmp/opencode.tar.gz;
|
||||
|
||||
|
||||
################################################################################
|
||||
## CADDYSERVER SETUP
|
||||
@ -67,7 +94,7 @@ RUN set -eux; \
|
||||
|
||||
FROM base AS setup-caddy
|
||||
|
||||
ENV CADDY_VERSION=2.11.2
|
||||
ENV CADDY_VERSION=2.11.4
|
||||
|
||||
RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
@ -99,7 +126,7 @@ RUN set -eux; \
|
||||
FROM base AS setup-jvm
|
||||
|
||||
# https://clojure.org/releases/tools
|
||||
ENV CLOJURE_VERSION=1.12.4.1618
|
||||
ENV CLOJURE_VERSION=1.12.5.1654
|
||||
|
||||
RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
@ -181,12 +208,12 @@ RUN set -eux; \
|
||||
|
||||
FROM base AS setup-utils
|
||||
|
||||
ENV CLJKONDO_VERSION=2026.04.15 \
|
||||
BABASHKA_VERSION=1.12.208 \
|
||||
ENV CLJKONDO_VERSION=2026.05.25 \
|
||||
BABASHKA_VERSION=1.12.218 \
|
||||
CLJFMT_VERSION=0.16.4 \
|
||||
PIXI_VERSION=0.67.2 \
|
||||
GITHUB_CLI_VERSION=2.91.0 \
|
||||
K6_VERSION=2.0.0 \
|
||||
GITHUB_CLI_VERSION=2.96.0 \
|
||||
UV_VERSION=0.11.9 \
|
||||
UV_TOOL_DIR=/opt/uv/tools \
|
||||
UV_TOOL_BIN_DIR=/opt/utils/bin \
|
||||
@ -494,6 +521,7 @@ COPY --from=setup-rust /opt/cargo /opt/cargo
|
||||
COPY --from=setup-rust /opt/rustup /opt/rustup
|
||||
COPY --from=setup-rust /opt/emsdk /opt/emsdk
|
||||
COPY --from=setup-caddy /usr/bin/caddy /usr/bin/caddy
|
||||
COPY --from=setup-opencode /tmp/opencode/opencode /opt/utils/bin/opencode
|
||||
|
||||
COPY files/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY files/nginx-mime.types /etc/nginx/mime.types
|
||||
|
||||
@ -23,3 +23,11 @@ alias lsf='ls -h *(.)'
|
||||
if [ -f "$HOME/.bashrc.local" ]; then
|
||||
. "$HOME/.bashrc.local"
|
||||
fi
|
||||
|
||||
# pnpm
|
||||
export PNPM_HOME="/home/penpot/.local/share/pnpm"
|
||||
case ":$PATH:" in
|
||||
*":$PNPM_HOME:"*) ;;
|
||||
*) export PATH="$PNPM_HOME:$PATH" ;;
|
||||
esac
|
||||
# pnpm end
|
||||
|
||||
@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
NODE_VERSION=v24.16.0 \
|
||||
NODE_VERSION=v24.18.0 \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PATH=/opt/node/bin:/opt/imagick/bin:$PATH \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/penpot/browsers
|
||||
|
||||
@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
NODE_VERSION=v22.21.1 \
|
||||
NODE_VERSION=v24.18.0 \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PATH=/opt/node/bin:$PATH \
|
||||
PENPOT_MCP_SERVER_HOST=0.0.0.0
|
||||
|
||||
BIN
docs/img/styling/background-blur.webp
Normal file
BIN
docs/img/styling/background-blur.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@ -27,17 +27,17 @@
|
||||
"@11ty/eleventy": "^3.1.6",
|
||||
"@11ty/eleventy-navigation": "^1.0.5",
|
||||
"@11ty/eleventy-plugin-rss": "^3.0.0",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||
"@tigersway/eleventy-plugin-ancestry": "^1.0.3",
|
||||
"@types/markdown-it": "14.1.2",
|
||||
"elasticlunr": "^0.9.5",
|
||||
"eleventy-plugin-metagen": "^1.8.3",
|
||||
"eleventy-plugin-metagen": "^1.8.4",
|
||||
"eleventy-plugin-nesting-toc": "^1.3.0",
|
||||
"eleventy-plugin-youtube-embed": "^1.10.2",
|
||||
"luxon": "^3.4.4",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-anchor": "^9.0.1",
|
||||
"eleventy-plugin-youtube-embed": "^1.13.2",
|
||||
"luxon": "^3.7.2",
|
||||
"markdown-it": "^14.2.0",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"markdown-it-plantuml": "^1.4.1"
|
||||
},
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620"
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b"
|
||||
}
|
||||
|
||||
28
docs/pnpm-lock.yaml
generated
28
docs/pnpm-lock.yaml
generated
@ -18,7 +18,7 @@ importers:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
'@11ty/eleventy-plugin-syntaxhighlight':
|
||||
specifier: ^5.0.0
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
'@tigersway/eleventy-plugin-ancestry':
|
||||
specifier: ^1.0.3
|
||||
@ -30,22 +30,22 @@ importers:
|
||||
specifier: ^0.9.5
|
||||
version: 0.9.5
|
||||
eleventy-plugin-metagen:
|
||||
specifier: ^1.8.3
|
||||
specifier: ^1.8.4
|
||||
version: 1.8.4
|
||||
eleventy-plugin-nesting-toc:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
eleventy-plugin-youtube-embed:
|
||||
specifier: ^1.10.2
|
||||
specifier: ^1.13.2
|
||||
version: 1.13.2
|
||||
luxon:
|
||||
specifier: ^3.4.4
|
||||
specifier: ^3.7.2
|
||||
version: 3.7.2
|
||||
markdown-it:
|
||||
specifier: ^14.1.0
|
||||
specifier: ^14.2.0
|
||||
version: 14.2.0
|
||||
markdown-it-anchor:
|
||||
specifier: ^9.0.1
|
||||
specifier: ^9.2.0
|
||||
version: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.2.0)
|
||||
markdown-it-plantuml:
|
||||
specifier: ^1.4.1
|
||||
@ -436,8 +436,8 @@ packages:
|
||||
resolution: {integrity: sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
||||
js-yaml@3.14.2:
|
||||
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
|
||||
js-yaml@3.15.0:
|
||||
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
|
||||
hasBin: true
|
||||
|
||||
js-yaml@4.2.0:
|
||||
@ -692,8 +692,8 @@ packages:
|
||||
uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
|
||||
undici@7.27.2:
|
||||
resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
|
||||
undici@7.28.0:
|
||||
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unpipe@1.0.0:
|
||||
@ -943,7 +943,7 @@ snapshots:
|
||||
parse5: 7.3.0
|
||||
parse5-htmlparser2-tree-adapter: 7.1.0
|
||||
parse5-parser-stream: 7.1.2
|
||||
undici: 7.27.2
|
||||
undici: 7.28.0
|
||||
whatwg-mimetype: 4.0.0
|
||||
|
||||
chokidar@3.6.0:
|
||||
@ -1126,7 +1126,7 @@ snapshots:
|
||||
|
||||
gray-matter@4.0.3:
|
||||
dependencies:
|
||||
js-yaml: 3.14.2
|
||||
js-yaml: 3.15.0
|
||||
kind-of: 6.0.3
|
||||
section-matter: 1.0.0
|
||||
strip-bom-string: 1.0.0
|
||||
@ -1188,7 +1188,7 @@ snapshots:
|
||||
|
||||
iso-639-1@3.1.5: {}
|
||||
|
||||
js-yaml@3.14.2:
|
||||
js-yaml@3.15.0:
|
||||
dependencies:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
@ -1410,7 +1410,7 @@ snapshots:
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
undici@7.27.2: {}
|
||||
undici@7.28.0: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
minimumReleaseAgeExclude:
|
||||
- undici@7.28.0
|
||||
- js-yaml@3.15.0
|
||||
@ -31,6 +31,7 @@ desc: Learn how to create, manage and apply Penpot Design Tokens using W3C DTCG
|
||||
<li><strong>Description:</strong> You can also choose to add a description to your token.</li>
|
||||
</ul>
|
||||
<p>Once you have named the token and assigned it a value, click <strong>Save</strong> to store the token and start referencing it.</p>
|
||||
<p>Token value fields suggest existing tokens as you type. Pick a suggestion to insert a reference, or keep typing a custom value.</p>
|
||||
|
||||
<h2 id="design-tokens-aliases">Referencing tokens into values (aliases)</h2>
|
||||
<p>When assigning a value to a token, you can reference existing tokens - these are called aliases at the <a href="https://www.designtokens.org/glossary/" target="_blank">DTCG Glossary</a>.</p>
|
||||
@ -111,7 +112,7 @@ desc: Learn how to create, manage and apply Penpot Design Tokens using W3C DTCG
|
||||
<p>This ensures the spacing is at least 8, even if the base token is smaller.</p>
|
||||
|
||||
<h2 id="design-tokens-edit">Editing a token</h2>
|
||||
<p>Tokens can be edited by right-clicking the token and selecting <strong>Edit token</strong>. This will allow you to change the tokens name, value and description. Once the changes are made, click <strong>Save</strong>.</p>
|
||||
<p>Tokens can be edited by right-clicking the token and selecting <strong>Edit token</strong>. This will allow you to change the tokens name, value and description. Value fields offer the same token suggestions as when creating a token. Once the changes are made, click <strong>Save</strong>.</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/05-tokens-edit.webp" alt="Tokens edit" />
|
||||
</figure>
|
||||
@ -141,12 +142,15 @@ desc: Learn how to create, manage and apply Penpot Design Tokens using W3C DTCG
|
||||
<li><strong>Size and position:</strong> width, height, x, y</li>
|
||||
<li><strong>Rotation and border radius</strong></li>
|
||||
<li><strong>Layout spacing:</strong> gaps, paddings, margins, min and max widths and heights</li>
|
||||
<li><strong>Typography:</strong> letter spacing and line height</li>
|
||||
<li><strong>Typography:</strong> letter spacing, line height, and composite typography tokens on text layers</li>
|
||||
<li><strong>Stroke width</strong></li>
|
||||
<li><strong>Shadows:</strong> x, y, blur, spread</li>
|
||||
<li><strong>Blur</strong> amount</li>
|
||||
</ul>
|
||||
<p>Click the token control on a field to open a list of applicable tokens from your active sets and themes. Select a token to apply it; a token pill shows the bound token. Use the pill menu to detach the token when you need a custom value again.</p>
|
||||
<p>On text layers, the Typography section shows a composite typography token row when one is applied. Open the token list from the section header to pick or switch tokens without going to the Tokens panel. Hover a token name to see its resolved values (font family, size, weight, and the rest).</p>
|
||||
<p>When you multi-select text layers that share typography tokens, the Typography section shows the applied token. If the selection mixes different tokens, detach them from there or apply a new one to the whole selection.</p>
|
||||
<p>Typography options in the dropdown list show resolved values on hover, so you can tell styles apart before applying them.</p>
|
||||
<p class="advice">When a numeric field has a token applied, drag-to-change is disabled for that field. Edit the token or detach it first.</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/41-design-tab-token-dropdown.webp" alt="Token list on a numeric field in the Design sidebar">
|
||||
@ -528,6 +532,7 @@ ExtraBold Italic
|
||||
|
||||
<h4 id="apply-typography-token">Apply a Typography token</h4>
|
||||
<p>A <strong>Typography composite token</strong> can be applied to a full text layer to set all typography properties at once. This lets you manage complete text styles using a single token instead of combining multiple individual ones.</p>
|
||||
<p>Apply it from the Tokens panel, or from the Typography section in the Design sidebar when a text layer is selected.</p>
|
||||
<p>When applying a Typography composite token to a layer, any previously applied <em>Typography composite token</em> or <em>style</em> will be detached. The same happens in reverse. Only one of them can be active at a time.</p>
|
||||
|
||||
<h3 id="design-tokens-shadow">Shadow</h3>
|
||||
|
||||
@ -57,7 +57,7 @@ desc: Style your designs with Penpot's options! Learn about color fills, gradien
|
||||
<li><strong>Color type</strong> - Solid, gradient, or image.</li>
|
||||
<li><strong>Sliders</strong> - Easily manage settings like brightness, saturation or opacity.</li>
|
||||
<li><strong>Values</strong> - Set precise color values. Available formats depend on the selected color model.</li>
|
||||
<li><strong>Libraries</strong> - Switch between recent colors and libraries.</li>
|
||||
<li><strong>Libraries</strong> - Switch between recent colors and libraries. Color tokens in a library can be shown as a grid or a list. Use the list view when you need to scan names or work with long token lists.</li>
|
||||
<li><strong>Color palette</strong> - A quick launcher of the palette with the selected library.</li>
|
||||
</ol>
|
||||
|
||||
@ -116,6 +116,7 @@ desc: Style your designs with Penpot's options! Learn about color fills, gradien
|
||||
<li><strong>Style</strong> - solid, dotted, dashed, mixed</li>
|
||||
<li><strong>Visibility</strong> - show or hide the stroke without removing it</li>
|
||||
</ul>
|
||||
<p>When the style is set to <strong>dashed</strong>, two extra fields appear to control the <strong>dash</strong> and <strong>gap</strong> length in pixels.</p>
|
||||
<p>You can add as many strokes as you want to the same layer.</p>
|
||||
<figure>
|
||||
<img alt="Multiple strokes" src="/img/styling/stroke-multiple.webp"/>
|
||||
|
||||
@ -461,8 +461,18 @@ You can choose to edit individual nodes or create new ones. Press <kbd>Esc</kbd>
|
||||
</ul>
|
||||
|
||||
<h3 id="blur">Blur</h3>
|
||||
<p>You can set a blur for each and every layer at Penpot.</p>
|
||||
<p><strong></strong>Applying a lot and/or big values for blurs can affect Penpot’s performance as it requires a lot from the browser.</p>
|
||||
<p>You can add blur effects to layers from the Design sidebar. Penpot supports two types:</p>
|
||||
<ul>
|
||||
<li><strong>Layer blur</strong> blurs the layer content itself.</li>
|
||||
<li><strong>Background blur</strong> blurs whatever sits behind the layer. Use it for frosted-glass or depth effects. Background blur requires the <a href="/user-guide/first-steps/troubleshooting-webgl/">WebGL renderer</a> to be enabled.</li>
|
||||
</ul>
|
||||
<p class="advice">Background blur is not included in exported files yet (PNG, SVG, PDF, and others). The exporter still uses the legacy SVG renderer, so the effect only shows in the workspace and View mode. Export support is a current priority and will land in an upcoming release.</p>
|
||||
<figure>
|
||||
<img src="/img/styling/background-blur.webp" alt="Colorful shapes behind a frosted panel with background blur applied" />
|
||||
<figcaption>Background blur affects the layers behind the shape, not the shape itself.</figcaption>
|
||||
</figure>
|
||||
<p>Click <strong>+</strong> in the Blur section to add an effect. You can apply both types on the same layer. Use the type selector to switch between them, the eye icon to show or hide an effect without removing it, and the value field to set the blur amount.</p>
|
||||
<p class="advice">Applying many or large blur values can affect Penpot’s performance, as it requires a lot from the browser.</p>
|
||||
<figure>
|
||||
<video title="Apply blur to a layer" muted="" playsinline="" controls="" width="100%" poster="/img/styling/blur.webp" height="auto">
|
||||
<source src="/img/styling/blur.mp4" type="video/mp4">
|
||||
|
||||
@ -42,6 +42,10 @@ desc: This Penpot user guide explains how to prototype interactions, connect boa
|
||||
<strong>10)</strong> Flow indicator and launcher
|
||||
</p>
|
||||
|
||||
<h2 id="interaction-settings">Interaction settings</h2>
|
||||
<p>When a layer with interactions is selected, the Prototype sidebar lists every connection attached to it. From there you can change the trigger, action, destination, animation, and other options.</p>
|
||||
<p>Destination dropdowns include a search field. Type to filter boards on the current page when your file has many screens.</p>
|
||||
|
||||
<h2 id="interaction-triggers">Interaction triggers</h2>
|
||||
<figure>
|
||||
<img src="/img/prototype/prototype-trigger.webp" alt="Prototype trigger options">
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.5.3+sha512.7ac1c919341c213a34dc0d02afb7143c5c26ac26ee8c4782deea821b8ac64d2134a081fd8941dae6e29bbb48f58dfc2b7fbceeccc07cb2f09d219d342a4969ed",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
@ -18,7 +18,7 @@
|
||||
"generic-pool": "^3.9.0",
|
||||
"inflation": "^2.1.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"playwright": "^1.61.0",
|
||||
"playwright": "^1.61.1",
|
||||
"raw-body": "^3.0.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"undici": "^8.5.0",
|
||||
|
||||
18
exporter/pnpm-lock.yaml
generated
18
exporter/pnpm-lock.yaml
generated
@ -35,8 +35,8 @@ importers:
|
||||
specifier: ^5.11.1
|
||||
version: 5.11.1
|
||||
playwright:
|
||||
specifier: ^1.61.0
|
||||
version: 1.61.0
|
||||
specifier: ^1.61.1
|
||||
version: 1.61.1
|
||||
raw-body:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
@ -330,13 +330,13 @@ packages:
|
||||
resolution: {integrity: sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
playwright-core@1.61.0:
|
||||
resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==}
|
||||
playwright-core@1.61.1:
|
||||
resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.61.0:
|
||||
resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==}
|
||||
playwright@1.61.1:
|
||||
resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@ -714,11 +714,11 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 2.0.0
|
||||
|
||||
playwright-core@1.61.0: {}
|
||||
playwright-core@1.61.1: {}
|
||||
|
||||
playwright@1.61.0:
|
||||
playwright@1.61.1:
|
||||
dependencies:
|
||||
playwright-core: 1.61.0
|
||||
playwright-core: 1.61.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
|
||||
@ -46,6 +46,9 @@
|
||||
|
||||
:dev
|
||||
{:extra-paths ["dev"]
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
|
||||
:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "3.2.2"}
|
||||
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
@ -58,18 +58,18 @@
|
||||
"@penpot/tokenscript": "link:packages/tokenscript",
|
||||
"@penpot/ua-parser": "penpot/ua-parser#1.0.0",
|
||||
"@penpot/ui": "link:packages/ui",
|
||||
"@playwright/test": "1.61.0",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@storybook/addon-docs": "10.4.6",
|
||||
"@storybook/addon-themes": "10.4.6",
|
||||
"@storybook/addon-vitest": "10.4.6",
|
||||
"@storybook/react-vite": "10.4.6",
|
||||
"@tokens-studio/sd-transforms": "2.0.3",
|
||||
"@types/node": "^25.9.3",
|
||||
"@types/node": "^26.1.0",
|
||||
"@vitest/browser": "4.1.9",
|
||||
"@vitest/browser-playwright": "^4.1.9",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"autoprefixer": "^10.5.2",
|
||||
"compression": "^1.8.1",
|
||||
"concurrently": "^10.0.3",
|
||||
"date-fns": "^4.4.0",
|
||||
@ -80,7 +80,7 @@
|
||||
"getopts": "^2.3.0",
|
||||
"gettext-parser": "^9.0.2",
|
||||
"highlight.js": "^11.10.0",
|
||||
"js-beautify": "^1.15.4",
|
||||
"js-beautify": "^2.0.3",
|
||||
"jsdom": "^29.0.2",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
@ -92,12 +92,12 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"opentype.js": "^2.0.0",
|
||||
"p-limit": "^7.3.0",
|
||||
"playwright": "1.61.0",
|
||||
"postcss": "^8.5.15",
|
||||
"playwright": "1.61.1",
|
||||
"postcss": "^8.5.16",
|
||||
"postcss-clean": "^1.2.2",
|
||||
"postcss-modules": "^6.0.1",
|
||||
"postcss-modules": "^9.0.0",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"prettier": "3.8.4",
|
||||
"prettier": "3.9.4",
|
||||
"pretty-time": "^1.1.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"randomcolor": "^0.6.2",
|
||||
@ -114,19 +114,19 @@
|
||||
"source-map-support": "^0.5.21",
|
||||
"storybook": "10.4.6",
|
||||
"style-dictionary": "5.4.4",
|
||||
"stylelint": "^17.13.0",
|
||||
"stylelint": "^17.14.0",
|
||||
"stylelint-config-standard-scss": "^17.0.0",
|
||||
"stylelint-scss": "^7.2.0",
|
||||
"stylelint-plugin-logical-css": "^2.1.0",
|
||||
"stylelint-scss": "^7.2.0",
|
||||
"svg-sprite": "^2.0.4",
|
||||
"tdigest": "^0.1.2",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.16",
|
||||
"vite": "^8.1.2",
|
||||
"vitest": "^4.1.9",
|
||||
"wait-on": "^9.0.4",
|
||||
"watcher": "^2.3.1",
|
||||
"workerpool": "^10.0.1",
|
||||
"workerpool": "^10.0.3",
|
||||
"xregexp": "^5.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,12 +4,12 @@
|
||||
"description": "Penpot Draft-JS Wrapper",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"author": "Andrey Antukh",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0",
|
||||
"immutable": "^5.1.6"
|
||||
"immutable": "^5.1.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=0.17.0",
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "Simple library for handling keyboard shortcuts",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"author": "Craig Campbell",
|
||||
"license": "Apache-2.0 WITH LLVM-exception"
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620",
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"author": "Andrey Antukh",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
|
||||
@ -14,15 +14,15 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^8.0.0",
|
||||
"@babel/preset-react": "^8.0.0",
|
||||
"@babel/core": "^8.0.1",
|
||||
"@babel/preset-react": "^8.0.1",
|
||||
"@storybook/react": "10.4.6",
|
||||
"@storybook/react-vite": "10.4.6",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
@ -30,7 +30,7 @@
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"react-compiler-runtime": "^1.0.0",
|
||||
"storybook": "10.4.6",
|
||||
"vite-plugin-dts": "^5.0.2"
|
||||
"vite-plugin-dts": "^5.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19.2",
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{"~:total": 2}
|
||||
@ -389,7 +389,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 281.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "line-through",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2111,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae134c87eab3": {
|
||||
@ -758,7 +788,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 281.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2719,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae1409412914": {
|
||||
@ -1143,7 +1203,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 488.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2707,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae1488974651": {
|
||||
@ -1293,7 +1383,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 111.419998168945,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2313,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.2399978637695,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae149cc8c2f1": {
|
||||
@ -1432,7 +1552,32 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 201.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2714,
|
||||
"~:fills": [],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae149cc8c2f0": {
|
||||
@ -1571,7 +1716,32 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 201.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2514,
|
||||
"~:fills": [],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae136831d5b7": {
|
||||
@ -2116,7 +2286,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 281.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2519,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae1409412913": {
|
||||
@ -2368,7 +2568,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 201.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "underline",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2106,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae149fbf3112": {
|
||||
@ -2525,7 +2755,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 281.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2318,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae1409412912": {
|
||||
@ -4487,7 +4747,44 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 111.419998168945,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2714,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-opacity": 1,
|
||||
"~:fill-image": {
|
||||
"~:id": "~uaa0a383a-7553-808a-8006-ae13a3c575eb",
|
||||
"~:width": 100,
|
||||
"~:height": 100,
|
||||
"~:mtype": "image/jpeg",
|
||||
"~:keep-aspect-ratio": true,
|
||||
"~:name": "sample"
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.2399978637695,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae136eba4581": {
|
||||
@ -5550,7 +5847,32 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 201.419982910156,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2313,
|
||||
"~:fills": [],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.239990234375,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae148b39a12f": {
|
||||
@ -5700,7 +6022,37 @@
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 111.419998168945,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2514,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.2399978637695,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u13fc1849-119a-8028-8006-ae145f7fe46e": {
|
||||
@ -6244,7 +6596,37 @@
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 63.999981880188,
|
||||
"~:flip-y": null
|
||||
"~:flip-y": null,
|
||||
"~:position-data": [
|
||||
{
|
||||
"~:y": 111.419998168945,
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:font-size": "72px",
|
||||
"~:font-weight": "900",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:width": 169.070068359375,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0px",
|
||||
"~:x": 2106,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:height": 93.2399978637695,
|
||||
"~:text": "HOLA"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -6258,4 +6640,4 @@
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -637,7 +637,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🔥"
|
||||
},
|
||||
@ -654,7 +659,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👩🏿\u200d🚀"
|
||||
},
|
||||
@ -671,7 +681,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👺"
|
||||
},
|
||||
@ -688,7 +703,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🚀"
|
||||
}
|
||||
@ -706,7 +726,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
},
|
||||
{
|
||||
@ -726,7 +751,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": ""
|
||||
}
|
||||
@ -744,7 +774,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
@ -2395,7 +2430,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🔥"
|
||||
},
|
||||
@ -2412,7 +2452,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👩🏿\u200d🚀"
|
||||
},
|
||||
@ -2429,7 +2474,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👺"
|
||||
},
|
||||
@ -2446,7 +2496,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🚀"
|
||||
}
|
||||
@ -2464,7 +2519,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
},
|
||||
{
|
||||
@ -2484,7 +2544,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": ""
|
||||
}
|
||||
@ -2502,7 +2567,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
@ -3433,7 +3503,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🔥"
|
||||
},
|
||||
@ -3450,7 +3525,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👩🏿\u200d🚀"
|
||||
},
|
||||
@ -3467,7 +3547,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "👺"
|
||||
},
|
||||
@ -3484,7 +3569,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "🚀"
|
||||
}
|
||||
@ -3502,7 +3592,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
},
|
||||
{
|
||||
@ -3522,7 +3617,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": ""
|
||||
}
|
||||
@ -3540,7 +3640,12 @@
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#B1B2B5",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
|
||||
@ -0,0 +1,372 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"render-wasm/v1",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type",
|
||||
"text-editor-wasm/v1"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~u1091e979-bbec-8194-8005-f7aa420b5660",
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "simple-text",
|
||||
"~:revn": 7,
|
||||
"~:modified-at": "~m1749629891313",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
"legacy-2",
|
||||
"legacy-3",
|
||||
"legacy-5",
|
||||
"legacy-6",
|
||||
"legacy-7",
|
||||
"legacy-8",
|
||||
"legacy-9",
|
||||
"legacy-10",
|
||||
"legacy-11",
|
||||
"legacy-12",
|
||||
"legacy-13",
|
||||
"legacy-14",
|
||||
"legacy-16",
|
||||
"legacy-17",
|
||||
"legacy-18",
|
||||
"legacy-19",
|
||||
"legacy-25",
|
||||
"legacy-26",
|
||||
"legacy-27",
|
||||
"legacy-28",
|
||||
"legacy-29",
|
||||
"legacy-31",
|
||||
"legacy-32",
|
||||
"legacy-33",
|
||||
"legacy-34",
|
||||
"legacy-36",
|
||||
"legacy-37",
|
||||
"legacy-38",
|
||||
"legacy-39",
|
||||
"legacy-40",
|
||||
"legacy-41",
|
||||
"legacy-42",
|
||||
"legacy-43",
|
||||
"legacy-44",
|
||||
"legacy-45",
|
||||
"legacy-46",
|
||||
"legacy-47",
|
||||
"legacy-48",
|
||||
"legacy-49",
|
||||
"legacy-50",
|
||||
"legacy-51",
|
||||
"legacy-52",
|
||||
"legacy-53",
|
||||
"legacy-54",
|
||||
"legacy-55",
|
||||
"legacy-56",
|
||||
"legacy-57",
|
||||
"legacy-59",
|
||||
"legacy-62",
|
||||
"legacy-65",
|
||||
"legacy-66",
|
||||
"legacy-67",
|
||||
"0001-remove-tokens-from-groups",
|
||||
"0002-normalize-bool-content",
|
||||
"0002-clean-shape-interactions",
|
||||
"0003-fix-root-shape",
|
||||
"0003-convert-path-content",
|
||||
"0004-clean-shadow-and-colors",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0007-clear-invalid-strokes-and-fills-v2",
|
||||
"0008-fix-library-colors-opacity"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~u1091e979-bbec-8194-8005-f7aa420b8b07",
|
||||
"~:created-at": "~m1749629823499",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u3b0d758a-8c9d-8013-8006-52c8337e5c73"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u3b0d758a-8c9d-8013-8006-52c8337e5c73": {
|
||||
"~:objects": {
|
||||
"~u00000000-0000-0000-0000-000000000000": {
|
||||
"~#shape": {
|
||||
"~:y": 0,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:name": "Root Frame",
|
||||
"~:width": 0.01,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.01
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 0,
|
||||
"~:proportion": 1.0,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 0,
|
||||
"~:y": 0,
|
||||
"~:width": 0.01,
|
||||
"~:height": 0.01,
|
||||
"~:x1": 0,
|
||||
"~:y1": 0,
|
||||
"~:x2": 0.01,
|
||||
"~:y2": 0.01
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 0.01,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": [
|
||||
"~u7274a6af-66db-8009-8006-52c837bed25d"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u7274a6af-66db-8009-8006-52c837bed25d": {
|
||||
"~#shape": {
|
||||
"~:y": 368.000005463652,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:auto-width",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "13hr3ftth2o",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "1qm8gi1rphc",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "Hello "
|
||||
},
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "gfont-sora",
|
||||
"~:key": "2qm8gi1rphd",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "Sora",
|
||||
"~:text": "World"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "r8gahivbg7",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:type": "paragraph",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "this is a text",
|
||||
"~:width": 237.0000390021974,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 414.9999714372273,
|
||||
"~:y": 368.000005463652
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 652.0000104394247,
|
||||
"~:y": 368.000005463652
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 652.0000104394247,
|
||||
"~:y": 426.0000039162686
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 414.9999714372273,
|
||||
"~:y": 426.0000039162686
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:layout-item-h-sizing": "~:fix",
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:layout-item-v-sizing": "~:fix",
|
||||
"~:id": "~u7274a6af-66db-8009-8006-52c837bed25d",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:x": 414.9999714372274,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 414.9999714372274,
|
||||
"~:y": 368.000005463652,
|
||||
"~:width": 237.0000390021974,
|
||||
"~:height": 57.99999845261664,
|
||||
"~:x1": 414.9999714372274,
|
||||
"~:y1": 368.000005463652,
|
||||
"~:x2": 652.0000104394248,
|
||||
"~:y2": 426.0000039162686
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 57.99999845261664,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c73",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,372 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"render-wasm/v1",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type",
|
||||
"text-editor-wasm/v1"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~u1091e979-bbec-8194-8005-f7aa420b5660",
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "simple-text",
|
||||
"~:revn": 7,
|
||||
"~:modified-at": "~m1749629891313",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
"legacy-2",
|
||||
"legacy-3",
|
||||
"legacy-5",
|
||||
"legacy-6",
|
||||
"legacy-7",
|
||||
"legacy-8",
|
||||
"legacy-9",
|
||||
"legacy-10",
|
||||
"legacy-11",
|
||||
"legacy-12",
|
||||
"legacy-13",
|
||||
"legacy-14",
|
||||
"legacy-16",
|
||||
"legacy-17",
|
||||
"legacy-18",
|
||||
"legacy-19",
|
||||
"legacy-25",
|
||||
"legacy-26",
|
||||
"legacy-27",
|
||||
"legacy-28",
|
||||
"legacy-29",
|
||||
"legacy-31",
|
||||
"legacy-32",
|
||||
"legacy-33",
|
||||
"legacy-34",
|
||||
"legacy-36",
|
||||
"legacy-37",
|
||||
"legacy-38",
|
||||
"legacy-39",
|
||||
"legacy-40",
|
||||
"legacy-41",
|
||||
"legacy-42",
|
||||
"legacy-43",
|
||||
"legacy-44",
|
||||
"legacy-45",
|
||||
"legacy-46",
|
||||
"legacy-47",
|
||||
"legacy-48",
|
||||
"legacy-49",
|
||||
"legacy-50",
|
||||
"legacy-51",
|
||||
"legacy-52",
|
||||
"legacy-53",
|
||||
"legacy-54",
|
||||
"legacy-55",
|
||||
"legacy-56",
|
||||
"legacy-57",
|
||||
"legacy-59",
|
||||
"legacy-62",
|
||||
"legacy-65",
|
||||
"legacy-66",
|
||||
"legacy-67",
|
||||
"0001-remove-tokens-from-groups",
|
||||
"0002-normalize-bool-content",
|
||||
"0002-clean-shape-interactions",
|
||||
"0003-fix-root-shape",
|
||||
"0003-convert-path-content",
|
||||
"0004-clean-shadow-and-colors",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0007-clear-invalid-strokes-and-fills-v2",
|
||||
"0008-fix-library-colors-opacity"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~u1091e979-bbec-8194-8005-f7aa420b8b07",
|
||||
"~:created-at": "~m1749629823499",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u3b0d758a-8c9d-8013-8006-52c8337e5c73"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u3b0d758a-8c9d-8013-8006-52c8337e5c73": {
|
||||
"~:objects": {
|
||||
"~u00000000-0000-0000-0000-000000000000": {
|
||||
"~#shape": {
|
||||
"~:y": 0,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:name": "Root Frame",
|
||||
"~:width": 0.01,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.01
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 0,
|
||||
"~:proportion": 1.0,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 0,
|
||||
"~:y": 0,
|
||||
"~:width": 0.01,
|
||||
"~:height": 0.01,
|
||||
"~:x1": 0,
|
||||
"~:y1": 0,
|
||||
"~:x2": 0.01,
|
||||
"~:y2": 0.01
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 0.01,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": [
|
||||
"~u7274a6af-66db-8009-8006-52c837bed25d"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u7274a6af-66db-8009-8006-52c837bed25d": {
|
||||
"~#shape": {
|
||||
"~:y": 368.000005463652,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:auto-width",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "13hr3ftth2o",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "1qm8gi1rphc",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "Hello "
|
||||
},
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "italic",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "2qm8gi1rphd",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "600",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "600italic",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "World"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "r8gahivbg7",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:type": "paragraph",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "this is a text",
|
||||
"~:width": 237.0000390021974,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 414.9999714372273,
|
||||
"~:y": 368.000005463652
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 652.0000104394247,
|
||||
"~:y": 368.000005463652
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 652.0000104394247,
|
||||
"~:y": 426.0000039162686
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 414.9999714372273,
|
||||
"~:y": 426.0000039162686
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:layout-item-h-sizing": "~:fix",
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:layout-item-v-sizing": "~:fix",
|
||||
"~:id": "~u7274a6af-66db-8009-8006-52c837bed25d",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:x": 414.9999714372274,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 414.9999714372274,
|
||||
"~:y": 368.000005463652,
|
||||
"~:width": 237.0000390021974,
|
||||
"~:height": 57.99999845261664,
|
||||
"~:x1": 414.9999714372274,
|
||||
"~:y1": 368.000005463652,
|
||||
"~:x2": 652.0000104394248,
|
||||
"~:y2": 426.0000039162686
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 57.99999845261664,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c73",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
382
frontend/playwright/data/workspace/get-file-token-tooltip.json
Normal file
382
frontend/playwright/data/workspace/get-file-token-tooltip.json
Normal file
@ -0,0 +1,382 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"design-tokens/v1",
|
||||
"layout/grid",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "Tooltip check",
|
||||
"~:revn": 36,
|
||||
"~:modified-at": "~m1737542758402",
|
||||
"~:vern": 0,
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:is-shared": false,
|
||||
"~:version": 60,
|
||||
"~:project-id": "~u0df61468-6cbf-8067-8005-6b453ce996d0",
|
||||
"~:created-at": "~m1737536563847",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u4530574a-7a0a-807b-8008-0107b2c4628e"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u4530574a-7a0a-807b-8008-0107b2c4628e": {
|
||||
"~:id": "~u4530574a-7a0a-807b-8008-0107b2c4628e",
|
||||
"~:name": "Page 1",
|
||||
"~:objects": {
|
||||
"~#penpot/objects-map/v2": {
|
||||
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4530574a-7a0a-807b-8008-0107b2c4628e\",\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^I\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ua7df6ab8-bee5-802f-8008-010d9ff3da26\",\"~uf2256046-b127-808e-8008-411c4cba63d8\",\"~u1de8c150-538b-80b2-8008-40356208fc39\",\"~uf2256046-b127-808e-8008-411c4cba63d9\",\"~u1de8c150-538b-80b2-8008-4036773cc8d0\",\"~u886f2846-a2c5-808d-8008-4115e86119e9\",\"~u1de8c150-538b-80b2-8008-403b5150a59b\",\"~u886f2846-a2c5-808d-8008-4115e86119ea\",\"~u20f9ccb0-68e2-80d9-8008-413fa842a6b5\",\"~u20f9ccb0-68e2-80d9-8008-413fb1f2ef51\"]]]",
|
||||
"~u1de8c150-538b-80b2-8008-403b5150a59b": "[\"~#shape\",[\"^ \",\"~:y\",419,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Deleted token rect\",\"~:width\",162,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",-147,\"~:y\",419]],[\"^<\",[\"^ \",\"~:x\",15,\"~:y\",419]],[\"^<\",[\"^ \",\"~:x\",15,\"~:y\",565]],[\"^<\",[\"^ \",\"~:x\",-147,\"~:y\",565]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:r1\",20,\"~:id\",\"~u1de8c150-538b-80b2-8008-403b5150a59b\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:fill\",\"blue\",\"^A\",\"deleted\",\"^=\",\"deleted\",\"^@\",\"deleted\",\"~:r4\",\"deleted\"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",-147,\"~:proportion\",1,\"^F\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",-147,\"~:y\",419,\"^8\",162,\"~:height\",146,\"~:x1\",-147,\"~:y1\",419,\"~:x2\",15,\"~:y2\",565]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#0a20bf\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^L\",146,\"~:flip-y\",null]]",
|
||||
"~u20f9ccb0-68e2-80d9-8008-413fa842a6b5": "[\"~#shape\",[\"^ \",\"~:y\",369,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle with token\",\"~:width\",274.99999999999994,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",-536,\"~:y\",369]],[\"^<\",[\"^ \",\"~:x\",-261.00000000000006,\"~:y\",369]],[\"^<\",[\"^ \",\"~:x\",-261.00000000000006,\"~:y\",565]],[\"^<\",[\"^ \",\"~:x\",-536,\"~:y\",565]]],\"~:r2\",50,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",50,\"~:r1\",50,\"~:id\",\"~u20f9ccb0-68e2-80d9-8008-413fa842a6b5\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:fill\",\"out-ref\",\"^A\",\"base-50\",\"^=\",\"base-50\",\"^@\",\"base-50\",\"~:r4\",\"base-50\"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",-536,\"~:proportion\",1,\"^F\",50,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",-536,\"~:y\",369,\"^8\",274.99999999999994,\"~:height\",196,\"~:x1\",-536,\"~:y1\",369,\"~:x2\",-261.00000000000006,\"~:y2\",565]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#da1fea\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^L\",196,\"~:flip-y\",null]]",
|
||||
"~u1de8c150-538b-80b2-8008-4036773cc8d0": "[\"~#shape\",[\"^ \",\"~:y\",588,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^8\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^8\",[[\"^ \",\"^9\",\"1.2\",\"^:\",\"normal\",\"~:text-transform\",\"none\",\"~:text-align\",\"left\",\"~:font-id\",\"gfont-alegreya-sans-sc\",\"~:font-size\",\"23\",\"~:font-weight\",\"400\",\"~:text-direction\",\"ltr\",\"~:font-variant-id\",\"regular\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-opacity\",1]],\"~:font-family\",\"Alegreya Sans SC\",\"~:text\",\"Text with deleted token\"]],\"^;\",\"none\",\"^<\",\"left\",\"^=\",\"gfont-alegreya-sans-sc\",\"~:key\",\"fu3vh\",\"^>\",\"23\",\"^?\",\"400\",\"^@\",\"ltr\",\"^7\",\"paragraph\",\"^A\",\"regular\",\"^B\",\"none\",\"^C\",\"0\",\"^D\",[[\"^ \",\"^E\",\"#000000\",\"^F\",1]],\"^G\",\"Alegreya Sans SC\"]]]]],\"~:hide-in-viewer\",false,\"~:name\",\"Text with deleted token\",\"~:width\",198,\"^7\",\"^H\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",-161,\"~:y\",588]],[\"^N\",[\"^ \",\"~:x\",37,\"~:y\",588]],[\"^N\",[\"^ \",\"~:x\",37,\"~:y\",641]],[\"^N\",[\"^ \",\"~:x\",-161,\"~:y\",641]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u1de8c150-538b-80b2-8008-4036773cc8d0\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:typography\",\"deleted-typo\"],\"~:position-data\",[[\"~#rect\",[\"^ \",\"~:y\",613,\"^:\",\"normal\",\"^;\",\"none\",\"^>\",\"23px\",\"^?\",\"400\",\"~:y1\",2,\"^L\",176.203125,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",-161,\"~:x1\",0,\"~:y2\",25,\"^D\",[[\"^ \",\"^E\",\"#000000\",\"^F\",1]],\"~:x2\",176.203125,\"~:direction\",\"ltr\",\"^G\",\"\\\"Alegreya Sans SC\\\"\",\"~:height\",23,\"^H\",\"Text with deleted \"]],[\"^U\",[\"^ \",\"~:y\",640.59375,\"^:\",\"normal\",\"^;\",\"none\",\"^>\",\"23px\",\"^?\",\"400\",\"^V\",29.59375,\"^L\",56.9375,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",-161,\"^W\",0,\"^X\",52.59375,\"^D\",[[\"^ \",\"^E\",\"#000000\",\"^F\",1]],\"^Y\",56.9375,\"^Z\",\"ltr\",\"^G\",\"\\\"Alegreya Sans SC\\\"\",\"^[\",23,\"^H\",\"token\"]]],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:x\",-161,\"~:selrect\",[\"^U\",[\"^ \",\"~:x\",-161,\"~:y\",588,\"^L\",198,\"^[\",53,\"^W\",-161,\"^V\",588,\"^Y\",37,\"^X\",641]],\"~:flip-x\",null,\"^[\",53,\"~:flip-y\",null]]",
|
||||
"~u20f9ccb0-68e2-80d9-8008-413fb1f2ef51": "[\"~#shape\",[\"^ \",\"~:y\",609,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^8\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^8\",[[\"^ \",\"^9\",\"1.2\",\"^:\",\"normal\",\"~:text-transform\",\"none\",\"~:text-align\",\"left\",\"~:font-id\",\"gfont-arizonia\",\"~:font-size\",\"23\",\"~:font-weight\",\"400\",\"~:text-direction\",\"ltr\",\"~:font-variant-id\",\"regular\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#da1fea\",\"~:fill-opacity\",1]],\"~:font-family\",\"Arizonia\",\"~:text\",\"Text with token\"]],\"^;\",\"none\",\"^<\",\"left\",\"^=\",\"gfont-arizonia\",\"~:key\",\"fu3vh\",\"^>\",\"23\",\"^?\",\"400\",\"^@\",\"ltr\",\"^7\",\"paragraph\",\"^A\",\"regular\",\"^B\",\"none\",\"^C\",\"0\",\"^D\",[[\"^ \",\"^E\",\"#da1fea\",\"^F\",1]],\"^G\",\"Arizonia\"]]]]],\"~:hide-in-viewer\",false,\"~:name\",\"Text with token\",\"~:width\",198,\"^7\",\"^H\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",-516,\"~:y\",609]],[\"^N\",[\"^ \",\"~:x\",-318,\"~:y\",609]],[\"^N\",[\"^ \",\"~:x\",-318,\"~:y\",662]],[\"^N\",[\"^ \",\"~:x\",-516,\"~:y\",662]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u20f9ccb0-68e2-80d9-8008-413fb1f2ef51\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:typography\",\"out-typo\",\"~:fill\",\"out-ref\"],\"~:position-data\",[[\"~#rect\",[\"^ \",\"~:y\",637,\"^:\",\"normal\",\"^;\",\"none\",\"^>\",\"23px\",\"^?\",\"400\",\"~:y1\",-1,\"^L\",117.453125,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",-516,\"~:x1\",0,\"~:y2\",28,\"^D\",[[\"^ \",\"^E\",\"#da1fea\",\"^F\",1]],\"~:x2\",117.453125,\"~:direction\",\"ltr\",\"^G\",\"Arizonia\",\"~:height\",29,\"^H\",\"Text with token\"]]],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",-516,\"~:selrect\",[\"^V\",[\"^ \",\"~:x\",-516,\"~:y\",609,\"^L\",198,\"^10\",53,\"^X\",-516,\"^W\",609,\"^Z\",-318,\"^Y\",662]],\"~:flip-x\",null,\"^10\",53,\"~:flip-y\",null]]",
|
||||
"~uf2256046-b127-808e-8008-411c4cba63d8": "[\"~#shape\",[\"^ \",\"~:y\",267,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle with not active reference\",\"~:width\",287,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",390,\"~:y\",267]],[\"^<\",[\"^ \",\"~:x\",677,\"~:y\",267]],[\"^<\",[\"^ \",\"~:x\",677,\"~:y\",579]],[\"^<\",[\"^ \",\"~:x\",390,\"~:y\",579]]],\"~:r2\",50,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4530574a-7a0a-807b-8008-0107b2c4628e\",\"~:r3\",50,\"~:r1\",50,\"~:id\",\"~uf2256046-b127-808e-8008-411c4cba63d8\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:fill\",\"in-color\",\"^=\",\"in-br\",\"^A\",\"in-br\",\"^B\",\"in-br\",\"~:r4\",\"in-br\"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",390,\"~:proportion\",1,\"^G\",50,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",390,\"~:y\",267,\"^8\",287,\"~:height\",312,\"~:x1\",390,\"~:y1\",267,\"~:x2\",677,\"~:y2\",579]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#da1fea\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^M\",312,\"~:flip-y\",null]]",
|
||||
"~uf2256046-b127-808e-8008-411c4cba63d9": "[\"~#shape\",[\"^ \",\"~:y\",602,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^8\",[[\"^ \",\"~:line-height\",\"1.2\",\"^8\",[[\"^ \",\"^9\",\"1.2\",\"~:text-transform\",\"none\",\"~:text-align\",\"left\",\"~:font-id\",\"gfont-arizonia\",\"~:font-size\",\"14\",\"~:font-weight\",\"200\",\"~:text-direction\",\"ltr\",\"~:font-variant-id\",\"regular\",\"~:weight\",\"200\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#da1fea\",\"~:fill-opacity\",1]],\"~:font-family\",\"Arizonia\",\"~:text\",\"Text with not active reference\"]],\"^:\",\"none\",\"^;\",\"left\",\"^<\",\"gfont-arizonia\",\"~:key\",\"d22fo\",\"^=\",\"14\",\"^>\",\"200\",\"^?\",\"ltr\",\"^7\",\"paragraph\",\"^@\",\"regular\",\"^A\",\"200\",\"^B\",\"none\",\"^C\",\"0\",\"^D\",[[\"^ \",\"^E\",\"#da1fea\",\"^F\",1]],\"^G\",\"Arizonia\"]]]]],\"~:hide-in-viewer\",false,\"~:name\",\"Text with not active reference\",\"~:width\",139,\"^7\",\"^H\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",479,\"~:y\",602]],[\"^N\",[\"^ \",\"~:x\",618,\"~:y\",602]],[\"^N\",[\"^ \",\"~:x\",618,\"~:y\",630]],[\"^N\",[\"^ \",\"~:x\",479,\"~:y\",630]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~uf2256046-b127-808e-8008-411c4cba63d9\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:fill\",\"in-color\",\"~:typography\",\"in-typo\"],\"~:position-data\",[[\"~#rect\",[\"^ \",\"~:y\",619,\"~:font-style\",\"normal\",\"^:\",\"none\",\"^=\",\"14px\",\"^>\",\"400\",\"~:y1\",-1,\"^L\",97.46875,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",479,\"~:x1\",0,\"~:y2\",17,\"^D\",[[\"^ \",\"^E\",\"#da1fea\",\"^F\",1]],\"~:x2\",97.46875,\"~:direction\",\"ltr\",\"^G\",\"Arizonia\",\"~:height\",18,\"^H\",\"Text with not active \"]],[\"^V\",[\"^ \",\"~:y\",635.796875,\"^W\",\"normal\",\"^:\",\"none\",\"^=\",\"14px\",\"^>\",\"400\",\"^X\",15.796875,\"^L\",42.8125,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",479,\"^Y\",0,\"^Z\",33.796875,\"^D\",[[\"^ \",\"^E\",\"#da1fea\",\"^F\",1]],\"^[\",42.8125,\"^10\",\"ltr\",\"^G\",\"Arizonia\",\"^11\",18,\"^H\",\"reference\"]]],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:x\",479,\"~:selrect\",[\"^V\",[\"^ \",\"~:x\",479,\"~:y\",602,\"^L\",139,\"^11\",28,\"^Y\",479,\"^X\",602,\"^[\",618,\"^Z\",630]],\"~:flip-x\",null,\"^11\",28,\"~:flip-y\",null]]",
|
||||
"~u1de8c150-538b-80b2-8008-40356208fc39": "[\"~#shape\",[\"^ \",\"~:y\",609,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^8\",[[\"^ \",\"~:line-height\",\"1.2\",\"^8\",[[\"^ \",\"^9\",\"1.2\",\"~:text-transform\",\"none\",\"~:text-align\",\"left\",\"~:font-id\",\"gfont-ar-one-sans\",\"~:font-size\",\"14\",\"~:font-weight\",\"200\",\"~:text-direction\",\"ltr\",\"~:font-variant-id\",\"regular\",\"~:weight\",\"200\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ef0c0c\",\"~:fill-opacity\",1]],\"~:font-family\",\"AR One Sans\",\"~:text\",\"Text with not active token\"]],\"^:\",\"none\",\"^;\",\"left\",\"^<\",\"gfont-ar-one-sans\",\"~:key\",\"d22fo\",\"^=\",\"14\",\"^>\",\"200\",\"^?\",\"ltr\",\"^7\",\"paragraph\",\"^@\",\"regular\",\"^A\",\"200\",\"^B\",\"none\",\"^C\",\"0\",\"^D\",[[\"^ \",\"^E\",\"#ef0c0c\",\"^F\",1]],\"^G\",\"AR One Sans\"]]]]],\"~:hide-in-viewer\",false,\"~:name\",\"Text with not active token\",\"~:width\",139,\"^7\",\"^H\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",812,\"~:y\",609]],[\"^N\",[\"^ \",\"~:x\",951,\"~:y\",609]],[\"^N\",[\"^ \",\"~:x\",951,\"~:y\",637]],[\"^N\",[\"^ \",\"~:x\",812,\"~:y\",637]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u1de8c150-538b-80b2-8008-40356208fc39\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:fill\",\"red\",\"~:typography\",\"typo-2\"],\"~:position-data\",[[\"~#rect\",[\"^ \",\"~:y\",626,\"~:font-style\",\"normal\",\"^:\",\"none\",\"^=\",\"14px\",\"^>\",\"400\",\"~:y1\",-2,\"^L\",134.625,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",812,\"~:x1\",0,\"~:y2\",17,\"^D\",[[\"^ \",\"^E\",\"#ef0c0c\",\"^F\",1]],\"~:x2\",134.625,\"~:direction\",\"ltr\",\"^G\",\"\\\"AR One Sans\\\"\",\"~:height\",19,\"^H\",\"Text with not active \"]],[\"^V\",[\"^ \",\"~:y\",642.796875,\"^W\",\"normal\",\"^:\",\"none\",\"^=\",\"14px\",\"^>\",\"400\",\"^X\",14.796875,\"^L\",37.40625,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",812,\"^Y\",0,\"^Z\",33.796875,\"^D\",[[\"^ \",\"^E\",\"#ef0c0c\",\"^F\",1]],\"^[\",37.40625,\"^10\",\"ltr\",\"^G\",\"\\\"AR One Sans\\\"\",\"^11\",19,\"^H\",\"token\"]]],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:x\",812,\"~:selrect\",[\"^V\",[\"^ \",\"~:x\",812,\"~:y\",609,\"^L\",139,\"^11\",28,\"^Y\",812,\"^X\",609,\"^[\",951,\"^Z\",637]],\"~:flip-x\",null,\"^11\",28,\"~:flip-y\",null]]",
|
||||
"~u886f2846-a2c5-808d-8008-4115e86119ea": "[\"~#shape\",[\"^ \",\"~:y\",399,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle with deleted reference\",\"~:width\",162,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",193,\"~:y\",399]],[\"^<\",[\"^ \",\"~:x\",355,\"~:y\",399]],[\"^<\",[\"^ \",\"~:x\",355,\"~:y\",545]],[\"^<\",[\"^ \",\"~:x\",193,\"~:y\",545]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:r1\",20,\"~:id\",\"~u886f2846-a2c5-808d-8008-4115e86119ea\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:fill\",\"ref-grfeen\",\"^A\",\"ref-1\",\"^=\",\"ref-1\",\"^@\",\"ref-1\",\"~:r4\",\"ref-1\"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",193,\"~:proportion\",1,\"^F\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",193,\"~:y\",399,\"^8\",162,\"~:height\",146,\"~:x1\",193,\"~:y1\",399,\"~:x2\",355,\"~:y2\",545]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#0a20bf\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^L\",146,\"~:flip-y\",null]]",
|
||||
"~u886f2846-a2c5-808d-8008-4115e86119e9": "[\"~#shape\",[\"^ \",\"~:y\",586,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^8\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^8\",[[\"^ \",\"^9\",\"1.2\",\"^:\",\"normal\",\"~:text-transform\",\"none\",\"~:text-align\",\"left\",\"~:font-id\",\"gfont-adlam-display\",\"~:font-size\",\"23\",\"~:font-weight\",\"400\",\"~:text-direction\",\"ltr\",\"~:font-variant-id\",\"regular\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-opacity\",1]],\"~:font-family\",\"ADLaM Display\",\"~:text\",\"Text with deleted reference\"]],\"^;\",\"none\",\"^<\",\"left\",\"^=\",\"gfont-adlam-display\",\"~:key\",\"fu3vh\",\"^>\",\"23\",\"^?\",\"400\",\"^@\",\"ltr\",\"^7\",\"paragraph\",\"^A\",\"regular\",\"^B\",\"none\",\"^C\",\"0\",\"^D\",[[\"^ \",\"^E\",\"#000000\",\"^F\",1]],\"^G\",\"ADLaM Display\"]]]]],\"~:hide-in-viewer\",false,\"~:name\",\"Text with deleted reference\",\"~:width\",137,\"^7\",\"^H\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",198,\"~:y\",586]],[\"^N\",[\"^ \",\"~:x\",335,\"~:y\",586]],[\"^N\",[\"^ \",\"~:x\",335,\"~:y\",683]],[\"^N\",[\"^ \",\"~:x\",198,\"~:y\",683]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u886f2846-a2c5-808d-8008-4115e86119e9\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:fill\",\"ref-grfeen\",\"~:typography\",\"ref-typo\"],\"~:position-data\",[[\"~#rect\",[\"^ \",\"~:y\",614,\"^:\",\"normal\",\"^;\",\"none\",\"^>\",\"23px\",\"^?\",\"400\",\"~:y1\",-2,\"^L\",107.921875,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",198,\"~:x1\",0,\"~:y2\",28,\"^D\",[[\"^ \",\"^E\",\"#000000\",\"^F\",1]],\"~:x2\",107.921875,\"~:direction\",\"ltr\",\"^G\",\"\\\"ADLaM Display\\\"\",\"~:height\",30,\"^H\",\"Text with \"]],[\"^V\",[\"^ \",\"~:y\",641.59375,\"^:\",\"normal\",\"^;\",\"none\",\"^>\",\"23px\",\"^?\",\"400\",\"^W\",25.59375,\"^L\",88.890625,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",198,\"^X\",0,\"^Y\",55.59375,\"^D\",[[\"^ \",\"^E\",\"#000000\",\"^F\",1]],\"^Z\",88.890625,\"^[\",\"ltr\",\"^G\",\"\\\"ADLaM Display\\\"\",\"^10\",30,\"^H\",\"deleted \"]],[\"^V\",[\"^ \",\"~:y\",669.1875,\"^:\",\"normal\",\"^;\",\"none\",\"^>\",\"23px\",\"^?\",\"400\",\"^W\",53.1875,\"^L\",103.46875,\"^B\",\"none\",\"^C\",\"normal\",\"~:x\",198,\"^X\",0,\"^Y\",83.1875,\"^D\",[[\"^ \",\"^E\",\"#000000\",\"^F\",1]],\"^Z\",103.46875,\"^[\",\"ltr\",\"^G\",\"\\\"ADLaM Display\\\"\",\"^10\",30,\"^H\",\"reference\"]]],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:x\",198,\"~:selrect\",[\"^V\",[\"^ \",\"~:x\",198,\"~:y\",586,\"^L\",137,\"^10\",97,\"^X\",198,\"^W\",586,\"^Z\",335,\"^Y\",683]],\"~:flip-x\",null,\"^10\",97,\"~:flip-y\",null]]",
|
||||
"~ua7df6ab8-bee5-802f-8008-010d9ff3da26": "[\"~#shape\",[\"^ \",\"~:y\",274,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle with not active token\",\"~:width\",287,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",723,\"~:y\",274]],[\"^<\",[\"^ \",\"~:x\",1010,\"~:y\",274]],[\"^<\",[\"^ \",\"~:x\",1010,\"~:y\",586]],[\"^<\",[\"^ \",\"~:x\",723,\"~:y\",586]]],\"~:r2\",100,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4530574a-7a0a-807b-8008-0107b2c4628e\",\"~:r3\",100,\"~:r1\",100,\"~:id\",\"~ua7df6ab8-bee5-802f-8008-010d9ff3da26\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:applied-tokens\",[\"^ \",\"~:fill\",\"red\",\"^B\",\"border-radius\",\"^=\",\"border-radius\",\"^A\",\"border-radius\",\"~:r4\",\"border-radius\"],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",723,\"~:proportion\",1,\"^G\",100,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",723,\"~:y\",274,\"^8\",287,\"~:height\",312,\"~:x1\",723,\"~:y1\",274,\"~:x2\",1010,\"~:y2\",586]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ef0c0c\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^M\",312,\"~:flip-y\",null]]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:tokens-lib": {
|
||||
"~#penpot/tokens-lib": {
|
||||
"~:sets": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"S-Global",
|
||||
{
|
||||
"~#penpot/token-set": {
|
||||
"~:id": "~uf71719b3-63a1-8157-8008-40346917de8c",
|
||||
"~:name": "Global",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782813028640",
|
||||
"~:tokens": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"base-50",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uf71719b3-63a1-8157-8008-40346917de87",
|
||||
"~:name": "base-50",
|
||||
"~:type": "~:border-radius",
|
||||
"~:value": "50",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782742859871"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"out-ref",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uf2256046-b127-808e-8008-411bec9b1109",
|
||||
"~:name": "out-ref",
|
||||
"~:type": "~:color",
|
||||
"~:value": "rgb(218, 31, 234)",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782803549804"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"out-typo",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uf2256046-b127-808e-8008-411bf90df57e",
|
||||
"~:name": "out-typo",
|
||||
"~:type": "~:typography",
|
||||
"~:value": {
|
||||
"~:font-family": [
|
||||
"Arizonia"
|
||||
]
|
||||
},
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782803562551"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"G-Mode",
|
||||
{
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"S-Dark",
|
||||
{
|
||||
"~#penpot/token-set": {
|
||||
"~:id": "~uf71719b3-63a1-8157-8008-40346917de8d",
|
||||
"~:name": "Mode/Dark",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782803647245",
|
||||
"~:tokens": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"red",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uf71719b3-63a1-8157-8008-40346917de88",
|
||||
"~:name": "red",
|
||||
"~:type": "~:color",
|
||||
"~:value": "#ef0c0c",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782742859871"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"ref-grfeen",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u886f2846-a2c5-808d-8008-4115e5fdae2e",
|
||||
"~:name": "ref-grfeen",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{green}",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782801970166"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"ref-typo",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u886f2846-a2c5-808d-8008-41160c4641f4",
|
||||
"~:name": "ref-typo",
|
||||
"~:type": "~:typography",
|
||||
"~:value": "{typo1}",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782802009369"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"ref-1",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u886f2846-a2c5-808d-8008-411628261dc7",
|
||||
"~:name": "ref-1",
|
||||
"~:type": "~:border-radius",
|
||||
"~:value": "{b1}",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782802037912"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"in-br",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uf2256046-b127-808e-8008-411c0aced089",
|
||||
"~:name": "in-br",
|
||||
"~:type": "~:border-radius",
|
||||
"~:value": "{base-50}",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782803580731"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"in-color",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uf2256046-b127-808e-8008-411c263c999b",
|
||||
"~:name": "in-color",
|
||||
"~:type": "~:color",
|
||||
"~:value": "{out-ref}",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782803608818"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"in-typo",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uf2256046-b127-808e-8008-411c472f33d3",
|
||||
"~:name": "in-typo",
|
||||
"~:type": "~:typography",
|
||||
"~:value": "{out-typo}",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782803642556"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"S-Light",
|
||||
{
|
||||
"~#penpot/token-set": {
|
||||
"~:id": "~uf71719b3-63a1-8157-8008-40346917de8e",
|
||||
"~:name": "Mode/Light",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782812999332",
|
||||
"~:tokens": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"red",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uf71719b3-63a1-8157-8008-40346917de89",
|
||||
"~:name": "red",
|
||||
"~:type": "~:color",
|
||||
"~:value": "#ec9090",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782742859871"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"typo-2",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u1de8c150-538b-80b2-8008-40357ed6d13a",
|
||||
"~:name": "typo-2",
|
||||
"~:type": "~:typography",
|
||||
"~:value": {
|
||||
"~:font-family": [
|
||||
"AR One Sans"
|
||||
],
|
||||
"~:font-weight": "200"
|
||||
},
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782812999332"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"border-radius",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uf2256046-b127-808e-8008-4134c130c292",
|
||||
"~:name": "border-radius",
|
||||
"~:type": "~:border-radius",
|
||||
"~:value": "{base-50} * 2",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1782810058947"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"~:themes": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"",
|
||||
{
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"__PENPOT__HIDDEN__TOKEN__THEME__",
|
||||
{
|
||||
"~#penpot/token-theme": {
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:name": "__PENPOT__HIDDEN__TOKEN__THEME__",
|
||||
"~:group": "",
|
||||
"~:description": "",
|
||||
"~:is-source": false,
|
||||
"~:external-id": "",
|
||||
"~:modified-at": "~m1782812922760",
|
||||
"~:sets": {
|
||||
"~#set": [
|
||||
"Global"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"App",
|
||||
{
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"Web",
|
||||
{
|
||||
"~#penpot/token-theme": {
|
||||
"~:id": "~u37900259-7f15-8003-8007-9ee75dd2f2d2",
|
||||
"~:name": "Web",
|
||||
"~:group": "App",
|
||||
"~:description": "",
|
||||
"~:is-source": false,
|
||||
"~:external-id": "37900259-7f15-8003-8007-9ee75dd2f2d2",
|
||||
"~:modified-at": "~m1782810072529",
|
||||
"~:sets": {
|
||||
"~#set": [
|
||||
"Mode/Dark",
|
||||
"Global"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"Mobile",
|
||||
{
|
||||
"~#penpot/token-theme": {
|
||||
"~:id": "~u37900259-7f15-8003-8007-9ee77eb567bd",
|
||||
"~:name": "Mobile",
|
||||
"~:group": "App",
|
||||
"~:description": "",
|
||||
"~:is-source": false,
|
||||
"~:external-id": "37900259-7f15-8003-8007-9ee77eb567bd",
|
||||
"~:modified-at": "~m1782810072529",
|
||||
"~:sets": {
|
||||
"~#set": [
|
||||
"Mode/Light",
|
||||
"Global"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"~:active-themes": {
|
||||
"~#set": [
|
||||
"/__PENPOT__HIDDEN__TOKEN__THEME__"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u55608328-aed3-807a-8008-41341f2f7247",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -378,6 +378,29 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new auto-width Text Shape by single-clicking at the given
|
||||
* coordinates (as opposed to dragging a fixed-size box) and, optionally,
|
||||
* types an initial text.
|
||||
*
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {string} [initialText]
|
||||
* @param {*} [options]
|
||||
*/
|
||||
async createAutoWidthTextShape(x, y, initialText, options) {
|
||||
const timeToWait = options?.timeToWait ?? 100;
|
||||
await this.page.keyboard.press("T");
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
|
||||
await this.clickAt(x, y);
|
||||
|
||||
if (initialText) {
|
||||
await this.waitForSelectedShapeName("Text");
|
||||
await this.page.keyboard.type(initialText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the selected element into the clipboard, or copy the
|
||||
* content of the locator into the clipboard.
|
||||
|
||||
@ -162,7 +162,7 @@ test("Updates canvas background", async ({ page }) => {
|
||||
|
||||
const canvasBackgroundInput = workspace.page.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
}).first();
|
||||
await canvasBackgroundInput.fill("FABADA");
|
||||
await workspace.page.keyboard.press("Enter");
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user