Compare commits

..

7 Commits

Author SHA1 Message Date
Andrey Antukh
465b6361a2 WIP 2025-08-14 12:30:35 +02:00
Andrey Antukh
e6d4372948 Remove reflection calls from buffer macros 2025-08-14 11:15:46 +02:00
Andrey Antukh
193e5c88e8 Add several missing type hints for avoid reflection and autoboxing 2025-08-14 11:15:17 +02:00
Andrey Antukh
a79b98f1b7 Fix autoboxing on path type impl 2025-08-14 10:47:40 +02:00
Andrey Antukh
45a986d4e6 Remove reflection on geom rect code 2025-08-14 10:47:40 +02:00
Andrey Antukh
642d08b07d Remove reflection on geom matrix code 2025-08-14 09:05:55 +02:00
Andrey Antukh
94041b5293 Remove reflection calls on binfile v3 code 2025-08-14 09:05:21 +02:00
956 changed files with 45537 additions and 97456 deletions

View File

@@ -226,29 +226,14 @@ jobs:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
# Build frontend
- run:
name: "frontend build"
name: "integration tests"
working_directory: "./frontend"
command: |
yarn install
yarn run build:app:assets
yarn run build:app
yarn run build:app:libs
# Build the wasm bundle
- run:
name: "wasm build"
working_directory: "./render-wasm"
command: |
EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh
./build release
# Run integration tests
- run:
name: "integration tests"
working_directory: "./frontend"
command: |
yarn run playwright install chromium
yarn run test:e2e -x --workers=4

View File

