Compare commits

..

2 Commits

Author SHA1 Message Date
alonso.torres
c3da3b08e5 WIP 2026-01-28 09:40:21 +01:00
alonso.torres
d0fc7b814b 🐛 Disable thumbnails render in wasm 2026-01-28 09:40:21 +01:00
469 changed files with 82498 additions and 51635 deletions

View File

@@ -45,15 +45,6 @@
:potok/reify-type :potok/reify-type
{:level :error} {:level :error}
:redundant-primitive-coercion
{:level :off}
:unused-excluded-var
{:level :off}
:unresolved-excluded-var
{:level :off}
:missing-protocol-method :missing-protocol-method
{:level :off} {:level :off}

View File

@@ -2,11 +2,6 @@
:remove-multiple-non-indenting-spaces? false :remove-multiple-non-indenting-spaces? false
:remove-surrounding-whitespace? true :remove-surrounding-whitespace? true
:remove-consecutive-blank-lines? false :remove-consecutive-blank-lines? false
:indent-line-comments? true
:parallel? true
:align-form-columns? false
;; :align-map-columns? false
;; :align-single-column-lines? false
:extra-indents {rumext.v2/fnc [[:inner 0]] :extra-indents {rumext.v2/fnc [[:inner 0]]
cljs.test/async [[:inner 0]] cljs.test/async [[:inner 0]]
promesa.exec/thread [[:inner 0]] promesa.exec/thread [[:inner 0]]

View File

@@ -1,38 +0,0 @@
---
name: New Render Bug Report
about: Create a report about the bugs you have found in the new render
title: ''
labels: new render
assignees: claragvinola
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or screen recordings**
If applicable, add screenshots or screen recording to help illustrate your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -40,7 +40,7 @@ on:
jobs: jobs:
build-bundle: build-bundle:
name: Build and Upload Penpot Bundle name: Build and Upload Penpot Bundle
runs-on: penpot-runner-01 runs-on: ubuntu-24.04
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View File

@@ -7,14 +7,9 @@ jobs:
build-and-push: build-and-push:
name: Build and push DevEnv Docker image name: Build and push DevEnv Docker image
environment: release-admins environment: release-admins
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
steps: steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -19,14 +19,9 @@ on:
jobs: jobs:
build-and-push: build-and-push:
name: Build and Push Penpot Docker Images name: Build and Push Penpot Docker Images
runs-on: penpot-runner-02 runs-on: ubuntu-24.04-arm
steps: steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -71,15 +66,6 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
# To avoid the “429 Too Many Requests” error when downloading
# images from DockerHub for unregistered users.
# https://docs.docker.com/docker-hub/usage/
- name: Login to DockerHub Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) - name: Extract metadata (tags, labels)
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5

View File

@@ -0,0 +1,21 @@
name: _NITRATE MODULE
on:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "nitrate-module"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: "nitrate-module"

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check Commit Type - name: Check Commit Type
uses: gsactions/commit-message-checker@v2 uses: gsactions/commit-message-checker@v2
with: with:
pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert|Reapply).+[^.])$' pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert).+[^.])$'
flags: 'gm' flags: 'gm'
error: 'Commit should match CONTRIBUTING.md guideline' error: 'Commit should match CONTRIBUTING.md guideline'
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request

View File

@@ -7,11 +7,11 @@ on:
- staging - staging
- main - main
paths: paths:
- 'plugins/libs/plugin-types/index.d.ts' - "plugins/libs/plugin-types/index.d.ts"
- 'plugins/libs/plugin-types/REAME.md' - "plugins/libs/plugin-types/REAME.md"
- 'plugins/tools/typedoc.css' - "plugins/tools/typedoc.css"
- 'plugins/CHANGELOG.md' - "plugins/CHANGELOG.md"
- 'plugins/wrangler-penpot-plugins-api-doc.toml' - "plugins/wrangler-penpot-plugins-api-doc.toml"
workflow_dispatch: workflow_dispatch:
inputs: inputs:
gh_ref: gh_ref:
@@ -86,24 +86,12 @@ jobs:
run: | run: |
REF="${{ steps.vars.outputs.gh_ref }}" REF="${{ steps.vars.outputs.gh_ref }}"
case "$REF" in case "$REF" in
main) main) echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV ;;
echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV staging) echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV ;;
echo "WORKER_URI=doc.plugins.penpot.app" >> $GITHUB_ENV ;; develop) echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV ;;
staging)
echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV
echo "WORKER_URI=doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
develop)
echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV
echo "WORKER_URI=doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
*) echo "Unsupported branch ${REF}" && exit 1 ;; *) echo "Unsupported branch ${REF}" && exit 1 ;;
esac esac
- name: Set the custom url
working-directory: plugins
shell: bash
run: |
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml
- name: Deploy to Cloudflare Workers - name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3 uses: cloudflare/wrangler-action@v3
with: with:

View File