@@ -1,45 +1,18 @@
name: Bundles Builder
name: Build and Upload Penpot Bundles
on:
# Create bundle from manual action
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
build_wasm:
description: 'BUILD_WASM. Valid values: yes, no'
type: string
required: false
default: 'yes'
build_storybook:
description: 'BUILD_STORYBOOK. Valid values: yes, no'
type: string
required: false
default: 'yes'
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch or ref'
description: 'Name of the branch'
type: string
required: true
default: 'develop'
build_wasm:
description: 'BUILD_WASM. Valid values: yes, no'
type: string
required: false
default: 'yes'
build_storybook:
description: 'BUILD_STORYBOOK. Valid values: yes, no'
type: string
required: false
default: 'yes'
jobs:
build-bundle:
name: Build and Upload Penpot Bundle
build-bundles:
name: Build and Upload Penpot Bundles
runs-on: ubuntu-24.04
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -56,12 +29,13 @@ jobs:
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
echo "commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "gh_branch=${{ github.base_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Build bundle
env:
BUILD_WASM: ${{ inputs.build_wasm }}
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
- name: Set up Docker Buildx for multi-arch build
uses: docker/setup-buildx-action@v3
- name: Run manage.sh build-bundle from host
run: ./manage.sh build-bundle
- name: Prepare directories for zipping
@@ -69,14 +43,15 @@ jobs:
mkdir zips
mv bundles penpot
- name: Create zip bundle
- name: Create zip bundles
run: |
echo "📦 Packaging Penpot bundle..."
echo "📦 Packaging Penpot bundles..."
zip -r zips/penpot.zip penpot
- name: Upload Penpot bundle to S3
run: |
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_branch}}-latest.zip
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.commit_hash }}.zip
- name: Notify Mattermost
if: failure()
@@ -85,5 +60,5 @@ jobs:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
TEXT: |
❌ *[PENPOT] Error during the execution of the job*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
📄 Triggered from ref: `${{ steps.vars.outputs.gh_branch}}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}

View File

@@ -1,21 +1,12 @@
name: _DEVELOP
name: Build and Upload Penpot DEVELOP Bundles
on:
schedule:
- cron: '16 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "develop"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
build-develop-bundle:
uses: ./.github/workflows/build-bundles.yml
secrets: inherit
with:
gh_ref: "develop"

View File

@@ -1,36 +0,0 @@
name: DevEnv Docker Image Builder
on:
workflow_dispatch:
jobs:
build-and-push:
name: Build and push DevEnv Docker image
environment: release-admins
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Build and push DevEnv Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'penpotapp/devenv'
with:
context: ./docker/devenv/
file: ./docker/devenv/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.DOCKER_IMAGE }}:latest
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max

View File

@@ -1,101 +0,0 @@
name: Docker Images Builder
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
jobs:
build-and-push:
name: Build and Push Penpot Docker Images
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Download Penpot Bundles
env:
FILE_NAME: penpot-${{ steps.vars.outputs.gh_ref }}.zip
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
pushd docker/images
aws s3 cp s3://${{ secrets.S3_BUCKET }}/$FILE_NAME .
unzip $FILE_NAME > /dev/null
mv penpot/backend bundle-backend
mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter
popd
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Backend Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'backend'
BUNDLE_PATH: './bundle-backend'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.backend
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Frontend Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'frontend'
BUNDLE_PATH: './bundle-frontend'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.frontend
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Exporter Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'exporter'
BUNDLE_PATH: './bundle-exporter'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.exporter
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max

View File

@@ -1,21 +1,12 @@
name: _STAGING
name: Build and Upload Penpot STAGING Bundles
on:
schedule:
- cron: '36 5-20 * * 1-5'
- cron: '0 5 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
build-staging-bundle:
uses: ./.github/workflows/build-bundles.yml
secrets: inherit
with:
gh_ref: "staging"

View File

@@ -1,30 +0,0 @@
name: _TAG
on:
push:
tags:
- '*'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
build_wasm: "no"
build_storybook: "yes"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
# publish-final-tag:
# if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
# needs: build-docker
# uses: ./.github/workflows/release.yml
# secrets: inherit
# with:
# gh_ref: ${{ github.ref_name }}

View File

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

View File

@@ -1,95 +0,0 @@
name: Release Publisher
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Tag to release'
type: string
required: true
workflow_call:
inputs:
gh_ref:
description: 'Tag to release'
type: string
required: true
permissions:
contents: write
jobs:
release:
environment: release-admins
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.vars.outputs.gh_ref }}
release_notes: ${{ steps.extract_release_notes.outputs.release_notes }}
steps:
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.vars.outputs.gh_ref }}
# # --- Publicly release the docker images ---
# - name: Login to private registry
# uses: docker/login-action@v3
# with:
# registry: ${{ secrets.DOCKER_REGISTRY }}
# username: ${{ secrets.DOCKER_USERNAME }}
# password: ${{ secrets.DOCKER_PASSWORD }}
# - name: Login to DockerHub
# uses: docker/login-action@v3
# with:
# username: ${{ secrets.PUB_DOCKER_USERNAME }}
# password: ${{ secrets.PUB_DOCKER_PASSWORD }}
# - name: Publish docker images to DockerHub
# env:
# TAG: ${{ steps.vars.outputs.gh_ref }}
# REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
# HUB: ${{ secrets.PUB_DOCKER_HUB }}
# run: |
# IMAGES=("frontend" "backend" "exporter")
# EXTRA_TAGS=("main" "latest")
# for image in "${IMAGES[@]}"; do
# docker pull "$REGISTRY/penpotapp/$image:$TAG"
# docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$TAG"
# docker push "penpotapp/$image:$TAG"
# for tag in "${EXTRA_TAGS[@]}"; do
# docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$tag"
# docker push "penpotapp/$image:$tag"
# done
# done
# --- Release notes extraction ---
- name: Extract release notes from CHANGES.md
id: extract_release_notes
env:
TAG: ${{ steps.vars.outputs.gh_ref }}
run: |
RELEASE_NOTES=$(awk "/^## $TAG$/{flag=1; next} /^## /{flag=0} flag" CHANGES.md | awk '{$1=$1};1')
if [ -z "$RELEASE_NOTES" ]; then
RELEASE_NOTES="No changes for $TAG according to CHANGES.md"
fi
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# --- Create GitHub release ---
- name: Create GitHub release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.vars.outputs.gh_ref }}
name: ${{ steps.vars.outputs.gh_ref }}
body: ${{ steps.extract_release_notes.outputs.release_notes }}

1
.gitignore vendored
View File

@@ -31,7 +31,6 @@
/.clj-kondo/.cache
/_dump
/notes
/playground/
/backend/*.md
/backend/*.sql
/backend/*.txt

2
.nvmrc
View File

@@ -1 +1 @@
v22.19.0
v22.13.1

View File

@@ -1,86 +1,16 @@
# CHANGELOG
## 2.11.0 (Unreleased)
## 2.10.0 (Unreleased)
### :rocket: Epics and highlights
- Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be
removed in future versions:
- The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its
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_S3_BUCKET` becomes `PENPOT_OBJECTS_STORAGE_S3_BUCKET`
- The `PENPOT_STORAGE_ASSETS_S3_REGION` becomes `PENPOT_OBJECTS_STORAGE_S3_REGION`
- The `PENPOT_STORAGE_ASSETS_S3_ENDPOINT` becomes `PENPOT_OBJECTS_STORAGE_S3_ENDPOINT`
- The `PENPOT_STORAGE_ASSETS_S3_IO_THREADS` replaced (see below)
- Add `PENPOT_NETTY_IO_THREADS` and `PENPOT_EXECUTOR_THREADS` variables to provide the
control over concurrency of the shared resources used by netty. Penpot uses the netty IO
threads for AWS S3 SDK and Redis/Valkey communication, and the EXEC threads to perform
out of HTTP serving threads tasks such that cache invalidation, S3 response completion,
configuration reloading and many other auxiliar tasks. By default they use a half number
if available cpus with a minumum of 2 for both executors. You should not touch that
variables unless you are know what you are doing.
- Replace the `PENPOT_STORAGE_ASSETS_S3_IO_THREADS` with a more general configuration
`PENPOT_NETTY_IO_THREADS` used to configure a shared netty resources across different
services which use netty internally (redis connection, S3 SDK client). This
configuration is not very commonly used so don't expected real impact on any user.
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
- Show current Penpot version [Taiga #11603](https://tree.taiga.io/project/penpot/us/11603)
- Switch several variant copies at the same time [Taiga #11411](https://tree.taiga.io/project/penpot/us/11411)
- Invitations management improvements [Taiga #3479](https://tree.taiga.io/project/penpot/us/3479)
- Alternative ways of creating variants - Button Viewport [Taiga #11931](https://tree.taiga.io/project/penpot/us/11931)
### :bug: Bugs fixed
- Fix selection problems when devtools open [Taiga #11950](https://tree.taiga.io/project/penpot/issue/11950)
- Fix long font names overlap [Taiga #11844](https://tree.taiga.io/project/penpot/issue/11844)
- Fix paste behavior according to the selected element [Taiga #11979](https://tree.taiga.io/project/penpot/issue/11979)
- Fix problem with export size [Github #7160](https://github.com/penpot/penpot/issues/7160)
- Fix multi level library dependencies [Taiga #12155](https://tree.taiga.io/project/penpot/issue/12155)
- Fix component context menu options order in assets tab [Taiga #11941](https://tree.taiga.io/project/penpot/issue/11941)
## 2.10.0
### :rocket: Epics and highlights
- Variants
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
- Add efficiency enhancements to right sidebar [Github #7182](https://github.com/penpot/penpot/pull/7182)
- Add defaults for artboard drawing [Taiga #494](https://tree.taiga.io/project/penpot/us/494?milestone=465047)
- Continuous display of distances between elements when moving a layer with the keyboard [Taiga #1780](https://tree.taiga.io/project/penpot/us/1780)
- New Number token - unitless values [Taiga #10936](https://tree.taiga.io/project/penpot/us/10936)
- New font-family token [Taiga #10937](https://tree.taiga.io/project/penpot/us/10937)
- New text case token [Taiga #10942](https://tree.taiga.io/project/penpot/us/10942)
- New text-decoration token [Taiga #10941](https://tree.taiga.io/project/penpot/us/10941)
- New letter spacing token [Taiga #10940](https://tree.taiga.io/project/penpot/us/10940)
- New font weight token [Taiga #10939](https://tree.taiga.io/project/penpot/us/10939)
- Upgrade Node to v22.18.0 [Github #7283](https://github.com/penpot/penpot/pull/7283)
- Upgrade the base docker image for penpot frontend to v1.29.1 [Github #7283](https://github.com/penpot/penpot/pull/7283)
- Create variant from an existing component [Taiga #2088](https://tree.taiga.io/project/penpot/us/2088)
- Create variant from an existing variant [Taiga #8282](https://tree.taiga.io/project/penpot/us/8282)
- Actions over a component with variants [Taiga #10503](https://tree.taiga.io/project/penpot/us/10503)
- Create a variant by dragging a component into a component with variants [Taiga #8134](https://tree.taiga.io/project/penpot/us/8134)
- Transform a variant into an individual component [Taiga #8141](https://tree.taiga.io/project/penpot/us/8141)
- Delete variant [Taiga #6890](https://tree.taiga.io/project/penpot/us/6890)
- Restore an orphaned copy of a variant [Taiga #10446](https://tree.taiga.io/project/penpot/us/10446)
- Add, Edit & Delete variant properties name and value [Taiga #6892](https://tree.taiga.io/project/penpot/us/6892)
- Retrieve variants [Taiga #6888](https://tree.taiga.io/project/penpot/us/6888)
- Retrieve variants with nested components [Taiga #10277](https://tree.taiga.io/project/penpot/us/10277)
- Create variants in bulk from existing components [Taiga #7926](https://tree.taiga.io/project/penpot/us/7926)
- Alternative ways of creating variants - Button Design Tab [Taiga #10316](https://tree.taiga.io/project/penpot/us/10316)
- Fix problem with component swapping panel [Taiga #12175](https://tree.taiga.io/project/penpot/issue/12175)
### :bug: Bugs fixed
@@ -93,25 +23,8 @@
- Fix font size/variant not updated when editing a text [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11552)
- Fix issue where Alt + arrow keys shortcut interferes with letter-spacing when moving text layers [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11771)
- Fix consistency issues on how font variants are visualized [Taiga #11499](https://tree.taiga.io/project/penpot/us/11499)
- Fix parsing rx and ry SVG values for rect radius [Taiga #11861](https://tree.taiga.io/project/penpot/issue/11861)
- Fix misleading affordance in saved versions [Taiga #11887](https://tree.taiga.io/project/penpot/issue/11887)
- Fix pasting RTF text crashes penpot [Taiga #11717](https://tree.taiga.io/project/penpot/issue/11717)
- Fix navigation arrows in Libraries & Templates carousel [Taiga #10609](https://tree.taiga.io/project/penpot/issue/10609)
- Fix applying tokens with zero value to size [Taiga #11618](https://tree.taiga.io/project/penpot/issue/11618)
- Fix typo [Taiga #11969](https://tree.taiga.io/project/penpot/issue/11969)
- Fix typo [Taiga #11970](https://tree.taiga.io/project/penpot/issue/11970)
- Fix typos [Taiga #11971](https://tree.taiga.io/project/penpot/issue/11971)
- Fix inconsistent naming for "Flatten" [Taiga #8371](https://tree.taiga.io/project/penpot/issue/8371)
- Layout item tokens should be unapplied when moving out of a layout [Taiga #11012](https://tree.taiga.io/project/penpot/issue/11012)
- Fix incorrect date displayed for support plan [Taiga #11986](https://tree.taiga.io/project/penpot/issue/11986)
- Fix can't import 'borderWidth' type token [#132](https://github.com/tokens-studio/penpot/issues/132)
- Fix moving elements up or down while pressing alt [Taiga Issue #11992](https://tree.taiga.io/project/penpot/issue/11992)
- Fix conflicting shortcuts (remove dec/inc line height and letter spacing) [Taiga #12102](https://tree.taiga.io/project/penpot/issue/12102)
- Fix conflicting shortcuts (remove text-align shortcuts) [Taiga #12047](https://tree.taiga.io/project/penpot/issue/12047)
- Fix export file with empty tokens library [Taiga #12137](https://tree.taiga.io/project/penpot/issue/12137)
- Fix context menu on spacing tokens [Taiga #12141](https://tree.taiga.io/project/penpot/issue/12141)
## 2.9.0
## 2.9.0 (Unreleased)
### :rocket: Epics and highlights
@@ -189,7 +102,7 @@
**Penpot Library**
The initial prototype is completly reworked to provide a more consistent API
The initial prototype is completly reworked for provide a more consistent API
and to have proper validation and params decoding. All the details can be found
on [its own changelog](library/CHANGES.md)

View File

@@ -77,14 +77,17 @@ Provide your team or organization with a completely owned collaborative design t
### Integrations ###
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
### Building Design Systems: design tokens, components and variants ###
Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms.
### Whats great for design ###
With Penpot you can design libraries to share and reuse; turn design elements into components and tokens to allow reusability and scalability; and build realistic user flows and interactions.
### Design Tokens ###
With Penpots standardized [design tokens](https://penpot.dev/collaboration/design-tokens) format, you can easily reuse and sync tokens across different platforms, workflows, and disciplines.
<br />
<p align="center">
<img src="https://github.com/user-attachments/assets/cce75ad6-f783-473f-8803-da9eb8255fef">
<img src="https://img.plasmic.app/img-optimizer/v1/img?src=https%3A%2F%2Fimg.plasmic.app%2Fimg-optimizer%2Fv1%2Fimg%2F9dd677c36afb477e9666ccd1d3f009ad.png" alt="Open Source" style="width: 65%;">
</p>
<br />

View File

@@ -3,10 +3,10 @@
:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.12.2"}
org.clojure/clojure {:mvn/version "1.12.1"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-3"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.8.1.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.7.0.RELEASE"}
;; Minimal dependencies required by lettuce, we need to include them
;; explicitly because clojure dependency management does not support
;; yet the BOM format.
@@ -28,30 +28,29 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.6"
:git/sha "94dc017"
{:git/tag "v11.4"
:git/sha "ce50d42"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc
{:mvn/version "1.3.1070"}
{:mvn/version "1.3.1002"}
metosin/reitit-core {:mvn/version "0.9.1"}
nrepl/nrepl {:mvn/version "1.4.0"}
nrepl/nrepl {:mvn/version "1.3.1"}
org.postgresql/postgresql {:mvn/version "42.7.7"}
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
org.postgresql/postgresql {:mvn/version "42.7.6"}
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
io.whitfin/siphash {:mvn/version "2.0.0"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.2"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.0"}
org.jsoup/jsoup {:mvn/version "1.21.2"}
org.jsoup/jsoup {:mvn/version "1.20.1"}
org.im4java/im4java
{:git/tag "1.4.0-penpot-2"
:git/sha "e2b3e16"
@@ -61,12 +60,12 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
dawran6/emoji {:mvn/version "0.2.0"}
markdown-clj/markdown-clj {:mvn/version "1.12.4"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.12.3"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.33.10"}}
software.amazon.awssdk/s3 {:mvn/version "2.31.55"}}
:paths ["src" "resources" "target/classes"]
:aliases
@@ -81,14 +80,12 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
:ns-default build}
:test
{:main-opts ["-m" "kaocha.runner"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
"--sun-misc-unsafe-memory-access=allow"
"--enable-native-access=ALL-UNNAMED"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
:outdated

View File

@@ -6,14 +6,12 @@
(ns user
(:require
[app.binfile.common :as bfc]
[app.common.data :as d]
[app.common.debug :as debug]
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.fressian :as fres]
[app.common.geom.matrix :as gmt]
[app.common.json :as json]
[app.common.logging :as l]
[app.common.perf :as perf]
[app.common.pprint :as pp]
@@ -21,9 +19,8 @@
[app.common.schema.desc-js-like :as smdj]
[app.common.schema.desc-native :as smdn]
[app.common.schema.generators :as sg]
[app.common.schema.openapi :as oapi]
[app.common.spec :as us]
[app.common.time :as ct]
[app.common.json :as json]
[app.common.transit :as t]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
@@ -33,9 +30,9 @@
[app.srepl.helpers :as srepl.helpers]
[app.srepl.main :as srepl]
[app.util.blob :as blob]
[app.common.time :as ct]
[clj-async-profiler.core :as prof]
[clojure.contrib.humanize :as hum]
[clojure.datafy :refer [datafy]]
[clojure.java.io :as io]
[clojure.pprint :refer [pprint print-table]]
[clojure.repl :refer :all]

View File

@@ -1 +1 @@
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}
Invitation to join {{team}}

View File

@@ -31,7 +31,8 @@ export PENPOT_FLAGS="\
disable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation \
enable-subscriptions";
enable-subscriptions \
disable-subscriptions-old";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
@@ -77,14 +78,10 @@ export JAVA_OPTS="\
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
-Djdk.tracePinnedThreads=full \
-Dim4java.useV7=true \
-XX:+UseShenandoahGC \
-XX:+EnableDynamicAgentLoading \
-XX:-OmitStackTraceInFastThrow \
-XX:+UnlockExperimentalVMOptions \
-XX:+UnlockDiagnosticVMOptions \
-XX:+DebugNonSafepoints \
-XX:ShenandoahGCMode=generational \
-XX:+UseCompactObjectHeaders \
--sun-misc-unsafe-memory-access=allow \
--enable-preview \
--enable-native-access=ALL-UNNAMED";

View File

@@ -24,7 +24,8 @@ export PENPOT_FLAGS="\
disable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation \
enable-subscriptions";
enable-subscriptions \
disable-subscriptions-old";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"

View File

@@ -19,7 +19,6 @@
[app.common.time :as ct]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.common.weak :as weak]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
@@ -34,7 +33,8 @@
[clojure.set :as set]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[datoteka.io :as io]))
[datoteka.io :as io]
[promesa.exec :as px]))
(set! *warn-on-reflection* true)
@@ -187,9 +187,9 @@
and decoding."
[cfg file-id & {:as opts}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(when-let [row (db/get* conn :file {:id file-id}
(assoc opts ::db/remove-deleted false))]
(decode-file cfg row opts)))))
(some->> (db/get* conn :file {:id file-id}
(assoc opts ::db/remove-deleted false))
(decode-file cfg)))))
(defn clean-file-features
[file]
@@ -475,7 +475,7 @@
(vary-meta dissoc ::fmg/migrated))))
(defn encode-file
[cfg {:keys [id features] :as file}]
[{:keys [::wrk/executor] :as cfg} {:keys [id features] :as file}]
(let [file (if (and (contains? features "fdata/objects-map")
(:data file))
(fdata/enable-objects-map file)
@@ -492,7 +492,7 @@
(-> file
(d/update-when :features into-array)
(d/update-when :data blob/encode))))
(d/update-when :data (fn [data] (px/invoke! executor #(blob/encode data)))))))
(defn- file->params
[file]
@@ -606,19 +606,11 @@
(map decode-row))
(db/exec! conn [sql:get-file-libraries file-id])))
;; FIXME: this will use a lot of memory if file uses too many big
;; libraries, we should load required libraries on demand
(defn get-resolved-file-libraries
"Get all file libraries including itself. Returns an instance of
LoadableWeakValueMap that allows do not have strong references to
the loaded libraries and reduce possible memory pressure on having
all this libraries loaded at same time on processing file validation
or file migration.
This still requires at least one library at time to be loaded while
access to it is performed, but it improves considerable not having
the need of loading all the libraries at the same time."
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
(let [library-ids (->> (get-file-libraries conn (:id file))
(map :id)
(cons (:id file)))
load-fn #(get-file cfg % :migrate? false)]
(weak/loadable-weak-value-map library-ids load-fn {id file})))
"A helper for preload file libraries"
[{:keys [::db/conn] :as cfg} file]
(->> (get-file-libraries conn (:id file))
(into [file] (map #(get-file cfg (:id %))))
(d/index-by :id)))

View File

@@ -36,6 +36,11 @@
"fdata/shape-data-type"
nil
;; There is no migration needed, but we don't want to allow
;; copy paste nor import of variant files into no-variant teams
"variants/v1"
nil
(ex/raise :type :internal
:code :no-migration-defined
:hint (str/ffmt "no migation for feature '%' on file importation" feature)

View File

@@ -27,7 +27,7 @@
[app.common.types.page :as ctp]
[app.common.types.plugins :as ctpg]
[app.common.types.shape :as cts]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-lib :as cto]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -105,25 +105,25 @@
(sm/encoder ctp/schema:page sm/json-transformer))
(def encode-shape
(sm/encoder cts/schema:shape sm/json-transformer))
(sm/encoder ::cts/shape sm/json-transformer))
(def encode-media
(sm/encoder ctf/schema:media sm/json-transformer))
(sm/encoder ::ctf/media sm/json-transformer))
(def encode-component
(sm/encoder ctc/schema:component sm/json-transformer))
(sm/encoder ::ctc/component sm/json-transformer))
(def encode-color
(sm/encoder ctcl/schema:library-color sm/json-transformer))
(def encode-typography
(sm/encoder cty/schema:typography sm/json-transformer))
(sm/encoder ::cty/typography sm/json-transformer))
(def encode-tokens-lib
(sm/encoder ctob/schema:tokens-lib sm/json-transformer))
(sm/encoder ::cto/tokens-lib sm/json-transformer))
(def encode-plugin-data
(sm/encoder ctpg/schema:plugin-data sm/json-transformer))
(sm/encoder ::ctpg/plugin-data sm/json-transformer))
(def encode-storage-object
(sm/encoder schema:storage-object sm/json-transformer))
@@ -140,7 +140,7 @@
(sm/decoder ctf/schema:media sm/json-transformer))
(def decode-component
(sm/decoder ctc/schema:component sm/json-transformer))
(sm/decoder ::ctc/component sm/json-transformer))
(def decode-color
(sm/decoder ctcl/schema:library-color sm/json-transformer))
@@ -149,19 +149,19 @@
(sm/decoder schema:file sm/json-transformer))
(def decode-page
(sm/decoder ctp/schema:page sm/json-transformer))
(sm/decoder ::ctp/page sm/json-transformer))
(def decode-shape
(sm/decoder cts/schema:shape sm/json-transformer))
(sm/decoder ::cts/shape sm/json-transformer))
(def decode-typography
(sm/decoder cty/schema:typography sm/json-transformer))
(sm/decoder ::cty/typography sm/json-transformer))
(def decode-tokens-lib
(sm/decoder ctob/schema:tokens-lib sm/json-transformer))
(sm/decoder cto/schema:tokens-lib sm/json-transformer))
(def decode-plugin-data
(sm/decoder ctpg/schema:plugin-data sm/json-transformer))
(sm/decoder ::ctpg/plugin-data sm/json-transformer))
(def decode-storage-object
(sm/decoder schema:storage-object sm/json-transformer))
@@ -175,31 +175,31 @@
(sm/check-fn schema:manifest))
(def validate-file
(sm/check-fn ctf/schema:file))
(sm/check-fn ::ctf/file))
(def validate-page
(sm/check-fn ctp/schema:page))
(sm/check-fn ::ctp/page))
(def validate-shape
(sm/check-fn cts/schema:shape))
(sm/check-fn ::cts/shape))
(def validate-media
(sm/check-fn ctf/schema:media))
(sm/check-fn ::ctf/media))
(def validate-color
(sm/check-fn ctcl/schema:library-color))
(def validate-component
(sm/check-fn ctc/schema:component))
(sm/check-fn ::ctc/component))
(def validate-typography
(sm/check-fn cty/schema:typography))
(sm/check-fn ::cty/typography))
(def validate-tokens-lib
(sm/check-fn ctob/schema:tokens-lib))
(sm/check-fn ::cto/tokens-lib))
(def validate-plugin-data
(sm/check-fn ctpg/schema:plugin-data))
(sm/check-fn ::ctpg/plugin-data))
(def validate-storage-object
(sm/check-fn schema:storage-object))
@@ -349,8 +349,7 @@
typography (encode-typography object)]
(write-entry! output path typography)))
(when (and tokens-lib
(not (ctob/empty-lib? tokens-lib)))
(when tokens-lib
(let [path (str "files/" file-id "/tokens.json")
encoded-tokens (encode-tokens-lib tokens-lib)]
(write-entry! output path encoded-tokens)))))

View File

@@ -96,7 +96,7 @@
[:http-server-max-body-size {:optional true} ::sm/int]
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
[:http-server-io-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int]
[:http-server-worker-threads {:optional true} ::sm/int]
[:telemetry-uri {:optional true} :string]
[:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
@@ -214,21 +214,20 @@
[:media-uri {:optional true} :string]
[:assets-path {:optional true} :string]
[:netty-io-threads {:optional true} ::sm/int]
[:executor-threads {:optional true} ::sm/int]
;; DEPRECATED
;; Legacy, will be removed in 2.5
[:assets-storage-backend {:optional true} :keyword]
[:storage-assets-fs-directory {:optional true} :string]
[:storage-assets-s3-bucket {:optional true} :string]
[:storage-assets-s3-region {:optional true} :keyword]
[:storage-assets-s3-endpoint {:optional true} ::sm/uri]
[:storage-assets-s3-io-threads {:optional true} ::sm/int]
[:objects-storage-backend {:optional true} :keyword]
[:objects-storage-fs-directory {:optional true} :string]
[:objects-storage-s3-bucket {:optional true} :string]
[:objects-storage-s3-region {:optional true} :keyword]
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]]))
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]
[:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
(defn- parse-flags
[config]

View File

@@ -53,15 +53,8 @@
opts (cond-> opts
(::order-by opts) (assoc :order-by (::order-by opts))
(::columns opts) (assoc :columns (::columns opts))
(or (::db/for-update opts)
(::for-update opts))
(assoc :suffix "FOR UPDATE")
(or (::db/for-share opts)
(::for-share opts))
(assoc :suffix "FOR SHARE"))]
(::for-update opts) (assoc :suffix "FOR UPDATE")
(::for-share opts) (assoc :suffix "FOR SHARE"))]
(sql/for-query table where-params opts))))
(defn update

View File

@@ -12,14 +12,15 @@
[app.common.files.helpers :as cfh]
[app.common.files.migrations :as fmg]
[app.common.logging :as l]
[app.common.types.objects-map :as omap]
[app.common.types.path :as path]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.storage :as sto]
[app.util.blob :as blob]
[app.util.objects-map :as omap.legacy]
[app.util.pointer-map :as pmap]))
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.worker :as wrk]
[promesa.exec :as px]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OFFLOAD
@@ -37,7 +38,10 @@
[file & _opts]
(let [update-page
(fn [page]
(update page :objects omap/wrap))
(if (and (pmap/pointer-map? page)
(not (pmap/loaded? page)))
page
(update page :objects omap/wrap)))
update-data
(fn [fdata]
@@ -47,20 +51,6 @@
(update :data update-data)
(update :features conj "fdata/objects-map"))))
(defn disable-objects-map
[file & _opts]
(let [update-page
(fn [page]
(update page :objects #(into {} %)))
update-data
(fn [fdata]
(update fdata :pages-index d/update-vals update-page))]
(-> file
(update :data update-data)
(update :features disj "fdata/objects-map"))))
(defn process-objects
"Apply a function to all objects-map on the file. Usualy used for convert
the objects-map instances to plain maps"
@@ -70,8 +60,7 @@
(fn [page]
(update page :objects
(fn [objects]
(if (or (omap/objects-map? objects)
(omap.legacy/objects-map? objects))
(if (omap/objects-map? objects)
(update-fn objects)
objects)))))
fdata))
@@ -95,10 +84,10 @@
(assoc file :data data)))
(defn decode-file-data
[_system {:keys [data] :as file}]
[{:keys [::wrk/executor]} {:keys [data] :as file}]
(cond-> file
(bytes? data)
(assoc :data (blob/decode data))))
(assoc :data (px/invoke! executor #(blob/decode data)))))
(defn load-pointer
"A database loader pointer helper"

View File

@@ -17,7 +17,6 @@
[app.http.awsns :as-alias awsns]
[app.http.debug :as-alias debug]
[app.http.errors :as errors]
[app.http.management :as mgmt]
[app.http.middleware :as mw]
[app.http.session :as session]
[app.http.websocket :as-alias ws]
@@ -26,7 +25,9 @@
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc]
[app.setup :as-alias setup]
[app.worker :as wrk]
[integrant.core :as ig]
[promesa.exec :as px]
[reitit.core :as r]
[reitit.middleware :as rr]
[yetti.adapter :as yt]
@@ -53,8 +54,6 @@
[:map
[::port ::sm/int]
[::host ::sm/text]
[::io-threads {:optional true} ::sm/int]
[::max-worker-threads {:optional true} ::sm/int]
[::max-body-size {:optional true} ::sm/int]
[::max-multipart-body-size {:optional true} ::sm/int]
[::router {:optional true} [:fn r/router?]]
@@ -65,41 +64,31 @@
(assert (sm/check schema:server-params params)))
(defmethod ig/init-key ::server
[_ {:keys [::handler ::router ::host ::port ::mtx/metrics] :as cfg}]
[_ {:keys [::handler ::router ::host ::port ::wrk/executor] :as cfg}]
(l/info :hint "starting http server" :port port :host host)
(let [on-dispatch
(fn [_ start-at-ns]
(let [timing (- (System/nanoTime) start-at-ns)
timing (int (/ timing 1000000))]
(mtx/run! metrics
:id :http-server-dispatch-timing
:val timing)))
(let [options {:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (or (::io-threads cfg)
(max 3 (px/get-available-processors)))
:xnio/dispatch executor
:ring/compat :ring2
:socket/backlog 4069}
options
{:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (::io-threads cfg)
:xnio/max-worker-threads (::max-worker-threads cfg)
:ring/compat :ring2
:events/on-dispatch on-dispatch
:socket/backlog 4069}
handler (cond
(some? router)
(router-handler router)
handler
(cond
(some? router)
(router-handler router)
(some? handler)
handler
(some? handler)
handler
:else
(throw (UnsupportedOperationException. "handler or router are required")))
:else
(throw (UnsupportedOperationException. "handler or router are required")))
server
(yt/server handler (d/without-nils options))]
options (d/without-nils options)
server (yt/server handler options)]
(assoc cfg ::server (yt/start! server))))
@@ -154,7 +143,6 @@
[::debug/routes schema:routes]
[::mtx/routes schema:routes]
[::awsns/routes schema:routes]
[::mgmt/routes schema:routes]
::session/manager
::setup/props
::db/pool])
@@ -182,9 +170,6 @@
["/webhooks"
(::awsns/routes cfg)]
["/management"
(::mgmt/routes cfg)]
(::ws/routes cfg)
["/api" {:middleware [[mw/cors]]}

View File

@@ -17,9 +17,11 @@
[app.main :as-alias main]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.worker :as-alias wrk]
[clojure.data.json :as j]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.exec :as px]
[yetti.request :as yreq]
[yetti.response :as-alias yres]))
@@ -38,8 +40,8 @@
[_ cfg]
(letfn [(handler [request]
(let [data (-> request yreq/body slurp)]
(handle-request cfg data)
{::yres/status 200}))]
(px/run! :vthread (partial handle-request cfg data)))
{::yres/status 200})]
["/sns" {:handler handler
:allowed-methods #{:post}}]))

View File

@@ -1,234 +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.http.management
"Internal mangement HTTP API"
(: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.main :as-alias main]
[app.rpc.commands.profile :as cmd.profile]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.worker :as-alias wrk]
[integrant.core :as ig]
[yetti.response :as-alias yres]))
;; ---- ROUTES
(declare ^:private authenticate)
(declare ^:private get-customer)
(declare ^:private update-customer)
(defmethod ig/assert-key ::routes
[_ params]
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
(def ^:private default-system
{:name ::default-system
:compile
(fn [_ _]
(fn [handler cfg]
(fn [request]
(handler cfg request))))})
(def ^:private transaction
{:name ::transaction
:compile
(fn [data _]
(when (:transaction data)
(fn [handler]
(fn [cfg request]
(db/tx-run! cfg handler request)))))})
(defmethod ig/init-key ::routes
[_ cfg]
["" {:middleware [[default-system cfg]
[transaction]]}
["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["/get-customer"
{:handler get-customer
:transaction true
:allowed-methods #{:post}}]
["/update-customer"
{:handler update-customer
:allowed-methods #{:post}
:transaction true}]])
;; ---- HELPERS
(defn- coercer
[schema & {:as opts}]
(let [decode-fn (sm/decoder schema sm/json-transformer)
check-fn (sm/check-fn schema opts)]
(fn [data]
(-> data decode-fn check-fn))))
;; ---- API: AUTHENTICATE
(defn- authenticate
[cfg request]
(let [token (-> request :params :token)
props (get cfg ::setup/props)
result (tokens/verify props {:token token :iss "authentication"})]
{::yres/status 200
::yres/body result}))
;; ---- API: GET-CUSTOMER
(def ^:private schema:get-customer
[:map [:id ::sm/uuid]])
(def ^:private coerce-get-customer-params
(coercer schema:get-customer
:type :validation
:hint "invalid data provided for `get-customer` rpc call"))
(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)))
(defn- get-customer
[cfg request]
(let [profile-id (-> request :params coerce-get-customer-params :id)
profile (cmd.profile/get-profile cfg profile-id)
result {:id (get profile :id)
:name (get profile :fullname)
:email (get profile :email)
:num-editors (get-customer-slots cfg profile-id)
:subscription (-> profile :props :subscription)}]
{::yres/status 200
::yres/body result}))
;; ---- API: UPDATE-CUSTOMER
(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 ct/inst
:encode/string inst-ms
:decode/json ct/inst
:encode/json 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 schema:update-customer
[:map
[:id ::sm/uuid]
[:subscription [:maybe schema:subscription]]])
(def ^:private coerce-update-customer-params
(coercer schema:update-customer
:type :validation
:hint "invalid data provided for `update-customer` rpc call"))
(defn- update-customer
[cfg request]
(let [{:keys [id subscription]}
(-> request :params coerce-update-customer-params)
{:keys [props] :as profile}
(cmd.profile/get-profile cfg id ::db/for-update true)
props
(assoc props :subscription subscription)]
(l/dbg :hint "update customer"
:profile-id (str 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 id}
{::db/return-keys false})
{::yres/status 201
::yres/body nil}))

View File

@@ -54,7 +54,7 @@
::yres/body (yres/stream-body
(fn [_ output]
(let [channel (sp/chan :buf buf :xf (keep encode))
listener (events/spawn-listener
listener (events/start-listener
channel
(partial write! output)
(partial pu/close! output))]

View File

@@ -20,7 +20,6 @@
[app.http.awsns :as http.awsns]
[app.http.client :as-alias http.client]
[app.http.debug :as-alias http.debug]
[app.http.management :as mgmt]
[app.http.session :as-alias session]
[app.http.session.tasks :as-alias session.tasks]
[app.http.websocket :as http.ws]
@@ -42,7 +41,6 @@
[app.svgo :as-alias svgo]
[app.util.cron]
[app.worker :as-alias wrk]
[app.worker.executor]
[clojure.test :as test]
[clojure.tools.namespace.repl :as repl]
[cuerdas.core :as str]
@@ -149,11 +147,23 @@
::mdef/labels []
::mdef/type :histogram}
:http-server-dispatch-timing
{::mdef/name "penpot_http_server_dispatch_timing"
::mdef/help "Histogram of dispatch handler"
::mdef/labels []
::mdef/type :histogram}})
:executors-active-threads
{::mdef/name "penpot_executors_active_threads"
::mdef/help "Current number of threads available in the executor service."
::mdef/labels ["name"]
::mdef/type :gauge}
:executors-completed-tasks
{::mdef/name "penpot_executors_completed_tasks_total"
::mdef/help "Approximate number of completed tasks by the executor."
::mdef/labels ["name"]
::mdef/type :counter}
:executors-running-threads
{::mdef/name "penpot_executors_running_threads"
::mdef/help "Current number of threads with state RUNNING."
::mdef/labels ["name"]
::mdef/type :gauge}})
(def system-config
{::db/pool
@@ -165,12 +175,14 @@
::db/max-size (cf/get :database-max-pool-size 60)
::mtx/metrics (ig/ref ::mtx/metrics)}
;; Default netty IO pool (shared between several services)
::wrk/netty-io-executor
{:threads (cf/get :netty-io-threads)}
;; Default thread pool for IO operations
::wrk/executor
{}
::wrk/netty-executor
{:threads (cf/get :executor-threads)}
::wrk/monitor
{::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/executor)
::wrk/name "default"}
:app.migrations/migrations
{::db/pool (ig/ref ::db/pool)}
@@ -184,19 +196,14 @@
::rds/redis
{::rds/uri (cf/get :redis-uri)
::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/netty-executor
(ig/ref ::wrk/netty-executor)
::wrk/netty-io-executor
(ig/ref ::wrk/netty-io-executor)}
::wrk/executor (ig/ref ::wrk/executor)}
::mbus/msgbus
{::wrk/executor (ig/ref ::wrk/netty-executor)
{::wrk/executor (ig/ref ::wrk/executor)
::rds/redis (ig/ref ::rds/redis)}
:app.storage.tmp/cleaner
{::wrk/executor (ig/ref ::wrk/netty-executor)}
{::wrk/executor (ig/ref ::wrk/executor)}
::sto.gc-deleted/handler
{::db/pool (ig/ref ::db/pool)
@@ -224,10 +231,9 @@
::http/host (cf/get :http-server-host)
::http/router (ig/ref ::http/router)
::http/io-threads (cf/get :http-server-io-threads)
::http/max-worker-threads (cf/get :http-server-max-worker-threads)
::http/max-body-size (cf/get :http-server-max-body-size)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
::mtx/metrics (ig/ref ::mtx/metrics)}
::wrk/executor (ig/ref ::wrk/executor)}
::ldap/provider
{:host (cf/get :ldap-host)
@@ -267,10 +273,6 @@
::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist)}
::mgmt/routes
{::db/pool (ig/ref ::db/pool)
::setup/props (ig/ref ::setup/props)}
:app.http/router
{::session/manager (ig/ref ::session/manager)
::db/pool (ig/ref ::db/pool)
@@ -279,7 +281,6 @@
::setup/props (ig/ref ::setup/props)
::mtx/routes (ig/ref ::mtx/routes)
::oidc/routes (ig/ref ::oidc/routes)
::mgmt/routes (ig/ref ::mgmt/routes)
::http.debug/routes (ig/ref ::http.debug/routes)
::http.assets/routes (ig/ref ::http.assets/routes)
::http.ws/routes (ig/ref ::http.ws/routes)
@@ -305,17 +306,17 @@
::rpc/climit
{::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/netty-executor)
::wrk/executor (ig/ref ::wrk/executor)
::climit/config (cf/get :rpc-climit-config)
::climit/enabled (contains? cf/flags :rpc-climit)}
:app.rpc/rlimit
{::wrk/executor (ig/ref ::wrk/netty-executor)}
{::wrk/executor (ig/ref ::wrk/executor)}
:app.rpc/methods
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/netty-executor)
::wrk/executor (ig/ref ::wrk/executor)
::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider)
::sto/storage (ig/ref ::sto/storage)
@@ -469,14 +470,13 @@
(cf/get :objects-storage-s3-bucket))
::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads)
(cf/get :objects-storage-s3-io-threads))
::wrk/netty-io-executor
(ig/ref ::wrk/netty-io-executor)}
::wrk/executor (ig/ref ::wrk/executor)}
:app.storage.fs/backend
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
(cf/get :objects-storage-fs-directory))}})
(def worker-config
{::wrk/cron
{::wrk/registry (ig/ref ::wrk/registry)

View File

@@ -38,13 +38,15 @@
org.im4java.core.Info))
(def schema:upload
[:map {:title "Upload"}
[:filename :string]
[:size ::sm/int]
[:path ::fs/path]
[:mtype {:optional true} :string]
[:headers {:optional true}
[:map-of :string :string]]])
(sm/register!
^{::sm/type ::upload}
[:map {:title "Upload"}
[:filename :string]
[:size ::sm/int]
[:path ::fs/path]
[:mtype {:optional true} :string]
[:headers {:optional true}
[:map-of :string :string]]]))
(def ^:private schema:input
[:map {:title "Input"}

View File

@@ -444,10 +444,7 @@
:fn (mg/resource "app/migrations/sql/0140-mod-file-change-table.sql")}
{:name "0140-add-locked-by-column-to-file-change-table"
:fn (mg/resource "app/migrations/sql/0140-add-locked-by-column-to-file-change-table.sql")}
{:name "0141-add-idx-to-file-library-rel"
:fn (mg/resource "app/migrations/sql/0141-add-idx-to-file-library-rel.sql")}])
:fn (mg/resource "app/migrations/sql/0140-add-locked-by-column-to-file-change-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@@ -1,2 +0,0 @@
CREATE INDEX IF NOT EXISTS file_library_rel__library_file_id__idx
ON file_library_rel (library_file_id);

View File

@@ -216,7 +216,8 @@
(rds/add-listener sconn (create-listener rcv-ch))
(px/thread
{:name "penpot/msgbus"}
{:name "penpot/msgbus/io-loop"
:virtual true}
(try
(loop []
(let [timeout-ch (sp/timeout-chan 1000)

View File

@@ -21,7 +21,8 @@
[clojure.java.io :as io]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.core :as p])
[promesa.core :as p]
[promesa.exec :as px])
(:import
clojure.lang.MapEntry
io.lettuce.core.KeyValue
@@ -44,10 +45,8 @@
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
io.lettuce.core.resource.ClientResources
io.lettuce.core.resource.DefaultClientResources
io.netty.channel.nio.NioEventLoopGroup
io.netty.util.HashedWheelTimer
io.netty.util.Timer
io.netty.util.concurrent.EventExecutorGroup
java.lang.AutoCloseable
java.time.Duration))
@@ -112,15 +111,20 @@
(defmethod ig/expand-key ::redis
[k v]
{k (-> (d/without-nils v)
(assoc ::timeout (ct/duration "10s")))})
(let [cpus (px/get-available-processors)
threads (max 1 (int (* cpus 0.2)))]
{k (-> (d/without-nils v)
(assoc ::timeout (ct/duration "10s"))
(assoc ::io-threads (max 3 threads))
(assoc ::worker-threads (max 3 threads)))}))
(def ^:private schema:redis-params
[:map {:title "redis-params"}
::wrk/netty-io-executor
::wrk/netty-executor
::wrk/executor
::mtx/metrics
[::uri ::sm/uri]
[::worker-threads ::sm/int]
[::io-threads ::sm/int]
[::timeout ::ct/duration]])
(defmethod ig/assert-key ::redis
@@ -137,30 +141,17 @@
(defn- initialize-resources
"Initialize redis connection resources"
[{:keys [::uri ::mtx/metrics ::wrk/netty-io-executor ::wrk/netty-executor] :as params}]
[{:keys [::uri ::io-threads ::worker-threads ::wrk/executor ::mtx/metrics] :as params}]
(l/inf :hint "initialize redis resources"
:uri (str uri))
:uri (str uri)
:io-threads io-threads
:worker-threads worker-threads)
(let [timer (HashedWheelTimer.)
resources (.. (DefaultClientResources/builder)
(eventExecutorGroup ^EventExecutorGroup netty-executor)
;; We provide lettuce with a shared event loop
;; group instance instead of letting lettuce to
;; create its own
(eventLoopGroupProvider
(reify io.lettuce.core.resource.EventLoopGroupProvider
(allocate [_ _] netty-io-executor)
(threadPoolSize [_]
(.executorCount ^NioEventLoopGroup netty-io-executor))
(release [_ _ _ _ _]
;; Do nothing
)
(shutdown [_ _ _ _]
;; Do nothing
)))
(ioThreadPoolSize ^long io-threads)
(computationThreadPoolSize ^long worker-threads)
(timer ^Timer timer)
(build))
@@ -175,7 +166,7 @@
(l/trace :hint "evict connection (cache)" :key key :reason cause)
(some-> val d/close!))
cache (cache/create :executor netty-executor
cache (cache/create :executor executor
:on-remove on-remove
:keepalive "5m")]
(reify

View File

@@ -21,6 +21,7 @@
[clojure.set :as set]
[datoteka.fs :as fs]
[integrant.core :as ig]
[promesa.exec :as px]
[promesa.exec.bulkhead :as pbh])
(:import
clojure.lang.ExceptionInfo
@@ -288,9 +289,13 @@
(get-limits cfg)))
(defn invoke!
"Run a function in context of climit."
[{:keys [::rpc/climit] :as cfg} f params]
"Run a function in context of climit.
Intended to be used in virtual threads."
[{:keys [::executor ::rpc/climit] :as cfg} f params]
(let [f (if climit
(build-exec-chain cfg f)
(let [f (if (some? executor)
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
f)]
(build-exec-chain cfg f))
f)]
(f cfg params)))

View File

@@ -6,7 +6,6 @@
(ns app.rpc.commands.auth
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
@@ -63,7 +62,7 @@
(ex/raise :type :validation
:code :account-without-password
:hint "the current account does not have password")
(let [result (auth/verify-password password (:password profile))]
(let [result (profile/verify-password cfg password (:password profile))]
(when (:update result)
(l/trc :hint "updating profile password"
:id (str (:id profile))
@@ -157,7 +156,7 @@
(:profile-id tdata)))
(update-password [conn profile-id]
(let [pwd (auth/derive-password password)]
(let [pwd (profile/derive-password cfg password)]
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
nil))]
@@ -379,7 +378,7 @@
(not (contains? cf/flags :email-verification)))
params (-> params
(assoc :is-active is-active)
(update :password auth/derive-password))
(update :password #(profile/derive-password cfg %)))
profile (->> (create-profile! conn params)
(create-profile-rels! conn))]
(vary-meta profile assoc :created true))))

View File

@@ -28,6 +28,7 @@
[app.tasks.file-gc]
[app.util.services :as sv]
[app.worker :as-alias wrk]
[promesa.exec :as px]
[yetti.response :as yres]))
(set! *warn-on-reflection* true)
@@ -93,7 +94,7 @@
;; --- Command: import-binfile
(defn- import-binfile
[{:keys [::db/pool] :as cfg} {:keys [profile-id project-id version name file]}]
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id project-id version name file]}]
(let [team (teams/get-team pool
:profile-id profile-id
:project-id project-id)
@@ -104,9 +105,13 @@
(assoc ::bfc/name name)
(assoc ::bfc/input (:path file)))
;; NOTE: the importation process performs some operations that are
;; not very friendly with virtual threads, and for avoid
;; unexpected blocking of other concurrent operations we dispatch
;; that operation to a dedicated executor.
result (case (int version)
1 (bf.v1/import-files! cfg)
3 (bf.v3/import-files! cfg))]
1 (px/invoke! executor (partial bf.v1/import-files! cfg))
3 (px/invoke! executor (partial bf.v3/import-files! cfg)))]
(db/update! pool :project
{:modified-at (ct/now)}
@@ -122,7 +127,7 @@
[:project-id ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:version {:optional true} ::sm/int]
[:file media/schema:upload]])
[:file ::media/upload]])
(sv/defmethod ::import-binfile
"Import a penpot file in a binary format. If `file-id` is provided,

View File

@@ -7,7 +7,6 @@
(ns app.rpc.commands.demo
"A demo specific mutations."
(:require
[app.auth :refer [derive-password]]
[app.common.exceptions :as ex]
[app.common.time :as ct]
[app.config :as cf]
@@ -15,6 +14,7 @@
[app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[buddy.core.codecs :as bc]
@@ -46,7 +46,7 @@
:fullname fullname
:is-active true
:deleted-at (ct/in-future (cf/get-deletion-delay))
:password (derive-password password)
:password (profile/derive-password cfg password)
:props {}}
profile (db/tx-run! cfg (fn [{:keys [::db/conn]}]
(->> (auth/create-profile! conn params)

View File

@@ -39,7 +39,8 @@
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.worker :as wrk]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[promesa.exec :as px]))
;; --- FEATURES
@@ -77,7 +78,6 @@
;; --- FILE PERMISSIONS
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
@@ -195,7 +195,7 @@
(def schema:permissions-mixin
[:map {:title "PermissionsMixin"}
[:permissions perms/schema:permissions]])
[:permissions ::perms/permissions]])
(def schema:file-with-permissions
[:merge {:title "FileWithPermissions"}
@@ -250,7 +250,7 @@
(feat.fmigr/resolve-applied-migrations cfg file))))))
(defn get-file
[{:keys [::db/conn] :as cfg} id
[{:keys [::db/conn ::wrk/executor] :as cfg} id
& {:keys [project-id
migrate?
include-deleted?
@@ -272,8 +272,13 @@
::db/remove-deleted (not include-deleted?)
::sql/for-update lock-for-update?})
(feat.fmigr/resolve-applied-migrations cfg)
(feat.fdata/resolve-file-data cfg)
(decode-row))
(feat.fdata/resolve-file-data cfg))
;; NOTE: we perform the file decoding in a separate thread
;; because it has heavy and synchronous operations for
;; decoding file body that are not very friendly with virtual
;; threads.
file (px/invoke! executor #(decode-row file))
file (if (and migrate? (fmg/need-migration? file))
(migrate-file cfg file options)
@@ -336,24 +341,14 @@
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file)))
(as-> file file
;; This operation is needed for backward comapatibility with
;; frontends that does not support pointer-map resolution
;; mechanism; this just resolves the pointers on backend and
;; return a complete file
(if (and (contains? (:features file) "fdata/pointer-map")
(not (contains? (:features params) "fdata/pointer-map")))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(update file :data feat.fdata/process-pointers deref))
file)
;; This operation is needed for backward comapatibility with
;; frontends that does not support objects-map mechanism; this
;; just converts all objects map instaces to plain maps
(if (and (contains? (:features file) "fdata/objects-map")
(not (contains? (:features params) "fdata/objects-map")))
(update file :data feat.fdata/process-objects (partial into {}))
file)))))
;; This operation is needed for backward comapatibility with frontends that
;; does not support pointer-map resolution mechanism; this just resolves the
;; pointers on backend and return a complete file.
(if (and (contains? (:features file) "fdata/pointer-map")
(not (contains? (:features params) "fdata/pointer-map")))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(update file :data feat.fdata/process-pointers deref))
file))))
;; --- COMMAND QUERY: get-file-fragment (by id)
@@ -362,7 +357,7 @@
[:id ::sm/uuid]
[:file-id ::sm/uuid]
[:created-at ::ct/inst]
[:content ::sm/any]])
[:content any?]])
(def schema:get-file-fragment
[:map {:title "get-file-fragment"}
@@ -465,42 +460,8 @@
(:has-libraries row)))
;; --- COMMAND QUERY: get-library-usage
(declare get-library-usage)
(def schema:get-library-usage
[:map {:title "get-library-usage"}
[:file-id ::sm/uuid]])
:sample
(sv/defmethod ::get-library-usage
"Gets the number of files that use the specified library."
{::doc/added "2.10.0"
::sm/params schema:get-library-usage
::sm/result ::sm/int}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! pool profile-id file-id)
(get-library-usage conn file-id)))
(def ^:private sql:get-library-usage
"SELECT COUNT(*) AS used
FROM file_library_rel AS flr
JOIN file AS fl ON (flr.library_file_id = fl.id)
WHERE flr.library_file_id = ?::uuid
AND (fl.deleted_at IS NULL OR
fl.deleted_at > now())")
(defn- get-library-usage
[conn file-id]
(let [row (db/exec-one! conn [sql:get-library-usage file-id])]
{:used-in (:used row)}))
;; --- QUERY COMMAND: get-page
(defn- prune-objects
"Given the page data and the object-id returns the page data with all
other not needed objects removed from the `:objects` data
@@ -590,34 +551,8 @@
;; --- COMMAND QUERY: get-team-shared-files
(defn- components-and-variants
"Return a set with all the variant-ids, and a list of components, but with
only one component by variant"
[components]
(let [{:keys [variant-ids components]}
(reduce (fn [{:keys [variant-ids components] :as acc} {:keys [variant-id] :as component}]
(cond
(nil? variant-id)
{:variant-ids variant-ids :components (conj components component)}
(contains? variant-ids variant-id)
acc
:else
{:variant-ids (conj variant-ids variant-id) :components (conj components component)}))
{:variant-ids #{} :components []}
components)]
{:components components
:variant-ids variant-ids}))
;;coalesce(string_agg(flr.library_file_id::text, ','), '') as library_file_ids
(def ^:private sql:team-shared-files
"with file_library_agg as (
select flr.file_id,
coalesce(array_agg(flr.library_file_id) filter (where flr.library_file_id is not null), '{}') as library_file_ids
from file_library_rel flr
group by flr.file_id
)
select f.id,
"select f.id,
f.revn,
f.vern,
f.data,
@@ -630,12 +565,10 @@
f.version,
f.is_shared,
ft.media_id,
p.team_id,
fla.library_file_ids
p.team_id
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn and ft.deleted_at is null)
left join file_library_agg as fla on fla.file_id = f.id
where f.is_shared = true
and f.deleted_at is null
and p.deleted_at is null
@@ -651,13 +584,10 @@
:sample (into [] (take limit sorted-assets))}))]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(let [load-objects (fn [component]
(ctf/load-component-objects data component))
comps-and-variants (components-and-variants (ctkl/components-seq data))
components (into {} (map (juxt :id identity) (:components comps-and-variants)))
components-sample (-> (assets-sample components 4)
(update :sample #(mapv load-objects %))
(assoc :variants-count (-> comps-and-variants :variant-ids count)))]
(let [load-objects (fn [component]
(ctf/load-component-objects data component))
components-sample (-> (assets-sample (ctkl/components data) 4)
(update :sample #(mapv load-objects %)))]
{:components components-sample
:media (assets-sample (:media data) 3)
:colors (assets-sample (:colors data) 3)
@@ -679,8 +609,6 @@
(dissoc :media-id)
(assoc :thumbnail-id media-id))
(dissoc row :media-id))))
(map (fn [row]
(update row :library-file-ids db/decode-pgarray #{})))
(map #(assoc % :library-summary (get-library-summary cfg %)))
(map #(dissoc % :data))))))
@@ -713,7 +641,6 @@
;; --- COMMAND QUERY: Files that use this File library
(def ^:private sql:library-using-files
"SELECT f.id,
f.name
@@ -786,7 +713,6 @@
;; --- COMMAND QUERY: get-file-summary
(defn- get-file-summary
[{:keys [::db/conn] :as cfg} {:keys [profile-id id project-id] :as params}]
(check-read-permissions! conn profile-id id)
@@ -804,13 +730,11 @@
(cfeat/check-file-features! (:features file)))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(let [components-and-variants (components-and-variants (ctkl/components-seq (:data file)))]
{:name (:name file)
:components-count (-> components-and-variants :components count)
:variants-count (-> components-and-variants :variant-ids count)
:graphics-count (count (get-in file [:data :media] []))
:colors-count (count (get-in file [:data :colors] []))
:typography-count (count (get-in file [:data :typographies] []))}))))
{:name (:name file)
:components-count (count (ctkl/components-seq (:data file)))
:graphics-count (count (get-in file [:data :media] []))
:colors-count (count (get-in file [:data :colors] []))
:typography-count (count (get-in file [:data :typographies] []))})))
(sv/defmethod ::get-file-summary
"Retrieve a file summary by its ID. Only authenticated users."
@@ -822,7 +746,6 @@
;; --- COMMAND QUERY: get-file-info
(defn- get-file-info
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
(db/get* conn :file
@@ -1077,7 +1000,6 @@
[:library-id ::sm/uuid]])
(sv/defmethod ::link-file-to-library
"Link a file to a library. Returns the recursive list of libraries used by that library"
{::doc/added "1.17"
::webhooks/event? true
::sm/params schema:link-file-to-library}
@@ -1091,8 +1013,7 @@
(fn [{:keys [::db/conn]}]
(check-edition-permissions! conn profile-id file-id)
(check-edition-permissions! conn profile-id library-id)
(link-file-to-library conn params)
(bfc/get-libraries cfg [library-id]))))
(link-file-to-library conn params))))
;; --- MUTATION COMMAND: unlink-file-from-library