@@ -1,123 +0,0 @@
name: Plugins/styles-doc deployer
on:
push:
branches:
- develop
- staging
- main
paths:
- 'plugins/apps/example-styles/**'
- 'plugins/libs/plugins-styles/**'
- 'plugins/wrangler-penpot-plugins-styles-doc.toml'
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.vars.outputs.gh_ref }}
# START: Setup Node and PNPM enabling cache
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Enable PNPM
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path
id: pnpm-store
working-directory: ./plugins
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
# END: Setup Node and PNPM enabling cache
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
pnpm install --no-frozen-lockfile;
pnpm add -D -w wrangler@latest;
- name: Build styles
working-directory: plugins
shell: bash
run: npx nx run example-styles:build
- name: Select Worker name
run: |
REF="${{ steps.vars.outputs.gh_ref }}"
case "$REF" in
main)
echo "WORKER_NAME=penpot-plugins-styles-doc-pro" >> $GITHUB_ENV
echo "WORKER_URI=styles-doc.plugins.penpot.app" >> $GITHUB_ENV ;;
staging)
echo "WORKER_NAME=penpot-plugins-styles-doc-pre" >> $GITHUB_ENV
echo "WORKER_URI=styles-doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
develop)
echo "WORKER_NAME=penpot-plugins-styles-doc-hourly" >> $GITHUB_ENV
echo "WORKER_URI=styles-doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
*) echo "Unsupported branch ${REF}" && exit 1 ;;
esac
- name: Set the custom url
working-directory: plugins
shell: bash
run: |
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-styles-doc.toml
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: plugins
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --config wrangler-penpot-plugins-styles-doc.toml --name ${{ env.WORKER_NAME }}
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🧩💅 *[PENPOT PLUGINS] Error deploying Styles documentation.*
📄 Triggered from ref: `${{ inputs.gh_ref }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

6
.gitignore vendored
View File

@@ -21,7 +21,6 @@
.rebel_readline_history .rebel_readline_history
.repl .repl
.shadow-cljs .shadow-cljs
.pnpm-store/
/*.jpg /*.jpg
/*.md /*.md
/*.png /*.png
@@ -45,7 +44,6 @@
/backend/resources/public/media /backend/resources/public/media
/backend/target/ /backend/target/
/backend/experiments /backend/experiments
/backend/scripts/_env.local
/bundle* /bundle*
/cd.md /cd.md
/clj-profiler/ /clj-profiler/
@@ -56,8 +54,6 @@
/exporter/target /exporter/target
/frontend/.storybook/preview-body.html /frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html /frontend/.storybook/preview-head.html
/frontend/playwright-report/
/frontend/text-editor/src/wasm/
/frontend/dist/ /frontend/dist/
/frontend/npm-debug.log /frontend/npm-debug.log
/frontend/out/ /frontend/out/
@@ -66,7 +62,6 @@
/frontend/resources/public/* /frontend/resources/public/*
/frontend/storybook-static/ /frontend/storybook-static/
/frontend/target/ /frontend/target/
/frontend/test-results/
/other/ /other/
/scripts/ /scripts/
/telemetry/ /telemetry/
@@ -77,7 +72,6 @@
/library/target/ /library/target/
/library/*.zip /library/*.zip
/external /external
/penpot-nitrate
clj-profiler/ clj-profiler/
node_modules node_modules

105
.gitpod.yml Normal file
View File

@@ -0,0 +1,105 @@
image:
file: docker/gitpod/Dockerfile
ports:
# nginx
- port: 3449
onOpen: open-preview
# frontend nREPL
- port: 3447
onOpen: ignore
visibility: private
# frontend shadow server
- port: 3448
onOpen: ignore
visibility: private
# backend
- port: 6060
onOpen: ignore
# exporter shadow server
- port: 9630
onOpen: ignore
visibility: private
# exporter http server
- port: 6061
onOpen: ignore
# mailhog web interface
- port: 8025
onOpen: ignore
# mailhog postfix
- port: 1025
onOpen: ignore
# postgres
- port: 5432
onOpen: ignore
# redis
- port: 6379
onOpen: ignore
# openldap
- port: 389
onOpen: ignore
tasks:
# https://github.com/gitpod-io/gitpod/issues/666#issuecomment-534347856
- name: gulp
command: >
cd $GITPOD_REPO_ROOT/frontend/;
yarn && gp sync-done 'frontend-yarn';
npx gulp --theme=${PENPOT_THEME} watch
- name: frontend shadow watch
command: >
cd $GITPOD_REPO_ROOT/frontend/;
gp sync-await 'frontend-yarn';
npx shadow-cljs watch main
- init: gp await-port 5432 && psql -f $GITPOD_REPO_ROOT/docker/gitpod/files/postgresql_init.sql
name: backend
command: >
cd $GITPOD_REPO_ROOT/backend/;
./scripts/start-dev
- name: exporter shadow watch
command:
cd $GITPOD_REPO_ROOT/exporter/;
gp sync-await 'frontend-yarn';
yarn && npx shadow-cljs watch main
- name: exporter web server
command: >
cd $GITPOD_REPO_ROOT/exporter/;
./scripts/wait-and-start.sh
- name: signed terminal
before: >
[[ ! -z ${GNUGPG} ]] &&
cd ~ &&
rm -rf .gnupg &&
echo ${GNUGPG} | base64 -d | tar --no-same-owner -xzvf -
init: >
[[ ! -z ${GNUGPG_KEY} ]] &&
git config --global commit.gpgsign true &&
git config --global user.signingkey ${GNUGPG_KEY}
command: cd $GITPOD_REPO_ROOT
- name: redis
command: redis-server
- before: go get github.com/mailhog/MailHog
name: mailhog
command: MailHog
- name: Nginx
command: >
nginx &&
multitail /var/log/nginx/access.log -I /var/log/nginx/error.log

11
.yarnrc.yml Normal file
View File

@@ -0,0 +1,11 @@
enableGlobalCache: true
enableImmutableCache: false
enableImmutableInstalls: false
enableTelemetry: false
httpTimeout: 600000
nodeLinker: node-modules

View File

@@ -1,38 +1,5 @@
# CHANGELOG # CHANGELOG
## 2.14.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
- Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916)
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
- Remove tokens path node and bulk remove tokens [Taiga #13007](https://tree.taiga.io/project/penpot/us/13007)
- Replace themes management modal radio buttons for switches [Taiga #9215](https://tree.taiga.io/project/penpot/us/9215)
### :bug: Bugs fixed
- Remove whitespaces from asset export filename [Github #8133](https://github.com/penpot/penpot/pull/8133)
- Fix prototype connections lost when switching between variants [Taiga #12812](https://tree.taiga.io/project/penpot/issue/12812)
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040)
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
- Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
- Fix incorrect default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051)
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
- Fix displaying a hidden user avatar when there is only one more [Taiga #13058](https://tree.taiga.io/project/penpot/issue/13058)
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
## 2.13.0 (Unreleased) ## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
@@ -56,7 +23,6 @@
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339) - Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797) - Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
- Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915) - Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915)
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
- Fix color assets from shared libraries not appearing as assets in Selected colors panel [Taiga #12957](https://tree.taiga.io/project/penpot/issue/12957) - Fix color assets from shared libraries not appearing as assets in Selected colors panel [Taiga #12957](https://tree.taiga.io/project/penpot/issue/12957)
- Fix CSS generated box-shadow property [Taiga #12997](https://tree.taiga.io/project/penpot/issue/12997) - Fix CSS generated box-shadow property [Taiga #12997](https://tree.taiga.io/project/penpot/issue/12997)
- Fix inner shadow selector on shadow token [Taiga #12951](https://tree.taiga.io/project/penpot/issue/12951) - Fix inner shadow selector on shadow token [Taiga #12951](https://tree.taiga.io/project/penpot/issue/12951)
@@ -82,6 +48,7 @@
- Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935) - Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935)
- Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917) - Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
## 2.12.0 ## 2.12.0
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
@@ -93,6 +60,7 @@ The backend RPC API URLS are changed from `/api/rpc/command/<name>` to
compatibility; however, if you are a user of this API, it is strongly compatibility; however, if you are a user of this API, it is strongly
recommended that you adapt your code to use the new PATH. recommended that you adapt your code to use the new PATH.
#### Updated SSO Callback URL #### Updated SSO Callback URL
The OAuth / Single Sign-On (SSO) callback endpoint has changed to The OAuth / Single Sign-On (SSO) callback endpoint has changed to
@@ -125,6 +93,7 @@ This update standardizes all authentication flows under the single URL
and makis it more modular, enabling the ability to configure SSO auth and makis it more modular, enabling the ability to configure SSO auth
provider dinamically. provider dinamically.
#### Changes on default docker compose #### Changes on default docker compose
We have updated the `docker/images/docker-compose.yaml` with a small We have updated the `docker/images/docker-compose.yaml` with a small
@@ -188,6 +157,7 @@ example. It's still usable as before, we just removed the example.
- Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be - Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be
removed in future versions: removed in future versions:
- The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its - The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its
values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`) values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`)
- The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY` - The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY`

View File

@@ -120,12 +120,17 @@ them on your system, you can run them with:
```bash ```bash
# Check formatting # Check formatting
./scripts/fmt yarn fmt:clj:check
# Lint # Check and fix formatting
./scripts/lint yarn fmt:clj
# Run the linter
yarn lint:clj
``` ```
There are more choices in `package.json`.
Ideally, you should run these commands as git pre-commit hooks. A convenient way Ideally, you should run these commands as git pre-commit hooks. A convenient way
of defining them is to use [Husky](https://typicode.github.io/husky/#/). of defining them is to use [Husky](https://typicode.github.io/husky/#/).

7
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC", "author": "Kaleidos INC",
"private": true, "private": true,
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6", "packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "https://github.com/penpot/penpot"

306
backend/pnpm-lock.yaml generated
View File

@@ -1,306 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
luxon:
specifier: ^3.4.4
version: 3.7.2
sax:
specifier: ^1.4.1
version: 1.4.3
devDependencies:
nodemon:
specifier: ^3.1.2
version: 3.1.11
source-map-support:
specifier: ^0.5.21
version: 0.5.21
ws:
specifier: ^8.17.0
version: 8.18.3
packages:
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
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==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
chokidar@3.6.0:
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'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
luxon@3.7.2:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nodemon@3.1.11:
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
engines: {node: '>=10'}
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
pstree.remy@1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
sax@1.4.3:
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
hasBin: true
simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
touch@3.1.1:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
hasBin: true
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
snapshots:
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
balanced-match@1.0.2: {}
binary-extensions@2.3.0: {}
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
braces@3.0.3:
dependencies:
fill-range: 7.1.1
buffer-from@1.1.2: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
concat-map@0.0.1: {}
debug@4.4.3(supports-color@5.5.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 5.5.0
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
fsevents@2.3.3:
optional: true
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
has-flag@3.0.0: {}
ignore-by-default@1.0.1: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
luxon@3.7.2: {}
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
ms@2.1.3: {}
nodemon@3.1.11:
dependencies:
chokidar: 3.6.0
debug: 4.4.3(supports-color@5.5.0)
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
semver: 7.7.3
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.1
undefsafe: 2.0.5
normalize-path@3.0.0: {}
picomatch@2.3.1: {}
pstree.remy@1.1.8: {}
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
sax@1.4.3: {}
semver@7.7.3: {}
simple-update-notifier@2.0.0:
dependencies:
semver: 7.7.3
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
source-map@0.6.1: {}
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
touch@3.1.1: {}
undefsafe@2.0.5: {}
ws@8.18.3: {}

View File

View File

@@ -12,22 +12,43 @@ Debug Main Page
</nav> </nav>
<main class="dashboard"> <main class="dashboard">
<section class="widget"> <section class="widget">
<fieldset>
<legend>Error reports</legend>
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
</fieldset>
<fieldset> <fieldset>
<legend>CURRENT PROFILE</legend> <legend>Profile Management</legend>
<desc> <form method="post" action="/dbg/actions/resend-email-verification">
<p> <div class="row">
Name: <b>{{profile.fullname}}</b> <br /> <input type="email" name="email" placeholder="example@example.com" value="" />
Email: <b>{{profile.email}}</b> </div>
</p>
</desc> <div class="row">
<label for="force-verify">Are you sure?</label>
<input id="force-verify" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" name="resend" value="Resend Verification" />
<input type="submit" name="verify" value="Verify" />
</div>
<div class="row">
<input type="submit" class="danger" name="block" value="Block" />
<input type="submit" class="danger" name="unblock" value="Unblock" />
</div>
</form>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>VIRTUAL CLOCK</legend> <legend>VIRTUAL CLOCK</legend>
<desc> <desc>
<p><b>IMPORTANT:</b> The virtual clock is profile based and only affects the currently logged-in profile.</p>
<p> <p>
CURRENT CLOCK: <b>{{current-clock}}</b> CURRENT CLOCK: <b>{{current-clock}}</b>
<br /> <br />
@@ -60,93 +81,8 @@ Debug Main Page
</form> </form>
</fieldset> </fieldset>
<fieldset>
<legend>ERROR REPORTS</legend>
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
</fieldset>
</section> </section>
<section class="widget">
<fieldset>
<legend>Profile Management</legend>
<form method="post" action="/dbg/actions/resend-email-verification">
<div class="row">
<input type="email" name="email" placeholder="example@example.com" value="" />
</div>
<div class="row">
<label for="force-verify">Are you sure?</label>
<input id="force-verify" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" name="resend" value="Resend Verification" />
<input type="submit" name="verify" value="Verify" />
</div>
<div class="row">
<input type="submit" class="danger" name="block" value="Block" />
<input type="submit" class="danger" name="unblock" value="Unblock" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Feature Flags for Team</legend>
<desc>Add a feature flag to a team</desc>
<form method="post" action="/dbg/actions/handle-team-features">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<select type="text" style="width:100px" name="feature">
{% for feature in supported-features %}
<option value="{{feature}}">{{feature}}</option>
{% endfor %}
</select>
</div>
<div class="row">
<select style="width:100px" name="action">
<option value="">Action...</option>
<option value="show">Show</option>
<option value="enable">Enable</option>
<option value="disable">Disable</option>
</select>
</div>
<div class="row">
<label for="check-feature">Skip feature check</label>
<input id="check-feature" type="checkbox" name="skip-check" />
<br />
<small>
Do not check if the feature is supported
</small>
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
</section>
<section class="widget"> <section class="widget">
<fieldset> <fieldset>
@@ -237,5 +173,55 @@ Debug Main Page
</form> </form>
</fieldset> </fieldset>
</section> </section>
<section class="widget">
<fieldset>
<legend>Feature Flags for Team</legend>
<desc>Add a feature flag to a team</desc>
<form method="post" action="/dbg/actions/handle-team-features">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<select type="text" style="width:100px" name="feature">
{% for feature in supported-features %}
<option value="{{feature}}">{{feature}}</option>
{% endfor %}
</select>
</div>
<div class="row">
<select style="width:100px" name="action">
<option value="">Action...</option>
<option value="show">Show</option>
<option value="enable">Enable</option>
<option value="disable">Disable</option>
</select>
</div>
<div class="row">
<label for="check-feature">Skip feature check</label>
<input id="check-feature" type="checkbox" name="skip-check" />
<br />
<small>
Do not check if the feature is supported
</small>
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
</section>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,12 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
# DEPRECATED: only used for subscriptions
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
export PENPOT_HOST=devenv export PENPOT_HOST=devenv
export PENPOT_PUBLIC_URI=https://localhost:3449 export PENPOT_PUBLIC_URI=https://localhost:3449
@@ -18,7 +13,6 @@ export PENPOT_FLAGS="\
disable-login-with-google \ disable-login-with-google \
disable-login-with-github \ disable-login-with-github \
disable-login-with-gitlab \ disable-login-with-gitlab \
disable-telemetry \
enable-backend-worker \ enable-backend-worker \
enable-backend-asserts \ enable-backend-asserts \
disable-feature-fdata-pointer-map \ disable-feature-fdata-pointer-map \
@@ -61,8 +55,6 @@ export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000 export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000/control-center
export JAVA_OPTS="\ export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \ -Djdk.attach.allowAttachSelf \

View File

@@ -3,10 +3,6 @@
SCRIPT_DIR=$(dirname $0); SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env; source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
# Initialize MINIO config # Initialize MINIO config
setup_minio; setup_minio;

View File

@@ -3,11 +3,6 @@
SCRIPT_DIR=$(dirname $0); SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env; source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
export OPTIONS="-A:dev" export OPTIONS="-A:dev"
entrypoint=${1:-app.main}; entrypoint=${1:-app.main};

View File

@@ -3,10 +3,6 @@
SCRIPT_DIR=$(dirname $0); SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env; source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
# Initialize MINIO config # Initialize MINIO config
setup_minio; setup_minio;

View File

@@ -873,8 +873,11 @@
(import-storage-objects cfg) (import-storage-objects cfg)
(let [files (get manifest :files) (let [files (get manifest :files)
result (reduce (fn [result file] result (reduce (fn [result {:keys [id] :as file}]
(let [name' (get file :name) (let [name' (get file :name)
name' (if (map? name)
(get name id)
name')
file (assoc file :name name')] file (assoc file :name name')]
(conj result (import-file cfg file)))) (conj result (import-file cfg file))))
[] []

View File

@@ -102,8 +102,6 @@
[:http-server-io-threads {:optional true} ::sm/int] [:http-server-io-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int] [:http-server-max-worker-threads {:optional true} ::sm/int]
[:exporter-shared-key {:optional true} :string]
[:nitrate-shared-key {:optional true} :string]
[:management-api-key {:optional true} :string] [:management-api-key {:optional true} :string]
[:telemetry-uri {:optional true} :string] [:telemetry-uri {:optional true} :string]
@@ -227,8 +225,6 @@
[:netty-io-threads {:optional true} ::sm/int] [:netty-io-threads {:optional true} ::sm/int]
[:executor-threads {:optional true} ::sm/int] [:executor-threads {:optional true} ::sm/int]
[:nitrate-backend-uri {:optional true} ::sm/uri]
;; DEPRECATED ;; DEPRECATED
[:assets-storage-backend {:optional true} :keyword] [:assets-storage-backend {:optional true} :keyword]
[:storage-assets-fs-directory {:optional true} :string] [:storage-assets-fs-directory {:optional true} :string]

View File

@@ -49,16 +49,13 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn index-handler (defn index-handler
[cfg request] [_cfg _request]
(let [profile-id (::session/profile-id request) (let [{:keys [clock offset]} @clock/current]
offset (clock/get-offset profile-id)
profile (profile/get-profile cfg profile-id)]
{::yres/status 200 {::yres/status 200
::yres/headers {"content-type" "text/html"} ::yres/headers {"content-type" "text/html"}
::yres/body (-> (io/resource "app/templates/debug.tmpl") ::yres/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {:version (:full cf/version) (tmpl/render {:version (:full cf/version)
:profile profile :current-clock (str clock)
:current-clock ct/*clock*
:current-offset (if offset :current-offset (if offset
(ct/format-duration offset) (ct/format-duration offset)
"NO OFFSET") "NO OFFSET")
@@ -450,16 +447,15 @@
(defn- set-virtual-clock (defn- set-virtual-clock
[_ {:keys [params] :as request}] [_ {:keys [params] :as request}]
(let [offset (some-> params :offset str/trim not-empty ct/duration) (let [offset (some-> params :offset str/trim not-empty ct/duration)
profile-id (::session/profile-id request) reset? (contains? params :reset)]
reset? (contains? params :reset)]
(if (= "production" (cf/get :tenant)) (if (= "production" (cf/get :tenant))
{::yres/status 501 {::yres/status 501
::yres/body "OPERATION NOT ALLOWED"} ::yres/body "OPERATION NOT ALLOWED"}
(do (do
(if (or reset? (zero? (inst-ms offset))) (if (or reset? (zero? (inst-ms offset)))
(clock/assign-offset profile-id nil) (clock/set-offset! nil)
(clock/assign-offset profile-id offset)) (clock/set-offset! offset))
{::yres/status 302 {::yres/status 302
::yres/headers {"location" "/dbg"}})))) ::yres/headers {"location" "/dbg"}}))))
@@ -499,7 +495,7 @@
(defn authorized? (defn authorized?
[pool {:keys [::session/profile-id]}] [pool {:keys [::session/profile-id]}]
(or (and (= "devenv" (cf/get :host)) profile-id) (or (= "devenv" (cf/get :host))
(let [profile (ex/ignoring (profile/get-profile pool profile-id)) (let [profile (ex/ignoring (profile/get-profile pool profile-id))
admins (or (cf/get :admins) #{})] admins (or (cf/get :admins) #{})]
(contains? admins (:email profile))))) (contains? admins (:email profile)))))

View File

@@ -13,13 +13,13 @@
[app.common.time :as ct] [app.common.time :as ct]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http.middleware :as mw]
[app.main :as-alias main] [app.main :as-alias main]
[app.rpc.commands.profile :as cmd.profile] [app.rpc.commands.profile :as cmd.profile]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.worker :as-alias wrk] [app.worker :as-alias wrk]
[integrant.core :as ig] [integrant.core :as ig]
[yetti.request :as yreq]
[yetti.response :as-alias yres])) [yetti.response :as-alias yres]))
;; ---- ROUTES ;; ---- ROUTES
@@ -49,40 +49,28 @@
(fn [cfg request] (fn [cfg request]
(db/tx-run! cfg handler request)))))}) (db/tx-run! cfg handler request)))))})
(def ^:private shared-key-auth
{:name ::shared-key-auth
:compile
(fn [_ _]
(fn [handler key]
(if key
(fn [request]
(if-let [key' (yreq/get-header request "x-shared-key")]
(if (= key key')
(handler request)
{::yres/status 403})
{::yres/status 403}))
(fn [_ _]
{::yres/status 403}))))})
(defmethod ig/init-key ::routes (defmethod ig/init-key ::routes
[_ cfg] [_ {:keys [::setup/props] :as cfg}]
["" {:middleware [[shared-key-auth (cf/get :management-api-key)] (let [management-key (or (cf/get :management-api-key)
[default-system cfg] (get props :management-key))]
[transaction]]}
["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["/get-customer" ["" {:middleware [[mw/shared-key-auth management-key]
{:handler get-customer [default-system cfg]
:transaction true [transaction]]}
:allowed-methods #{:post}}] ["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["/update-customer" ["/get-customer"
{:handler update-customer {:handler get-customer
:allowed-methods #{:post} :transaction true
:transaction true}]]) :allowed-methods #{:post}}]
["/update-customer"
{:handler update-customer
:allowed-methods #{:post}
:transaction true}]]))
;; ---- HELPERS ;; ---- HELPERS

View File

@@ -16,6 +16,7 @@
[app.http.errors :as errors] [app.http.errors :as errors]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.pointer-map :as pmap] [app.util.pointer-map :as pmap]
[buddy.core.codecs :as bc]
[cuerdas.core :as str] [cuerdas.core :as str]
[yetti.adapter :as yt] [yetti.adapter :as yt]
[yetti.middleware :as ymw] [yetti.middleware :as ymw]
@@ -300,20 +301,16 @@
:compile (constantly wrap-auth)}) :compile (constantly wrap-auth)})
(defn- wrap-shared-key-auth (defn- wrap-shared-key-auth
[handler keys] [handler shared-key]
(if (seq keys) (if shared-key
(fn [request] (let [shared-key (if (string? shared-key)
(if-let [[key-id key] (some-> (yreq/get-header request "x-shared-key") shared-key
(str/split #"\s+" 2))] (bc/bytes->b64-str shared-key true))]
(let [key-id (-> key-id str/lower keyword)] (fn [request]
(if (and (string? key) (let [key (yreq/get-header request "x-shared-key")]
(contains? keys key-id) (if (= key shared-key)
(= key (get keys key-id))) (handler (assoc request ::http/auth-with-shared-key true))
(-> request {::yres/status 403}))))
(assoc ::http/auth-key-id key-id)
(handler))
{::yres/status 403}))
{::yres/status 403}))
(fn [_ _] (fn [_ _]
{::yres/status 403}))) {::yres/status 403})))

View File

@@ -20,7 +20,6 @@
[app.http.session.tasks :as-alias tasks] [app.http.session.tasks :as-alias tasks]
[app.main :as-alias main] [app.main :as-alias main]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.setup.clock :as clock]
[app.tokens :as tokens] [app.tokens :as tokens]
[integrant.core :as ig] [integrant.core :as ig]
[yetti.request :as yreq] [yetti.request :as yreq]
@@ -230,22 +229,18 @@
(let [{:keys [type token claims metadata]} (get request ::http/auth-data)] (let [{:keys [type token claims metadata]} (get request ::http/auth-data)]
(cond (cond
(= type :cookie) (= type :cookie)
(let [session (let [session (case (:ver metadata)
(case (:ver metadata) ;; BACKWARD COMPATIBILITY WITH OLD TOKENS
;; BACKWARD COMPATIBILITY WITH OLD TOKENS 0 (read-session manager token)
0 (read-session manager token) 1 (some->> (:sid claims) (read-session manager))
1 (some->> (:sid claims) (read-session manager)) nil)
nil)
request request (cond-> request
(cond-> request (some? session)
(some? session) (-> (assoc ::profile-id (:profile-id session))
(-> (assoc ::profile-id (:profile-id session)) (assoc ::session session)))
(assoc ::session session)))
response response (handler request)]
(binding [ct/*clock* (clock/get-clock (:profile-id session))]
(handler request))]
(if (and session (renew-session? session)) (if (and session (renew-session? session))
(let [session (->> session (let [session (->> session

View File

@@ -6,6 +6,7 @@
(ns app.http.sse (ns app.http.sse
"SSE (server sent events) helpers" "SSE (server sent events) helpers"
(:refer-clojure :exclude [tap])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.logging :as l] [app.common.logging :as l]

View File

@@ -9,7 +9,6 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.json :as json]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
@@ -140,14 +139,10 @@
client-version (get-client-version request) client-version (get-client-version request)
client-user-agent (get-client-user-agent request) client-user-agent (get-client-user-agent request)
session-id (get-external-session-id request) session-id (get-external-session-id request)
key-id (::http/auth-key-id request) token-id (::actoken/id request)]
token-id (::actoken/id request)
token-type (::actoken/type request)]
(d/without-nils (d/without-nils
{:external-session-id session-id {:external-session-id session-id
:initiator (or key-id "app")
:access-token-id (some-> token-id str) :access-token-id (some-> token-id str)
:access-token-type (some-> token-type str)
:client-event-origin client-event-origin :client-event-origin client-event-origin
:client-user-agent client-user-agent :client-user-agent client-user-agent
:client-version client-version :client-version client-version
@@ -228,7 +223,7 @@
(some? tnow) (some? tnow)
(assoc :tracked-at tnow)))) (assoc :tracked-at tnow))))
(defn- append-audit-entry (defn- append-audit-entry!
[cfg params] [cfg params]
(let [params (-> params (let [params (-> params
(update :props db/tjson) (update :props db/tjson)
@@ -241,16 +236,6 @@
(let [params (event->params event) (let [params (event->params event)
tnow (ct/now)] tnow (ct/now)]
(when (contains? cf/flags :audit-log-logger)
(l/log! ::l/logger "app.audit"
::l/level :info
:profile-id (str (::profile-id event))
:ip-addr (str (::ip-addr event))
:type (::type event)
:name (::name event)
:props (json/encode (::props event) :key-fn json/write-camel-key)
:context (json/encode (::context event) :key-fn json/write-camel-key)))
(when (contains? cf/flags :audit-log) (when (contains? cf/flags :audit-log)
;; NOTE: this operation may cause primary key conflicts on inserts ;; NOTE: this operation may cause primary key conflicts on inserts
;; because of the timestamp precission (two concurrent requests), in ;; because of the timestamp precission (two concurrent requests), in
@@ -258,7 +243,7 @@
(let [params (-> params (let [params (-> params
(assoc :created-at tnow) (assoc :created-at tnow)
(update :tracked-at #(or % tnow)))] (update :tracked-at #(or % tnow)))]
(append-audit-entry cfg params))) (append-audit-entry! cfg params)))
(when (and (or (contains? cf/flags :telemetry) (when (and (or (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled)) (cf/get :telemetry-enabled))
@@ -273,7 +258,7 @@
(update :tracked-at #(or % tnow)) (update :tracked-at #(or % tnow))
(assoc :props {}) (assoc :props {})
(assoc :context {}))] (assoc :context {}))]
(append-audit-entry cfg params))) (append-audit-entry! cfg params)))
(when (and (contains? cf/flags :webhooks) (when (and (contains? cf/flags :webhooks)
(::webhooks/event? event)) (::webhooks/event? event))
@@ -327,4 +312,4 @@
params (-> (event->params event) params (-> (event->params event)
(assoc :created-at tnow) (assoc :created-at tnow)
(update :tracked-at #(or % tnow)))] (update :tracked-at #(or % tnow)))]
(append-audit-entry cfg params))))))) (append-audit-entry! cfg params)))))))

View File

@@ -275,7 +275,8 @@
::email/whitelist (ig/ref ::email/whitelist)} ::email/whitelist (ig/ref ::email/whitelist)}
::mgmt/routes ::mgmt/routes
{::db/pool (ig/ref ::db/pool)} {::db/pool (ig/ref ::db/pool)
::setup/props (ig/ref ::setup/props)}
:app.http/router :app.http/router
{::session/manager (ig/ref ::session/manager) {::session/manager (ig/ref ::session/manager)
@@ -322,7 +323,6 @@
{::http.client/client (ig/ref ::http.client/client) {::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
::rds/pool (ig/ref ::rds/pool) ::rds/pool (ig/ref ::rds/pool)
:app.nitrate/client (ig/ref :app.nitrate/client)
::wrk/executor (ig/ref ::wrk/netty-executor) ::wrk/executor (ig/ref ::wrk/netty-executor)
::session/manager (ig/ref ::session/manager) ::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider) ::ldap/provider (ig/ref ::ldap/provider)
@@ -339,10 +339,6 @@
::email/blacklist (ig/ref ::email/blacklist) ::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist)} ::email/whitelist (ig/ref ::email/whitelist)}
:app.nitrate/client
{::http.client/client (ig/ref ::http.client/client)
::setup/shared-keys (ig/ref ::setup/shared-keys)}
:app.rpc/management-methods :app.rpc/management-methods
{::http.client/client (ig/ref ::http.client/client) {::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
@@ -352,18 +348,17 @@
::sto/storage (ig/ref ::sto/storage) ::sto/storage (ig/ref ::sto/storage)
::mtx/metrics (ig/ref ::mtx/metrics) ::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref ::mbus/msgbus) ::mbus/msgbus (ig/ref ::mbus/msgbus)
:app.nitrate/client (ig/ref :app.nitrate/client)
::rds/client (ig/ref ::rds/client) ::rds/client (ig/ref ::rds/client)
::setup/props (ig/ref ::setup/props)} ::setup/props (ig/ref ::setup/props)}
::rpc/routes ::rpc/routes
{::rpc/methods (ig/ref :app.rpc/methods) {::rpc/methods (ig/ref :app.rpc/methods)
::rpc/management-methods (ig/ref :app.rpc/management-methods) ::rpc/management-methods (ig/ref :app.rpc/management-methods)
;; FIXME: revisit if db/pool is necessary here ;; FIXME: revisit if db/pool is necessary here
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
::session/manager (ig/ref ::session/manager) ::session/manager (ig/ref ::session/manager)
::setup/shared-keys (ig/ref ::setup/shared-keys)} ::setup/props (ig/ref ::setup/props)}
::wrk/registry ::wrk/registry
{::mtx/metrics (ig/ref ::mtx/metrics) {::mtx/metrics (ig/ref ::mtx/metrics)
@@ -451,11 +446,6 @@
;; module requires the migrations to run before initialize. ;; module requires the migrations to run before initialize.
::migrations (ig/ref :app.migrations/migrations)} ::migrations (ig/ref :app.migrations/migrations)}
::setup/shared-keys
{::setup/props (ig/ref ::setup/props)
:nitrate (cf/get :nitrate-shared-key)
:exporter (cf/get :exporter-shared-key)}
::setup/clock ::setup/clock
{} {}

View File

@@ -1,130 +0,0 @@
(ns app.nitrate
"Module that make calls to the external nitrate aplication"
(:require
[app.common.logging :as l]
[app.common.schema :as sm]
[app.config :as cf]
[app.http.client :as http]
[app.rpc :as-alias rpc]
[app.setup :as-alias setup]
[app.util.json :as json]
[clojure.core :as c]
[integrant.core :as ig]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- request-builder
[cfg method uri shared-key profile-id]
(fn []
(http/req! cfg {:method method
:headers {"content-type" "application/json"
"accept" "application/json"
"x-shared-key" shared-key
"x-profile-id" (str profile-id)}
:uri uri
:version :http1.1})))
(defn- with-retries
[handler max-retries]
(fn []
(loop [attempt 1]
(let [result (try
(handler)
(catch Exception e
(if (< attempt max-retries)
::retry
(do
;; TODO Error handling
(l/error :hint "request fail after multiple retries" :cause e)
nil))))]
(if (= result ::retry)
(recur (inc attempt))
result)))))
(defn- with-validate [handler uri schema]
(fn []
(let [coercer-http (sm/coercer schema
:type :validation
:hint (str "invalid data received calling " uri))]
(try
(coercer-http (-> (handler) :body json/decode))
(catch Exception e
;; TODO Error handling
(l/error :hint "error validating json response" :cause e)
nil)))))
(defn- request-to-nitrate
[cfg method uri schema {:keys [::rpc/profile-id] :as params}]
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
full-http-call (-> (request-builder cfg method uri shared-key profile-id)
(with-retries 3)
(with-validate uri schema))]
(full-http-call)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn call
[cfg method params]
(when (contains? cf/flags :nitrate)
(let [client (get cfg ::client)
method (get client method)]
(method params))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:organization
[:map
[:id ::sm/text]
[:name ::sm/text]])
(def ^:private schema:user
[:map
[:valid ::sm/boolean]])
(defn- get-team-org
[cfg {:keys [team-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params)))
(defn- is-valid-user
[cfg {:keys [profile-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INITIALIZATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/init-key ::client
[_ cfg]
(when (contains? cf/flags :nitrate)
{:get-team-org (partial get-team-org cfg)
:is-valid-user (partial is-valid-user cfg)}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UTILS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn add-nitrate-licence-to-profile
[cfg profile]
(try
(let [nitrate-licence (call cfg :is-valid-user {:profile-id (:id profile)})]
(assoc profile :nitrate-licence (:valid nitrate-licence)))
(catch Throwable cause
(l/error :hint "failed to get nitrate licence"
:profile-id (:id profile)
:cause cause)
profile)))
(defn add-org-to-team
[cfg team params]
(let [params (assoc (or params {}) :team-id (:id team))
org (call cfg :get-team-org params)]
(assoc team :organization-id (:id org) :organization-name (:name org))))

View File

@@ -92,11 +92,11 @@
(fn [{:keys [params path-params method] :as request}] (fn [{:keys [params path-params method] :as request}]
(let [handler-name (:type path-params) (let [handler-name (:type path-params)
etag (yreq/get-header request "if-none-match") etag (yreq/get-header request "if-none-match")
key-id (get request ::http/auth-key-id)
profile-id (or (::session/profile-id request) profile-id (or (::session/profile-id request)
(::actoken/profile-id request) (::actoken/profile-id request)
(if key-id uuid/zero nil)) (if (::http/auth-with-shared-key request)
uuid/zero
nil))
ip-addr (inet/parse-request request) ip-addr (inet/parse-request request)
@@ -298,12 +298,10 @@
(defn- resolve-management-methods (defn- resolve-management-methods
[cfg] [cfg]
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing) (let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
mods (cond->> (list 'app.rpc.management.exporter) (->> (sv/scan-ns
(contains? cf/flags :nitrate) 'app.rpc.management.subscription
(cons 'app.rpc.management.nitrate))] 'app.rpc.management.exporter)
(->> (apply sv/scan-ns mods)
(map (partial process-method cfg "management" wrap-management)) (map (partial process-method cfg "management" wrap-management))
(into {})))) (into {}))))
@@ -347,20 +345,23 @@
(defmethod ig/assert-key ::routes (defmethod ig/assert-key ::routes
[_ params] [_ params]
(assert (map? (::setup/shared-keys params)))
(assert (db/pool? (::db/pool params)) "expect valid database pool") (assert (db/pool? (::db/pool params)) "expect valid database pool")
(assert (some? (::setup/props params)))
(assert (session/manager? (::session/manager params)) "expect valid session manager") (assert (session/manager? (::session/manager params)) "expect valid session manager")
(assert (valid-methods? (::methods params)) "expect valid methods map") (assert (valid-methods? (::methods params)) "expect valid methods map")
(assert (valid-methods? (::management-methods params)) "expect valid methods map")) (assert (valid-methods? (::management-methods params)) "expect valid methods map"))
(defmethod ig/init-key ::routes (defmethod ig/init-key ::routes
[_ {:keys [::methods ::management-methods ::setup/shared-keys] :as cfg}] [_ {:keys [::methods ::management-methods ::setup/props] :as cfg}]
(let [public-uri (cf/get :public-uri)
management-key (or (cf/get :management-api-key)
(get props :management-key))]
(let [public-uri (cf/get :public-uri)]
["/api" ["/api"
["/management" ["/management"
["/methods/:type" ["/methods/:type"
{:middleware [[mw/shared-key-auth shared-keys] {:middleware [[mw/shared-key-auth management-key]
[session/authz cfg]] [session/authz cfg]]
:handler (make-rpc-handler management-methods)}] :handler (make-rpc-handler management-methods)}]

View File

@@ -79,7 +79,7 @@
(db/insert-many! pool :audit-log event-columns events)))) (db/insert-many! pool :audit-log event-columns events))))
(def valid-event-types (def valid-event-types
#{"action" "identify" "trigger"}) #{"action" "identify"})
(def schema:event (def schema:event
[:map {:title "Event"} [:map {:title "Event"}

View File

@@ -21,7 +21,6 @@
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.main :as-alias main] [app.main :as-alias main]
[app.media :as media] [app.media :as media]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.climit :as climit] [app.rpc.climit :as climit]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -89,8 +88,6 @@
;; --- QUERY: Get profile (own) ;; --- QUERY: Get profile (own)
(sv/defmethod ::get-profile (sv/defmethod ::get-profile
{::rpc/auth false {::rpc/auth false
::doc/added "1.18" ::doc/added "1.18"
@@ -101,13 +98,9 @@
;; no profile-id is in session, and when db call raises not found. In all other ;; no profile-id is in session, and when db call raises not found. In all other
;; cases we need to reraise the exception. ;; cases we need to reraise the exception.
(try (try
(let [profile (-> (get-profile pool profile-id) (-> (get-profile pool profile-id)
(strip-private-attrs) (strip-private-attrs)
(update :props filter-props))] (update :props filter-props))
(if (contains? cf/flags :nitrate)
(nitrate/add-nitrate-licence-to-profile cfg profile)
profile))
(catch Throwable _ (catch Throwable _
{:id uuid/zero :fullname "Anonymous User"}))) {:id uuid/zero :fullname "Anonymous User"})))

View File

@@ -23,7 +23,6 @@
[app.main :as-alias main] [app.main :as-alias main]
[app.media :as media] [app.media :as media]
[app.msgbus :as mbus] [app.msgbus :as mbus]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -191,9 +190,7 @@
::sm/params schema:get-teams} ::sm/params schema:get-teams}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(cond->> (get-teams conn profile-id) (get-teams conn profile-id)))
(contains? cf/flags :nitrate)
(map #(nitrate/add-org-to-team cfg % params)))))
(def ^:private sql:get-owned-teams (def ^:private sql:get-owned-teams
"SELECT t.id, t.name, "SELECT t.id, t.name,

View File

@@ -248,11 +248,11 @@
invitations (into #{} invitations (into #{}
(comp (comp
;; We don't re-send invitations to ;; We don't re-send invitations to
;; already existing members ;; already existing members
(remove #(contains? team-members (:email %))) (remove #(contains? team-members (:email %)))
;; We don't send invitations to ;; We don't send invitations to
;; join-requested members ;; join-requested members
(remove #(contains? join-requests (:email %))) (remove #(contains? join-requests (:email %)))
(map (fn [{:keys [email role]}] (map (fn [{:keys [email role]}]
(create-invitation cfg (create-invitation cfg

View File

@@ -1,82 +0,0 @@
;; 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.management.nitrate
"Internal Nitrate HTTP RPC API. Provides authenticated access to
organization management and token validation endpoints."
(:require
[app.common.schema :as sm]
[app.common.types.profile :refer [schema:profile]]
[app.common.types.team :refer [schema:team]]
[app.common.uuid :as uuid]
[app.db :as db]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as doc]
[app.util.services :as sv]))
;; ---- API: authenticate
(sv/defmethod ::authenticate
"Authenticate the current user"
{::doc/added "2.14"
::sm/params [:map]
::sm/result schema:profile}
[cfg {:keys [::rpc/profile-id] :as params}]
(let [profile (profile/get-profile cfg profile-id)]
{:id (get profile :id)
:name (get profile :fullname)
:email (get profile :email)
:photo-url (files/resolve-public-uri (get profile :photo-id))}))
;; ---- API: get-teams
(def ^:private sql:get-teams
"SELECT t.*
FROM team AS t
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
WHERE tpr.profile_id = ?
AND tpr.is_owner IS TRUE
AND t.is_default IS FALSE
AND t.deleted_at IS NULL;")
(def ^:private schema:get-teams-result
[:vector schema:team])
(sv/defmethod ::get-teams
"List teams for which current user is owner"
{::doc/added "2.14"
::sm/params [:map]
::sm/result schema:get-teams-result}
[cfg {:keys [::rpc/profile-id]}]
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
(->> (db/exec! cfg [sql:get-teams current-user-id])
(map #(select-keys % [:id :name])))))
;; ---- API: notify-team-change
(def ^:private schema:notify-team-change
[:map
[:id ::sm/uuid]
[:organization-id ::sm/text]])
(sv/defmethod ::notify-team-change
"Notify to Penpot a team change from nitrate"
{::doc/added "2.14"
::sm/params schema:notify-team-change
::rpc/auth false}
[cfg {:keys [id organization-id organization-name]}]
(let [msgbus (::mbus/msgbus cfg)]
(mbus/pub! msgbus
;;TODO There is a bug on dashboard with teams notifications.
;;For now we send it to uuid/zero instead of team-id
:topic uuid/zero
:message {:type :team-org-change
:team-id id
:organization-id organization-id
:organization-name organization-name})))

View File

@@ -0,0 +1,183 @@
;; 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.management.subscription
(:require
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.time :as ct]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as doc]
[app.util.services :as sv]))
;; ---- RPC METHOD: AUTHENTICATE
(def ^:private
schema:authenticate-params
[:map {:title "authenticate-params"}])
(def ^:private
schema:authenticate-result
[:map {:title "authenticate-result"}
[:profile-id ::sm/uuid]])
(sv/defmethod ::auth
{::doc/added "2.12"
::sm/params schema:authenticate-params
::sm/result schema:authenticate-result}
[_ {:keys [::rpc/profile-id]}]
{:profile-id profile-id})
;; ---- RPC METHOD: GET-CUSTOMER
;; FIXME: move to app.common.time
(def ^:private schema:timestamp
(sm/type-schema
{:type ::timestamp
:pred ct/inst?
:type-properties
{:title "inst"
:description "The same as :app.common.time/inst but encodes to epoch"
:error/message "should be an instant"
:gen/gen (->> (sg/small-int)
(sg/fmap (fn [v] (ct/inst v))))
:decode/string #(some-> % ct/inst)
:encode/string #(some-> % inst-ms)
:decode/json #(some-> % ct/inst)
:encode/json #(some-> % inst-ms)}}))
(def ^:private schema:subscription
[:map {:title "Subscription"}
[:id ::sm/text]
[:customer-id ::sm/text]
[:type [:enum
"unlimited"
"professional"
"enterprise"]]
[:status [:enum
"active"
"canceled"
"incomplete"
"incomplete_expired"
"past_due"
"paused"
"trialing"
"unpaid"]]
[:billing-period [:enum
"month"
"day"
"week"
"year"]]
[:quantity :int]
[:description [:maybe ::sm/text]]
[:created-at schema:timestamp]
[:start-date [:maybe schema:timestamp]]
[:ended-at [:maybe schema:timestamp]]
[:trial-end [:maybe schema:timestamp]]
[:trial-start [:maybe schema:timestamp]]
[:cancel-at [:maybe schema:timestamp]]
[:canceled-at [:maybe schema:timestamp]]
[:current-period-end [:maybe schema:timestamp]]
[:current-period-start [:maybe schema:timestamp]]
[:cancel-at-period-end :boolean]
[:cancellation-details
[:map {:title "CancellationDetails"}
[:comment [:maybe ::sm/text]]
[:reason [:maybe ::sm/text]]
[:feedback [:maybe
[:enum
"customer_service"
"low_quality"
"missing_feature"
"other"
"switched_service"
"too_complex"
"too_expensive"
"unused"]]]]]])
(def ^:private sql:get-customer-slots
"WITH teams AS (
SELECT tpr.team_id AS id,
tpr.profile_id AS profile_id
FROM team_profile_rel AS tpr
WHERE tpr.is_owner IS true
AND tpr.profile_id = ?
), teams_with_slots AS (
SELECT tpr.team_id AS id,
count(*) AS total
FROM team_profile_rel AS tpr
WHERE tpr.team_id IN (SELECT id FROM teams)
AND tpr.can_edit IS true
GROUP BY 1
ORDER BY 2
)
SELECT max(total) AS total FROM teams_with_slots;")
(defn- get-customer-slots
[cfg profile-id]
(let [result (db/exec-one! cfg [sql:get-customer-slots profile-id])]
(:total result)))
(def ^:private schema:get-customer-params
[:map])
(def ^:private schema:get-customer-result
[:map
[:id ::sm/uuid]
[:name :string]
[:num-editors ::sm/int]
[:subscription {:optional true} schema:subscription]])
(sv/defmethod ::get-customer
{::doc/added "2.12"
::sm/params schema:get-customer-params
::sm/result schema:get-customer-result}
[cfg {:keys [::rpc/profile-id]}]
(let [profile (profile/get-profile cfg profile-id)]
{:id (get profile :id)
:name (get profile :fullname)
:email (get profile :email)
:num-editors (get-customer-slots cfg profile-id)
:subscription (-> profile :props :subscription)}))
;; ---- RPC METHOD: GET-CUSTOMER
(def ^:private schema:update-customer-params
[:map
[:subscription [:maybe schema:subscription]]])
(def ^:private schema:update-customer-result
[:map])
(sv/defmethod ::update-customer
{::doc/added "2.12"
::sm/params schema:update-customer-params
::sm/result schema:update-customer-result}
[cfg {:keys [::rpc/profile-id subscription]}]
(let [{:keys [props] :as profile}
(profile/get-profile cfg profile-id ::db/for-update true)
props
(assoc props :subscription subscription)]
(l/dbg :hint "update customer"
:profile-id (str profile-id)
:subscription-type (get subscription :type)
:subscription-status (get subscription :status)
:subscription-quantity (get subscription :quantity))
(db/update! cfg :profile
{:props (db/tjson props)}
{:id profile-id}
{::db/return-keys false})
nil))

View File

@@ -17,7 +17,6 @@
[app.setup.templates] [app.setup.templates]
[buddy.core.codecs :as bc] [buddy.core.codecs :as bc]
[buddy.core.nonce :as bn] [buddy.core.nonce :as bn]
[cuerdas.core :as str]
[integrant.core :as ig])) [integrant.core :as ig]))
(defn- generate-random-key (defn- generate-random-key
@@ -89,38 +88,7 @@
(-> (get-all-props conn) (-> (get-all-props conn)
(assoc :secret-key secret) (assoc :secret-key secret)
(assoc :tokens-key (keys/derive secret :salt "tokens")) (assoc :tokens-key (keys/derive secret :salt "tokens"))
(assoc :management-key (keys/derive secret :salt "management"))
(update :instance-id handle-instance-id conn (db/read-only? pool))))))) (update :instance-id handle-instance-id conn (db/read-only? pool)))))))
(sm/register! ::props [:map-of :keyword ::sm/any]) (sm/register! ::props [:map-of :keyword ::sm/any])
(defmethod ig/init-key ::shared-keys
[_ {:keys [::props] :as cfg}]
(let [secret (get props :secret-key)]
(d/without-nils
{:exporter
(let [key (or (get cfg :exporter)
(-> (keys/derive secret :salt "exporter")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "exporter key is disabled because empty string found")
nil)
(do
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
key)))
:nitrate
(let [key (or (get cfg :nitrate)
(-> (keys/derive secret :salt "nitrate")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "nitrate key is disabled because empty string found")
nil)
(do
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
key)))})))

View File

@@ -9,35 +9,48 @@
modification of time offset (useful for testing and time adjustments)." modification of time offset (useful for testing and time adjustments)."
(:require (:require
[app.common.logging :as l] [app.common.logging :as l]
[app.common.time :as ct])) [app.common.time :as ct]
[app.setup :as-alias setup]
[integrant.core :as ig])
(:import
java.time.Clock
java.time.Duration
java.time.Instant
java.time.ZoneId))
(defonce state (defonce current
(atom {})) (atom {:clock (Clock/systemDefaultZone)
:offset nil}))
(defn assign-offset (defmethod ig/init-key ::setup/clock
"Assign virtual clock offset to a specific user. Is the responsability [_ _]
of RPC module to properly bind the correct clock to the user (add-watch current ::common
request." (fn [_ _ _ {:keys [clock offset]}]
[profile-id duration] (let [clock (if (ct/duration? offset)
(swap! state (fn [state] (Clock/offset ^Clock clock
(if (nil? duration) ^Duration offset)
(dissoc state profile-id) clock)]
(assoc state profile-id duration))))) (l/wrn :hint "altering clock" :clock (str clock))
(alter-var-root #'ct/*clock* (constantly clock))))))
(defn get-offset
[profile-id]
(get @state profile-id))
(defn get-clock (defmethod ig/halt-key! ::setup/clock
[profile-id] [_ _]
(if-let [offset (get-offset profile-id)] (remove-watch current ::common))
(ct/offset-clock offset)
(ct/get-system-clock)))
(defn set-global-clock (defn fixed
"Get fixed clock, mainly used in tests"
[instant]
(Clock/fixed ^Instant (ct/inst instant)
^ZoneId (ZoneId/of "Z")))
(defn set-offset!
[duration]
(swap! current assoc :offset (some-> duration ct/duration)))
(defn set-clock!
([] ([]
(set-global-clock (ct/get-system-clock))) (swap! current assoc :clock (Clock/systemDefaultZone)))
([clock] ([clock]
(assert (ct/clock? clock) "expected valid clock instance") (when (instance? Clock clock)
(l/wrn :hint "altering clock" :clock (str clock)) (swap! current assoc :clock clock))))
(alter-var-root #'ct/*clock* (constantly clock))))

View File

@@ -8,7 +8,7 @@
"A generic asynchronous events notifications subsystem; used mainly "A generic asynchronous events notifications subsystem; used mainly
for mark event points in functions and be able to attach listeners for mark event points in functions and be able to attach listeners
to them. Mainly used in http.sse for progress reporting." to them. Mainly used in http.sse for progress reporting."
(:refer-clojure :exclude [run!]) (:refer-clojure :exclude [tap run!])
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]

View File

@@ -86,7 +86,7 @@
(t/deftest shared-key-auth (t/deftest shared-key-auth
(let [handler (#'app.http.middleware/wrap-shared-key-auth (let [handler (#'app.http.middleware/wrap-shared-key-auth
(fn [req] {::yres/status 200}) (fn [req] {::yres/status 200})
{:test1 "secret-key"})] "secret-key")]
(let [response (handler (->DummyRequest {} {}))] (let [response (handler (->DummyRequest {} {}))]
(t/is (= 403 (::yres/status response)))) (t/is (= 403 (::yres/status response))))
@@ -95,9 +95,6 @@
(t/is (= 403 (::yres/status response)))) (t/is (= 403 (::yres/status response))))
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))] (let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))]
(t/is (= 403 (::yres/status response))))
(let [response (handler (->DummyRequest {"x-shared-key" "test1 secret-key"} {}))]
(t/is (= 200 (::yres/status response)))))) (t/is (= 200 (::yres/status response))))))
(t/deftest access-token-authz (t/deftest access-token-authz

View File

@@ -17,6 +17,7 @@
[app.db.sql :as sql] [app.db.sql :as sql]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto] [app.storage :as sto]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
@@ -133,7 +134,7 @@
;; this will run pending task triggered by deleting user snapshot ;; this will run pending task triggered by deleting user snapshot
(th/run-pending-tasks!) (th/run-pending-tasks!)
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
;; delete 2 snapshots and 2 file data entries ;; delete 2 snapshots and 2 file data entries
(t/is (= 4 (:processed res))))))))) (t/is (= 4 (:processed res)))))))))

View File

@@ -19,6 +19,7 @@
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.setup.clock :as clock]
[app.storage :as sto] [app.storage :as sto]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
@@ -841,7 +842,7 @@
out (th/command! data) out (th/command! data)
error (:error out)] error (:error out)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found)))) (t/is (th/ex-of-type? error :not-found))))
@@ -863,7 +864,7 @@
out (th/command! data) out (th/command! data)
error (:error out)] error (:error out)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found)))) (t/is (th/ex-of-type? error :not-found))))
@@ -921,7 +922,7 @@
(t/is (= 0 (:processed result)))) (t/is (= 0 (:processed result))))
;; run permanent deletion ;; run permanent deletion
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed result))))) (t/is (= 3 (:processed result)))))
@@ -1261,7 +1262,7 @@
(t/is (= 1 (count rows))) (t/is (= 1 (count rows)))
(t/is (every? #(some? (:data %)) rows))) (t/is (every? #(some? (:data %)) rows)))
;; Mark the file ellegible again for GC ;; Mark the file ellegible again for GC
(th/db-update! :file (th/db-update! :file
{:has-media-trimmed false} {:has-media-trimmed false}
{:id (:id file)}) {:id (:id file)})
@@ -1318,7 +1319,7 @@
{:file-id (:id file) {:file-id (:id file)
:type "fragment"} :type "fragment"}
{:order-by [:created-at]})] {:order-by [:created-at]})]
;; (pp/pprint rows) ;; (pp/pprint rows)
(t/is (= 2 (count rows))) (t/is (= 2 (count rows)))
(t/is (nil? (:data row1))) (t/is (nil? (:data row1)))
(t/is (= "storage" (:backend row1))) (t/is (= "storage" (:backend row1)))
@@ -1874,7 +1875,7 @@
file-id (uuid/next) file-id (uuid/next)
now (ct/inst "2025-10-31T00:00:00Z")] now (ct/inst "2025-10-31T00:00:00Z")]
(binding [ct/*clock* (ct/fixed-clock now)] (binding [ct/*clock* (clock/fixed now)]
(let [data {::th/type :create-file (let [data {::th/type :create-file
::rpc/profile-id (:id prof) ::rpc/profile-id (:id prof)
:project-id proj-id :project-id proj-id
@@ -1936,7 +1937,7 @@
file-id (uuid/next) file-id (uuid/next)
now (ct/inst "2025-10-31T00:00:00Z")] now (ct/inst "2025-10-31T00:00:00Z")]
(binding [ct/*clock* (ct/fixed-clock now)] (binding [ct/*clock* (clock/fixed now)]
(let [data {::th/type :create-file (let [data {::th/type :create-file
::rpc/profile-id (:id prof) ::rpc/profile-id (:id prof)
:project-id proj-id :project-id proj-id
@@ -1999,7 +2000,7 @@
team-id (:default-team-id profile) team-id (:default-team-id profile)
now (ct/inst "2025-10-31T00:00:00Z")] now (ct/inst "2025-10-31T00:00:00Z")]
(binding [ct/*clock* (ct/fixed-clock now)] (binding [ct/*clock* (clock/fixed now)]
(let [project (th/create-project* 1 {:profile-id (:id profile) (let [project (th/create-project* 1 {:profile-id (:id profile)
:team-id team-id}) :team-id team-id})
file (th/create-file* 1 {:profile-id (:id profile) file (th/create-file* 1 {:profile-id (:id profile)

View File

@@ -85,7 +85,7 @@
(t/is (map? (:result out)))) (t/is (map? (:result out))))
;; run the task again ;; run the task again
(let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))] (let [res (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 31}))]
(th/run-task! "storage-gc-touched" {}))] (th/run-task! "storage-gc-touched" {}))]
(t/is (= 2 (:freeze res)))) (t/is (= 2 (:freeze res))))
@@ -136,7 +136,7 @@
(t/is (some? (sto/get-object storage (:media-id row2)))) (t/is (some? (sto/get-object storage (:media-id row2))))
;; run the task again ;; run the task again
(let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))] (let [res (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 31}))]
(th/run-task! :storage-gc-touched {}))] (th/run-task! :storage-gc-touched {}))]
(t/is (= 1 (:delete res))) (t/is (= 1 (:delete res)))
(t/is (= 0 (:freeze res)))) (t/is (= 0 (:freeze res))))
@@ -147,7 +147,7 @@
;; Run the storage gc deleted task, it should permanently delete ;; Run the storage gc deleted task, it should permanently delete
;; all storage objects related to the deleted thumbnails ;; all storage objects related to the deleted thumbnails
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :storage-gc-deleted {})] (let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted res))))) (t/is (= 1 (:deleted res)))))
@@ -247,7 +247,7 @@
;; Run the storage gc deleted task, it should permanently delete ;; Run the storage gc deleted task, it should permanently delete
;; all storage objects related to the deleted thumbnails ;; all storage objects related to the deleted thumbnails
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :storage-gc-deleted {})] (let [result (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted result))))) (t/is (= 1 (:deleted result)))))

View File

@@ -12,6 +12,7 @@
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto] [app.storage :as sto]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
@@ -146,7 +147,7 @@
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res)))) (t/is (= 0 (:delete res))))
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res)))) (t/is (= 2 (:processed res))))
@@ -207,7 +208,7 @@
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res)))) (t/is (= 0 (:delete res))))
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed res)))) (t/is (= 1 (:processed res))))
@@ -267,7 +268,7 @@
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res)))) (t/is (= 0 (:delete res))))
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed res)))) (t/is (= 1 (:processed res))))

View File

@@ -536,7 +536,7 @@
:token rtoken} :token rtoken}
{:keys [result error] :as out} (th/command! data)] {:keys [result error] :as out} (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? error)) (t/is (nil? error))
(t/is (map? result)) (t/is (map? result))
(t/is (string? (:invitation-token result)))))) (t/is (string? (:invitation-token result))))))

View File

@@ -12,6 +12,7 @@
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t])) [clojure.test :as t]))
@@ -30,7 +31,7 @@
:team-id (:id team) :team-id (:id team)
:name "test project"} :name "test project"}
out (th/command! data)] out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
@@ -93,7 +94,7 @@
:id project-id} :id project-id}
out (th/command! data)] out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out)))) (t/is (nil? (:result out))))
@@ -227,7 +228,7 @@
(t/is (= 0 (count result))))) (t/is (= 0 (count result)))))
;; run permanent deletion ;; run permanent deletion
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed result))))) (t/is (= 1 (:processed result)))))

View File

@@ -13,6 +13,7 @@
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto] [app.storage :as sto]
[app.tokens :as tokens] [app.tokens :as tokens]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
@@ -525,7 +526,7 @@
(t/is (= :not-found (:type edata))))) (t/is (= :not-found (:type edata)))))
;; run permanent deletion ;; run permanent deletion
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed result))))) (t/is (= 2 (:processed result)))))
@@ -582,7 +583,7 @@
(t/is (= 1 (count rows))) (t/is (= 1 (count rows)))
(t/is (ct/inst? (:deleted-at (first rows))))) (t/is (ct/inst? (:deleted-at (first rows)))))
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
(t/is (= 7 (:processed result))))))) (t/is (= 7 (:processed result)))))))

View File

@@ -11,6 +11,7 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto] [app.storage :as sto]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
@@ -98,14 +99,14 @@
::sto/expired-at (ct/in-future {:hours 1}) ::sto/expired-at (ct/in-future {:hours 1})
:content-type "text/plain"})] :content-type "text/plain"})]
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 0}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 0}))]
(let [res (th/run-task! :storage-gc-deleted {})] (let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted res))))) (t/is (= 1 (:deleted res)))))
(let [res (th/db-exec-one! ["select count(*) from storage_object;"])] (let [res (th/db-exec-one! ["select count(*) from storage_object;"])]
(t/is (= 2 (:count res)))) (t/is (= 2 (:count res))))
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 61}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 61}))]
(let [res (th/run-task! :storage-gc-deleted {})] (let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted res))))) (t/is (= 1 (:deleted res)))))
@@ -330,22 +331,22 @@
:content-type "text/plain"})] :content-type "text/plain"})]
(binding [ct/*clock* (ct/fixed-clock now)] (binding [ct/*clock* (clock/fixed now)]
(let [res (th/run-task! :storage-gc-touched {})] (let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res))))) (t/is (= 0 (:delete res)))))
(binding [ct/*clock* (ct/fixed-clock (ct/plus now {:minutes 1}))] (binding [ct/*clock* (clock/fixed (ct/plus now {:minutes 1}))]
(let [res (th/run-task! :storage-gc-touched {})] (let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 1 (:delete res))))) (t/is (= 1 (:delete res)))))
(binding [ct/*clock* (ct/fixed-clock (ct/plus now {:hours 1}))] (binding [ct/*clock* (clock/fixed (ct/plus now {:hours 1}))]
(let [res (th/run-task! :storage-gc-deleted {})] (let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 0 (:deleted res))))) (t/is (= 0 (:deleted res)))))
(binding [ct/*clock* (ct/fixed-clock (ct/plus now {:hours 2}))] (binding [ct/*clock* (clock/fixed (ct/plus now {:hours 2}))]
(let [res (th/run-task! :storage-gc-deleted {})] (let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 0 (:deleted res))))))) (t/is (= 0 (:deleted res)))))))

1145
backend/yarn.lock Normal file
View File

File diff suppressed because it is too large Load Diff

7
common/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View File

@@ -59,7 +59,7 @@
thheller/shadow-cljs {:mvn/version "3.2.0"} thheller/shadow-cljs {:mvn/version "3.2.0"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"} com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"} com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "0.4.6"} criterium/criterium {:mvn/version "RELEASE"}
mockery/mockery {:mvn/version "RELEASE"}} mockery/mockery {:mvn/version "RELEASE"}}
:extra-paths ["test" "dev"]} :extra-paths ["test" "dev"]}

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC", "author": "Kaleidos INC",
"private": true, "private": true,
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264", "packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c",
"type": "module", "type": "module",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -23,9 +23,9 @@
"fmt:clj:check": "cljfmt check --parallel=false src/ test/", "fmt:clj:check": "cljfmt check --parallel=false src/ test/",
"fmt:clj": "cljfmt fix --parallel=true src/ test/", "fmt:clj": "cljfmt fix --parallel=true src/ test/",
"lint:clj": "clj-kondo --parallel=true --lint src/", "lint:clj": "clj-kondo --parallel=true --lint src/",
"lint": "pnpm run lint:clj", "lint": "yarn run lint:clj",
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests/ --exec 'node target/tests/test.js'\"", "watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests/ --exec 'node target/tests/test.js'\"",
"build:test": "clojure -M:dev:shadow-cljs compile test", "build:test": "clojure -M:dev:shadow-cljs compile test",
"test": "pnpm run build:test && node target/tests/test.js" "test": "yarn run build:test && node target/tests/test.js"
} }
} }

489
common/pnpm-lock.yaml generated
View File

@@ -1,489 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
date-fns:
specifier: ^4.1.0
version: 4.1.0
devDependencies:
concurrently:
specifier: ^9.1.2
version: 9.2.1
nodemon:
specifier: ^3.1.10
version: 3.1.11
source-map-support:
specifier: ^0.5.21
version: 0.5.21
ws:
specifier: ^8.18.2
version: 8.18.3
packages:
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
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==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
concurrently@9.2.1:
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
engines: {node: '>=18'}
hasBin: true
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nodemon@3.1.11:
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
engines: {node: '>=10'}
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
pstree.remy@1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
hasBin: true
shell-quote@1.8.3:
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
engines: {node: '>= 0.4'}
simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
supports-color@8.1.1:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
touch@3.1.1:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
hasBin: true
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
snapshots:
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
balanced-match@1.0.2: {}
binary-extensions@2.3.0: {}
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
braces@3.0.3:
dependencies:
fill-range: 7.1.1
buffer-from@1.1.2: {}
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
cliui@8.0.1:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
concat-map@0.0.1: {}
concurrently@9.2.1:
dependencies:
chalk: 4.1.2
rxjs: 7.8.2
shell-quote: 1.8.3
supports-color: 8.1.1
tree-kill: 1.2.2
yargs: 17.7.2
date-fns@4.1.0: {}
debug@4.4.3(supports-color@5.5.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 5.5.0
emoji-regex@8.0.0: {}
escalade@3.2.0: {}
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
fsevents@2.3.3:
optional: true
get-caller-file@2.0.5: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
has-flag@3.0.0: {}
has-flag@4.0.0: {}
ignore-by-default@1.0.1: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
ms@2.1.3: {}
nodemon@3.1.11:
dependencies:
chokidar: 3.6.0
debug: 4.4.3(supports-color@5.5.0)
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
semver: 7.7.3
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.1
undefsafe: 2.0.5
normalize-path@3.0.0: {}
picomatch@2.3.1: {}
pstree.remy@1.1.8: {}
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
require-directory@2.1.1: {}
rxjs@7.8.2:
dependencies:
tslib: 2.8.1
semver@7.7.3: {}
shell-quote@1.8.3: {}
simple-update-notifier@2.0.0:
dependencies:
semver: 7.7.3
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
source-map@0.6.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-color@8.1.1:
dependencies:
has-flag: 4.0.0
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
touch@3.1.1: {}
tree-kill@1.2.2: {}
tslib@2.8.1: {}
undefsafe@2.0.5: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
ws@8.18.3: {}
y18n@5.0.8: {}
yargs-parser@21.1.1: {}
yargs@17.7.2:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1

View File

View File

@@ -3,5 +3,5 @@
set -ex set -ex
corepack enable; corepack enable;
corepack install; corepack install;
pnpm install; yarn install;
pnpm run test; yarn run test;

View File

@@ -1092,9 +1092,9 @@
(if (number? num) (if (number? num)
(try (try
(let [num-str (mth/to-fixed num precision) (let [num-str (mth/to-fixed num precision)
;; Remove all trailing zeros after the comma 100.00000 ;; Remove all trailing zeros after the comma 100.00000
num-str (str/replace num-str trail-zeros-regex-1 "")] num-str (str/replace num-str trail-zeros-regex-1 "")]
;; Remove trailing zeros after a decimal number: 0.001|00| ;; Remove trailing zeros after a decimal number: 0.001|00|
(if-let [m (re-find trail-zeros-regex-2 num-str)] (if-let [m (re-find trail-zeros-regex-2 num-str)]
(str/replace num-str (first m) (second m)) (str/replace num-str (first m) (second m))
num-str)) num-str))

View File

@@ -241,20 +241,7 @@
(with-out-str (with-out-str
(print-all cause))))) (print-all cause)))))
(defn print-throwable #?(:clj
[cause & {:as opts}] (defn print-throwable
#?(:clj [cause & {:as opts}]
(println (format-throwable cause opts)) (println (format-throwable cause opts))))
:cljs
(let [prefix (get opts :prefix "exception")
title (str prefix ": " (ex-message cause))
exdata (ex-data cause)]
(js/console.group title)
(when-let [explain (get exdata ::sm/explain)]
(println (sm/humanize-explain explain)))
(js/console.log "\nData:")
(pp/pprint (dissoc exdata ::sm/explain))
(js/console.log "\nTrace:")
(js/console.error (.-stack cause)))))

View File

@@ -44,7 +44,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Set parent to root frame. ; Set parent to root frame.
(log/debug :hint " -> set to " :parent-id uuid/zero) (log/debug :hint " -> set to " :parent-id uuid/zero)
(assoc shape :parent-id uuid/zero))] (assoc shape :parent-id uuid/zero))]
@@ -57,7 +57,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [parent-shape] (fn [parent-shape]
;; Add shape to parent's children list ; Add shape to parent's children list
(log/debug :hint " -> add children to" :parent-id (:id parent-shape)) (log/debug :hint " -> add children to" :parent-id (:id parent-shape))
(update parent-shape :shapes conj (:id shape)))] (update parent-shape :shapes conj (:id shape)))]
@@ -70,7 +70,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Remove duplicated ; Remove duplicated
(log/debug :hint " -> remove duplicated children") (log/debug :hint " -> remove duplicated children")
(update shape :shapes distinct))] (update shape :shapes distinct))]
@@ -102,7 +102,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Locate the first frame in parents and set frame-id to it. ; Locate the first frame in parents and set frame-id to it.
(let [page (ctpl/get-page file-data page-id) (let [page (ctpl/get-page file-data page-id)
frame (cfh/get-frame (:objects page) (:parent-id shape)) frame (cfh/get-frame (:objects page) (:parent-id shape))
frame-id (or (:id frame) uuid/zero)] frame-id (or (:id frame) uuid/zero)]
@@ -118,7 +118,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Locate the first frame in parents and set frame-id to it. ; Locate the first frame in parents and set frame-id to it.
(let [page (ctpl/get-page file-data page-id) (let [page (ctpl/get-page file-data page-id)
frame (cfh/get-frame (:objects page) (:parent-id shape)) frame (cfh/get-frame (:objects page) (:parent-id shape))
frame-id (or (:id frame) uuid/zero)] frame-id (or (:id frame) uuid/zero)]
@@ -134,7 +134,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Set the :shape as main instance root ; Set the :shape as main instance root
(log/debug :hint " -> set :main-instance") (log/debug :hint " -> set :main-instance")
(assoc shape :main-instance true))] (assoc shape :main-instance true))]
@@ -147,13 +147,12 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Set :component-file to local file ; Set :component-file to local file
(log/debug :hint " -> set :component-file to local file") (log/debug :hint " -> set :component-file to local file")
(assoc shape :component-file (:id file-data)))] (assoc shape :component-file (:id file-data)))]
; There is no solution that may recover it with confidence
;; There is no solution that may recover it with confidence ;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") ;; shape)]
;; shape)]
(log/dbg :hint "repairing shape :component-main-external" :id (:id shape) :name (:name shape) :page-id page-id) (log/dbg :hint "repairing shape :component-main-external" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id) (-> (pcb/empty-changes nil page-id)
@@ -167,12 +166,12 @@
repair-shape repair-shape
(fn [shape] (fn [shape]
;; Detach the shape and convert it to non instance. ; Detach the shape and convert it to non instance.
(log/debug :hint " -> detach shape" :shape-id (:id shape)) (log/debug :hint " -> detach shape" :shape-id (:id shape))
(ctk/detach-shape shape))] (ctk/detach-shape shape))]
;; There is no solution that may recover it with confidence ; There is no solution that may recover it with confidence
;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") ;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
;; shape)] ;; shape)]
(log/dbg :hint "repairing shape :component-not-found" :id (:id shape) :name (:name shape) :page-id page-id) (log/dbg :hint "repairing shape :component-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id) (-> (pcb/empty-changes nil page-id)
@@ -185,7 +184,7 @@
repair-component repair-component
(fn [component] (fn [component]
;; Assign main instance in the component to current shape ; Assign main instance in the component to current shape
(log/debug :hint " -> assign main-instance-id" :component-id (:id component)) (log/debug :hint " -> assign main-instance-id" :component-id (:id component))
(assoc component :main-instance-id (:id shape))) (assoc component :main-instance-id (:id shape)))
@@ -208,7 +207,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-component (let [repair-component
(fn [component] (fn [component]
;; Assign main instance in the component to current shape ; Assign main instance in the component to current shape
(log/debug :hint " -> assign main-instance-page" :component-id (:id component)) (log/debug :hint " -> assign main-instance-page" :component-id (:id component))
(assoc component :main-instance-page page-id))] (assoc component :main-instance-page page-id))]
(log/dbg :hint "repairing shape :invalid-main-instance-page" :id (:id shape) :name (:name shape) :page-id page-id) (log/dbg :hint "repairing shape :invalid-main-instance-page" :id (:id shape) :name (:name shape) :page-id page-id)
@@ -220,7 +219,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; There is no solution that may recover it with confidence ; There is no solution that may recover it with confidence
(log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
shape)] shape)]
@@ -233,7 +232,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Unset the :shape as main instance root ; Unset the :shape as main instance root
(log/debug :hint " -> unset :main-instance") (log/debug :hint " -> unset :main-instance")
(dissoc shape :main-instance))] (dissoc shape :main-instance))]
@@ -246,7 +245,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Convert the shape in a top copy root. ; Convert the shape in a top copy root.
(log/debug :hint " -> set :component-root") (log/debug :hint " -> set :component-root")
(assoc shape :component-root true))] (assoc shape :component-root true))]
@@ -259,7 +258,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Convert the shape in a nested copy root. ; Convert the shape in a nested copy root.
(log/debug :hint " -> unset :component-root") (log/debug :hint " -> unset :component-root")
(dissoc shape :component-root))] (dissoc shape :component-root))]
@@ -308,8 +307,8 @@
(log/debug :hint " -> detach shape" :shape-id (:id shape)) (log/debug :hint " -> detach shape" :shape-id (:id shape))
(ctk/detach-shape shape))] (ctk/detach-shape shape))]
;; If the shape still refers to the remote component, try to find the corresponding near one ; If the shape still refers to the remote component, try to find the corresponding near one
;; and link to it. If not, detach the shape. ; and link to it. If not, detach the shape.
(log/dbg :hint "repairing shape :ref-shape-not-found" :id (:id shape) :name (:name shape) :page-id page-id) (log/dbg :hint "repairing shape :ref-shape-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
(if (some? matching-shape) (if (some? matching-shape)
(-> (pcb/empty-changes nil page-id) (-> (pcb/empty-changes nil page-id)
@@ -325,7 +324,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Convert shape in a normal copy, removing nested copy status ; Convert shape in a normal copy, removing nested copy status
(log/debug :hint " -> unhead shape") (log/debug :hint " -> unhead shape")
(ctk/unhead-shape shape))] (ctk/unhead-shape shape))]
@@ -338,7 +337,7 @@
[_ {:keys [shape page-id args] :as error} file-data _] [_ {:keys [shape page-id args] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Convert shape in a nested head, adding component info ; Convert shape in a nested head, adding component info
(log/debug :hint " -> reroot shape") (log/debug :hint " -> reroot shape")
(ctk/rehead-shape shape (:component-file args) (:component-id args)))] (ctk/rehead-shape shape (:component-file args) (:component-id args)))]
@@ -351,9 +350,8 @@
[_ {:keys [shape args] :as error} file-data _] [_ {:keys [shape args] :as error} file-data _]
(let [repair-component (let [repair-component
(fn [component] (fn [component]
(let [objects (:objects component) (let [objects (:objects component) ;; we only have encounter this on deleted components,
;; we only have encounter this on deleted components, ;; so the relevant objects are inside the component
;; so the relevant objects are inside the component
to-detach (->> (:cycles-ids args) to-detach (->> (:cycles-ids args)
(map #(get objects %)) (map #(get objects %))
(map #(ctn/get-head-shape objects %)) (map #(ctn/get-head-shape objects %))
@@ -380,7 +378,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Remove shape-ref ; Remove shape-ref
(log/debug :hint " -> unset :shape-ref") (log/debug :hint " -> unset :shape-ref")
(dissoc shape :shape-ref))] (dissoc shape :shape-ref))]
@@ -393,7 +391,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Convert the shape in a nested main head. ; Convert the shape in a nested main head.
(log/debug :hint " -> unset :component-root") (log/debug :hint " -> unset :component-root")
(dissoc shape :component-root))] (dissoc shape :component-root))]
@@ -406,7 +404,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Convert the shape in a top main head. ; Convert the shape in a top main head.
(log/debug :hint " -> set :component-root") (log/debug :hint " -> set :component-root")
(assoc shape :component-root true))] (assoc shape :component-root true))]
@@ -420,7 +418,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Convert the shape in a nested copy head. ; Convert the shape in a nested copy head.
(log/debug :hint " -> unset :component-root") (log/debug :hint " -> unset :component-root")
(dissoc shape :component-root))] (dissoc shape :component-root))]
@@ -433,7 +431,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Convert the shape in a top copy root. ; Convert the shape in a top copy root.
(log/debug :hint " -> set :component-root") (log/debug :hint " -> set :component-root")
(assoc shape :component-root true))] (assoc shape :component-root true))]
@@ -446,7 +444,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Detach the shape and convert it to non instance. ; Detach the shape and convert it to non instance.
(log/debug :hint " -> detach shape" :shape-id (:id shape)) (log/debug :hint " -> detach shape" :shape-id (:id shape))
(ctk/detach-shape shape))] (ctk/detach-shape shape))]
@@ -459,7 +457,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Detach the shape and convert it to non instance. ; Detach the shape and convert it to non instance.
(log/debug :hint " -> detach shape" :shape-id (:id shape)) (log/debug :hint " -> detach shape" :shape-id (:id shape))
(ctk/detach-shape shape))] (ctk/detach-shape shape))]
@@ -472,7 +470,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; There is no solution that may recover it with confidence ; There is no solution that may recover it with confidence
(log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.") (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
shape)] shape)]
@@ -485,7 +483,7 @@
[_ {:keys [shape page-id] :as error} file-data _] [_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape (let [repair-shape
(fn [shape] (fn [shape]
;; Convert the shape in a frame. ; Convert the shape in a frame.
(log/debug :hint " -> set :type :frame") (log/debug :hint " -> set :type :frame")
(assoc shape :type :frame (assoc shape :type :frame
:fills [] :fills []
@@ -504,7 +502,7 @@
[_ {:keys [shape] :as error} file-data _] [_ {:keys [shape] :as error} file-data _]
(let [repair-component (let [repair-component
(fn [component] (fn [component]
;; Remove the objects key, or set it to {} if the component is deleted ; Remove the objects key, or set it to {} if the component is deleted
(if (:deleted component) (if (:deleted component)
(do (do
(log/debug :hint " -> set :objects {}") (log/debug :hint " -> set :objects {}")

View File

@@ -430,8 +430,8 @@
(assoc :frame-id frame-id) (assoc :frame-id frame-id)
(assoc :svg-viewbox vbox) (assoc :svg-viewbox vbox)
(assoc :svg-attrs props) (assoc :svg-attrs props)
;; We need to ensure fills are empty on import process ;; We need to ensure fills are empty on import process
;; because setup-shape assings one by default. ;; because setup-shape assings one by default.
(assoc :fills []) (assoc :fills [])
(merge radius-attrs))))) (merge radius-attrs)))))

View File

@@ -62,7 +62,6 @@
#{:audit-log #{:audit-log
:audit-log-archive :audit-log-archive
:audit-log-gc :audit-log-gc
:audit-log-logger
:auto-file-snapshot :auto-file-snapshot
;; enables the `/api/doc` endpoint that lists all the rpc methods available. ;; enables the `/api/doc` endpoint that lists all the rpc methods available.
:backend-api-doc :backend-api-doc
@@ -148,10 +147,7 @@
;; A temporal flag, enables backend code use more extensivelly ;; A temporal flag, enables backend code use more extensivelly
;; redis for caching data ;; redis for caching data
:redis-cache :redis-cache})
;; Activates the nitrate module
:nitrate})
(def all-flags (def all-flags
(set/union email login varia)) (set/union email login varia))

View File

@@ -75,25 +75,20 @@
#?(:cljs #?(:cljs
(defn ->clj (defn ->clj
[o & {:keys [key-fn val-fn recursive] :or {key-fn read-kebab-key val-fn identity recursive true}}] [o & {:keys [key-fn val-fn] :or {key-fn read-kebab-key val-fn identity}}]
(let [f (fn this-fn [x] (let [f (fn this-fn [x]
(let [x (val-fn x)] (let [x (val-fn x)]
(cond (cond
(array? x) (array? x)
(persistent! (persistent!
(.reduce ^js/Array x (.reduce ^js/Array x
#(conj! %1 (if recursive #(conj! %1 (this-fn %2))
(this-fn %2)
%2))
(transient []))) (transient [])))
(identical? (type x) js/Object) (identical? (type x) js/Object)
(persistent! (persistent!
(.reduce ^js/Array (js-keys x) (.reduce ^js/Array (js-keys x)
#(assoc! %1 (key-fn %2) #(assoc! %1 (key-fn %2) (this-fn (unchecked-get x %2)))
(if recursive
(this-fn (unchecked-get x %2))
(unchecked-get x %2)))
(transient {}))) (transient {})))
:else :else

View File

@@ -49,9 +49,9 @@
(def log-container-ids #{}) (def log-container-ids #{})
(def updatable-attrs (->> (seq (keys ctk/sync-attrs)) (def updatable-attrs (->> (seq (keys ctk/sync-attrs))
;; We don't update the flex-child attrs ;; We don't update the flex-child attrs
(remove ctk/swap-keep-attrs) (remove ctk/swap-keep-attrs)
;; We don't do automatic update of the `layout-grid-cells` property. ;; We don't do automatic update of the `layout-grid-cells` property.
(remove #(= :layout-grid-cells %)))) (remove #(= :layout-grid-cells %))))
(defn enabled-shape? (defn enabled-shape?
@@ -1898,10 +1898,10 @@
(gsh/absolute-move shape new-pos))) (gsh/absolute-move shape new-pos)))
(defn- switch-path-change-value (defn- switch-path-change-value
[prev-shape ; The shape before the switch [prev-shape ;; The shape before the switch
current-shape ; The shape after the switch (a clean copy) current-shape ;; The shape after the switch (a clean copy)
ref-shape ; The referenced shape on the main component ref-shape ;; The referenced shape on the main component
; before the switch ;; before the switch
attr] attr]
(let [old-width (-> ref-shape :selrect :width) (let [old-width (-> ref-shape :selrect :width)
new-width (-> prev-shape :selrect :width) new-width (-> prev-shape :selrect :width)
@@ -1918,9 +1918,10 @@
(defn- switch-text-change-value (defn- switch-text-change-value
[prev-content ; The :content of the text before the switch [prev-content ;; The :content of the text before the switch
current-content ; The :content of the text after the switch (a clean copy) current-content ;; The :content of the text after the switch (a clean copy)
ref-content touched] ; The :content of the referenced text on the main component before the switch ref-content touched] ;; The :content of the referenced text on the main component
;; before the switch
(let [;; We need the differences between the contents on the main (let [;; We need the differences between the contents on the main
;; components. current-content is the content of a clean copy, ;; components. current-content is the content of a clean copy,
;; so for all effects its the same as the content on its main ;; so for all effects its the same as the content on its main
@@ -2496,7 +2497,7 @@
(-> changes (-> changes
(cls/generate-delete-shapes (cls/generate-delete-shapes
file page objects (d/ordered-set (:id shape)) file page objects (d/ordered-set (:id shape))
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn :ignore-mask true :ignore-flows-for #{(:id shape)}})) {:allow-altering-copies true :ignore-children-fn ignore-swapped-fn :ignore-mask true}))
[new-shape changes] [new-shape changes]
(-> changes (-> changes
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))] (generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]
@@ -2844,8 +2845,8 @@
duplicating-component? duplicating-component?
true true
(and remove-swap-slot? (and remove-swap-slot?
;; only remove swap slot of children when the current shape ;; only remove swap slot of children when the current shape
;; is not a subinstance head nor a instance root ;; is not a subinstance head nor a instance root
(not subinstance-head?) (not subinstance-head?)
(not instance-root?)) (not instance-root?))
variant-props)) variant-props))
@@ -2901,7 +2902,7 @@
variant-props) variant-props)
changes)) changes))
;; We need to check the changes to get the ids-map ;; We need to check the changes to get the ids-map
ids-map ids-map
(into {} (into {}
(comp (comp

View File

@@ -124,11 +124,9 @@
;; on the deletion process. It should receive a shape and ;; on the deletion process. It should receive a shape and
;; return a boolean ;; return a boolean
ignore-children-fn ignore-children-fn
ignore-mask ignore-mask]
ignore-flows-for]
:or {ignore-children-fn (constantly false) :or {ignore-children-fn (constantly false)
ignore-mask false ignore-mask false}}]
ignore-flows-for #{}}}]
(let [objects (pcb/get-objects changes) (let [objects (pcb/get-objects changes)
data (pcb/get-library-data changes) data (pcb/get-library-data changes)
page-id (pcb/get-page-id changes) page-id (pcb/get-page-id changes)
@@ -138,12 +136,12 @@
ids (cfh/clean-loops objects ids) ids (cfh/clean-loops objects ids)
in-component-copy? in-component-copy?
(fn [shape-id] (fn [shape-id]
;; Look for shapes that are inside a component copy, but are ;; Look for shapes that are inside a component copy, but are
;; not the root. In this case, they must not be deleted, ;; not the root. In this case, they must not be deleted,
;; but hidden (to be able to recover them more easily). ;; but hidden (to be able to recover them more easily).
;; If we want to specifically allow altering the copies, this is ;; If we want to specifically allow altering the copies, this is
;; a special case, like a component swap, in which case we want ;; a special case, like a component swap, in which case we want
;; to delete the old shape ;; to delete the old shape
(let [shape (get objects shape-id)] (let [shape (get objects shape-id)]
(and (ctn/has-any-copy-parent? objects shape) (and (ctn/has-any-copy-parent? objects shape)
(not allow-altering-copies)))) (not allow-altering-copies))))
@@ -168,9 +166,9 @@
groups-to-unmask groups-to-unmask
(when-not ignore-mask (when-not ignore-mask
(reduce (fn [group-ids id] (reduce (fn [group-ids id]
;; When the shape to delete is the mask of a masked group, ;; When the shape to delete is the mask of a masked group,
;; the mask condition must be removed, and it must be ;; the mask condition must be removed, and it must be
;; converted to a normal group. ;; converted to a normal group.
(let [obj (lookup id) (let [obj (lookup id)
parent (lookup (:parent-id obj))] parent (lookup (:parent-id obj))]
(if (and (:masked-group parent) (if (and (:masked-group parent)
@@ -183,8 +181,8 @@
interacting-shapes interacting-shapes
(filter (fn [shape] (filter (fn [shape]
;; If any of the deleted shapes is the destination of ;; If any of the deleted shapes is the destination of
;; some interaction, this must be deleted, too. ;; some interaction, this must be deleted, too.
(let [interactions (:interactions shape)] (let [interactions (:interactions shape)]
(some #(and (ctsi/has-destination %) (some #(and (ctsi/has-destination %)
(contains? ids-to-delete (:destination %))) (contains? ids-to-delete (:destination %)))
@@ -196,8 +194,7 @@
(->> (:flows page) (->> (:flows page)
(reduce (reduce
(fn [changes [id flow]] (fn [changes [id flow]]
(if (and (id-to-delete? (:starting-frame flow)) (if (id-to-delete? (:starting-frame flow))
(not (contains? ignore-flows-for (:starting-frame flow))))
(-> changes (-> changes
(pcb/with-page page) (pcb/with-page page)
(pcb/set-flow id nil)) (pcb/set-flow id nil))
@@ -207,7 +204,7 @@
all-parents all-parents
(reduce (fn [res id] (reduce (fn [res id]
;; All parents of any deleted shape must be resized. ;; All parents of any deleted shape must be resized.
(into res (cfh/get-parent-ids objects id))) (into res (cfh/get-parent-ids objects id)))
(d/ordered-set) (d/ordered-set)
(concat ids-to-delete ids-to-hide)) (concat ids-to-delete ids-to-hide))
@@ -239,10 +236,10 @@
(recursive-find-empty-parents parents)))) (recursive-find-empty-parents parents))))
empty-parents empty-parents
;; Any parent whose children are all deleted, must be deleted too. ;; Any parent whose children are all deleted, must be deleted too.
;; If we want to specifically allow altering the copies, this is a special case, ;; If we want to specifically allow altering the copies, this is a special case,
;; for example during a component swap. in this case we are replacing a shape by ;; for example during a component swap. in this case we are replacing a shape by
;; other one, so must not delete empty parents. ;; other one, so must not delete empty parents.
(if-not allow-altering-copies (if-not allow-altering-copies
(into (d/ordered-set) (find-all-empty-parents #{})) (into (d/ordered-set) (find-all-empty-parents #{}))
#{}) #{})
@@ -274,8 +271,8 @@
guides-to-delete) guides-to-delete)
changes (reduce (fn [changes component-id] changes (reduce (fn [changes component-id]
;; It's important to delete the component before the main instance, because we ;; It's important to delete the component before the main instance, because we
;; need to store the instance position if we want to restore it later. ;; need to store the instance position if we want to restore it later.
(pcb/delete-component changes component-id (:id page))) (pcb/delete-component changes component-id (:id page)))
changes changes
components-to-delete) components-to-delete)
@@ -327,7 +324,7 @@
result #{}] result #{}]
(if-not current-id (if-not current-id
;; Base case, no next element ;; Base case, no next element
result result
(let [group (get objects current-id)] (let [group (get objects current-id)]
@@ -335,14 +332,14 @@
(not= current-id parent-id) (not= current-id parent-id)
(empty? (remove removed-id? (:shapes group)))) (empty? (remove removed-id? (:shapes group))))
;; Adds group to the remove and check its parent ;; Adds group to the remove and check its parent
(let [to-check (concat to-check [(cfh/get-parent-id objects current-id)])] (let [to-check (concat to-check [(cfh/get-parent-id objects current-id)])]
(recur (first to-check) (recur (first to-check)
(rest to-check) (rest to-check)
(conj removed-id? current-id) (conj removed-id? current-id)
(conj result current-id))) (conj result current-id)))
;; otherwise recur ;; otherwise recur
(recur (first to-check) (recur (first to-check)
(rest to-check) (rest to-check)
removed-id? removed-id?

View File

@@ -111,7 +111,7 @@
"Check if any ancestor of a shape (between base-parent-id and shape) was swapped" "Check if any ancestor of a shape (between base-parent-id and shape) was swapped"
[shape objects base-parent-id] [shape objects base-parent-id]
(let [ancestors (->> (ctn/get-parent-heads objects shape) (let [ancestors (->> (ctn/get-parent-heads objects shape)
;; Ignore ancestors ahead of base-parent ;; Ignore ancestors ahead of base-parent
(drop-while #(not= base-parent-id (:id %))) (drop-while #(not= base-parent-id (:id %)))
seq) seq)
num-ancestors (count ancestors) num-ancestors (count ancestors)

View File

@@ -132,94 +132,3 @@ Some naming conventions:
(if-let [last-period (str/last-index-of s ".")] (if-let [last-period (str/last-index-of s ".")]
[(subs s 0 (inc last-period)) (subs s (inc last-period))] [(subs s 0 (inc last-period)) (subs s (inc last-period))]
[s ""])) [s ""]))
;; Tree building functions --------------------------------------------------
"Build tree structure from flat list of paths"
"`build-tree-root` is the main function to build the tree."
"Receives a list of segments with 'name' properties representing paths,
and a separator string."
"E.g segments = [{... :name 'one/two/three'} {... :name 'one/two/four'} {... :name 'one/five'}]"
"Transforms into a tree structure like:
[{:name 'one'
:path 'one'
:depth 0
:leaf nil
:children-fn (fn [] [{:name 'two'
:path 'one.two'
:depth 1
:leaf nil
:children-fn (fn [] [{... :name 'three'} {... :name 'four'}])}
{:name 'five'
:path 'one.five'
:depth 1
:leaf {... :name 'five'}
...}])}]"
(defn- sort-by-children
"Sorts segments so that those with children come first."
[segments separator]
(sort-by (fn [segment]
(let [path (split-path (:name segment) :separator separator)
path-length (count path)]
(if (= path-length 1)
1
0)))
segments))
(defn- group-by-first-segment
"Groups segments by their first path segment and update segment name."
[segments separator]
(reduce (fn [acc segment]
(let [[first-segment & remaining-segments] (split-path (:name segment) :separator separator)
rest-path (when (seq remaining-segments) (join-path remaining-segments :separator separator :with-spaces? false))]
(update acc first-segment (fnil conj [])
(if rest-path
(assoc segment :name rest-path)
segment))))
{}
segments))
(defn- sort-and-group-segments
"Sorts elements and groups them by their first path segment."
[segments separator]
(let [sorted (sort-by-children segments separator)
grouped (group-by-first-segment sorted separator)]
grouped))
(defn- build-tree-node
"Builds a single tree node with lazy children."
[segment-name remaining-segments separator parent-path depth]
(let [current-path (if parent-path
(str parent-path "." segment-name)
segment-name)
is-leaf? (and (seq remaining-segments)
(every? (fn [segment]
(let [remaining-segment-name (first (split-path (:name segment) :separator separator))]
(= segment-name remaining-segment-name)))
remaining-segments))
leaf-segment (when is-leaf? (first remaining-segments))
node {:name segment-name
:path current-path
:depth depth
:leaf leaf-segment
:children-fn (when-not is-leaf?
(fn []
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
(mapv (fn [[child-segment-name remaining-child-segments]]
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
grouped-elements))))}]
node))
(defn build-tree-root
"Builds the root level of the tree."
[segments separator]
(let [grouped-elements (sort-and-group-segments segments separator)]
(mapv (fn [[segment-name remaining-segments]]
(build-tree-node segment-name remaining-segments separator nil 0))
grouped-elements)))

View File

@@ -222,7 +222,7 @@
:else :else
(cons [node-style (dm/str head-text "" (:text node))] (rest acc))) (cons [node-style (dm/str head-text "" (:text node))] (rest acc)))
;; We add an end-of-line when finish a paragraph ;; We add an end-of-line when finish a paragraph
new-acc new-acc
(if (= (:type node) "paragraph") (if (= (:type node) "paragraph")
(let [[hs ht] (first new-acc)] (let [[hs ht] (first new-acc)]

View File

@@ -64,33 +64,8 @@
java.time.temporal.TemporalAmount java.time.temporal.TemporalAmount
java.time.temporal.TemporalUnit))) java.time.temporal.TemporalUnit)))
(declare inst)
#?(:clj (def ^:dynamic *clock* (Clock/systemDefaultZone))) #?(:clj (def ^:dynamic *clock* (Clock/systemDefaultZone)))
#?(:clj
(defn clock?
[o]
(instance? Clock o)))
#?(:clj
(defn get-system-clock
[]
(Clock/systemDefaultZone)))
#?(:clj
(defn offset-clock
[offset]
(Clock/offset ^Clock (Clock/systemDefaultZone) ^Duration offset)))
#?(:clj
(defn fixed-clock
[instant]
(Clock/fixed ^Instant (inst instant)
^ZoneId (ZoneId/of "Z"))))
(defn now (defn now
[] []
#?(:clj (Instant/now *clock*) #?(:clj (Instant/now *clock*)

View File

@@ -140,8 +140,7 @@
:layout-item-min-w :layout-item-min-w
:layout-item-absolute :layout-item-absolute
:layout-item-z-index :layout-item-z-index
:layout-item-align-self :layout-item-align-self})
:interactions})
(defn component-attr? (defn component-attr?
"Check if some attribute is one that is involved in component syncrhonization. "Check if some attribute is one that is involved in component syncrhonization.

View File

@@ -458,13 +458,13 @@
(map #(cfh/components-nesting-loop? objects (:id %) (:id parent))) (map #(cfh/components-nesting-loop? objects (:id %) (:id parent)))
(every? nil?)))] (every? nil?)))]
(or (or
;;We don't want to change the structure of component copies ;;We don't want to change the structure of component copies
(ctk/in-component-copy? parent) (ctk/in-component-copy? parent)
(has-any-copy-parent? objects parent) (has-any-copy-parent? objects parent)
;; If we are moving something containing a main instance the container can't be part of a component (neither main nor copy) ;; If we are moving something containing a main instance the container can't be part of a component (neither main nor copy)
(and selected-main-instance? parent-in-component?) (and selected-main-instance? parent-in-component?)
;; Avoid placing a shape as a direct or indirect child of itself, ;; Avoid placing a shape as a direct or indirect child of itself,
;; or inside its main component if it's in a copy. ;; or inside its main component if it's in a copy.
comps-nesting-loop?))) comps-nesting-loop?)))
(defn find-valid-parent-and-frame-ids (defn find-valid-parent-and-frame-ids

View File

@@ -775,9 +775,9 @@
file-data (cond-> file-data file-data (cond-> file-data
(d/not-empty? used-components) (d/not-empty? used-components)
(absorb-components used-components library-data)) (absorb-components used-components library-data))
;; Note that absorbed components may also be using colors ;; Note that absorbed components may also be using colors
;; and typographies. This is the reason of doing this first ;; and typographies. This is the reason of doing this first
;; and accumulating file data for the next ones. ;; and accumulating file data for the next ones.
used-colors (find-asset-type-usages file-data library-data :color) used-colors (find-asset-type-usages file-data library-data :color)
file-data (cond-> file-data file-data (cond-> file-data
@@ -1017,7 +1017,7 @@
libs-to-show libs-to-show
(-> libs-to-show (-> libs-to-show
(add-component library-id component-id)))))) (add-component library-id component-id))))))
;; (find-used-components-cumulative page root) ;; (find-used-components-cumulative page root)
libs-to-show libs-to-show
components)) components))

View File

@@ -306,7 +306,7 @@
(-write-to [_ heap offset] (-write-to [_ heap offset]
(let [buffer' (.-buffer ^js/DataView dbuffer) (let [buffer' (.-buffer ^js/DataView dbuffer)
;; Calculate byte size: 4 bytes header + (size * FILL-U8-SIZE) ;; Calculate byte size: 4 bytes header + (size * FILL-U8-SIZE)
byte-size (+ 4 (* size FILL-U8-SIZE)) byte-size (+ 4 (* size FILL-U8-SIZE))
;; Create Uint32Array with exact size needed (convert bytes to u32 elements) ;; Create Uint32Array with exact size needed (convert bytes to u32 elements)
u32-array (js/Uint32Array. buffer' 0 (/ byte-size 4))] u32-array (js/Uint32Array. buffer' 0 (/ byte-size 4))]

View File

@@ -13,15 +13,14 @@
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.generators :as sg])) [app.common.schema.generators :as sg]))
;; WARNING: options are not deleted when changing event or action ;; WARNING: options are not deleted when changing event or action type, so it can be
;; type, so it can be restored if the user changes it back later. ;; restored if the user changes it back later.
;; ;;
;; But that means that an interaction may have for example a delay or ;; But that means that an interaction may have for example a delay or
;; destination, even if its type does not require it (but a previous ;; destination, even if its type does not require it (but a previous type did).
;; type did).
;; ;;
;; So make sure to use has-delay/has-destination... functions, or ;; So make sure to use has-delay/has-destination... functions, or similar,
;; similar, before reading them. ;; before reading them.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA ;; SCHEMA
@@ -453,15 +452,16 @@
(gpt/point 0 0))) (gpt/point 0 0)))
(defn calc-overlay-position (defn calc-overlay-position
[interaction ; interaction data [interaction ;; interaction data
shape ; Shape with the interaction shape ;; Shape with the interaction
objects ; the objects tree objects ;; the objects tree
relative-to-shape ; the interaction position is realtive to this shape relative-to-shape ;; the interaction position is realtive to this
base-frame ; the base frame of the current interaction ;; sape
dest-frame ; the frame to display with this interaction base-frame ;; the base frame of the current interaction
frame-offset] ; if this interaction starts in a frame opened dest-frame ;; the frame to display with this interaction
; on another interaction, this is the position frame-offset] ;; if this interaction starts in a frame opened
; of that frame ;; on another interaction, this is the position
;; of that frame
(assert (check-interaction interaction)) (assert (check-interaction interaction))
(assert (has-overlay-opts interaction) (assert (has-overlay-opts interaction)
"expected compatible interaction map") "expected compatible interaction map")

View File

@@ -382,9 +382,9 @@
keep-ids? (:id shape) keep-ids? (:id shape)
:else (uuid/next)) :else (uuid/next))
;; Assign the correct frame-id for the given parent. It's the parent-id (if parent is frame) ;; Assign the correct frame-id for the given parent. It's the parent-id (if parent is frame)
;; or the parent's frame-id otherwise. Only for the first cloned shapes. In recursive calls ;; or the parent's frame-id otherwise. Only for the first cloned shapes. In recursive calls
;; this is not needed. ;; this is not needed.
frame-id (cond frame-id (cond
(and (nil? frame-id) (cfh/frame-shape? dest-objects parent-id)) (and (nil? frame-id) (cfh/frame-shape? dest-objects parent-id))
parent-id parent-id

View File

@@ -19,10 +19,3 @@
(def schema:role (def schema:role
[::sm/one-of {:title "TeamRole"} valid-roles]) [::sm/one-of {:title "TeamRole"} valid-roles])
;; FIXME: specify more fields
(def schema:team
[:map {:title "Team"}
[:id ::sm/uuid]
[:name :string]])

View File

@@ -393,7 +393,7 @@
:else :else
(cons [node-style (dm/str head-text "" (:text node))] (rest acc))) (cons [node-style (dm/str head-text "" (:text node))] (rest acc)))
;; We add an end-of-line when finish a paragraph ;; We add an end-of-line when finish a paragraph
new-acc new-acc
(if (= (:type node) "paragraph") (if (= (:type node) "paragraph")
(let [[hs ht] (first new-acc)] (let [[hs ht] (first new-acc)]

View File

@@ -47,18 +47,6 @@
self-reference? (get token-references token-name)] self-reference? (get token-references token-name)]
self-reference?)) self-reference?))
(defn references-token?
"Recursively check if a value references the token name. Handles strings, maps, and sequences."
[value token-name]
(cond
(string? value)
(boolean (some #(= % token-name) (find-token-value-references value)))
(map? value)
(some true? (map #(references-token? % token-name) (vals value)))
(sequential? value)
(some true? (map #(references-token? % token-name) value))
:else false))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA ;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -474,10 +462,8 @@
:height #{:sizing :dimensions} :height #{:sizing :dimensions}
:max-width #{:sizing :dimensions} :max-width #{:sizing :dimensions}
:max-height #{:sizing :dimensions} :max-height #{:sizing :dimensions}
:min-width #{:sizing :dimensions} :x #{:spacing :dimensions}
:min-height #{:sizing :dimensions} :y #{:spacing :dimensions}
:x #{:dimensions}
:y #{:dimensions}
:rotation #{:number :rotation} :rotation #{:number :rotation}
:border-radius #{:border-radius :dimensions} :border-radius #{:border-radius :dimensions}
:row-gap #{:spacing :dimensions} :row-gap #{:spacing :dimensions}
@@ -489,8 +475,6 @@
:vertical-margin #{:spacing :dimensions} :vertical-margin #{:spacing :dimensions}
:sided-margins #{:spacing :dimensions} :sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number} :line-height #{:line-height :number}
:opacity #{:opacity}
:stroke-width #{:stroke-width :dimensions}
:font-size #{:font-size} :font-size #{:font-size}
:letter-spacing #{:letter-spacing} :letter-spacing #{:letter-spacing}
:fill #{:color} :fill #{:color}
@@ -574,18 +558,3 @@
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token." "Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
[token-value] [token-value]
(string? token-value)) (string? token-value))
(defn update-token-value-references
"Recursively update token references within a token value, supporting complex token values (maps, sequences, strings)."
[value old-name new-name]
(cond
(string? value)
(str/replace value
(re-pattern (str "\\{" (str/replace old-name "." "\\.") "\\}"))
(str "{" new-name "}"))
(map? value)
(d/update-vals value #(update-token-value-references % old-name new-name))
(sequential? value)
(mapv #(update-token-value-references % old-name new-name) value)
:else
value))

View File

@@ -909,8 +909,7 @@ Will return a value that matches this schema:
`:all` All of the nested sets are active `:all` All of the nested sets are active
`:partial` Mixed active state of nested sets") `:partial` Mixed active state of nested sets")
(get-tokens-in-active-sets [_] "set of set names that are active in the the active themes") (get-tokens-in-active-sets [_] "set of set names that are active in the the active themes")
(get-all-tokens [_] "all tokens in the lib, as a sequence") (get-all-tokens [_] "all tokens in the lib")
(get-all-tokens-map [_] "all tokens in the lib, as a map name -> token")
(get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name")) (get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name"))
(declare parse-multi-set-dtcg-json) (declare parse-multi-set-dtcg-json)
@@ -1307,10 +1306,6 @@ Will return a value that matches this schema:
tokens)) tokens))
(get-all-tokens [this] (get-all-tokens [this]
(mapcat #(vals (get-tokens- %))
(get-sets this)))
(get-all-tokens-map [this]
(reduce (reduce
(fn [tokens' set] (fn [tokens' set]
(into tokens' (map (fn [x] [(:name x) x]) (vals (get-tokens- set))))) (into tokens' (map (fn [x] [(:name x) x]) (vals (get-tokens- set)))))

View File

@@ -693,7 +693,7 @@
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)} #{(:id main-child)}
(fn [shape] (fn [shape]
;; Update the attrs on all the content tree ;; Update the attrs on all the content tree
(-> shape (-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32") (assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32") (assoc-in [:content :children 0 :children 0 :font-size] "32")
@@ -851,7 +851,7 @@
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)} #{(:id main-child)}
(fn [shape] (fn [shape]
;; Update the attrs on all the content tree ;; Update the attrs on all the content tree
(-> shape (-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32") (assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32") (assoc-in [:content :children 0 :children 0 :font-size] "32")

View File

@@ -267,7 +267,7 @@
page' (thf/current-page file') page' (thf/current-page file')
objects' (:objects page')] objects' (:objects page')]
;; ==== Check ;; ==== Check
(thf/validate-file! file') (thf/validate-file! file')
(t/is (= (count (:components data)) 2)) (t/is (= (count (:components data)) 2))
(t/is (= (count (:components data')) 4)) (t/is (= (count (:components data')) 4))

1291
common/yarn.lock Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -179,9 +179,9 @@ RUN set -eux; \
FROM base AS setup-utils FROM base AS setup-utils
ENV CLJKONDO_VERSION=2026.01.19 \ ENV CLJKONDO_VERSION=2025.07.28 \
BABASHKA_VERSION=1.12.208 \ BABASHKA_VERSION=1.12.208 \
CLJFMT_VERSION=0.15.6 CLJFMT_VERSION=0.13.1
RUN set -ex; \ RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \ ARCH="$(dpkg --print-architecture)"; \
@@ -398,6 +398,7 @@ COPY files/Caddyfile /home/
COPY files/selfsigned.crt /home/ COPY files/selfsigned.crt /home/
COPY files/selfsigned.key /home/ COPY files/selfsigned.key /home/
COPY files/start-tmux.sh /home/start-tmux.sh COPY files/start-tmux.sh /home/start-tmux.sh
COPY files/start-tmux-back.sh /home/start-tmux-back.sh
COPY files/entrypoint.sh /home/entrypoint.sh COPY files/entrypoint.sh /home/entrypoint.sh
COPY files/init.sh /home/init.sh COPY files/init.sh /home/init.sh

View File

@@ -1,17 +1,16 @@
{ {
auto_https off auto_https off
} }
localhost:3449 { localhost:3449 {
reverse_proxy localhost:4449 reverse_proxy localhost:4449
tls /home/selfsigned.crt /home/selfsigned.key tls /home/selfsigned.crt /home/selfsigned.key
header -Strict-Transport-Security
} }
http://localhost:3450 { http://localhost:3450 {
reverse_proxy localhost:4449 reverse_proxy localhost:4449
} }
http://penpot-devenv-main:3450 { http://penpot-devenv-main:3450 {
reverse_proxy localhost:4449 reverse_proxy localhost:4449
} }

View File

@@ -141,14 +141,8 @@ http {
proxy_pass http://127.0.0.1:5000; proxy_pass http://127.0.0.1:5000;
} }
location /control-center { location /nitrate/ {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
location /wasm-playground { location /wasm-playground {

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
sudo chown penpot:users /home/penpot
cd ~;
source ~/.bashrc
set -e;
echo "[start-tmux.sh] Installing node dependencies"
pushd ~/penpot/exporter/
yarn install
popd
tmux -2 new-session -d -s penpot
tmux rename-window -t penpot:0 'exporter'
tmux select-window -t penpot:0
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter
tmux split-window -v
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t penpot './scripts/wait-and-start.sh' enter
tmux new-window -t penpot:1 -n 'backend'
tmux select-window -t penpot:1
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
tmux send-keys -t penpot './scripts/start-dev' enter
tmux -2 attach-session -t penpot

View File

@@ -112,6 +112,10 @@ COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
WORKDIR /opt/penpot/exporter WORKDIR /opt/penpot/exporter
USER penpot:penpot USER penpot:penpot
RUN ./setup RUN set -ex; \
corepack install; \
yarn install; \
yarn run playwright install chromium; \
rm -rf /opt/penpot/.yarn
CMD ["node", "app.js"] CMD ["node", "app.js"]

View File

@@ -29,9 +29,8 @@ update_flags /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060} export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061} export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \ envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf < /tmp/nginx.conf.template > /etc/nginx/nginx.conf
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)" PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"

View File

@@ -139,20 +139,12 @@ http {
proxy_pass $PENPOT_BACKEND_URI/ws/notifications; proxy_pass $PENPOT_BACKEND_URI/ws/notifications;
} }
location /control-center {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $http_cf_connecting_ip;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass $PENPOT_NITRATE_URI$request_uri;
}
include /etc/nginx/overrides/server.d/*.conf; include /etc/nginx/overrides/server.d/*.conf;
location / { location / {
include /etc/nginx/overrides/location.d/*.conf; include /etc/nginx/overrides/location.d/*.conf;
location ~* \.(js|css|jpg|png|svg|gif|ttf|woff|woff2|wasm|map)$ { location ~* \.(js|css|jpg|png|svg|gif|ttf|woff|woff2|wasm)$ {
add_header Cache-Control "public, max-age=604800" always; # 7 days add_header Cache-Control "public, max-age=604800" always; # 7 days
} }
@@ -160,10 +152,8 @@ http {
return 301 " /404"; return 301 " /404";
} }
add_header X-Frame-Options SAMEORIGIN always;
add_header Cache-Control "no-store, no-cache, max-age=0" always; add_header Cache-Control "no-store, no-cache, max-age=0" always;
try_files $uri /index.html$is_args$args /index.html =404; try_files $uri /index.html$is_args$args /index.html =404;
} }
} }
} }

View File

@@ -10,15 +10,16 @@ To view this site locally, first set up the environment:
# only if necessary # only if necessary
nvm install nvm install
nvm use nvm use
# only if necessary
corepack enable corepack enable
pnpm install yarn install
``` ```
And launch a development server: And launch a development server:
```sh ```sh
pnpm start yarn start
``` ```
You can then point a browser to [http://localhost:8080](http://localhost:8080). You can then point a browser to [http://localhost:8080](http://localhost:8080).

View File

@@ -39,5 +39,5 @@
"markdown-it-anchor": "^9.0.1", "markdown-it-anchor": "^9.0.1",
"markdown-it-plantuml": "^1.4.1" "markdown-it-plantuml": "^1.4.1"
}, },
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48" "packageManager": "yarn@4.3.1"
} }

View File

@@ -6,4 +6,4 @@ desc: Create, deploy, and use the Penpot plugin API with our comprehensive docum
# Penpot plugins API # Penpot plugins API
We've got all the documentation you need for the API right <a target="_blank" href="https://doc.plugins.penpot.app/">here</a>. We've got all the documentation you need for the API right <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/">here</a>.

View File

@@ -9,13 +9,13 @@ desc: See the Penpot plugin API changelog for version 1.0! Find breaking changes
### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"></g-emoji> Epics and highlights</code> ### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"></g-emoji> Epics and highlights</code>
- This marks the release of version 1.0, and from this point forward, well do our best to avoid making any more breaking changes (or make deprecations backward compatible). - This marks the release of version 1.0, and from this point forward, well do our best to avoid making any more breaking changes (or make deprecations backward compatible).
- Weve redone the documentation. You can check the API here: - Weve redone the documentation. You can check the API here:
[https://doc.plugins.penpot.app/](https://doc.plugins.penpot.app/) [https://penpot-plugins-api-doc.pages.dev/](https://penpot-plugins-api-doc.pages.dev/)
- New samples repository with lots of samples to use the API: - New samples repository with lots of samples to use the API:
[https://github.com/penpot/penpot-plugins-samples](https://github.com/penpot/penpot-plugins-samples) [https://github.com/penpot/penpot-plugins-samples](https://github.com/penpot/penpot-plugins-samples)
### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"></g-emoji> Breaking changes & Deprecations ### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"></g-emoji> Breaking changes & Deprecations
- Changed types names to remove the Penpot prefix. So for example: <code class="language-js">PenpotShape</code> becomes <code class="language-js">Shape</code>; <code class="language-js">PenpotFile</code> becomes <code class="language-js">File</code>, and so on. Check the [API documentation](https://doc.plugins.penpot.app/) for more details. - Changed types names to remove the Penpot prefix. So for example: <code class="language-js">PenpotShape</code> becomes <code class="language-js">Shape</code>; <code class="language-js">PenpotFile</code> becomes <code class="language-js">File</code>, and so on. Check the [API documentation](https://penpot-plugins-api-doc.pages.dev/) for more details.
- Changes on the <code class="language-js">penpot.on</code> and <code class="language-js">penpot.off</code> methods. - Changes on the <code class="language-js">penpot.on</code> and <code class="language-js">penpot.off</code> methods.
Previously you had to send the original callback to the off method in order to remove an event listener. Now, <code class="language-js">penpot.on</code> will return an *id* that you can pass to the <code class="language-js">penpot.off</code> method in order to remove the listener. Previously you had to send the original callback to the off method in order to remove an event listener. Now, <code class="language-js">penpot.on</code> will return an *id* that you can pass to the <code class="language-js">penpot.off</code> method in order to remove the listener.

View File

@@ -49,7 +49,7 @@ There are two libraries that can help you with your plugin's development. They a
### Plugin styles ### Plugin styles
<code class="language-js">@penpot/plugin-styles</code> contains styles to help build the UI for Penpot plugins. To check the styles go to <a target="_blank" href="https://styles-doc.plugins.penpot.app/">Plugin styles</a>. <code class="language-js">@penpot/plugin-styles</code> contains styles to help build the UI for Penpot plugins. To check the styles go to <a target="_blank" href="https://penpot-plugins-styles.pages.dev/">Plugin styles</a>.
```bash ```bash
npm install @penpot/plugin-styles npm install @penpot/plugin-styles
@@ -139,7 +139,7 @@ parent.postMessage(responseMessage, targetOrigin);
By using these message-based events, any data retrieved through the Penpot API can be communicated to and from your plugin interface seamlessly. By using these message-based events, any data retrieved through the Penpot API can be communicated to and from your plugin interface seamlessly.
For more detailed information, refer to the [Penpot Plugins API Documentation](https://doc.plugins.penpot.app/). For more detailed information, refer to the [Penpot Plugins API Documentation](https://penpot-plugins-api-doc.pages.dev/).
## 2.5. Step 5. Build the plugin file ## 2.5. Step 5. Build the plugin file

View File

@@ -86,7 +86,7 @@ penpot.library.local.createTypography();
Penpot has dark and light modes, and you can easily add this to your plugin so your interface adapts to both themes. When you add theme support, your plugin will automatically sync with Penpot's interface settings, so the user experience is consistent no matter which mode is selected. This makes your plugin look better and also ensures it stays in line with Penpot's overall design. Penpot has dark and light modes, and you can easily add this to your plugin so your interface adapts to both themes. When you add theme support, your plugin will automatically sync with Penpot's interface settings, so the user experience is consistent no matter which mode is selected. This makes your plugin look better and also ensures it stays in line with Penpot's overall design.
Just a heads-up: if you use the <a target="_blank" href="https://styles-doc.plugins.penpot.app/">plugin-styles library</a>, many elements will automatically adapt to dark or light mode without any extra effort from you. However, if you need to customize specific elements, be sure to use the selectors provided in the <code class="language-bash">styles.css</code> of the example. Just a heads-up: if you use the <a target="_blank" href="https://penpot-plugins-styles.pages.dev/">plugin-styles library</a>, many elements will automatically adapt to dark or light mode without any extra effort from you. However, if you need to customize specific elements, be sure to use the selectors provided in the <code class="language-bash">styles.css</code> of the example.
<a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/theme">Theme example</a> <a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/theme">Theme example</a>

View File

@@ -40,7 +40,7 @@ The plugin <a target="_blank" href="https://www.npmjs.com/package/@penpot/plugin
### Is the API ready to use the prototyping features? ### Is the API ready to use the prototyping features?
Absolutely! You can definitely create flows and interactions in the same elements as in the interface, like frames, shapes, and groups. Just check out the API documentation for the methods: createFlow, addInteraction, or removeInteraction. And if you need more help, you can always check out the <a target="_blank" href="https://doc.plugins.penpot.app/interfaces/Flow">Flow</a> or <a target="_blank" href="https://doc.plugins.penpot.app/interfaces/Interaction">Interaction</a> interfaces. Absolutely! You can definitely create flows and interactions in the same elements as in the interface, like frames, shapes, and groups. Just check out the API documentation for the methods: createFlow, addInteraction, or removeInteraction. And if you need more help, you can always check out the <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/interfaces/PenpotFlow">PenpotFlow</a> or <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/interfaces/PenpotInteraction">PenpotInteraction</a> interfaces.
### Are there any security or quality criteria I should be aware of? ### Are there any security or quality criteria I should be aware of?
@@ -48,8 +48,7 @@ There are no set requirements. However, we can recommend the use of <a target="_
### Is it necessary to create plugins with a UI? ### Is it necessary to create plugins with a UI?
No, its completely optional, in fact, we have an example of a plugin without UI. Try the plugin using this url to install it: <code class="language-js">https:\/\/create-palette.plugins.penpot.app/assets/manifest.json</code> or check the code <a target="_blank" href="https://github.com/penpot/penpot/tree/main/plugins/apps/create-palette-plugin">here</a> No, its completely optional, in fact, we have an example of a plugin without UI. Try the plugin using this url to install it: <code class="language-js">https:\/\/create-palette-penpot-plugin.pages.dev/assets/manifest.json</code> or check the code <a target="_blank" href="https://github.com/penpot/penpot-plugins/tree/main/apps/create-palette-plugin">here</a>
### Can I create components? ### Can I create components?
@@ -59,7 +58,7 @@ Yes, it is possible to create components using:
createComponent(shapes: Shape[]): LibraryComponent; createComponent(shapes: Shape[]): LibraryComponent;
``` ```
Take a look at the Penpot Library methods in the <a target="_blank" href="https://doc.plugins.penpot.app/interfaces/Library">API documentation</a> or this <a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/components-library">simple example</a>. Take a look at the Penpot Library methods in the <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/interfaces/Library">API documentation</a> or this <a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/components-library">simple example</a>.
### Is there a place where I can share my plugin? ### Is there a place where I can share my plugin?

View File

@@ -69,13 +69,12 @@ You need to provide the plugin's manifest URL for the installation. If there are
| Name | URL | | Name | URL |
| ------------- | ------------------------------------------------------------------- | | ------------- | ------------------------------------------------------------------- |
| Color palette | https://create-palette.plugins.penpot.app/assets/manifest.json | | Lorem Ipsum | https://lorem-ipsum-penpot-plugin.pages.dev/assets/manifest.json |
| Contrast | https://contrast.plugins.penpot.app/assets/manifest.json | | Contrast | https://contrast-penpot-plugin.pages.dev/assets/manifest.json |
| Feather icons | https://icons.plugins.penpot.app/assets/manifest.json | | Feather icons | https://icons-penpot-plugin.pages.dev/assets/manifest.json |
| Lorem ipsum | https://lorem-ipsum.plugins.penpot.app/assets/manifest.json | | Tables | https://table-penpot-plugin.pages.dev/assets/manifest.json |
| Rename layers | https://rename-layers.plugins.penpot.app/assets/manifest.json | | Color palette | https://create-palette-penpot-plugin.pages.dev/assets/manifest.json |
| Tables | https://table.plugins.penpot.app/assets/manifest.json | | Rename layers | https://rename-layers-penpot-plugin.pages.dev/assets/manifest.json |
## 1.4. Plugin's basics ## 1.4. Plugin's basics

Some files were not shown because too many files have changed in this diff Show More