View File

@@ -112,15 +112,14 @@
;; FIXME: IMPORTANT: this code can have race conditions, because
;; we have no locks for updating team so, creating two files
;; concurrently can lead to lost team features updating
(when-let [features (-> features
(set/difference (:features team))
(set/difference cfeat/no-team-inheritable-features)
(not-empty))]
(let [features (-> features
(set/union (:features team))
(set/difference cfeat/no-team-inheritable-features)
(into-array))]
(let [features (->> features
(set/union (:features team))
(db/create-array conn "text"))]
(db/update! conn :team
{:features features}
{:id (:id team)}

View File

@@ -79,9 +79,10 @@
;; --- MUTATION COMMAND: update-temp-file
(def ^:private schema:update-temp-file
[:map {:title "update-temp-file"}
[:changes [:vector cpc/schema:change]]
[:changes [:vector ::cpc/change]]
[:revn [::sm/int {:min 0}]]
[:session-id ::sm/uuid]
[:id ::sm/uuid]])

View File

@@ -271,7 +271,7 @@
[:map {:title "create-file-object-thumbnail"}
[:file-id ::sm/uuid]
[:object-id [:string {:max 250}]]
[:media media/schema:upload]
[:media ::media/upload]
[:tag {:optional true} [:string {:max 50}]]])
(sv/defmethod ::create-file-object-thumbnail
@@ -381,7 +381,7 @@
[:map {:title "create-file-thumbnail"}
[:file-id ::sm/uuid]
[:revn ::sm/int]
[:media media/schema:upload]])
[:media ::media/upload]])
(sv/defmethod ::create-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the

View File

@@ -37,7 +37,9 @@
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[clojure.set :as set]))
[app.worker :as wrk]
[clojure.set :as set]
[promesa.exec :as px]))
(declare ^:private get-lagged-changes)
(declare ^:private send-notifications!)
@@ -62,10 +64,10 @@
[:revn {:min 0} ::sm/int]
[:vern {:min 0} ::sm/int]
[:features {:optional true} ::cfeat/features]
[:changes {:optional true} [:vector cpc/schema:change]]
[:changes {:optional true} [:vector ::cpc/change]]
[:changes-with-metadata {:optional true}
[:vector [:map
[:changes [:vector cpc/schema:change]]
[:changes [:vector ::cpc/change]]
[:hint-origin {:optional true} :keyword]
[:hint-events {:optional true} [:vector [:string {:max 250}]]]]]]
[:skip-validate {:optional true} ::sm/boolean]])
@@ -74,7 +76,7 @@
schema:update-file-result
[:vector {:title "update-file-result"}
[:map
[:changes [:vector cpc/schema:change]]
[:changes [:vector ::cpc/change]]
[:file-id ::sm/uuid]
[:id ::sm/uuid]
[:revn {:min 0} ::sm/int]
@@ -158,6 +160,7 @@
tpoint (ct/tpoint)]
(when (not= (:vern params)
(:vern file))
(ex/raise :type :validation
@@ -180,15 +183,15 @@
(set/difference (:features team))
(set/difference cfeat/no-team-inheritable-features)
(not-empty))]
(let [features (-> features
(set/union (:features team))
(set/difference cfeat/no-team-inheritable-features)
(into-array))]
(let [features (->> features
(set/union (:features team))
(db/create-array conn "text"))]
(db/update! conn :team
{:features features}
{:id (:id team)}
{::db/return-keys false})))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
(binding [l/*context* (some-> (meta params)
@@ -206,7 +209,7 @@
Follow the inner implementation to `update-file-data!` function.
Only intended for internal use on this module."
[{:keys [::db/conn ::timestamp] :as cfg}
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
{:keys [profile-id file team features changes session-id skip-validate] :as params}]
(let [;; Retrieve the file data
@@ -219,11 +222,15 @@
;; We create a new lexycal scope for clearly delimit the result of
;; executing this update file operation and all its side effects
(let [file (binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))]
(let [file (px/invoke! executor
(fn []
;; Process the file data on separated thread for avoid to do
;; the CPU intensive operation on vthread.
(binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))))]
(feat.fmigr/upsert-migrations! conn file)
(persist-file! cfg file)
@@ -401,6 +408,7 @@
(not skip-validate))
(bfc/get-resolved-file-libraries cfg file))
;; The main purpose of this atom is provide a contextual state
;; for the changes subsystem where optionally some hints can
;; be provided for the changes processing. Right now we are

View File

@@ -26,7 +26,9 @@
[app.rpc.helpers :as rph]
[app.rpc.quotes :as quotes]
[app.storage :as sto]
[app.util.services :as sv]))
[app.util.services :as sv]
[app.worker :as-alias wrk]
[promesa.exec :as px]))
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"})
@@ -35,13 +37,14 @@
(def ^:private
schema:get-font-variants
[:and
[:map {:title "get-font-variants"}
[:team-id {:optional true} ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:project-id {:optional true} ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]]
[::sm/contains-any #{:team-id :file-id :project-id}]])
[:schema {:title "get-font-variants"}
[:and
[:map
[:team-id {:optional true} ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:project-id {:optional true} ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]]
[::sm/contains-any #{:team-id :file-id :project-id}]]])
(sv/defmethod ::get-font-variants
{::doc/added "1.18"
@@ -103,7 +106,7 @@
(create-font-variant cfg (assoc params :profile-id profile-id)))))
(defn create-font-variant
[{:keys [::sto/storage ::db/conn]} {:keys [data] :as params}]
[{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}]
(letfn [(generate-missing! [data]
(let [data (media/run {:cmd :generate-fonts :input data})]
(when (and (not (contains? data "font/otf"))
@@ -155,7 +158,7 @@
:otf-file-id (:id otf)
:ttf-file-id (:id ttf)}))]
(let [data (generate-missing! data)
(let [data (px/invoke! executor (partial generate-missing! data))
assets (persist-fonts-files! data)
result (insert-font-variant! assets)]
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))

View File

@@ -28,7 +28,9 @@
[app.setup :as-alias setup]
[app.setup.templates :as tmpl]
[app.storage.tmp :as tmp]
[app.util.services :as sv]))
[app.util.services :as sv]
[app.worker :as-alias wrk]
[promesa.exec :as px]))
;; --- COMMAND: Duplicate File
@@ -311,14 +313,15 @@
;; Update the modification date of the all affected projects
;; ensuring that the destination project is the most recent one.
(loop [project-ids (into (list project-id) source)
modified-at (ct/now)]
(when-let [project-id (first project-ids)]
(db/update! conn :project
{:modified-at modified-at}
{:id project-id})
(recur (rest project-ids)
(ct/plus modified-at 10))))
(doseq [project-id (into (list project-id) source)]
;; NOTE: as this is executed on virtual thread, sleeping does
;; not causes major issues, and allows an easy way to set a
;; trully different modification date to each file.
(px/sleep 10)
(db/update! conn :project
{:modified-at (ct/now)}
{:id project-id}))
nil))
@@ -393,7 +396,12 @@
;; --- COMMAND: Clone Template
(defn clone-template
[{:keys [::db/pool] :as cfg} {:keys [project-id profile-id] :as params} template]
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [project-id profile-id] :as params} template]
;; NOTE: the importation process performs some operations
;; that are not very friendly with virtual threads, and for
;; avoid unexpected blocking of other concurrent operations
;; we dispatch that operation to a dedicated executor.
(let [template (tmp/tempfile-from template
:prefix "penpot.template."
:suffix ""
@@ -411,8 +419,8 @@
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
result (if (= format :binfile-v3)
(bf.v3/import-files! cfg)
(bf.v1/import-files! cfg))]
(px/invoke! executor (partial bf.v3/import-files! cfg))
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]

View File

@@ -24,8 +24,10 @@
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.util.services :as sv]
[app.worker :as-alias wrk]
[cuerdas.core :as str]
[datoteka.io :as io]))
[datoteka.io :as io]
[promesa.exec :as px]))
(def default-max-file-size
(* 1024 1024 10)) ; 10 MiB
@@ -46,7 +48,7 @@
[:file-id ::sm/uuid]
[:is-local ::sm/boolean]
[:name [:string {:max 250}]]
[:content media/schema:upload]])
[:content ::media/upload]])
(sv/defmethod ::upload-file-media-object
{::doc/added "1.17"
@@ -151,9 +153,9 @@
(assoc ::image (process-main-image info)))))
(defn- create-file-media-object
[{:keys [::sto/storage ::db/conn] :as cfg}
[{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
{:keys [id file-id is-local name content]}]
(let [result (process-image content)
(let [result (px/invoke! executor (partial process-image content))
image (sto/put-object! storage (::image result))
thumb (when-let [params (::thumb result)]
(sto/put-object! storage params))]

View File

@@ -30,13 +30,16 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.worker :as wrk]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[promesa.exec :as px]))
(declare check-profile-existence!)
(declare decode-row)
(declare derive-password)
(declare filter-props)
(declare get-profile)
(declare strip-private-attrs)
(declare verify-password)
(def schema:props-notifications
[:map {:title "props-notifications"}
@@ -128,7 +131,9 @@
;; NOTE: we need to retrieve the profile independently if we use
;; it or not for explicit locking and avoid concurrent updates of
;; the same row/object.
(let [profile (get-profile conn profile-id ::db/for-update true)
(let [profile (-> (db/get-by-id conn :profile profile-id ::sql/for-update true)
(decode-row))
;; Update the profile map with direct params
profile (-> profile
(assoc :fullname fullname)
@@ -138,9 +143,9 @@
(db/update! conn :profile
{:fullname fullname
:lang lang
:theme theme}
{:id profile-id}
{::db/return-keys false})
:theme theme
:props (db/tjson (:props profile))}
{:id profile-id})
(-> profile
(strip-private-attrs)
@@ -189,7 +194,7 @@
[{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
(let [profile (db/get-by-id conn :profile profile-id ::sql/for-update true)]
(when (and (not= (:password profile) "!")
(not (:valid (auth/verify-password old-password (:password profile)))))
(not (:valid (verify-password cfg old-password (:password profile)))))
(ex/raise :type :validation
:code :old-password-not-match))
profile))
@@ -198,7 +203,7 @@
[{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}]
(when-not (db/read-only? conn)
(db/update! conn :profile
{:password (auth/derive-password password)}
{:password (derive-password cfg password)}
{:id id})
nil))
@@ -223,22 +228,21 @@
(defn- update-notifications!
[{:keys [::db/conn] :as cfg} {:keys [profile-id dashboard-comments email-comments email-invites]}]
(let [profile
(get-profile conn profile-id ::db/for-update true)
(let [profile (get-profile conn profile-id)
notifications
{:dashboard-comments dashboard-comments
:email-comments email-comments
:email-invites email-invites}
:email-invites email-invites}]
props
(-> (get profile :props)
(assoc :notifications notifications))]
(db/update!
conn :profile
{:props
(-> (:props profile)
(assoc :notifications notifications)
(db/tjson))}
{:id (:id profile)})
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id}
{::db/return-keys false})
nil))
;; --- MUTATION: Update Photo
@@ -249,7 +253,7 @@
(def ^:private
schema:update-profile-photo
[:map {:title "update-profile-photo"}
[:file media/schema:upload]])
[:file ::media/upload]])
(sv/defmethod ::update-profile-photo
{:doc/added "1.1"
@@ -300,11 +304,12 @@
:content-type (:mtype thumb)}))
(defn upload-photo
[{:keys [::sto/storage] :as cfg} {:keys [file] :as params}]
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file] :as params}]
(let [params (-> cfg
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
[:process-image/global]])
(assoc ::climit/label "upload-photo")
(assoc ::climit/executor executor)
(climit/invoke! generate-thumbnail! file))]
(sto/put-object! storage params)))
@@ -406,7 +411,7 @@
(defn update-profile-props
[{:keys [::db/conn] :as cfg} profile-id props]
(let [profile (get-profile conn profile-id ::db/for-update true)
(let [profile (get-profile conn profile-id ::sql/for-update true)
props (reduce-kv (fn [props k v]
;; We don't accept namespaced keys
(if (simple-ident? k)
@@ -419,17 +424,16 @@
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id}
{::db/return-keys false})
{:id profile-id})
(filter-props props)))
(sv/defmethod ::update-profile-props
{::doc/added "1.0"
::sm/params schema:update-profile-props
::db/transaction true}
::sm/params schema:update-profile-props}
[cfg {:keys [::rpc/profile-id props]}]
(update-profile-props cfg profile-id props))
(db/tx-run! cfg (fn [cfg]
(update-profile-props cfg profile-id props))))
;; --- MUTATION: Delete Profile
@@ -467,26 +471,6 @@
(-> (rph/wrap nil)
(rph/with-transform (session/delete-fn cfg)))))
(def sql:get-subscription-editors
"SELECT DISTINCT
p.id,
p.fullname AS name,
p.email AS email
FROM team_profile_rel AS tpr1
JOIN team_profile_rel AS tpr2
ON (tpr1.team_id = tpr2.team_id)
JOIN profile AS p
ON (tpr2.profile_id = p.id)
WHERE tpr1.profile_id = ?
AND tpr1.is_owner IS true
AND tpr2.can_edit IS true")
(sv/defmethod ::get-subscription-usage
{::doc/added "2.9"}
[cfg {:keys [::rpc/profile-id]}]
(let [editors (db/exec! cfg [sql:get-subscription-editors profile-id])]
{:editors editors}))
;; --- HELPERS
(def sql:owned-teams
@@ -544,6 +528,15 @@
[props]
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
(defn derive-password
[{:keys [::wrk/executor]} password]
(when password
(px/invoke! executor (partial auth/derive-password password))))
(defn verify-password
[{:keys [::wrk/executor]} password password-data]
(px/invoke! executor (partial auth/verify-password password password-data)))
(defn decode-row
[{:keys [props] :as row}]
(cond-> row

View File

@@ -12,7 +12,7 @@
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.team :as types.team]
[app.common.types.team :as tt]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -503,7 +503,7 @@
(let [features (-> (cfeat/get-enabled-features cf/flags)
(set/difference cfeat/frontend-only-features)
(set/difference cfeat/no-team-inheritable-features))
(cfeat/check-client-features! (:features params)))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
@@ -629,7 +629,7 @@
;; assign owner role to new profile
(db/update! conn :team-profile-rel
(get types.team/permissions-for-role :owner)
(get tt/permissions-for-role :owner)
{:team-id id :profile-id reassign-to}))
;; and finally, if all other conditions does not match and the
@@ -742,7 +742,7 @@
:team-id team-id
:role role})
(let [params (get types.team/permissions-for-role role)]
(let [params (get tt/permissions-for-role role)]
;; Only allow single owner on team
(when (= role :owner)
(db/update! conn :team-profile-rel
@@ -760,7 +760,7 @@
[:map {:title "update-team-member-role"}
[:team-id ::sm/uuid]
[:member-id ::sm/uuid]
[:role types.team/schema:role]])
[:role ::tt/role]])
(sv/defmethod ::update-team-member-role
{::doc/added "1.17"
@@ -810,7 +810,7 @@
(def ^:private schema:update-team-photo
[:map {:title "update-team-photo"}
[:team-id ::sm/uuid]
[:file media/schema:upload]])
[:file ::media/upload]])
(sv/defmethod ::update-team-photo
{::doc/added "1.17"

View File

@@ -75,7 +75,7 @@
[:map
[:id ::sm/uuid]
[:fullname :string]]]
[:role types.team/schema:role]
[:role ::types.team/role]
[:email ::sm/email]])
(def ^:private check-create-invitation-params
@@ -224,112 +224,62 @@
(def ^:private xf:map-email (map :email))
(defn- create-team-invitations
"Unified function to handle both create and resend team invitations.
Accepts either:
- emails (set) + role (single role for all emails)
- invitations (vector of {:email :role} maps)"
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails invitations] :as params}]
(let [;; Normalize input to a consistent format: [{:email :role}]
invitation-data (cond
;; Case 1: emails + single role (create invitations style)
(and emails role)
(map (fn [email] {:email email :role role}) emails)
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
(let [emails (set emails)
;; Case 2: invitations with individual roles (resend invitations style)
(some? invitations)
invitations
join-requests (->> (get-valid-access-request-profiles conn (:id team))
(d/index-by :email))
:else
(throw (ex-info "Invalid parameters: must provide either emails+role or invitations" {})))
team-members (into #{} xf:map-email
(teams/get-team-members conn (:id team)))
invitation-emails (into #{} (map :email) invitation-data)
join-requests (->> (get-valid-access-request-profiles conn (:id team))
(d/index-by :email))
team-members (into #{} xf:map-email
(teams/get-team-members conn (:id team)))
invitations (into #{}
(comp
;; We don't re-send invitations to
;; already existing members
(remove #(contains? team-members (:email %)))
invitations (into #{}
(comp
;; We don't re-send inviation to
;; already existing members
(remove team-members)
;; We don't send invitations to
;; join-requested members
(remove #(contains? join-requests (:email %)))
(map (fn [{:keys [email role]}]
(create-invitation cfg
(-> params
(assoc :email email)
(assoc :role role)))))
(remove nil?))
invitation-data)]
(remove join-requests)
(map (fn [email] (assoc params :email email)))
(keep (partial create-invitation cfg)))
emails)]
;; For requested invitations, do not send invitation emails, add
;; the user directly to the team
(->> join-requests
(filter #(contains? invitation-emails (key %)))
(map (fn [[email member]]
(let [role (:role (first (filter #(= (:email %) email) invitation-data)))]
(add-member-to-team conn profile team role member))))
(doall))
(filter #(contains? emails (key %)))
(map val)
(run! (partial add-member-to-team conn profile team role)))
invitations))
(def ^:private schema:create-team-invitations
[:and
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
;; Support both formats:
;; 1. emails (set) + role (single role for all)
;; 2. invitations (vector of {:email :role} maps)
[:emails {:optional true} [::sm/set ::sm/email]]
[:role {:optional true} types.team/schema:role]
[:invitations {:optional true} [:vector [:map
[:email ::sm/email]
[:role types.team/schema:role]]]]]
;; Ensure exactly one format is provided
[:fn (fn [params]
(let [has-emails-role (and (contains? params :emails)
(contains? params :role))
has-invitations (contains? params :invitations)]
(and (or has-emails-role has-invitations)
(not (and has-emails-role has-invitations)))))]])
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
[:role ::types.team/role]
[:emails [::sm/set ::sm/email]]])
(def ^:private max-invitations-by-request-threshold
"The number of invitations can be sent in a single rpc request"
25)
(sv/defmethod ::create-team-invitations
"A rpc call that allows to send single or multiple invitations to join the team.
Supports two parameter formats:
1. emails (set) + role (single role for all emails)
2. invitations (vector of {:email :role} maps for individual roles)"
"A rpc call that allow to send a single or multiple invitations to
join the team."
{::doc/added "1.17"
::doc/module :teams
::sm/params schema:create-team-invitations}
[cfg {:keys [::rpc/profile-id team-id role emails] :as params}]
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
(let [perms (teams/get-permissions cfg profile-id team-id)
profile (db/get-by-id cfg :profile profile-id)
;; Determine which format is being used
using-emails-format? (and emails role)
;; Handle both parameter formats
emails (if using-emails-format?
(into #{} (map profile/clean-email) emails)
#{})
;; Calculate total invitation count for both formats
invitation-count (if using-emails-format?
(count emails)
(count (:invitations params)))]
emails (into #{} (map profile/clean-email) emails)]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(when (> invitation-count max-invitations-by-request-threshold)
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
@@ -338,7 +288,7 @@
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id team-id)
(assoc ::quotes/incr invitation-count)
(assoc ::quotes/incr (count emails))
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
@@ -354,12 +304,7 @@
(-> params
(assoc :profile profile)
(assoc :team team)
;; Pass parameters in the correct format for the unified function
(cond-> using-emails-format?
;; If using emails+role format, ensure both are present
(assoc :emails emails :role role)
;; If using invitations format, the :invitations key is already in params
(not using-emails-format?) identity)))]
(assoc :emails emails)))]
(with-meta {:total (count invitations)
:invitations invitations}
@@ -373,7 +318,7 @@
[:features {:optional true} ::cfeat/features]
[:id {:optional true} ::sm/uuid]
[:emails [::sm/set ::sm/email]]
[:role types.team/schema:role]])
[:role ::types.team/role]])
(sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"
@@ -458,7 +403,7 @@
[:map {:title "update-team-invitation-role"}
[:team-id ::sm/uuid]
[:email ::sm/email]
[:role types.team/schema:role]])
[:role ::types.team/role]])
(sv/defmethod ::update-team-invitation-role
{::doc/added "1.17"

View File

@@ -128,7 +128,7 @@
[:iss :keyword]
[:exp ::ct/inst]
[:profile-id ::sm/uuid]
[:role types.team/schema:role]
[:role ::types.team/role]
[:team-id ::sm/uuid]
[:member-email ::sm/email]
[:member-id {:optional true} ::sm/uuid]])

View File

@@ -166,6 +166,9 @@
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
;; :description "penpot backend"
}]
:security
{:api_key []}
:paths paths
:components {:schemas @definitions}}))

View File

@@ -10,14 +10,15 @@
[app.common.exceptions :as ex]
[app.common.schema :as sm]))
(def schema:permissions
[:map {:title "Permissions"}
[:type {:gen/elements [:membership :share-link]} :keyword]
[:is-owner ::sm/boolean]
[:is-admin ::sm/boolean]
[:can-edit ::sm/boolean]
[:can-read ::sm/boolean]
[:is-logged ::sm/boolean]])
(sm/register!
^{::sm/type ::permissions}
[:map {:title "Permissions"}
[:type {:gen/elements [:membership :share-link]} :keyword]
[:is-owner ::sm/boolean]
[:is-admin ::sm/boolean]
[:can-edit ::sm/boolean]
[:can-read ::sm/boolean]
[:is-logged ::sm/boolean]])
(def valid-roles
#{:admin :owner :editor :viewer})

View File

@@ -7,7 +7,7 @@
(ns app.srepl.cli
"PREPL API for external usage (CLI or ADMIN)"
(:require
[app.auth :refer [derive-password]]
[app.auth :as auth]
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
@@ -54,7 +54,7 @@
(some-> (get-current-system)
(db/tx-run!
(fn [{:keys [::db/conn] :as system}]
(let [password (derive-password password)
(let [password (cmd.profile/derive-password system password)
params {:id (uuid/next)
:email email
:fullname fullname
@@ -74,7 +74,7 @@
(assoc :fullname fullname)
(some? password)
(assoc :password (derive-password password))
(assoc :password (auth/derive-password password))
(some? is-active)
(assoc :is-active is-active))]
@@ -124,7 +124,7 @@
(defmethod exec-command "derive-password"
[{:keys [password]}]
(derive-password password))
(auth/derive-password password))
(defmethod exec-command "authenticate"
[{:keys [token]}]

View File

@@ -40,7 +40,6 @@
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.worker :as wrk]
[clojure.datafy :refer [datafy]]
[clojure.java.io :as io]
[clojure.pprint :refer [print-table]]
[clojure.stacktrace :as strace]

View File

@@ -27,9 +27,7 @@
(defn get-legacy-backend
[]
(when-let [name (cf/get :assets-storage-backend)]
(l/wrn :hint "using deprecated configuration, please read 2.11 release notes"
:href "https://github.com/penpot/penpot/releases/tag/2.11.0")
(let [name (cf/get :assets-storage-backend)]
(case name
:assets-fs :fs
:assets-s3 :s3

View File

@@ -31,13 +31,13 @@
java.time.Duration
java.util.Collection
java.util.Optional
java.util.concurrent.atomic.AtomicLong
org.reactivestreams.Subscriber
software.amazon.awssdk.core.ResponseBytes
software.amazon.awssdk.core.async.AsyncRequestBody
software.amazon.awssdk.core.async.AsyncResponseTransformer
software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
software.amazon.awssdk.regions.Region
@@ -87,11 +87,12 @@
(def ^:private schema:config
[:map {:title "s3-backend-config"}
::wrk/netty-io-executor
::wrk/executor
[::region {:optional true} :keyword]
[::bucket {:optional true} ::sm/text]
[::prefix {:optional true} ::sm/text]
[::endpoint {:optional true} ::sm/uri]])
[::endpoint {:optional true} ::sm/uri]
[::io-threads {:optional true} ::sm/int]])
(defmethod ig/expand-key ::backend
[k v]
@@ -109,7 +110,6 @@
presigner (build-s3-presigner params)]
(assoc params
::sto/type :s3
::counter (AtomicLong. 0)
::client @client
::presigner presigner
::close-fn #(.close ^java.lang.AutoCloseable client)))))
@@ -121,7 +121,7 @@
(defmethod ig/halt-key! ::backend
[_ {:keys [::close-fn]}]
(when (fn? close-fn)
(close-fn)))
(px/run! close-fn)))
(def ^:private schema:backend
[:map {:title "s3-backend"}
@@ -198,16 +198,19 @@
(Region/of (name region)))
(defn- build-s3-client
[{:keys [::region ::endpoint ::wrk/netty-io-executor]}]
[{:keys [::region ::endpoint ::io-threads ::wrk/executor]}]
(let [aconfig (-> (ClientAsyncConfiguration/builder)
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
(.build))
sconfig (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))
thr-num (or io-threads (min 16 (px/get-available-processors)))
hclient (-> (NettyNioAsyncHttpClient/builder)
(.eventLoopGroup (SdkEventLoopGroup/create netty-io-executor))
(.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder)
(.numberOfThreads (int thr-num))))
(.connectionAcquisitionTimeout default-timeout)
(.connectionTimeout default-timeout)
(.readTimeout default-timeout)
@@ -259,7 +262,7 @@
(.close ^InputStream input))))
(defn- make-request-body
[counter content]
[executor content]
(let [size (impl/get-size content)]
(reify
AsyncRequestBody
@@ -269,19 +272,16 @@
(^void subscribe [_ ^Subscriber subscriber]
(let [delegate (AsyncRequestBody/forBlockingInputStream (long size))
input (io/input-stream content)]
(px/thread-call (partial write-input-stream delegate input)
{:name (str "penpot/storage/" (.getAndIncrement ^AtomicLong counter))})
(px/run! executor (partial write-input-stream delegate input))
(.subscribe ^BlockingInputStreamAsyncRequestBody delegate
^Subscriber subscriber))))))
(defn- put-object
[{:keys [::client ::bucket ::prefix ::counter]} {:keys [id] :as object} content]
[{:keys [::client ::bucket ::prefix ::wrk/executor]} {:keys [id] :as object} content]
(let [path (dm/str prefix (impl/id->path id))
mdata (meta object)
mtype (:content-type mdata "application/octet-stream")
rbody (make-request-body counter content)
rbody (make-request-body executor content)
request (.. (PutObjectRequest/builder)
(bucket bucket)
(contentType mtype)

View File

@@ -44,7 +44,7 @@
[_ cfg]
(fs/create-dir default-tmp-dir)
(px/fn->thread (partial io-loop cfg)
{:name "penpot/storage/tmp-cleaner"}))
{:name "penpot/storage/tmp-cleaner" :virtual true}))
(defmethod ig/halt-key! ::cleaner
[_ thread]

View File

@@ -27,7 +27,7 @@
(sp/put! channel [type data])
nil)))
(defn spawn-listener
(defn start-listener
[channel on-event on-close]
(assert (sp/chan? channel) "expected active events channel")
@@ -51,7 +51,7 @@
[f on-event]
(binding [*channel* (sp/chan :buf 32)]
(let [listener (spawn-listener *channel* on-event (constantly nil))]
(let [listener (start-listener *channel* on-event (constantly nil))]
(try
(f)
(finally

View File

@@ -112,7 +112,7 @@
(if (db/read-only? pool)
(l/wrn :hint "not started (db is read-only)")
(px/fn->thread dispatcher :name "penpot/worker-dispatcher"))))
(px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual false))))
(defmethod ig/halt-key! ::wrk/dispatcher
[_ thread]

View File

@@ -7,79 +7,97 @@
(ns app.worker.executor
"Async tasks abstraction (impl)."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.math :as mth]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.metrics :as mtx]
[app.worker :as-alias wrk]
[integrant.core :as ig]
[promesa.exec :as px])
(:import
io.netty.channel.nio.NioEventLoopGroup
io.netty.util.concurrent.DefaultEventExecutorGroup
java.util.concurrent.ExecutorService
java.util.concurrent.ThreadFactory))
java.util.concurrent.ThreadPoolExecutor))
(set! *warn-on-reflection* true)
(sm/register!
{:type ::wrk/executor
:pred #(instance? ExecutorService %)
:pred #(instance? ThreadPoolExecutor %)
:type-properties
{:title "executor"
:description "Instance of ExecutorService"}})
(sm/register!
{:type ::wrk/netty-io-executor
:pred #(instance? NioEventLoopGroup %)
:type-properties
{:title "executor"
:description "Instance of NioEventLoopGroup"}})
(sm/register!
{:type ::wrk/netty-executor
:pred #(instance? DefaultEventExecutorGroup %)
:type-properties
{:title "executor"
:description "Instance of DefaultEventExecutorGroup"}})
:description "Instance of ThreadPoolExecutor"}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IO Executor
;; EXECUTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/assert-key ::wrk/netty-io-executor
[_ {:keys [threads]}]
(assert (or (nil? threads) (int? threads))
"expected valid threads value, revisit PENPOT_NETTY_IO_THREADS environment variable"))
(defmethod ig/init-key ::wrk/executor
[_ _]
(let [factory (px/thread-factory :prefix "penpot/default/")
executor (px/cached-executor :factory factory :keepalive 60000)]
(l/inf :hint "executor started")
executor))
(defmethod ig/init-key ::wrk/netty-io-executor
[_ {:keys [threads]}]
(let [factory (px/thread-factory :prefix "penpot/netty-io/")
nthreads (or threads (mth/round (/ (px/get-available-processors) 2)))
nthreads (max 2 nthreads)]
(l/inf :hint "start netty io executor" :threads nthreads)
(NioEventLoopGroup. (int nthreads) ^ThreadFactory factory)))
(defmethod ig/halt-key! ::wrk/netty-io-executor
(defmethod ig/halt-key! ::wrk/executor
[_ instance]
(px/shutdown! instance))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IO Offload Executor
;; MONITOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/assert-key ::wrk/netty-executor
[_ {:keys [threads]}]
(assert (or (nil? threads) (int? threads))
"expected valid threads value, revisit PENPOT_EXEC_THREADS environment variable"))
(defn- get-stats
[^ThreadPoolExecutor executor]
{:active (.getPoolSize ^ThreadPoolExecutor executor)
:running (.getActiveCount ^ThreadPoolExecutor executor)
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
(defmethod ig/init-key ::wrk/netty-executor
[_ {:keys [threads]}]
(let [factory (px/thread-factory :prefix "penpot/exec/")
nthreads (or threads (mth/round (/ (px/get-available-processors) 2)))
nthreads (max 2 nthreads)]
(l/inf :hint "start default executor" :threads nthreads)
(DefaultEventExecutorGroup. (int nthreads) ^ThreadFactory factory)))
(defmethod ig/expand-key ::wrk/monitor
[k v]
{k (-> (d/without-nils v)
(assoc ::interval (ct/duration "2s")))})
(defmethod ig/halt-key! ::wrk/netty-executor
[_ instance]
(px/shutdown! instance))
(defmethod ig/init-key ::wrk/monitor
[_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}]
(letfn [(monitor! [executor prev-completed]
(let [labels (into-array String [(d/name name)])
stats (get-stats executor)
completed (:completed stats)
completed-inc (- completed prev-completed)
completed-inc (if (neg? completed-inc) 0 completed-inc)]
(mtx/run! metrics
:id :executor-active-threads
:labels labels
:val (:active stats))
(mtx/run! metrics
:id :executor-running-threads
:labels labels
:val (:running stats))
(mtx/run! metrics
:id :executors-completed-tasks
:labels labels
:inc completed-inc)
completed-inc))]
(px/thread
{:name "penpot/executors-monitor" :virtual true}
(l/inf :hint "monitor started" :name name)
(try
(loop [completed 0]
(px/sleep interval)
(recur (long (monitor! executor completed))))
(catch InterruptedException _cause
(l/trc :hint "monitor: interrupted" :name name))
(catch Throwable cause
(l/err :hint "monitor: unexpected error" :name name :cause cause))
(finally
(l/inf :hint "monitor: terminated" :name name))))))
(defmethod ig/halt-key! ::wrk/monitor
[_ thread]
(px/interrupt! thread))

View File

@@ -248,7 +248,7 @@
(defn- start-thread!
[{:keys [::rds/redis ::id ::queue ::wrk/tenant] :as cfg}]
(px/thread
{:name (str "penpot/worker-runner/" id)}
{:name (format "penpot/worker/runner:%s" id)}
(l/inf :hint "started" :id id :queue queue)
(try
(dm/with-open [rconn (rds/connect redis)]
@@ -303,7 +303,7 @@
(l/wrn :hint "not started (db is read-only)" :queue queue :parallelism parallelism)
(doall
(->> (range parallelism)
(map #(assoc cfg ::id (str queue "/" %)))
(map #(assoc cfg ::id %))
(map start-thread!))))))
(defmethod ig/halt-key! ::wrk/runner

View File

@@ -113,6 +113,7 @@
:app.auth.oidc.providers/generic
:app.setup/templates
:app.auth.oidc/routes
:app.worker/monitor
:app.http.oauth/handler
:app.notifications/handler
:app.loggers.mattermost/reporter

View File

@@ -1,96 +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 backend-tests.http-management-test
(:require
[app.common.data :as d]
[app.common.time :as ct]
[app.db :as db]
[app.http.access-token]
[app.http.management :as mgmt]
[app.http.session :as sess]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[backend-tests.helpers :as th]
[clojure.test :as t]
[mockery.core :refer [with-mocks]]
[yetti.response :as-alias yres]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest authenticate-method
(let [profile (th/create-profile* 1)
props (get th/*system* :app.setup/props)
token (#'sess/gen-token props {:profile-id (:id profile)})
request {:params {:token token}}
response (#'mgmt/authenticate th/*system* request)]
(t/is (= 200 (::yres/status response)))
(t/is (= "authentication" (-> response ::yres/body :iss)))
(t/is (= (:id profile) (-> response ::yres/body :uid)))))
(t/deftest get-customer-method
(let [profile (th/create-profile* 1)
request {:params {:id (:id profile)}}
response (#'mgmt/get-customer th/*system* request)]
(t/is (= 200 (::yres/status response)))
(t/is (= (:id profile) (-> response ::yres/body :id)))
(t/is (= (:fullname profile) (-> response ::yres/body :name)))
(t/is (= (:email profile) (-> response ::yres/body :email)))
(t/is (= 1 (-> response ::yres/body :num-editors)))
(t/is (nil? (-> response ::yres/body :subscription)))))
(t/deftest update-customer-method
(let [profile (th/create-profile* 1)
subs {:type "unlimited"
:description nil
:id "foobar"
:customer-id (str (:id profile))
:status "past_due"
:billing-period "week"
:quantity 1
:created-at (ct/truncate (ct/now) :day)
:cancel-at-period-end true
:start-date nil
:ended-at nil
:trial-end nil
:trial-start nil
:cancel-at nil
:canceled-at nil
:current-period-end nil
:current-period-start nil
:cancellation-details
{:comment "other"
:reason "other"
:feedback "other"}}
request {:params {:id (:id profile)
:subscription subs}}
response (#'mgmt/update-customer th/*system* request)]
(t/is (= 201 (::yres/status response)))
(t/is (nil? (::yres/body response)))
(let [request {:params {:id (:id profile)}}
response (#'mgmt/get-customer th/*system* request)]
(t/is (= 200 (::yres/status response)))
(t/is (= (:id profile) (-> response ::yres/body :id)))
(t/is (= (:fullname profile) (-> response ::yres/body :name)))
(t/is (= (:email profile) (-> response ::yres/body :email)))
(t/is (= 1 (-> response ::yres/body :num-editors)))
(let [subs' (-> response ::yres/body :subscription)]
(t/is (= subs' subs))))))

View File

@@ -1,36 +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 backend-tests.rpc-doc-test
"Internal binfile test, no RPC involved"
(:require
[app.common.json :as json]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.schema.test :as smt]
[app.rpc :as-alias rpc]
[app.rpc.doc :as rpc.doc]
[backend-tests.helpers :as th]
[clojure.test :as t]))
(t/use-fixtures :once th/state-init)
(t/deftest openapi-context-json-encode
(smt/check!
(smt/for [context (->> sg/int
(sg/fmap (fn [_]
(rpc.doc/prepare-openapi-context (::rpc/methods th/*system*)))))]
(try
(json/encode context)
true
(catch Throwable _cause
false)))
{:num 30}))

View File

@@ -86,7 +86,7 @@
(t/deftest internal-encode-decode
(smt/check!
(smt/for [data (->> (cg/map cg/uuid (sg/generator cts/schema:shape))
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
(cg/not-empty))]
(let [obj1 (omap/wrap data)
obj2 (omap/create (deref obj1))
@@ -103,7 +103,7 @@
(t/deftest fressian-encode-decode
(smt/check!
(smt/for [data (->> (cg/map cg/uuid (sg/generator cts/schema:shape))
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
(cg/not-empty)
(cg/fmap omap/wrap)
(cg/fmap (fn [o] {:objects o})))]
@@ -119,7 +119,7 @@
(t/deftest transit-encode-decode
(smt/check!
(smt/for [data (->> (cg/map cg/uuid (sg/generator cts/schema:shape))
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
(cg/not-empty)
(cg/fmap omap/wrap)
(cg/fmap (fn [o] {:objects o})))]

View File

@@ -1,5 +1,5 @@
{:deps
{org.clojure/clojure {:mvn/version "1.12.2"}
{org.clojure/clojure {:mvn/version "1.12.1"}
org.clojure/data.json {:mvn/version "2.5.1"}
org.clojure/tools.cli {:mvn/version "1.1.230"}
org.clojure/test.check {:mvn/version "1.1.1"}
@@ -7,25 +7,25 @@
org.clojure/clojurescript {:mvn/version "1.12.42"}
;; Logging
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-api {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.40"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
selmer/selmer {:mvn/version "1.12.62"}
criterium/criterium {:mvn/version "0.4.6"}
metosin/jsonista {:mvn/version "0.3.13"}
metosin/malli {:mvn/version "0.19.1"}
metosin/malli {:mvn/version "0.18.0"}
expound/expound {:mvn/version "0.9.0"}
com.cognitect/transit-clj {:mvn/version "1.0.333"}
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
integrant/integrant {:mvn/version "1.0.0"}
integrant/integrant {:mvn/version "0.13.1"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2025.06.16-414"}
@@ -43,11 +43,11 @@
frankiesardo/linked {:mvn/version "1.3.0"}
com.sun.mail/jakarta.mail {:mvn/version "2.0.2"}
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
org.la4j/la4j {:mvn/version "0.6.0"}
;; exception printing
fipp/fipp {:mvn/version "0.6.29"}
fipp/fipp {:mvn/version "0.6.27"}
me.flowthing/pp {:mvn/version "2024-11-13.77"}
@@ -59,7 +59,7 @@
{:dev
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "3.2.0"}
thheller/shadow-cljs {:mvn/version "3.1.5"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
@@ -68,7 +68,7 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
:ns-default build}
:test

View File

@@ -5,8 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.buffer
"A collection of helpers and macros for work with byte
buffer (ByteBuffer on JVM and DataView on JS)."
"A collection of helpers and macros for work with byte buffers"
(:refer-clojure :exclude [clone])
(:require
[app.common.uuid :as uuid])
@@ -50,13 +49,6 @@
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(long (.getInt ~target (unchecked-int ~offset))))))
(defmacro read-long
[target offset]
(if (:ns &env)
`(.getInt64 ~target ~offset true)
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.getLong ~target (unchecked-int ~offset)))))
(defmacro read-float
[target offset]
(if (:ns &env)
@@ -82,40 +74,6 @@
(finally
(.order ~target ByteOrder/LITTLE_ENDIAN))))))
(defmacro read-bytes
"Get a byte array from buffer. It is potentially unsafe because on
JS/CLJS it returns a subarray without doing any copy of data."
[target offset size]
(if (:ns &env)
`(new js/Uint8Array
(.-buffer ~target)
(+ (.-byteOffset ~target) ~offset)
~size)
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})
bbuf (with-meta (gensym "bbuf") {:tag bytes})]
`(let [~bbuf (byte-array ~size)]
(.get ~target
(unchecked-int ~offset)
~bbuf
0
~size)
~bbuf))))
;; FIXME: implement in cljs
(defmacro write-bytes
([target offset src size]
`(write-bytes ~target ~offset ~src 0 ~size))
([target offset src src-offset size]
(if (:ns &env)
(throw (ex-info "not implemented" {}))
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})
src (with-meta src {:tag 'bytes})]
`(.put ~target
(unchecked-int ~offset)
~src
(unchecked-int ~src-offset)
(unchecked-int ~size))))))
(defmacro write-byte
[target offset value]
(if (:ns &env)
@@ -123,13 +81,6 @@
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.put ~target (unchecked-int ~offset) (unchecked-byte ~value)))))
(defmacro write-u8
[target offset value]
(if (:ns &env)
`(.setUint8 ~target ~offset ~value true)
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.put ~target ~offset (unchecked-byte ~value)))))
(defmacro write-bool
[target offset value]
(if (:ns &env)
@@ -151,18 +102,6 @@
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.putInt ~target (unchecked-int ~offset) (unchecked-int ~value)))))
(defmacro write-u32
[target offset value]
(if (:ns &env)
`(.setUint32 ~target ~offset ~value true)
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.putInt ~target ~offset (unchecked-int ~value)))))
(defmacro write-i32
"Idiomatic alias for `write-int`"
[target offset value]
`(write-int ~target ~offset ~value))
(defmacro write-float
[target offset value]
(if (:ns &env)
@@ -170,11 +109,6 @@
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.putFloat ~target (unchecked-int ~offset) (unchecked-float ~value)))))
(defmacro write-f32
"Idiomatic alias for `write-float`."
[target offset value]
`(write-float ~target ~offset ~value))
(defmacro write-uuid
[target offset value]
(if (:ns &env)
@@ -185,15 +119,13 @@
(.setUint32 ~target (+ ~offset 12) (aget barray# 3) true))
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})
value (with-meta value {:tag 'java.util.UUID})
prev (with-meta (gensym "prev-") {:tag 'java.nio.ByteOrder})]
`(let [~prev (.order ~target)]
(try
(.order ~target ByteOrder/BIG_ENDIAN)
(.putLong ~target (unchecked-int (+ ~offset 0)) (.getMostSignificantBits ~value))
(.putLong ~target (unchecked-int (+ ~offset 8)) (.getLeastSignificantBits ~value))
(finally
(.order ~target ~prev)))))))
value (with-meta value {:tag 'java.util.UUID})]
`(try
(.order ~target ByteOrder/BIG_ENDIAN)
(.putLong ~target (unchecked-int (+ ~offset 0)) (.getMostSignificantBits ~value))
(.putLong ~target (unchecked-int (+ ~offset 8)) (.getLeastSignificantBits ~value))
(finally
(.order ~target ByteOrder/LITTLE_ENDIAN))))))
(defn wrap
[data]
@@ -203,7 +135,7 @@
(defn allocate
[size]
#?(:clj (let [buffer (ByteBuffer/allocate (unchecked-int size))]
#?(:clj (let [buffer (ByteBuffer/allocate (int size))]
(.order buffer ByteOrder/LITTLE_ENDIAN))
:cljs (new js/DataView (new js/ArrayBuffer size))))
@@ -224,14 +156,6 @@
(.set dst-view src-view)
(js/DataView. dst-buff))))
;; FIXME: cljs impl
#?(:clj
(defn copy-bytes
[src src-offset size dst dst-offset]
(let [tmp (byte-array size)]
(.get ^ByteBuffer src src-offset tmp 0 size)
(.put ^ByteBuffer dst dst-offset tmp 0 size))))
(defn equals?
[buffer-a buffer-b]
#?(:clj
@@ -259,18 +183,3 @@
[o]
#?(:clj (instance? ByteBuffer o)
:cljs (instance? js/DataView o)))
(defn slice
[buffer offset size]
#?(:cljs
(let [offset (+ (.-byteOffset buffer) offset)]
(new js/DataView (.-buffer buffer) offset size))
:clj
(-> (.slice ^ByteBuffer buffer (unchecked-int offset) (unchecked-int size))
(.order ByteOrder/LITTLE_ENDIAN))))
(defn size
[o]
#?(:cljs (.-byteLength ^js o)
:clj (.capacity ^ByteBuffer o)))

View File

@@ -51,7 +51,6 @@
"styles/v2"
"layout/grid"
"plugins/runtime"
"tokens/numeric-input"
"design-tokens/v1"
"text-editor/v2"
"render-wasm/v1"
@@ -65,8 +64,12 @@
"layout/grid"
"components/v2"
"plugins/runtime"
"design-tokens/v1"
"variants/v1"})
"design-tokens/v1"})
;; A set of features that should not be propagated to team on creating
;; or modifying a file
(def no-team-inheritable-features
#{"fdata/path-data"})
;; A set of features which only affects on frontend and can be enabled
;; and disabled freely by the user any time. This features does not
@@ -76,20 +79,13 @@
#{"styles/v2"
"plugins/runtime"
"text-editor/v2"
"tokens/numeric-input"
"render-wasm/v1"})
;; Features that are mainly backend only or there are a proper
;; fallback when frontend reports no support for it
(def backend-only-features
#{"fdata/pointer-map"
"fdata/objects-map"})
;; A set of features that should not be propagated to team on creating
;; or modifying a file or creating or modifying a team
(def no-team-inheritable-features
#{"fdata/path-data"
"fdata/shape-data-type"})
#{"fdata/objects-map"
"fdata/pointer-map"})
;; This is a set of features that does not require an explicit
;; migration like components/v2 or the migration is not mandatory to
@@ -99,9 +95,7 @@
(-> #{"layout/grid"
"design-tokens/v1"
"fdata/shape-data-type"
"fdata/path-data"
"tokens/numeric-input"
"variants/v1"}
"fdata/path-data"}
(into frontend-only-features)
(into backend-only-features)))
@@ -125,7 +119,6 @@
:feature-text-editor-v2 "text-editor/v2"
:feature-render-wasm "render-wasm/v1"
:feature-variants "variants/v1"
:feature-token-input "tokens/numeric-input"
nil))
(defn migrate-legacy-features
@@ -227,6 +220,8 @@
:hint (str/ffmt "enabled feature '%' not present in file (missing migration)"
not-supported)))
(check-supported-features! file-features)
;; Components v1 is deprecated
(when-not (contains? file-features "components/v2")
(ex/raise :type :restriction

View File

@@ -83,25 +83,24 @@
[:multi {:decode/json #(update % :grid-type keyword)
:gen/gen gen
:title "SetDefaultGridChange"
:dispatch :grid-type
::smd/simplified true}
[:square
[:map {:title "SetDefautSquareGridAttrs"}
[:map
[:type [:= :set-default-grid]]
[:page-id ::sm/uuid]
[:grid-type [:= :square]]
[:params [:maybe ctg/schema:square-params]]]]
[:column
[:map {:title "SetDefaultColumnGridAttrs"}
[:map
[:type [:= :set-default-grid]]
[:page-id ::sm/uuid]
[:grid-type [:= :column]]
[:params [:maybe ctg/schema:column-params]]]]
[:row
[:map {:title "SetDefaultRowGridAttrs"}
[:map
[:type [:= :set-default-grid]]
[:page-id ::sm/uuid]
[:grid-type [:= :row]]
@@ -112,20 +111,20 @@
[:type [:= :set-guide]]
[:page-id ::sm/uuid]
[:id ::sm/uuid]
[:params [:maybe ctp/schema:guide]]]
[:params [:maybe ::ctp/guide]]]
gen (->> (sg/generator schema)
(sg/fmap (fn [change]
(if (some? (:params change))
(update change :params assoc :id (:id change))
change))))]
(sm/update-properties schema assoc :gen/gen gen)))
[:schema {:gen/gen gen} schema]))
(def schema:set-flow-change
(let [schema [:map {:title "SetFlowChange"}
[:type [:= :set-flow]]
[:page-id ::sm/uuid]
[:id ::sm/uuid]
[:params [:maybe ctp/schema:flow]]]
[:params [:maybe ::ctp/flow]]]
gen (->> (sg/generator schema)
(sg/fmap (fn [change]
@@ -133,7 +132,7 @@
(update change :params assoc :id (:id change))
change))))]
(sm/update-properties schema assoc :gen/gen gen)))
[:schema {:gen/gen gen} schema]))
(def schema:set-plugin-data-change
(let [types #{:file :page :shape :color :typography :component}
@@ -170,265 +169,287 @@
:else
(dissoc change :page-id)))))]
[:and (sm/update-properties schema assoc :gen/gen gen) check1]))
[:and {:gen/gen gen} schema check1]))
(def schema:change
[:multi {:dispatch :type
:title "Change"
:decode/json #(update % :type keyword)
::smd/simplified true}
[:schema
[:multi {:dispatch :type
:title "Change"
:decode/json #(update % :type keyword)
::smd/simplified true}
[:set-option
[:set-comment-thread-position
[:map {:title "SetCommentThreadPositionChange"}
[:comment-thread-id ::sm/uuid]
[:page-id ::sm/uuid]
[:frame-id [:maybe ::sm/uuid]]
[:position [:maybe ::gpt/point]]]]
;; DEPRECATED: remove before 2.3 release
;;
;; Is still there for not cause error when event is received
[:map {:title "SetOptionChange"}]]
[:add-obj
[:map {:title "AddObjChange"}
[:type [:= :add-obj]]
[:id ::sm/uuid]
[:obj cts/schema:shape]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:frame-id ::sm/uuid]
[:parent-id {:optional true} [:maybe ::sm/uuid]]
[:index {:optional true} [:maybe :int]]
[:ignore-touched {:optional true} :boolean]]]
[:set-comment-thread-position
[:map
[:comment-thread-id ::sm/uuid]
[:page-id ::sm/uuid]
[:frame-id [:maybe ::sm/uuid]]
[:position [:maybe ::gpt/point]]]]
[:mod-obj
[:map {:title "ModObjChange"}
[:type [:= :mod-obj]]
[:id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:operations [:vector {:gen/max 5} schema:operation]]]]
[:add-obj
[:map {:title "AddObjChange"}
[:type [:= :add-obj]]
[:id ::sm/uuid]
[:obj :map]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:frame-id ::sm/uuid]
[:parent-id {:optional true} [:maybe ::sm/uuid]]
[:index {:optional true} [:maybe :int]]
[:ignore-touched {:optional true} :boolean]]]
[:del-obj
[:map {:title "DelObjChange"}
[:type [:= :del-obj]]
[:id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]]]
[:mod-obj
[:map {:title "ModObjChange"}
[:type [:= :mod-obj]]
[:id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:operations [:vector {:gen/max 5} schema:operation]]]]
[:set-guide schema:set-guide-change]
[:set-flow schema:set-flow-change]
[:set-default-grid schema:set-default-grid-change]
[:del-obj
[:map {:title "DelObjChange"}
[:type [:= :del-obj]]
[:id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]]]
[:fix-obj
[:map {:title "FixObjChange"}
[:type [:= :fix-obj]]
[:id ::sm/uuid]
[:fix {:optional true} :keyword]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]]]
[:set-guide schema:set-guide-change]
[:set-flow schema:set-flow-change]
[:set-default-grid schema:set-default-grid-change]
[:mov-objects
[:map {:title "MovObjectsChange"}
[:type [:= :mov-objects]]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]
[:parent-id ::sm/uuid]
[:shapes ::sm/any]
[:index {:optional true} [:maybe :int]]
[:after-shape {:optional true} ::sm/any]
[:allow-altering-copies {:optional true} :boolean]]]
[:fix-obj
[:map {:title "FixObjChange"}
[:type [:= :fix-obj]]
[:id ::sm/uuid]
[:fix {:optional true} :keyword]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]]]
[:reorder-children
[:map {:title "ReorderChildrenChange"}
[:type [:= :reorder-children]]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]
[:parent-id ::sm/uuid]
[:shapes ::sm/any]]]
[:mov-objects
[:map {:title "MovObjectsChange"}
[:type [:= :mov-objects]]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]
[:parent-id ::sm/uuid]
[:shapes ::sm/any]
[:index {:optional true} [:maybe :int]]
[:after-shape {:optional true} ::sm/any]
[:allow-altering-copies {:optional true} :boolean]]]
[:add-page
[:map {:title "AddPageChange"}
[:type [:= :add-page]]
[:id {:optional true} ::sm/uuid]
[:name {:optional true} :string]
[:page {:optional true} ::sm/any]]]
[:reorder-children
[:map {:title "ReorderChildrenChange"}
[:type [:= :reorder-children]]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]
[:parent-id ::sm/uuid]
[:shapes ::sm/any]]]
[:mod-page
[:map {:title "ModPageChange"}
[:type [:= :mod-page]]
[:id ::sm/uuid]
;; All props are optional, background can be nil because is the
;; way to remove already set background
[:background {:optional true} [:maybe ctc/schema:hex-color]]
[:name {:optional true} :string]]]
[:add-page
[:map {:title "AddPageChange"}
[:type [:= :add-page]]
[:id {:optional true} ::sm/uuid]
[:name {:optional true} :string]
[:page {:optional true} ::sm/any]]]
[:set-plugin-data schema:set-plugin-data-change]
[:mod-page
[:map {:title "ModPageChange"}
[:type [:= :mod-page]]
[:id ::sm/uuid]
;; All props are optional, background can be nil because is the
;; way to remove already set background
[:background {:optional true} [:maybe ctc/schema:hex-color]]
[:name {:optional true} :string]]]
[:del-page
[:map {:title "DelPageChange"}
[:type [:= :del-page]]
[:id ::sm/uuid]]]
[:set-plugin-data schema:set-plugin-data-change]
[:mov-page
[:map {:title "MovPageChange"}
[:type [:= :mov-page]]
[:id ::sm/uuid]
[:index :int]]]
[:del-page
[:map {:title "DelPageChange"}
[:type [:= :del-page]]
[:id ::sm/uuid]]]
[:reg-objects
[:map {:title "RegObjectsChange"}
[:type [:= :reg-objects]]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:shapes [:vector {:gen/max 5} ::sm/uuid]]]]
[:mov-page
[:map {:title "MovPageChange"}
[:type [:= :mov-page]]
[:id ::sm/uuid]
[:index :int]]]
[:add-color
[:map {:title "AddColorChange"}
[:type [:= :add-color]]
[:color ctc/schema:library-color]]]
[:reg-objects
[:map {:title "RegObjectsChange"}
[:type [:= :reg-objects]]
[:page-id {:optional true} ::sm/uuid]
[:component-id {:optional true} ::sm/uuid]
[:shapes [:vector {:gen/max 5} ::sm/uuid]]]]
[:mod-color
[:map {:title "ModColorChange"}
[:type [:= :mod-color]]
[:color ctc/schema:library-color]]]
[:add-color
[:map {:title "AddColorChange"}
[:type [:= :add-color]]
[:color ctc/schema:library-color]]]
[:del-color
[:map {:title "DelColorChange"}
[:type [:= :del-color]]
[:id ::sm/uuid]]]
[:mod-color
[:map {:title "ModColorChange"}
[:type [:= :mod-color]]
[:color ctc/schema:library-color]]]
[:add-media
[:map {:title "AddMediaChange"}
[:type [:= :add-media]]
[:object ctf/schema:media]]]
[:del-color
[:map {:title "DelColorChange"}
[:type [:= :del-color]]
[:id ::sm/uuid]]]
[:mod-media
[:map {:title "ModMediaChange"}
[:type [:= :mod-media]]
[:object ctf/schema:media]]]
;; DEPRECATED: remove before 2.3
[:add-recent-color
[:map {:title "AddRecentColorChange"}]]
[:del-media
[:map {:title "DelMediaChange"}
[:type [:= :del-media]]
[:id ::sm/uuid]]]
[:add-media
[:map {:title "AddMediaChange"}
[:type [:= :add-media]]
[:object ctf/schema:media]]]
[:add-component
[:map {:title "AddComponentChange"}
[:type [:= :add-component]]
[:id ::sm/uuid]
[:name :string]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
[:path {:optional true} :string]
[:main-instance-id ::sm/uuid]
[:main-instance-page ::sm/uuid]]]
[:mod-media
[:map {:title "ModMediaChange"}
[:type [:= :mod-media]]
[:object ctf/schema:media]]]
[:mod-component
[:map {:title "ModComponentChange"}
[:type [:= :mod-component]]
[:id ::sm/uuid]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
[:name {:optional true} :string]
[:variant-id {:optional true} ::sm/uuid]
[:variant-properties {:optional true} [:vector ctv/schema:variant-property]]]]
[:del-media
[:map {:title "DelMediaChange"}
[:type [:= :del-media]]
[:id ::sm/uuid]]]
[:del-component
[:map {:title "DelComponentChange"}
[:type [:= :del-component]]
[:id ::sm/uuid]
;; when it is an undo of a cut-paste, we need to undo the movement
;; of the shapes so we need to move them delta
[:delta {:optional true} ::gpt/point]
[:skip-undelete? {:optional true} :boolean]]]
[:add-component
[:map {:title "AddComponentChange"}
[:type [:= :add-component]]
[:id ::sm/uuid]
[:name :string]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
[:path {:optional true} :string]]]
[:restore-component
[:map {:title "RestoreComponentChange"}
[:type [:= :restore-component]]
[:id ::sm/uuid]
[:page-id ::sm/uuid]]]
[:mod-component
[:map {:title "ModCompoenentChange"}
[:type [:= :mod-component]]
[:id ::sm/uuid]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
[:name {:optional true} :string]
[:variant-id {:optional true} ::sm/uuid]
[:variant-properties {:optional true} [:vector ::ctv/variant-property]]]]
[:purge-component
[:map {:title "PurgeComponentChange"}
[:type [:= :purge-component]]
[:id ::sm/uuid]]]
[:del-component
[:map {:title "DelComponentChange"}
[:type [:= :del-component]]
[:id ::sm/uuid]
;; when it is an undo of a cut-paste, we need to undo the movement
;; of the shapes so we need to move them delta
[:delta {:optional true} ::gpt/point]
[:skip-undelete? {:optional true} :boolean]]]
[:add-typography
[:map {:title "AddTypogrphyChange"}
[:type [:= :add-typography]]
[:typography ctt/schema:typography]]]
[:restore-component
[:map {:title "RestoreComponentChange"}
[:type [:= :restore-component]]
[:id ::sm/uuid]
[:page-id ::sm/uuid]]]
[:mod-typography
[:map {:title "ModTypogrphyChange"}
[:type [:= :mod-typography]]
[:typography ctt/schema:typography]]]
[:purge-component
[:map {:title "PurgeComponentChange"}
[:type [:= :purge-component]]
[:id ::sm/uuid]]]
[:del-typography
[:map {:title "DelTypogrphyChange"}
[:type [:= :del-typography]]
[:id ::sm/uuid]]]
[:add-typography
[:map {:title "AddTypogrphyChange"}
[:type [:= :add-typography]]
[:typography ::ctt/typography]]]
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib ::sm/any]]] ;; TODO: we should define a plain object schema for tokens-lib
[:mod-typography
[:map {:title "ModTypogrphyChange"}
[:type [:= :mod-typography]]
[:typography ::ctt/typography]]]
[:set-token
[:map {:title "SetTokenChange"}
[:type [:= :set-token]]
[:set-id ::sm/uuid]
[:token-id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-attrs]]]]
[:del-typography
[:map {:title "DelTypogrphyChange"}
[:type [:= :del-typography]]
[:id ::sm/uuid]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}
[:type [:= :set-token-set]]
[:id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-set-attrs]]]]
[:update-active-token-themes
[:map {:title "UpdateActiveTokenThemes"}
[:type [:= :update-active-token-themes]]
[:theme-paths [:set :string]]]]
[:set-token-theme
[:map {:title "SetTokenThemeChange"}
[:type [:= :set-token-theme]]
[:id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-theme-attrs]]]]
[:rename-token-set-group
[:map {:title "RenameTokenSetGroup"}
[:type [:= :rename-token-set-group]]
[:set-group-path [:vector :string]]
[:set-group-fname :string]]]
[:set-active-token-themes
[:map {:title "SetActiveTokenThemes"}
[:type [:= :set-active-token-themes]]
[:theme-paths [:set :string]]]]
[:move-token-set
[:map {:title "MoveTokenSet"}
[:type [:= :move-token-set]]
[:from-path [:vector :string]]
[:to-path [:vector :string]]
[:before-path [:maybe [:vector :string]]]
[:before-group [:maybe :boolean]]]]
[:rename-token-set-group
[:map {:title "RenameTokenSetGroup"}
[:type [:= :rename-token-set-group]]
[:set-group-path [:vector :string]]
[:set-group-fname :string]]]
[:move-token-set-group
[:map {:title "MoveTokenSetGroup"}
[:type [:= :move-token-set-group]]
[:from-path [:vector :string]]
[:to-path [:vector :string]]
[:before-path [:maybe [:vector :string]]]
[:before-group [:maybe :boolean]]]]
[:move-token-set
[:map {:title "MoveTokenSet"}
[:type [:= :move-token-set]]
[:from-path [:vector :string]]
[:to-path [:vector :string]]
[:before-path [:maybe [:vector :string]]]
[:before-group [:maybe :boolean]]]]
[:set-token-theme
[:map {:title "SetTokenThemeChange"}
[:type [:= :set-token-theme]]
[:theme-name :string]
[:group :string]
[:theme [:maybe ctob/schema:token-theme-attrs]]]]
[:move-token-set-group
[:map {:title "MoveTokenSetGroup"}
[:type [:= :move-token-set-group]]
[:from-path [:vector :string]]
[:to-path [:vector :string]]
[:before-path [:maybe [:vector :string]]]
[:before-group [:maybe :boolean]]]]
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib ::sm/any]]]
[:set-base-font-size
[:map {:title "ModBaseFontSize"}
[:type [:= :set-base-font-size]]
[:base-font-size :string]]]])
[:set-token-set
[:map {:title "SetTokenSetChange"}
[:type [:= :set-token-set]]
[:set-name :string]
[:group? :boolean]
;; FIXME: we should not pass private types as part of changes
;; protocol, the changes protocol should reflect a
;; method/protocol for perform surgical operations on file data,
;; this has nothing todo with internal types of a file data
;; structure.
[:token-set {:gen/gen (sg/generator ctob/schema:token-set)}
[:maybe [:fn ctob/token-set?]]]]]
[:set-token
[:map {:title "SetTokenChange"}
[:type [:= :set-token]]
[:set-name :string]
[:token-id ::sm/uuid]
[:token [:maybe ctob/schema:token-attrs]]]]
[:set-base-font-size
[:map {:title "ModBaseFontSize"}
[:type [:= :set-base-font-size]]
[:base-font-size :string]]]]])
(def schema:changes
[:sequential {:gen/max 5 :gen/min 1} schema:change])
(sm/register! ::change schema:change)
(sm/register! ::changes schema:changes)
(def valid-change?
(sm/lazy-validator schema:change))
(def check-changes
(def check-changes!
(sm/check-fn schema:changes))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -513,7 +534,7 @@
;; When verify? false we spec the schema validation. Currently used
;; to make just 1 validation even if the changes are applied twice
(when verify?
(check-changes items))
(check-changes! items))
(binding [*touched-changes* (volatile! #{})
cts/*wasm-sync* true]
@@ -526,6 +547,11 @@
#?(:clj (validate-shapes! data result items))
result))))
;; DEPRECATED: remove after 2.3 release
(defmethod process-change :set-option
[data _]
data)
;; --- Comment Threads
(defmethod process-change :set-comment-thread-position
@@ -919,6 +945,12 @@
[data {:keys [id]}]
(ctl/delete-color data id))
;; DEPRECATED: remove before 2.3
(defmethod process-change :add-recent-color
[data _]
data)
;; -- Media
(defmethod process-change :add-media
@@ -969,63 +1001,64 @@
[data {:keys [id]}]
(ctyl/delete-typography data id))
;; -- Design Tokens
;; -- Tokens
(defmethod process-change :set-tokens-lib
[data {:keys [tokens-lib]}]
(assoc data :tokens-lib tokens-lib))
(defmethod process-change :set-token
[data {:keys [set-id token-id attrs]}]
[data {:keys [set-name token-id token]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not attrs)
(ctob/delete-token lib' set-id token-id)
(not token)
(ctob/delete-token-from-set lib' set-name token-id)
(not (ctob/get-token lib' set-id token-id))
(ctob/add-token lib' set-id (ctob/make-token attrs))
(not (ctob/get-token-in-set lib' set-name token-id))
(ctob/add-token-in-set lib' set-name (ctob/make-token token))
:else
(ctob/update-token lib' set-id token-id
(fn [prev-token]
(ctob/make-token (merge prev-token attrs)))))))))
(ctob/update-token-in-set lib' set-name token-id (fn [prev-token]
(ctob/make-token (merge prev-token token)))))))))
(defmethod process-change :set-token-set
[data {:keys [id attrs]}]
[data {:keys [set-name group? token-set]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not attrs)
(ctob/delete-set lib' id)
(not token-set)
(if group?
(ctob/delete-set-group lib' set-name)
(ctob/delete-set lib' set-name))
(not (ctob/get-set lib' id))
(ctob/add-set lib' (ctob/make-token-set attrs))
(not (ctob/get-set lib' set-name))
(ctob/add-set lib' token-set)
:else
(ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs))))))))
(ctob/update-set lib' set-name (fn [_] token-set)))))))
(defmethod process-change :set-token-theme
[data {:keys [id attrs]}]
[data {:keys [group theme-name theme]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not attrs)
(ctob/delete-theme lib' id)
(not theme)
(ctob/delete-theme lib' group theme-name)
(not (ctob/get-theme lib' id))
(ctob/add-theme lib' (ctob/make-token-theme attrs))
(not (ctob/get-theme lib' group theme-name))
(ctob/add-theme lib' (ctob/make-token-theme theme))
:else
(ctob/update-theme lib'
id
group theme-name
(fn [prev-token-theme]
(ctob/make-token-theme (merge prev-token-theme attrs)))))))))
(ctob/make-token-theme (merge prev-token-theme theme)))))))))
(defmethod process-change :set-active-token-themes
(defmethod process-change :update-active-token-themes
[data {:keys [theme-paths]}]
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
(ctob/set-active-themes theme-paths))))
@@ -1049,7 +1082,7 @@
(ctob/ensure-tokens-lib)
(ctob/move-set-group from-path to-path before-path before-group))))
;; === Design Tokens configuration
;; === Base font size
(defmethod process-change :set-base-font-size
[data {:keys [base-font-size]}]
@@ -1058,23 +1091,21 @@
;; === Operations
(def decode-shape-attrs
(sm/decoder cts/schema:shape-attrs sm/json-transformer))
(def ^:private decode-shape
(sm/decoder cts/schema:shape sm/json-transformer))
(defmethod process-operation :assign
[{:keys [type] :as shape} {:keys [value] :as op}]
(let [modifications (assoc value :type type)
modifications (decode-shape-attrs modifications)]
modifications (decode-shape modifications)]
(reduce-kv (fn [shape k v]
(if (not= v (get shape k))
(process-operation shape {:type :set
:attr k
:val v
:ignore-touched (:ignore-touched op)
:ignore-geometry (:ignore-geometry op)})
shape))
(process-operation shape {:type :set
:attr k
:val v
:ignore-touched (:ignore-touched op)
:ignore-geometry (:ignore-geometry op)}))
shape
(dissoc modifications :type))))
modifications)))
(defmethod process-operation :set
[shape op]

View File

@@ -21,11 +21,10 @@
[app.common.types.path :as path]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[clojure.datafy :refer [datafy]]))
[app.common.uuid :as uuid]))
;; Auxiliary functions to help create a set of changes (undo + redo)
;; TODO: this is a duplicate schema
(def schema:changes
(sm/register!
^{::sm/type ::changes}
@@ -37,7 +36,7 @@
[:stack-undo? {:optional true} boolean?]
[:undo-group {:optional true} ::sm/any]]))
(def check-changes
(def check-changes!
(sm/check-fn schema:changes))
(defn empty-changes
@@ -169,8 +168,9 @@
(defn apply-changes-local
[changes & {:keys [apply-to-library?]}]
(assert (check-changes changes)
"expected valid changes")
(assert
(check-changes! changes)
"expected valid changes")
(if-let [file-data (::file-data (meta changes))]
(let [library-data (::library-data (meta changes))
@@ -718,7 +718,6 @@
(reduce resize-parent changes all-parents)))
;; Library changes
(defn add-color
[changes color]
(-> changes
@@ -800,6 +799,160 @@
(update :undo-changes conj {:type :add-typography :typography prev-typography})
(apply-changes-local))))
(defn update-active-token-themes
[changes active-theme-paths prev-active-theme-paths]
(-> changes
(update :redo-changes conj {:type :update-active-token-themes :theme-paths active-theme-paths})
(update :undo-changes conj {:type :update-active-token-themes :theme-paths prev-active-theme-paths})
(apply-changes-local)))
(defn set-token-theme [changes group theme-name theme]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-theme (some-> (get library-data :tokens-lib)
(ctob/get-theme group theme-name))]
(-> changes
(update :redo-changes conj {:type :set-token-theme
:theme-name theme-name
:group group
:theme theme})
(update :undo-changes conj (if prev-theme
{:type :set-token-theme
:group group
:theme-name (or
;; Undo of edit
(:name theme)
;; Undo of delete
theme-name)
:theme prev-theme}
;; Undo of create
{:type :set-token-theme
:group group
:theme-name theme-name
:theme nil}))
(apply-changes-local))))
(defn rename-token-set-group
[changes set-group-path set-group-fname]
(let [undo-path (ctob/replace-last-path-name set-group-path set-group-fname)
undo-fname (last set-group-path)]
(-> changes
(update :redo-changes conj {:type :rename-token-set-group :set-group-path set-group-path :set-group-fname set-group-fname})
(update :undo-changes conj {:type :rename-token-set-group :set-group-path undo-path :set-group-fname undo-fname})
(apply-changes-local))))
(defn move-token-set
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?] :as opts}]
(-> changes
(update :redo-changes conj {:type :move-token-set
:from-path from-path
:to-path to-path
:before-path before-path
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn move-token-set-group
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?]}]
(-> changes
(update :redo-changes conj {:type :move-token-set-group
:from-path from-path
:to-path to-path
:before-path before-path
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set-group
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn set-tokens-lib
[changes tokens-lib]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-tokens-lib (get library-data :tokens-lib)]
(-> changes
(update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib})
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
(apply-changes-local))))
(defn set-token [changes set-name token-id token]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token (some-> (get library-data :tokens-lib)
(ctob/get-set set-name)
(ctob/get-token token-id))]
(-> changes
(update :redo-changes conj {:type :set-token
:set-name set-name
:token-id token-id
:token token})
(update :undo-changes conj (if prev-token
{:type :set-token
:set-name set-name
:token-id (or
;; Undo of edit
(:id token)
;; Undo of delete
token-id)
:token prev-token}
;; Undo of create token
{:type :set-token
:set-name set-name
:token-id token-id
:token nil}))
(apply-changes-local))))
(defn rename-token-set
[changes name new-name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set name))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:set-name name
:token-set (ctob/rename prev-token-set new-name)
:group? false})
(update :undo-changes conj {:type :set-token-set
:set-name new-name
:token-set prev-token-set
:group? false})
(apply-changes-local))))
(defn set-token-set
[changes set-name group? token-set]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set set-name))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:set-name set-name
:token-set token-set
:group? group?})
(update :undo-changes conj (if prev-token-set
{:type :set-token-set
:set-name (if token-set
;; Undo of edit
(ctob/get-name token-set)
;; Undo of delete
set-name)
:token-set prev-token-set
:group? group?}
;; Undo of create
{:type :set-token-set
:set-name set-name
:token-set nil
:group? group?}))
(apply-changes-local))))
(defn add-component
([changes id path name updated-shapes main-instance-id main-instance-page]
(add-component changes id path name updated-shapes main-instance-id main-instance-page nil nil nil))
@@ -929,144 +1082,6 @@
:id id
:delta delta})))
;; Design Tokens changes
(defn set-tokens-lib
[changes tokens-lib]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-tokens-lib (get library-data :tokens-lib)]
(-> changes
(update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib})
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
(apply-changes-local))))
(defn set-token [changes set-id token-id token]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token (some-> (get library-data :tokens-lib)
(ctob/get-token set-id token-id))]
(-> changes
(update :redo-changes conj {:type :set-token
:set-id set-id
:token-id token-id
:attrs (datafy token)})
(update :undo-changes conj {:type :set-token
:set-id set-id
:token-id token-id
:attrs (datafy prev-token)})
(apply-changes-local))))
(defn set-token-set
[changes id token-set]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set id))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:id id
:attrs (datafy token-set)})
(update :undo-changes conj {:type :set-token-set
:id id
:attrs (datafy prev-token-set)})
(apply-changes-local))))
(defn rename-token-set
[changes id new-name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set id))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:id id
:attrs (datafy (ctob/rename prev-token-set new-name))})
(update :undo-changes conj {:type :set-token-set
:id id
:attrs (datafy prev-token-set)})
(apply-changes-local))))
(defn set-token-theme [changes id theme]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-theme (some-> (get library-data :tokens-lib)
(ctob/get-theme id))]
(-> changes
(update :redo-changes conj {:type :set-token-theme
:id id
:attrs (datafy theme)})
(update :undo-changes conj {:type :set-token-theme
:id id
:attrs (datafy prev-theme)})
(apply-changes-local))))
(defn set-active-token-themes
[changes active-theme-paths]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-active-theme-paths (d/nilv (some-> (get library-data :tokens-lib)
(ctob/get-active-theme-paths))
#{})]
(-> changes
(update :redo-changes conj {:type :set-active-token-themes :theme-paths active-theme-paths})
(update :undo-changes conj {:type :set-active-token-themes :theme-paths prev-active-theme-paths})
(apply-changes-local))))
(defn rename-token-set-group
[changes set-group-path set-group-fname]
(let [undo-path (ctob/replace-last-path-name set-group-path set-group-fname)
undo-fname (last set-group-path)]
(-> changes
(update :redo-changes conj {:type :rename-token-set-group :set-group-path set-group-path :set-group-fname set-group-fname})
(update :undo-changes conj {:type :rename-token-set-group :set-group-path undo-path :set-group-fname undo-fname})
(apply-changes-local))))
(defn move-token-set
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?] :as opts}]
(-> changes
(update :redo-changes conj {:type :move-token-set
:from-path from-path
:to-path to-path
:before-path before-path
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn move-token-set-group
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?]}]
(-> changes
(update :redo-changes conj {:type :move-token-set-group
:from-path from-path
:to-path to-path
:before-path before-path
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set-group
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn set-base-font-size
[changes new-base-font-size]
(assert-file-data! changes)
(let [file-data (::file-data (meta changes))
previous-font-size (ctf/get-base-font-size file-data)]
(-> changes
(update :redo-changes conj {:type :set-base-font-size
:base-font-size new-base-font-size})
(update :undo-changes conj {:type :set-base-font-size
:base-font-size previous-font-size})
(apply-changes-local))))
;; Misc changes
(defn reorder-children
[changes id children]
(assert-page-id! changes)
@@ -1149,3 +1164,15 @@
[changes]
(::page-id (meta changes)))
(defn set-base-font-size
[changes new-base-font-size]
(assert-file-data! changes)
(let [file-data (::file-data (meta changes))
previous-font-size (ctf/get-base-font-size file-data)]
(-> changes
(update :redo-changes conj {:type :set-base-font-size
:base-font-size new-base-font-size})
(update :undo-changes conj {:type :set-base-font-size
:base-font-size previous-font-size})
(apply-changes-local))))

View File

@@ -426,15 +426,15 @@
(defn components-nesting-loop?
"Check if a nesting loop would be created if the given shape is moved below the given parent"
([objects shape-id parent-id]
(let [children (get-children-with-self objects shape-id)
parents (get-parents-with-self objects parent-id)]
(components-nesting-loop? children parents)))
([children parents]
(let [xf-get-component-id (keep :component-id)
child-components (into #{} xf-get-component-id children)
parent-components (into #{} xf-get-component-id parents)]
(seq (set/intersection child-components parent-components)))))
[objects shape-id parent-id]
(let [xf-get-component-id (keep :component-id)
children (get-children-with-self objects shape-id)
child-components (into #{} xf-get-component-id children)
parents (get-parents-with-self objects parent-id)
parent-components (into #{} xf-get-component-id parents)]
(seq (set/intersection child-components parent-components))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ALGORITHMS & TRANSFORMATIONS FOR SHAPES
@@ -692,9 +692,122 @@
(walk/postwalk process-form data)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHAPES ORGANIZATION
;; SHAPES ORGANIZATION (PATH MANAGEMENT)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-path
"Decompose a string in the form 'one / two / three' into
a vector of strings, normalizing spaces."
[path]
(let [xf (comp (map str/trim)
(remove str/empty?))]
(->> (str/split path "/")
(into [] xf))))
(defn join-path
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join " / " path-vec))
(defn join-path-with-dot
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join "\u00A0\u2022\u00A0" path-vec))
(defn clean-path
"Remove empty items from the path."
[path]
(->> (split-path path)
(join-path)))
(defn parse-path-name
"Parse a string in the form 'group / subgroup / name'.
Retrieve the path and the name in separated values, normalizing spaces."
[path-name]
(let [path-name-split (split-path path-name)
path (str/join " / " (butlast path-name-split))
name (or (last path-name-split) "")]
[path name]))
(defn merge-path-item
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path " / " name)
path)
name))
(defn merge-path-item-with-dot
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path "\u00A0\u2022\u00A0" name)
path)
name))
(defn compact-path
"Separate last item of the path, and truncate the others if too long:
'one' -> ['' 'one' false]
'one / two / three' -> ['one / two' 'three' false]
'one / two / three / four' -> ['one / two / ...' 'four' true]
'one-item-but-very-long / two' -> ['...' 'two' true] "
[path max-length dot?]
(let [path-split (split-path path)
last-item (last path-split)
merge-path (if dot?
merge-path-item-with-dot
merge-path-item)]
(loop [other-items (seq (butlast path-split))
other-path ""]
(if-let [item (first other-items)]
(let [full-path (-> other-path
(merge-path item)
(merge-path last-item))]
(if (> (count full-path) max-length)
[(merge-path other-path "...") last-item true]
(recur (next other-items)
(merge-path other-path item))))
[other-path last-item false]))))
(defn butlast-path
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path (butlast split)))))
(defn butlast-path-with-dots
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path-with-dot (butlast split)))))
(defn last-path
"Returns the last item of the path."
[path]
(last (split-path path)))
(defn compact-name
"Append the first item of the path and the name."
[path name]
(let [path-split (split-path path)]
(merge-path-item (first path-split) name)))
(defn split-by-last-period
"Splits a string into two parts:
the text before and including the last period,
and the text after the last period."
[s]
(if-let [last-period (str/last-index-of s ".")]
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
[s ""]))
(defn get-frame-objects
"Retrieves a new objects map only with the objects under frame-id (with frame-id)"
[objects frame-id]

View File

@@ -6,29 +6,14 @@
(ns app.common.files.indices
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.uuid :as uuid]))
(defn- generate-index
"An optimized algorithm for calculate parents index that walk from top
to down starting from a provided shape-id. Usefull when you want to
create an index for the whole objects or subpart of the tree."
[index objects shape-id parents]
(let [shape (get objects shape-id)
index (assoc index shape-id parents)
parents (cons shape-id parents)]
(reduce (fn [index shape-id]
(generate-index index objects shape-id parents))
index
(:shapes shape))))
(defn generate-child-all-parents-index
"Creates an index where the key is the shape id and the value is a set
with all the parents"
([objects]
(generate-index {} objects uuid/zero []))
(generate-child-all-parents-index objects (vals objects)))
([objects shapes]
(let [shape->entry
@@ -39,25 +24,24 @@
(defn create-clip-index
"Retrieves the mask information for an object"
[objects parents-index]
(let [get-clip-parents
(fn [shape]
(let [shape-id (dm/get-prop shape :id)]
(cond-> []
(or (and (cfh/frame-shape? shape)
(not (:show-content shape))
(not= uuid/zero shape-id))
(cfh/bool-shape? shape))
(conj shape)
(:masked-group shape)
(conj (get objects (->> shape :shapes first))))))
xform
(comp (map (d/getf objects))
(mapcat get-clip-parents))
populate-with-clips
(let [retrieve-clips
(fn [parents]
(into [] xform parents))]
(let [lookup-object (fn [id] (get objects id))
get-clip-parents
(fn [shape]
(cond-> []
(or (and (= :frame (:type shape))
(not (:show-content shape))
(not= uuid/zero (:id shape)))
(cfh/bool-shape? shape))
(conj shape)
(d/update-vals parents-index populate-with-clips)))
(:masked-group shape)
(conj (get objects (->> shape :shapes first)))))]
(into []
(comp (map lookup-object)
(mapcat get-clip-parents))
parents)))]
(-> parents-index
(update-vals retrieve-clips))))

View File

@@ -31,7 +31,6 @@
[app.common.types.shape :as cts]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.shadow :as ctss]
[app.common.types.shape.text :as ctst]
[app.common.types.text :as types.text]
[app.common.uuid :as uuid]
[clojure.set :as set]
@@ -1569,41 +1568,6 @@
(-> data
(update :pages-index d/update-vals update-page))))
(defmethod migrate-data "0011-fix-invalid-text-touched-flags"
[data _]
(letfn [(fix-shape [shape]
(let [touched-groups (ctk/normal-touched-groups shape)
content-touched? (touched-groups :content-group)
text-touched? (or (touched-groups :text-content-text)
(touched-groups :text-content-attribute)
(touched-groups :text-content-structure))]
(if (and text-touched? (not content-touched?))
(update shape :touched ctk/set-touched-group :content-group)
shape)))
(update-page [page]
(d/update-when page :objects d/update-vals fix-shape))]
(-> data
(update :pages-index d/update-vals update-page))))
(defmethod migrate-data "0012-fix-position-data"
[data _]
(let [decode-fn
(sm/decoder ctst/schema:position-data sm/json-transformer)
update-object
(fn [object]
(if (cfh/text-shape? object)
(d/update-when object :position-data decode-fn)
object))
update-container
(fn [container]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(def available-migrations
(into (d/ordered-set)
@@ -1671,6 +1635,4 @@
"0008-fix-library-colors-v4"
"0009-clean-library-colors"
"0009-add-partial-text-touched-flags"
"0010-fix-swap-slots-pointing-non-existent-shapes"
"0011-fix-invalid-text-touched-flags"
"0012-fix-position-data"]))
"0010-fix-swap-slots-pointing-non-existent-shapes"]))

View File

@@ -320,31 +320,6 @@
(pcb/with-file-data file-data)
(pcb/update-shapes shape-ids detach-shape))))))
(defmethod repair-error :ref-shape-is-not-head
[_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape
(fn [shape]
; Convert shape in a normal copy, removing nested copy status
(log/debug :hint " -> unhead shape")
(ctk/unhead-shape shape))]
(log/dbg :hint "repairing shape :shape-ref-is-not-head" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :ref-shape-is-head
[_ {:keys [shape page-id args] :as error} file-data _]
(let [repair-shape
(fn [shape]
; Convert shape in a nested head, adding component info
(log/debug :hint " -> reroot shape")
(ctk/rehead-shape shape (:component-file args) (:component-id args)))]
(log/dbg :hint "repairing shape :shape-ref-is-head" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :shape-ref-cycle
[_ {:keys [shape args] :as error} file-data _]
@@ -516,19 +491,6 @@
(pcb/with-library-data file-data)
(pcb/update-component (:id shape) repair-component))))
(defmethod repair-error :invalid-text-touched
[_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape
(fn [shape]
;; Add content group
(log/debug :hint " -> add :content-group to :touched-groups")
(update shape :touched ctk/set-touched-group :content-group))]
(log/dbg :hint "repairing shape :invalid-text-touched" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :misplaced-slot
[_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape

View File

@@ -269,22 +269,6 @@
(d/parse-double width 1)
(d/parse-double height 1)))
(defn- parse-radius-attrs
[attrs]
(if (or (contains? attrs :rx) (contains? attrs :ry))
(let [rx-val (d/parse-double (:rx attrs) 0)
ry-val (d/parse-double (:ry attrs) 0)
radius (cond
(and (contains? attrs :rx) (contains? attrs :ry))
(min rx-val ry-val)
(contains? attrs :rx)
rx-val
(contains? attrs :ry)
ry-val
:else 0)]
{:r1 radius :r2 radius :r3 radius :r4 radius})
{}))
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
@@ -296,9 +280,7 @@
(update :y - (:y origin)))
props (-> (dissoc attrs :x :y :width :height :rx :ry :transform)
(csvg/attrs->props))
radius-attrs (parse-radius-attrs attrs)]
(csvg/attrs->props))]
(cts/setup-shape
(-> (calculate-rect-metadata rect transform)
(assoc :type :rect)
@@ -306,10 +288,13 @@
(assoc :frame-id frame-id)
(assoc :svg-viewbox vbox)
(assoc :svg-attrs props)
;; We need to ensure fills are empty on import process
;; because setup-shape assings one by default.
;; We need to ensure fills are empty on import process
;; because setup-shape assings one by default.
(assoc :fills [])
(merge radius-attrs)))))
(cond-> (contains? attrs :rx)
(assoc :rx (d/parse-double (:rx attrs) 0)))
(cond-> (contains? attrs :ry)
(assoc :ry (d/parse-double (:ry attrs) 0)))))))
(defn- parse-circle-attrs
[attrs]
@@ -523,7 +508,6 @@
:else (dm/str tag))]
(dm/str "svg-" suffix)))
(defn parse-svg-element
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames]

View File

@@ -11,7 +11,6 @@
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.path-names :as cpn]
[app.common.schema :as sm]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
@@ -48,8 +47,6 @@
:should-be-component-root
:should-not-be-component-root
:ref-shape-not-found
:ref-shape-is-head
:ref-shape-is-not-head
:shape-ref-in-main
:root-main-not-allowed
:nested-main-not-allowed
@@ -60,7 +57,6 @@
:not-component-not-allowed
:component-nil-objects-not-allowed
:instance-head-not-frame
:invalid-text-touched
:misplaced-slot
:missing-slot
:shape-ref-cycle
@@ -308,28 +304,6 @@
"Shape inside main instance should not have shape-ref"
shape file page)))
(defn- check-ref-is-not-head
"Validate that the referenced shape is not a nested copy root."
[shape file page libraries]
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)]
(when (and (some? ref-shape)
(ctk/instance-head? ref-shape))
(report-error :ref-shape-is-head
(str/ffmt "Referenced shape % is a component, so the copy must also be" (:shape-ref shape))
shape file page))))
(defn- check-ref-is-head
"Validate that the referenced shape is a nested copy root."
[shape file page libraries]
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)]
(when (and (some? ref-shape)
(not (ctk/instance-head? ref-shape)))
(report-error :ref-shape-is-not-head
(str/ffmt "Referenced shape % of a head copy must also be a head" (:shape-ref shape))
shape file page
:component-file (:component-file ref-shape)
:component-id (:component-id ref-shape)))))
(defn- check-empty-swap-slot
"Validate that this shape does not have any swap slot."
[shape file page]
@@ -354,20 +328,6 @@
"This shape has children with the same swap slot"
shape file page)))
(defn- check-valid-touched
"Validate that the text touched flags are coherent."
[shape file page]
(let [touched-groups (ctk/normal-touched-groups shape)
content-touched? (touched-groups :content-group)
text-touched? (or (touched-groups :text-content-text)
(touched-groups :text-content-attribute)
(touched-groups :text-content-structure))]
;; For now we only check this combination, that has been reported in some bugs
(when (and text-touched? (not content-touched?))
(report-error :invalid-text-touched
"This thape has text type touched but not content touched"
shape file page))))
(defn- check-shape-main-root-top
"Root shape of a top main instance:
@@ -407,10 +367,8 @@
(check-component-not-main-head shape file page libraries)
(check-component-root shape file page)
(check-component-ref shape file page libraries)
(check-ref-is-head shape file page libraries)
(check-empty-swap-slot shape file page)
(check-duplicate-swap-slot shape file page)
(check-valid-touched shape file page)
(run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape))))
(defn- check-shape-copy-root-nested
@@ -421,12 +379,10 @@
[shape file page libraries library-exists]
(check-component-not-main-head shape file page libraries)
(check-component-not-root shape file page)
(check-valid-touched shape file page)
;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached
;; so we only validate the shape-ref if the ancestor is from a valid library
(when library-exists
(check-component-ref shape file page libraries)
(check-ref-is-head shape file page libraries))
(check-component-ref shape file page libraries))
(run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape)))
(defn- check-shape-main-not-root
@@ -444,9 +400,7 @@
(check-component-not-main-not-head shape file page)
(check-component-not-root shape file page)
(check-component-ref shape file page libraries)
(check-ref-is-not-head shape file page libraries)
(check-empty-swap-slot shape file page)
(check-valid-touched shape file page)
(run! #(check-shape % file page libraries :context :copy-any) (:shapes shape)))
(defn- check-shape-not-component
@@ -512,7 +466,7 @@
(report-error :variant-bad-name
(str/ffmt "Variant % has an invalid name" (:id shape))
shape file page))
(when-not (= (:name parent) (cpn/merge-path-item (:path component) (:name component)))
(when-not (= (:name parent) (cfh/merge-path-item (:path component) (:name component)))
(report-error :variant-component-bad-name
(str/ffmt "Component % has an invalid name" (:id shape))
shape file page))
@@ -571,7 +525,7 @@
;; mains can't be nested into mains
(if (or (= context :not-component) (= context :main-top))
(report-error :nested-main-not-allowed
"Component main not allowed inside other component"
"Nested main component only allowed inside other component"
shape file page)
(check-shape-main-root-nested shape file page libraries))
@@ -636,20 +590,6 @@
(str/ffmt "Shape % should be a variant" (:id main-component))
main-component file component-page))))
(defn- check-main-inside-main
[component file]
(let [component-page (ctf/get-component-page (:data file) component)
main-instance (ctst/get-shape component-page (:main-instance-id component))
main-parents? (->> main-instance
:id
(cfh/get-parents (:objects component-page))
(some ctk/main-instance?)
boolean)]
(when main-parents?
(report-error :nested-main-not-allowed
"Component main not allowed inside other component"
main-instance file component-page))))
(defn- check-component
"Validate semantic coherence of a component. Report all errors found."
[component file]
@@ -657,8 +597,6 @@
(report-error :component-nil-objects-not-allowed
"Objects list cannot be nil"
component file nil))
(when-not (:deleted component)
(check-main-inside-main component file))
(when (:deleted component)
(check-component-duplicate-swap-slot component file)
(check-ref-cycles component file))

View File

@@ -119,10 +119,7 @@
;; Only for developtment.
:tiered-file-data-storage
:token-units
:token-base-font-size
:token-color
:token-typography-types
:token-typography-composite
:transit-readable-response
:user-feedback
;; TODO: remove this flag.
@@ -134,8 +131,7 @@
:hide-release-modal
:subscriptions
:subscriptions-old
:frontend-binary-fills
:inspect-styles})
:frontend-binary-fills})
(def all-flags
(set/union email login varia))
@@ -157,9 +153,7 @@
:enable-dashboard-templates-section
:enable-google-fonts-provider
:enable-component-thumbnails
:enable-render-wasm-dpr
:enable-token-units
:enable-token-typography-types])
:enable-render-wasm-dpr])
(defn parse
[& flags]

View File

@@ -96,9 +96,10 @@
(if (and (some? layout-line) (<= from-idx max-idx))
(let [to-idx (+ from-idx (:num-children layout-line))
children (subvec children from-idx to-idx)
[_ modif-tree]
(reduce set-child-modifiers [layout-line modif-tree] children)]
(recur modif-tree (first pending) (rest pending) to-idx))
(recur modif-tree (first pending) (rest pending) (long to-idx)))
modif-tree)))))

View File

@@ -88,11 +88,8 @@
([shape]
(get-shape-filter-bounds shape false))
([shape ignore-shadow-margin?]
(if (or (and (cfh/svg-raw-shape? shape)
(not= :svg (dm/get-in shape [:content :tag])))
;; If no shadows or blur, we return the selrect as is
(and (empty? (-> shape :shadow))
(zero? (-> shape :blur :value (or 0)))))
(if (and (cfh/svg-raw-shape? shape)
(not= :svg (dm/get-in shape [:content :tag])))
(dm/get-prop shape :selrect)
(let [filters (shape->filters shape)
blur-value (or (-> shape :blur :value) 0)

View File

@@ -177,7 +177,7 @@
(rest children)))))]
(recur next-areas
(+ from-idx (:num-children current-line))
(+ from-idx (long (:num-children current-line)))
(+ (:x line-area) (:width line-area))
(+ (:y line-area) (:height line-area))
(rest lines)))))))

View File

@@ -49,7 +49,6 @@
[app.common.exceptions :as ex]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[cuerdas.core :as str]
[promesa.exec :as px]
@@ -222,42 +221,36 @@
#?(:clj (inst-ms (java.time.Instant/now))
:cljs (js/Date.now)))
(defn emit-log
[props cause context logger level sync?]
(let [props (cond-> props sync? deref)
ts (current-timestamp)
gcontext *context*
logfn (fn []
(let [props (if sync? props (deref props))
props (into (d/ordered-map) props)
context (if (and (empty? gcontext)
(empty? context))
{}
(d/without-nils (merge gcontext context)))
lrecord {::id (uuid/next)
::timestamp ts
::message (delay (build-message props))
::props props
::context context
::level level
::logger logger}
lrecord (cond-> lrecord
(some? cause)
(assoc ::cause cause
::trace (delay (build-stack-trace cause))))]
(swap! log-record (constantly lrecord))))]
(if sync?
(logfn)
(px/exec! *default-executor* logfn))))
(defmacro log!
"Emit a new log record to the global log-record state (asynchronously). "
[& props]
(let [{:keys [::level ::logger ::context ::sync? cause] :or {sync? false}} props
props (into [] msg-props-xf props)]
`(when (enabled? ~logger ~level)
(emit-log (delay ~props) ~cause ~context ~logger ~level ~sync?))))
(let [props# (cond-> (delay ~props) ~sync? deref)
ts# (current-timestamp)
context# *context*
logfn# (fn []
(let [props# (if ~sync? props# (deref props#))
props# (into (d/ordered-map) props#)
cause# ~cause
context# (d/without-nils
(merge context# ~context))
lrecord# {::id (uuid/next)
::timestamp ts#
::message (delay (build-message props#))
::props props#
::context context#
::level ~level
::logger ~logger}
lrecord# (cond-> lrecord#
(some? cause#)
(assoc ::cause cause#
::trace (delay (build-stack-trace cause#))))]
(swap! log-record (constantly lrecord#))))]
(if ~sync?
(logfn#)
(px/exec! *default-executor* logfn#))))))
#?(:clj
(defn slf4j-log-handler
@@ -283,8 +276,7 @@
(when (enabled? logger level)
(let [hstyles (str/ffmt "font-weight: 600; color: %" (level->color level))
mstyles (str/ffmt "font-weight: 300; color: %" (level->color level))
ts (ct/format-inst (ct/now) "kk:mm:ss.SSSS")
header (str/concat "%c" (level->name level) " " ts " [" logger "] ")
header (str/concat "%c" (level->name level) " [" logger "] ")
message (str/concat header "%c" @message)]
(js/console.group message hstyles mstyles)

View File

@@ -11,13 +11,11 @@
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.logging :as log]
[app.common.logic.shapes :as cls]
[app.common.logic.variant-properties :as clvp]
[app.common.path-names :as cpn]
[app.common.spec :as us]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
@@ -987,7 +985,7 @@
(defn generate-rename-component
"Generate the changes for rename the component with the given id, in the current file library."
[changes id new-name library-data]
(let [[path name] (cpn/split-group-name new-name)]
(let [[path name] (cfh/parse-path-name new-name)]
(-> changes
(pcb/with-library-data library-data)
(pcb/update-component id #(assoc % :path path :name name)))))
@@ -1735,17 +1733,6 @@
[(conj roperations roperation)
(conj uoperations uoperation)]))
(defn- check-detached-main
[changes dest-shape origin-shape]
;; Only for direct updates (from main to copy). Check if the main shape
;; has been detached. If so, the copy shape must be unheaded (i.e. converted
;; into a normal copy and not a nested instance).
(if (and (= (:shape-ref dest-shape) (:id origin-shape))
(ctk/subcopy-head? dest-shape)
(not (ctk/instance-head? origin-shape)))
(pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true})
changes))
(defn- update-attrs
"The main function that implements the attribute sync algorithm. Copy
attributes that have changed in the origin shape to the dest shape.
@@ -1786,8 +1773,6 @@
(seq roperations)
(add-update-attr-changes dest-shape container roperations uoperations)
:always
(check-detached-main dest-shape origin-shape)
:always
(generate-update-tokens container dest-shape origin-shape touched omit-touched?))
(let [attr-group (get ctk/sync-attrs attr)
@@ -1811,6 +1796,7 @@
(= :content attr)
(touched attr-group))
skip-operations?
(or (= (get origin-shape attr) (get dest-shape attr))
(and (touched attr-group)
@@ -2034,6 +2020,7 @@
skip-operations? (or skip-operations?
(= attr-val (get current-shape attr)))
;; On a text-change, we want to force a position-data reset
;; so it's calculated again
[roperations uoperations]
@@ -2041,16 +2028,6 @@
(add-update-attr-operations :position-data current-shape roperations uoperations nil)
[roperations uoperations])
;; On a rotation operation we need to keep also the transformation matrixes
[roperations uoperations]
(if (and (not skip-operations?) (= attr :rotation))
(let [[roperations uoperations]
(add-update-attr-operations
:transform current-shape roperations uoperations (:transform previous-shape))]
(add-update-attr-operations
:transform-inverse current-shape roperations uoperations (:transform-inverse previous-shape)))
[roperations uoperations])
[roperations' uoperations']
(if skip-operations?
[roperations uoperations]
@@ -2236,7 +2213,7 @@
variant-id (when (ctk/is-variant? root) (:parent-id root))
props (when (ctk/is-variant? root) (get variant-props (:component-id root)))
[path name] (cpn/split-group-name name)
[path name] (cfh/parse-path-name name)
[root-shape updated-shapes]
(ctn/convert-shape-in-component root objects file-id)
@@ -2528,10 +2505,9 @@
frames)))
(defn- duplicate-variant
[changes library component base-pos parent page-id into-new-variant?]
[changes library component base-pos parent-id page-id]
(let [component-page (ctpl/get-page (:data library) (:main-instance-page component))
objects (:objects component-page)
component-shape (get objects (:main-instance-id component))
component-shape (dm/get-in component-page [:objects (:main-instance-id component)])
orig-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract base-pos orig-pos)
new-component-id (uuid/next)
@@ -2541,27 +2517,11 @@
new-component-id
{:apply-changes-local-library? true
:delta delta
:new-variant-id (if into-new-variant? nil (:id parent))
:page-id page-id})
value (when into-new-variant?
(str ctv/value-prefix
(-> (cfv/extract-properties-values (:data library) objects (:id parent))
last
:value
count
inc)))]
:new-variant-id parent-id
:page-id page-id})]
[shape
(cond-> changes
into-new-variant?
(clvp/generate-make-shapes-variant [shape] parent)
;; If it has the same parent, update the value of the last property
(and into-new-variant? (= (:variant-id component) (:id parent)))
(clvp/generate-update-property-value new-component-id (-> component :variant-properties count dec) value)
:always
(pcb/change-parent (:id parent) [shape] 0))]))
(-> changes
(pcb/change-parent parent-id [shape]))]))
(defn generate-duplicate-component-change
@@ -2573,13 +2533,11 @@
pos (as-> (gsh/move main delta) $
(gpt/point (:x $) (:y $)))
parent (get objects parent-id)
;; When we duplicate a variant alone, we will instanciate it
;; When we duplicate a variant along with its variant-container, we will duplicate it
in-variant-container? (contains? ids-map (:variant-id main))
restore-component
#(let [{:keys [shape changes]}
(prepare-restore-component changes
@@ -2592,42 +2550,29 @@
frame-id)]
[shape changes])
[_shape changes]
(cond
(nil? component)
(if (nil? component)
(restore-component)
(if (and (ctk/is-variant? main) in-variant-container?)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent-id
(:id page))
(and (ctk/is-variant? main) in-variant-container?)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent
(:id page)
false)
(ctk/is-variant-container? parent)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent
(:id page)
true)
:else
(generate-instantiate-component changes
objects
file-id
component-id
pos
page
libraries
main-id
parent-id
frame-id
ids-map
{}))]
(generate-instantiate-component changes
objects
file-id
component-id
pos
page
libraries
main-id
parent-id
frame-id
ids-map
{})))]
changes))
(defn generate-duplicate-shape-change
@@ -2786,8 +2731,7 @@
changes (-> changes
(pcb/with-page page)
(pcb/with-objects all-objects)
(pcb/with-library-data library-data))
(pcb/with-objects all-objects))
changes
(->> shapes
(reduce #(generate-duplicate-shape-change %1

View File

@@ -17,13 +17,12 @@
[app.common.types.pages-list :as ctpl]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.layout :as ctl]
[app.common.types.shape.token :as ctst]
[app.common.types.text :as ctt]
[app.common.types.token :as cto]
[app.common.uuid :as uuid]
[clojure.set :as set]))
(def text-typography-style-attrs (set ctt/text-typography-attrs))
(def text-typography-attrs (set ctt/text-typography-attrs))
(defn- generate-unapply-tokens
"When updating attributes that have a token applied, we must unapply it, because the value
@@ -39,14 +38,10 @@
(let [new-shape (get new-objects (:id shape))
attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))
attrs (cond-> attrs
;; Unapply token when applying typography asset style
(seq (set/intersection text-typography-style-attrs attrs))
(into cto/typography-keys)
;; Unapply font-weight when changing the font-family attribute
(and (:font-id attrs) (ctst/font-weight-applied? shape))
(conj :font-weight))]
;; Unapply token when applying typography asset style
attrs (if (seq (set/intersection text-typography-attrs attrs))
(into attrs cto/typography-keys)
attrs)]
(apply set/union (map cto/shape-attr->token-attrs attrs))))
check-attr
@@ -185,17 +180,15 @@
interactions)))
(vals objects))
id-to-delete? (set ids-to-delete)
changes
(->> (:flows page)
(reduce
(fn [changes [id flow]]
(if (id-to-delete? (:starting-frame flow))
(-> changes
(pcb/with-page page)
(pcb/set-flow id nil))
changes))
changes))
(reduce (fn [changes {:keys [id] :as flow}]
(if (contains? ids-to-delete (:starting-frame flow))
(-> changes
(pcb/with-page page)
(pcb/set-flow id nil))
changes))
changes
(:flows page))
all-parents
@@ -407,10 +400,9 @@
(remove #(= % parent-id) all-parents))]
(-> changes
;; Remove layout-item properties and tokens when moving a shape outside a layout
;; Remove layout-item properties when moving a shape outside a layout
(cond-> (not (ctl/any-layout? parent))
(-> (pcb/update-shapes ids ctl/remove-layout-item-data)
(pcb/update-shapes ids cto/unapply-layout-item-tokens)))
(pcb/update-shapes ids ctl/remove-layout-item-data))
;; Remove the hide in viewer flag
(cond-> (and (not= uuid/zero parent-id) (cfh/frame-shape? parent))

View File

@@ -17,16 +17,18 @@
Use this for managing sets active state without having to modify a
user created theme (\"no themes selected\" state in the ui)."
[changes tokens-lib update-theme-fn]
(let [active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
(let [prev-active-token-themes (ctob/get-active-theme-paths tokens-lib)
active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
hidden-theme (ctob/get-hidden-theme tokens-lib)
hidden-theme' (-> (some-> hidden-theme
(ctob/set-sets active-token-set-names))
(update-theme-fn))]
prev-hidden-theme (ctob/get-hidden-theme tokens-lib)
hidden-theme (-> (some-> prev-hidden-theme (ctob/set-sets active-token-set-names))
(update-theme-fn))]
(-> changes
(pcb/set-active-token-themes #{(ctob/get-theme-path hidden-theme')})
(pcb/set-token-theme (ctob/get-id hidden-theme)
hidden-theme'))))
(pcb/update-active-token-themes #{(ctob/theme-path hidden-theme)} prev-active-token-themes)
(pcb/set-token-theme (:group prev-hidden-theme)
(:name prev-hidden-theme)
hidden-theme))))
(defn generate-toggle-token-set
"Toggle a token set at `set-name` in `tokens-lib` without modifying a
@@ -137,12 +139,3 @@
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
(pcb/move-token-set-group changes params)
changes))
(defn generate-delete-token-set-group
"Create changes for deleting a token set group."
[changes tokens-lib path]
(let [sets (ctob/get-sets-at-path tokens-lib path)]
(reduce (fn [changes set]
(pcb/set-token-set changes (ctob/get-id set) nil))
changes
sets)))

View File

@@ -7,8 +7,8 @@
(:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.path-names :as cpn]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctcl]
[app.common.types.variant :as ctv]
@@ -127,7 +127,7 @@
(defn- generate-make-shape-no-variant
[changes shape]
(let [new-name (ctv/variant-name-to-name shape)
[cpath cname] (cpn/split-group-name new-name)]
[cpath cname] (cfh/parse-path-name new-name)]
(-> changes
(pcb/update-component (:component-id shape)
#(-> (dissoc % :variant-id :variant-properties)
@@ -146,8 +146,8 @@
(defn- create-new-properties-from-variant
[shape min-props data container-name base-properties]
(let [component (ctcl/get-component data (:component-id shape) true)
component-full-name (cpn/merge-path-item (:path component) (:name component))
add-name? (not= component-full-name container-name)
add-name? (not= (:name component) container-name)
props (ctv/merge-properties base-properties
(:variant-properties component))
new-props (- min-props
@@ -188,7 +188,7 @@
(map #(assoc % :value "")))
num-base-props (count base-props)
[cpath cname] (cpn/split-group-name (:name variant-container))
[cpath cname] (cfh/parse-path-name (:name variant-container))
container-name (:name variant-container)
create-new-properties

View File

@@ -31,11 +31,9 @@
component-id
new-component-id
{:new-shape-id new-shape-id :apply-changes-local-library? true}))]
(cond-> changes
(>= prop-num 0)
(clvp/generate-update-property-value new-component-id prop-num value)
:always
(pcb/change-parent (:parent-id shape) [new-shape] 0))))
(-> changes
(clvp/generate-update-property-value new-component-id prop-num value)
(pcb/change-parent (:parent-id shape) [new-shape] 0))))
(defn- generate-path
[path objects base-id shape]

View File

@@ -1,134 +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.common.path-names
(:require
[cuerdas.core :as str]))
"Functions to manipulate entity names that represent groups with paths,
e.g. 'Group / Subgroup / Name'.
Some naming conventions:
- Path string: the full string with groups and name, e.g. 'Group / Subgroup / Name'.
- Path: a vector of strings with the full path, e.g. ['Group' 'Subgroup' 'Name'].
- Group string: the group part of the path string, e.g. 'Group / Subgroup'.
- Group: a vector of strings with the group part of the path, e.g. ['Group' 'Subgroup'].
- Name: the final name part of the path, e.g. 'Name'."
(defn split-path
"Decompose a path string in the form 'one / two / three' into a vector
of strings, trimming spaces (e.g. ['one' 'two' 'three'])."
[path-str & {:keys [separator] :or {separator "/"}}]
(let [xf (comp (map str/trim)
(remove str/empty?))]
(->> (str/split path-str separator)
(into [] xf))))
(defn join-path
"Regenerate a path as a string, from a vector.
(e.g. ['one' 'two' 'three'] -> 'one / two / three')"
[path & {:keys [separator with-spaces?] :or {separator "/" with-spaces? true}}]
(if with-spaces?
(str/join (str " " separator " ") path)
(str/join separator path)))
(defn split-group-name
"Parse a path string. Retrieve the group and the name in separated values,
normalizing spaces (e.g. 'group / subgroup / name' -> ['group / subgroup' 'name'])."
[path-str & {:keys [separator with-spaces?] :or {separator "/" with-spaces? true}}]
(let [path (split-path path-str :separator separator)
group-str (join-path (butlast path) :separator separator :with-spaces? with-spaces?)
name (or (last path) "")]
[group-str name]))
(defn join-path-with-dot
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join "\u00A0\u2022\u00A0" path-vec))
(defn clean-path
"Remove empty items from the path."
[path]
(->> (split-path path)
(join-path)))
(defn merge-path-item
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path " / " name)
path)
name))
(defn merge-path-item-with-dot
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path "\u00A0\u2022\u00A0" name)
path)
name))
(defn compact-path
"Separate last item of the path, and truncate the others if too long:
'one' -> ['' 'one' false]
'one / two / three' -> ['one / two' 'three' false]
'one / two / three / four' -> ['one / two / ...' 'four' true]
'one-item-but-very-long / two' -> ['...' 'two' true] "
[path max-length dot?]
(let [path-split (split-path path)
last-item (last path-split)
merge-path (if dot?
merge-path-item-with-dot
merge-path-item)]
(loop [other-items (seq (butlast path-split))
other-path ""]
(if-let [item (first other-items)]
(let [full-path (-> other-path
(merge-path item)
(merge-path last-item))]
(if (> (count full-path) max-length)
[(merge-path other-path "...") last-item true]
(recur (next other-items)
(merge-path other-path item))))
[other-path last-item false]))))
(defn butlast-path
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path (butlast split)))))
(defn butlast-path-with-dots
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path-with-dot (butlast split)))))
(defn last-path
"Returns the last item of the path."
[path]
(last (split-path path)))
(defn inside-path? [child parent]
(let [child-path (split-path child)
parent-path (split-path parent)]
(and (<= (count parent-path) (count child-path))
(= parent-path (take (count parent-path) child-path)))))
(defn split-by-last-period
"Splits a string into two parts:
the text before and including the last period,
and the text after the last period."
[s]
(if-let [last-period (str/last-index-of s ".")]
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
[s ""]))

View File

@@ -131,10 +131,34 @@
(->> (entries schema)
(into #{} xf:map-key)))
(defn update-properties
[s f & args]
(let [s (schema s)]
(apply m/-update-properties s f args)))
;; (defn key-transformer
;; [& {:as opts}]
;; (mt/key-transformer opts))
;; (defn- transform-map-keys
;; [f o]
;; (cond
;; (record? o)
;; (reduce-kv (fn [res k v]
;; (let [k' (f k)]
;; (if (= k k')
;; res
;; (-> res
;; (assoc k' v)
;; (dissoc k)))))
;; o
;; o)
;; (map? o)
;; (persistent!
;; (reduce-kv (fn [res k v]
;; (assoc! res (f k) v))
;; (transient {})
;; o))
;; :else
;; o))
(defn -transform-map-keys
([f]
@@ -655,7 +679,8 @@
identity)]
{:pred #(contains? options %)
:type-properties
{:title "enum"
{:title "one-of"
:description "One of the Set"
:gen/gen (sg/elements options)
:decode/string decode
:decode/json decode
@@ -698,14 +723,15 @@
{:pred pred
:type-properties
{:title "integer"
:description "integer"
{:title "int"
:description "int"
:error/message "expected to be int/long"
:error/code "errors.invalid-integer"
:gen/gen gen
:decode/string parse-long
:decode/json parse-long
::oapi/type "integer"}}))})
::oapi/type "integer"
::oapi/format "int64"}}))})
(defn parse-double
[v]
@@ -767,8 +793,8 @@
{:pred pred
:type-properties
{:title "number"
:description "number"
{:title "int"
:description "int"
:error/message "expected to be number"
:error/code "errors.invalid-number"
:gen/gen gen
@@ -818,7 +844,10 @@
#(some (fn [prop]
(contains? % prop))
choices))]
{:pred pred}))})
{:pred pred
:type-properties
{:title "contains any"
:description "contains predicate"}}))})
;; (register!
;; {:type ::inst
@@ -939,7 +968,6 @@
:type-properties
{:title "string"
:description "not whitespace string"
::oapi/type "string"
:gen/gen (sg/word-string)
:error/fn
(fn [{:keys [value schema]}]

View File

@@ -91,15 +91,11 @@
(defmethod visit :int [_ schema _ _] (str "integer" (-titled schema) (-min-max-suffix-number schema)))
(defmethod visit :double [_ schema _ _] (str "double" (-titled schema) (-min-max-suffix-number schema)))
(defmethod visit :select-keys [_ schema _ options] (describe* (m/deref schema) options))
(defmethod visit :and [_ s children _]
(str (str/join " && " (filter some? children)) (-titled s)))
(defmethod visit :and [_ s children _] (str (str/join " && " children) (-titled s)))
(defmethod visit :enum [_ s children _options] (str "enum" (-titled s) " of " (str/join ", " children)))
(defmethod visit :maybe [_ _ children _] (str (first children) " nullable"))
(defmethod visit :tuple [_ _ children _] (str "(" (str/join ", " children) ")"))
(defmethod visit :re [_ _ children _]
(let [pattern (first children)]
(str "string & regex pattern /" (str pattern) "/")))
(defmethod visit :re [_ s _ options] (str "regex pattern " (-titled s) "matching " (pr-str (first (m/children s options)))))
(defmethod visit :any [_ s _ _] (str "anything" (-titled s)))
(defmethod visit :some [_ _ _ _] "anything but null")
(defmethod visit :nil [_ _ _ _] "null")
@@ -112,11 +108,10 @@
(defmethod visit :uuid [_ _ _ _] "uuid")
(defmethod visit :boolean [_ _ _ _] "boolean")
(defmethod visit :keyword [_ _ _ _] "string")
(defmethod visit :fn [_ _ _ _]
nil)
(defmethod visit :fn [_ _ _ _] "FN")
(defmethod visit :vector [_ _ children _]
(str "[" (str/trim (last children)) "]"))
(str "[" (last children) "]"))
(defn -tagged [children] (map (fn [[tag _ c]] (str c " (tag: " tag ")")) children))
@@ -142,15 +137,8 @@
(some? suffix)
(str suffix))))
(defmethod visit :map-of
[_ schema children _]
(let [props (m/properties schema)
title (some->> (:title props) str/camel str/capital)]
(str (if title
(str "type " title ": ")
"")
"map[" (first children) "," (second children) "]")))
(defmethod visit :map-of [_ _ children _]
(str "map[" (first children) "," (second children) "]"))
(defmethod visit :union [_ _ children _]
(str/join " | " children))
@@ -168,104 +156,61 @@
(or (:title props)
"*")))
(defn- format-map
[schema children]
(let [props (m/properties schema)
closed? (get props :closed)
title (some->> (:title props) str/camel str/capital)
optional (into #{} (comp (filter (m/-comp :optional second))
(map first))
children)
entries (->> children
(map (fn [[k _ s]]
;; NOTE: maybe we can detect multiple lines
;; and then just insert a break line
(str " " (str/camel k)
(when (contains? optional k) "?")
": " (str/trim s))))
(str/join ",\n"))
header (cond-> (str "type " title)
closed? (str "!")
(some? title) (str " "))]
(str header "{\n" entries "\n}")))
(defmethod visit :map
[_ schema children {:keys [::level] :as options}]
(let [props (m/properties schema)
extracted? (get props ::extracted false)]
[_ schema children {:keys [::level ::max-level] :as options}]
(let [props (m/properties schema)
closed? (:closed props)
title (some->> (:title props) str/camel str/capital)]
(cond
(or (= level 0) extracted?)
(format-map schema children)
(if (>= level max-level)
(or (some-> title str)
"<untitled>")
(let [optional (into #{} (comp (filter (m/-comp :optional second))
(map first))
children)
entries (->> children
(map (fn [[k _ s]]
(str (pad " " level) (str/camel k)
(when (contains? optional k) "?")
": " s)))
(str/join ",\n"))
:else
(let [schema (mu/update-properties schema assoc ::extracted true)
title (or (some->> (:title props) str/camel str/capital) "<untitled>")]
(swap! *definitions* conj (format-map schema children))
title))))
header (cond-> (str "type " title)
closed? (str "!")
(some? title) (str " "))]
(defn format-multi
[s children]
(let [props (m/properties s)
title (or (some-> (:title props) str/camel str/capital) "<untitled>")
dispatcher (or (-> s m/properties :dispatch-description)
(-> s m/properties :dispatch))
entries (->> children
(map (fn [[_ _ entry]]
(pad entry 1)))
(str/join ",\n"))
header (str "type " title " [dispatch=" (d/name dispatcher) "]")]
(str header " {\n" entries "\n}")))
(str (pad header level) "{\n" entries "\n" (pad "}\n" level))))))
(defmethod visit :multi
[_ schema children {:keys [::level] :as options}]
(let [props (m/properties schema)
title (or (some-> (:title props) str/camel str/capital) "<untitled>")
extracted? (get props ::extracted false)]
[_ s children {:keys [::level ::max-level] :as options}]
(let [props (m/properties s)
title (some-> (:title props) str/camel str/capital)]
(if (>= level max-level)
title
(let [dispatcher (or (-> s m/properties :dispatch-description)
(-> s m/properties :dispatch))
(cond
(or (zero? level) extracted?)
(format-multi schema children)
prefix (apply str (take (inc level) (repeat " ")))
:else
(let [schema (mu/update-properties schema assoc ::extracted true)]
(swap! *definitions* conj (format-multi schema children))
title))))
entries (->> children
(map (fn [[_ _ shape]]
(str prefix shape)))
(str/join ",\n"))
(defn- format-merge
[schema children]
header (cond-> "multi"
(some? title) (str " " title)
:always (str " [dispatch=" (d/name dispatcher) "]"))]
(let [props (m/properties schema)
entries (->> children
(map (fn [shape]
(pad shape 1)))
(str/join ",\n"))
title (some-> (:title props) str/camel str/capital)
(str header " {\n" entries "\n" (pad "}" level))))))
header (str "merge type " title)]
(str header " {\n" entries "\n}")))
(defmethod visit :merge
[_ schema children {:keys [::level] :as options}]
(let [props (m/properties schema)
title (some-> (:title props) str/camel str/capital)
extracted? (get props ::extracted false)]
(cond
(or (zero? level) extracted?)
(format-merge schema children)
:else
(let [schema (mu/update-properties schema assoc ::extracted true)]
(swap! *definitions* conj
(format-merge schema children))
title))))
[_ schema children _]
(let [entries (str/join ",\n" children)
props (m/properties schema)
title (or (some-> (:title props) str/camel str/capital)
"<untitled>")]
(str "merge type " title " { \n" entries "\n}\n")))
(defmethod visit ::sm/one-of
[_ _ children _]
@@ -274,37 +219,45 @@
(map d/name)
(str/join "|")) ")")))
(defmethod visit :schema
[_ schema children options]
(let [props (m/properties schema)
title (some-> (:title props) str/camel str/capital)
extracted? (get props ::extracted false)]
(cond
(not title)
(visit ::m/schema schema children options)
extracted?
(let [title (or title "<untitled>")]
(str "type " title ": "
(visit ::m/schema schema children options)))
:else
(let [schema (mu/update-properties schema assoc ::extracted true)
title (or title "<untitled>")]
(swap! *definitions* conj
(str "type " title ": "
(visit ::m/schema schema children (update options ::level inc))))
title))))
(defmethod visit :schema [_ schema children options]
(visit ::m/schema schema children options))
(defmethod visit ::m/schema
[_ schema _ {:keys [::level] :as options}]
(let [schema' (m/deref schema)]
(describe* schema' (assoc options ::base-level level))))
[_ schema _ {:keys [::level ::limit ::max-level] :as options}]
(let [schema' (m/deref schema)
props (merge
(m/properties schema)
(m/properties schema'))
ref (m/-ref schema)
title (:title props)]
(cond
(::inline props)
(do
(if (>= limit max-level)
title
(describe* schema' options)))
(and ref title)
(do
(when (<= limit max-level)
(swap! *definitions* conj (describe* schema' (assoc options ::base-limit limit))))
title)
(>= limit max-level)
(or title
(some-> ref d/name str/camel str/capital)
"<untitled>")
:else
(describe* schema' (assoc options ::base-level level ::base-limit limit)))))
(defn describe* [s options]
(letfn [(walk-fn [schema path children {:keys [::base-level] :or {base-level 0} :as options}]
(let [options (assoc options ::level (+ base-level (count path)))]
(letfn [(walk-fn [schema path children {:keys [::base-level ::base-limit] :or {base-level 0 base-limit 0} :as options}]
(let [options (assoc options
::limit (+ base-limit (count path))
::level (+ base-level (count path)))]
(visit (m/type schema) schema children options)))]
(m/walk s walk-fn options)))
@@ -322,7 +275,8 @@
(mu/update-properties assoc ::root true))
options (into {::m/walk-entry-vals true
::level 0}
::level 0
::max-level 300}
options)]
(binding [*definitions* defs]

View File

@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.schema.generators
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double not-empty])
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double])
#?(:cljs (:require-macros [app.common.schema.generators]))
(:require
[app.common.math :as mth]
@@ -146,5 +146,3 @@
(def any
(tg/one-of [text boolean double int keyword]))
(def not-empty tg/not-empty)

View File

@@ -6,8 +6,6 @@
(ns app.common.schema.openapi
(:require
[app.common.data :as d]
[app.common.schema :as-alias sm]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.core :as m]))
@@ -17,44 +15,16 @@
(declare transform*)
(defmulti visit (fn [name _schema _children _options] name) :default ::default)
(defmethod visit ::default [_ schema _ _]
(let [props (m/type-properties schema)]
(d/without-nils
{:type (get props ::type)
:format (get props ::format)
:title (get props :title)
:description (get props :description)})))
(defmethod visit ::default [_ _ _ _] {})
(defmethod visit :> [_ _ [value] _] {:type "number" :exclusiveMinimum value})
(defmethod visit :>= [_ _ [value] _] {:type "number" :minimum value})
(defmethod visit :< [_ _ [value] _] {:type "number" :exclusiveMaximum value})
(defmethod visit :<= [_ _ [value] _] {:type "number" :maximum value})
(defmethod visit := [_ schema children _]
(let [props (m/properties schema)
type (get props :type :string)]
(d/without-nils
{:type (or (get props ::type)
(d/name type))
:enum (if (= :string type)
(mapv d/name children)
(vec children))})))
(defmethod visit := [_ _ [value] _] {:const value})
(defmethod visit :not= [_ _ _ _] {})
(defmethod visit :fn [_ _ _ _]
nil)
(defmethod visit ::sm/contains-any [_ _ _ _]
nil)
(defmethod visit :not [_ _ children _] {:not (last children)})
(defmethod visit :and [_ _ children _]
{:allOf (keep not-empty children)})
(defmethod visit :and [_ _ children _] {:allOf children})
(defmethod visit :or [_ _ children _] {:anyOf children})
(defmethod visit :orn [_ _ children _] {:anyOf (map last children)})
@@ -101,28 +71,14 @@
:minProperties
:maxProperties))
(defmethod visit :any [_ _ _ _]
{:description "Any Value"})
(defmethod visit ::sm/set [_ schema children _]
(minmax-properties
{:type "array", :items (first children), :uniqueItems true}
schema
:minItems
:maxItems))
(defmethod visit ::sm/vec [_ schema children _]
(minmax-properties
{:type "array", :items (first children)}
schema
:minItems
:maxItems))
(defmethod visit :vector [_ schema children options]
(visit ::sm/vec schema children options))
(defmethod visit :set [_ schema children options]
(visit ::sm/set schema children options))
(defmethod visit :vector [_ schema children _]
(let [child (-> schema m/children first)
props (m/properties (m/deref child))]
(minmax-properties
{:type "array", :items (first children) :title (:title props)}
schema
:minItems
:maxItems)))
(defmethod visit :sequential [_ schema children _]
(minmax-properties
@@ -131,64 +87,36 @@
:minItems
:maxItems))
(defmethod visit :enum [_ _ children options]
(merge (some-> (m/-infer children) (transform* options)) {:enum children}))
(defmethod visit :maybe [_ _ children _]
(let [children (first children)]
(assoc children :nullable true)))
(defmethod visit :tuple [_ _ children _]
{:type "array", :items children, :additionalItems false})
(defmethod visit :set [_ schema children _]
(minmax-properties
{:type "array", :items (first children), :uniqueItems true}
schema
:minItems
:maxItems))
(defmethod visit :enum [_ _ children options] (merge (some-> (m/-infer children) (transform* options)) {:enum children}))
(defmethod visit :maybe [_ _ children _] {:oneOf (conj children {:type "null"})})
(defmethod visit :tuple [_ _ children _] {:type "array", :items children, :additionalItems false})
(defmethod visit :re [_ schema _ options]
{:type "string", :pattern (str (first (m/children schema options)))})
(defmethod visit :nil [_ _ _ _] {:type "null"})
(defmethod visit :string [_ schema _ _]
(merge {:type "string"} (-> schema m/properties (select-keys [:min :max]) (set/rename-keys {:min :minLength, :max :maxLength}))))
(defmethod visit ::sm/one-of [_ _ children _]
(let [options (->> (first children)
(mapv d/name))]
{:type "string"
:enum options}))
(defmethod visit :int [_ schema _ _]
(minmax-properties
{:type "integer"}
schema
:minimum
:maximum))
(merge {:type "integer"} (-> schema m/properties (select-keys [:min :max]) (set/rename-keys {:min :minimum, :max :maximum}))))
(defmethod visit :double [_ schema _ _]
(minmax-properties
{:type "number"
:format "double"}
schema
:minimum
:maximum))
(defmethod visit ::sm/int
[_ schema children options]
(visit :int schema children options))
(defmethod visit ::sm/double
[_ schema children options]
(visit :double schema children options))
(merge {:type "number"}
(-> schema m/properties (select-keys [:min :max]) (set/rename-keys {:min :minimum, :max :maximum}))))
(defmethod visit :boolean [_ _ _ _] {:type "boolean"})
(defmethod visit ::sm/boolean [_ _ _ _] {:type "boolean"})
(defmethod visit :keyword [_ _ _ _] {:type "string"})
(defmethod visit :qualified-keyword [_ _ _ _] {:type "string"})
(defmethod visit :symbol [_ _ _ _] {:type "string"})
(defmethod visit :qualified-symbol [_ _ _ _] {:type "string"})
(defmethod visit :uuid [_ _ _ _] {:type "string" :format "uuid"})
(defmethod visit ::sm/uuid [_ _ _ _] {:type "string" :format "uuid"})
(defmethod visit :schema [_ schema children options]
(visit ::m/schema schema children options))
@@ -196,41 +124,11 @@
(defmethod visit ::m/schema [_ schema _ options]
(let [result (transform* (m/deref schema) options)
defpath (::definitions-path options "#/definitions/")]
(if (::embed options)
result
(if-let [ref (m/-ref schema)]
(let [nname (namespace ref)
tname (name ref)
tname (str/capital (str/camel tname))
nname (cond
(or (= nname "app.common.schema")
(= nname "app.common.time")
(= nname "app.common.features"))
""
(= nname "datoteka.fs")
"Filesystem"
(str/starts-with? nname "app.common.geom")
(-> (str/replace nname #"app\.common\.geom\.\w+" "geom")
(str/camel)
(str/capital))
(str/starts-with? nname "app.")
(-> (subs nname 4)
(str/camel)
(str/capital))
:else
(str/capital (str/camel nname)))
rkey (str nname tname)]
(some-> *definitions* (swap! assoc rkey result))
{"$ref" (str/concat defpath rkey)})
result))))
(if-let [ref (m/-ref schema)]
(let [rkey (str/concat (str/camel (namespace ref)) "$" (name ref))]
(some-> *definitions* (swap! assoc rkey result))
{"$ref" (str/concat defpath rkey)})
result)))
(defmethod visit :merge [_ schema _ options] (transform* (m/deref schema) options))
(defmethod visit :union [_ schema _ options] (transform* (m/deref schema) options))

View File

@@ -12,7 +12,6 @@
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.logic.libraries :as cll]
[app.common.path-names :as cpn]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
@@ -37,7 +36,7 @@
updated-root (first updated-shapes) ; Can't use new-root because it has a new id
[path name] (cpn/split-group-name (:name updated-root))]
[path name] (cfh/parse-path-name (:name updated-root))]
(thi/set-id! label (:component-id updated-root))
(ctf/update-file-data
@@ -73,10 +72,6 @@
[file id]
(ctkl/get-component (:data file) id))
(defn get-components
[file]
(ctkl/components (:data file)))
(defn- set-children-labels!
[file shape-label children-labels]
(doseq [[label id]

View File

@@ -307,7 +307,6 @@
file' (thf/apply-changes file changes)]
(when new-shape-label
(thi/rm-id! (:id new-shape))
(thi/set-id! new-shape-label (:id new-shape)))
(if propagate-fn
(propagate-fn file')

View File

@@ -108,8 +108,7 @@
page (if (some? page-label)
(:id (get-page file page-label))
(current-page-id file))
libraries (or libraries
{(:id file) file})]
libraries (or libraries {})]
(ctf/dump-tree file page libraries params)))

View File

@@ -21,10 +21,6 @@
[label id]
(swap! idmap assoc label id))
(defn rm-id!
[id]
(swap! idmap #(into {} (remove (comp #{id} val) %))))
(defn new-id!
[label]
(let [id (uuid/next)]

View File

@@ -28,10 +28,12 @@
(ctf/update-file-data file #(update % :tokens-lib f)))
(defn get-token
[file set-id token-id]
[file set-name token-id]
(let [tokens-lib (:tokens-lib (:data file))]
(when tokens-lib
(ctob/get-token tokens-lib set-id token-id))))
(-> tokens-lib
(ctob/get-set set-name)
(ctob/get-token token-id)))))
(defn token-data-eq?
"Compare token data without comparing unstable fields."
@@ -75,25 +77,22 @@
[file shape-label token-name token-attrs shape-attrs resolved-value]
(let [page (thf/current-page file)
shape (ths/get-shape file shape-label)
shape' (when shape
(as-> shape $
(cto/apply-token-to-shape {:shape $
:token {:name token-name}
:attributes token-attrs})
(reduce (fn [shape attr]
(case attr
:stroke-width (set-stroke-width shape resolved-value)
:stroke-color (set-stroke-color shape resolved-value)
:fill (set-fill-color shape resolved-value)
(ctn/set-shape-attr shape attr resolved-value {:ignore-touched true})))
$
shape-attrs)))]
shape' (as-> shape $
(cto/apply-token-to-shape {:shape $
:token {:name token-name}
:attributes token-attrs})
(reduce (fn [shape attr]
(case attr
:stroke-width (set-stroke-width shape resolved-value)
:stroke-color (set-stroke-color shape resolved-value)
:fill (set-fill-color shape resolved-value)
(ctn/set-shape-attr shape attr resolved-value {:ignore-touched true})))
$
shape-attrs))]
(if shape'
(ctf/update-file-data
file
(fn [file-data]
(ctpl/update-page file-data
(:id page)
#(ctst/set-shape % shape'))))
file)))
(ctf/update-file-data
file
(fn [file-data]
(ctpl/update-page file-data
(:id page)
#(ctst/set-shape % shape'))))))

View File

@@ -56,22 +56,6 @@
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property 1" :value "Value2"}]}))))
(defn add-variant-with-copy
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label component-copy-label]
(let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
variant-id (thi/id variant-label)]
(-> file
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
(thc/instantiate-component component-copy-label child1-label :parent-label root1-label)
(thc/instantiate-component component-copy-label child2-label :parent-label root2-label)
(thc/make-component component1-label root1-label)
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property 1" :value "Value1"}]})
(thc/make-component component2-label root2-label)
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property 1" :value "Value2"}]}))))
(defn add-variant-with-text
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label text1 text2
& {:keys [text1-params text2-params]}]

View File

@@ -119,9 +119,9 @@
[o]
(instance? Duration o)))
#?(:clj
(defn duration
[ms-or-obj]
(defn duration
[ms-or-obj]
#?(:clj
(cond
(string? ms-or-obj)
(Duration/parse (str "PT" ms-or-obj))
@@ -130,10 +130,14 @@
ms-or-obj
(integer? ms-or-obj)
(Duration/ofMillis ms-or-obj)
:else
(obj->duration ms-or-obj))))
(obj->duration ms-or-obj))
:cljs
(clj->js ms-or-obj)))
#?(:clj
(defn parse-duration
@@ -258,9 +262,6 @@
(defn inst
[s]
(cond
(nil? s)
s
(inst? s)
s
@@ -291,7 +292,7 @@
(defn plus
[d ta]
(let [ta #?(:clj (duration ta) :cljs ta)]
(let [ta (duration ta)]
(cond
#?@(:clj [(duration? d) (.plus ^Duration d ^TemporalAmount ta)])
@@ -306,7 +307,7 @@
(defn minus
[d ta]
(let [ta #?(:clj (duration ta) :cljs ta)]
(let [^TemporalAmount ta (duration ta)]
(cond
#?@(:clj [(duration? d) (.minus ^Duration d ^TemporalAmount ta)])
@@ -428,8 +429,3 @@
:encode/json format-duration
::oapi/type "string"
::oapi/format "duration"}})))
#?(:cljs
(extend-protocol cljs.core/IEncodeJS
js/Date
(-clj->js [x] x)))

View File

@@ -60,17 +60,16 @@
{:type ::hex-color
:pred hex-color-string?
:type-properties
{:title "HexColor"
{:title "hex-color"
:description "HEX Color String"
:error/message "expected a valid HEX color"
:error/code "errors.invalid-hex-color"
:gen/gen hex-color-generator
::oapi/type "string"
::oapi/format "rgb"}}))
::oapi/type "integer"
::oapi/format "int64"}}))
(def schema:plain-color
[:map {:title "PlainColorAttrs"}
[:color schema:hex-color]])
[:map [:color schema:hex-color]])
(def schema:image
[:map {:title "ImageColor" :closed true}
@@ -86,8 +85,7 @@
(sm/keys schema:image))
(def schema:image-color
[:map {:title "ImageColorAttrs"}
[:image schema:image]])
[:map [:image schema:image]])
(def gradient-types
#{:linear :radial})
@@ -112,11 +110,10 @@
(sm/keys schema:gradient))
(def schema:gradient-color
[:map {:title "GradientColorAttrs"}
[:gradient schema:gradient]])
[:map [:gradient schema:gradient]])
(def schema:color-attrs
[:map {:title "GenericColorAttrs" :closed true}
[:map {:title "ColorAttrs" :closed true}
[:opacity {:optional true} [::sm/number {:min 0 :max 1}]]
[:ref-id {:optional true} ::sm/uuid]
[:ref-file {:optional true} ::sm/uuid]])
@@ -135,13 +132,13 @@
(into required-color-attrs (sm/keys schema:color-attrs)))
(def schema:library-color-attrs
[:map {:title "LibraryColorAttrs" :closed true}
[:map {:title "ColorAttrs" :closed true}
[:id ::sm/uuid]
[:name ::sm/text]
[:path {:optional true} :string]
[:opacity {:optional true} [::sm/number {:min 0 :max 1}]]
[:modified-at {:optional true} ::ct/inst]
[:plugin-data {:optional true} ctpg/schema:plugin-data]])
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:library-color
"Used for in-transit representation of a color (per example when user

View File

@@ -19,17 +19,19 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def schema:component
[:merge
[:map
[:id ::sm/uuid]
[:name :string]
[:path {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::ct/inst]
[:objects {:gen/max 10 :optional true} ctp/schema:objects]
[:main-instance-id ::sm/uuid]
[:main-instance-page ::sm/uuid]
[:plugin-data {:optional true} ctpg/schema:plugin-data]]
ctv/schema:variant-component])
(sm/register!
^{::sm/type ::component}
[:merge
[:map
[:id ::sm/uuid]
[:name :string]
[:path {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::ct/inst]
[:objects {:gen/max 10 :optional true} ctp/schema:objects]
[:main-instance-id ::sm/uuid]
[:main-instance-page ::sm/uuid]
[:plugin-data {:optional true} ctpg/schema:plugin-data]]
ctv/schema:variant-component]))
(def check-component
(sm/check-fn schema:component))
@@ -97,8 +99,6 @@
:exports :exports-group
:grids :grids-group
:show-content :show-content
:layout :layout-container
:layout-align-content :layout-align-content
@@ -145,12 +145,9 @@
(defn component-attr?
"Check if some attribute is one that is involved in component syncrhonization.
Note that design tokens also are involved, although they go by an alternate
route and thus they are not part of :sync-attrs.
Also when detaching a nested copy it also needs to trigger a synchronization,
even though :shape-ref is not a synced attribute per se"
route and thus they are not part of :sync-attrs."
[attr]
(or (get sync-attrs attr)
(= :shape-ref attr)
(= :applied-tokens attr)))
(defn instance-root?
@@ -220,16 +217,19 @@
(and (= shape-id (:main-instance-id component))
(= page-id (:main-instance-page component))))
(defn is-variant?
"Check if this shape or component is a variant component"
[item]
(some? (:variant-id item)))
(defn is-variant-container?
"Check if this shape is a variant container"
[shape]
(:is-variant-container shape))
(defn set-touched-group
[touched group]
(when group
@@ -310,22 +310,6 @@
:shape-ref
:touched))
(defn unhead-shape
"Make the shape not be a component head, but keep its :shape-ref and :touched if it was a nested copy"
[shape]
(dissoc shape
:component-root
:component-file
:component-id
:main-instance))
(defn rehead-shape
"Make the shape a component head, by adding component info"
[shape component-file component-id]
(assoc shape
:component-file component-file
:component-id component-id))
(defn- extract-ids [shape]
(if (map? shape)
(let [current-id (:id shape)

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