mirror of
https://github.com/penpot/penpot.git
synced 2026-01-02 19:38:48 -05:00
Compare commits
200 Commits
2.10.0
...
juan-compo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c565915007 | ||
|
|
2de6b6460e | ||
|
|
f905dfc699 | ||
|
|
c79f110177 | ||
|
|
f644b3744a | ||
|
|
0722af3a2f | ||
|
|
b4c6bbb191 | ||
|
|
cad9d03ca1 | ||
|
|
1d6389a3eb | ||
|
|
913a8d3148 | ||
|
|
34e3453f24 | ||
|
|
6f362f9211 | ||
|
|
979b4276ca | ||
|
|
a32fe40528 | ||
|
|
b602df549e | ||
|
|
7f1ab08ec8 | ||
|
|
1263ea11fa | ||
|
|
ce26c52b30 | ||
|
|
5c8b3ac3d6 | ||
|
|
bd4d576172 | ||
|
|
e10169b3db | ||
|
|
f119a9548d | ||
|
|
c097aef152 | ||
|
|
000fa51c73 | ||
|
|
d815494ffa | ||
|
|
a25ba6b482 | ||
|
|
e8434c3370 | ||
|
|
7cf4ec2792 | ||
|
|
365ce25996 | ||
|
|
01ef55e4f4 | ||
|
|
3b81c1d750 | ||
|
|
40b34da788 | ||
|
|
732c79b7b5 | ||
|
|
d0f34f06a9 | ||
|
|
23d5bdd20b | ||
|
|
9f2dc06c95 | ||
|
|
62563d28d0 | ||
|
|
21e2ee9904 | ||
|
|
e6c418eb9c | ||
|
|
de5ff227d2 | ||
|
|
0f67730198 | ||
|
|
3da02e2b6b | ||
|
|
ab80021fb1 | ||
|
|
f31e9b8ac9 | ||
|
|
7d16515eb7 | ||
|
|
cd9ba482e3 | ||
|
|
dff1ca23d3 | ||
|
|
c363d4d937 | ||
|
|
de25a24a6d | ||
|
|
accc9a173f | ||
|
|
2d364dde5c | ||
|
|
c892a9f254 | ||
|
|
aaae35fb51 | ||
|
|
960b76f760 | ||
|
|
d921e7eaa3 | ||
|
|
49f06b25fa | ||
|
|
5ffb7ae2ec | ||
|
|
27945ace65 | ||
|
|
e39bf0b439 | ||
|
|
deee7f7334 | ||
|
|
20d61cbce2 | ||
|
|
9ad8d3fd08 | ||
|
|
4c35571336 | ||
|
|
37679b7ec6 | ||
|
|
194eded930 | ||
|
|
4e607d8da2 | ||
|
|
f5fd978a07 | ||
|
|
b28be62845 | ||
|
|
d76a5c615c | ||
|
|
03e05da41e | ||
|
|
5f886e141a | ||
|
|
021b8f81ca | ||
|
|
f32112544e | ||
|
|
27e311277a | ||
|
|
b9030fcc73 | ||
|
|
e1519f0ee4 | ||
|
|
7fefe6dbc8 | ||
|
|
fdf70ae9c1 | ||
|
|
528315b75c | ||
|
|
42d03a0325 | ||
|
|
0346c48b03 | ||
|
|
1d54fe2e24 | ||
|
|
255f5af2e3 | ||
|
|
eea65b12dd | ||
|
|
473066cf5c | ||
|
|
5e84bda404 | ||
|
|
c1058c7fdb | ||
|
|
9d907071aa | ||
|
|
c32b94abcf | ||
|
|
9d8ad0ea6e | ||
|
|
2b1e107a44 | ||
|
|
2196318cfc | ||
|
|
b3d1701698 | ||
|
|
042bd03beb | ||
|
|
a39a127f03 | ||
|
|
bd665f70bf | ||
|
|
9b90236b72 | ||
|
|
bf6cdf729d | ||
|
|
361bdb4a04 | ||
|
|
3827aa6bd4 | ||
|
|
adf7b0df50 | ||
|
|
97b4491a27 | ||
|
|
015bd9e453 | ||
|
|
49d5987b15 | ||
|
|
a5e4de97e3 | ||
|
|
378be9473d | ||
|
|
412cf61d7d | ||
|
|
754a1b6fa2 | ||
|
|
ec94d08f4a | ||
|
|
b6b2d28464 | ||
|
|
32770c685a | ||
|
|
441dc33e38 | ||
|
|
3f87e768a7 | ||
|
|
09e9340ba6 | ||
|
|
d5ff7b4144 | ||
|
|
ef0aee0a09 | ||
|
|
1e9682376e | ||
|
|
c9b61745a0 | ||
|
|
974b76d7bd | ||
|
|
f505fcfa0d | ||
|
|
e4d610d503 | ||
|
|
5c23a678cc | ||
|
|
fb3923924b | ||
|
|
c882e8347a | ||
|
|
c1fd1a3b42 | ||
|
|
b1fe32baea | ||
|
|
fb7a7d02da | ||
|
|
20dfc2a216 | ||
|
|
d7d2d36e0a | ||
|
|
07904bcc5d | ||
|
|
9686075104 | ||
|
|
436e0e847d | ||
|
|
d50b070a64 | ||
|
|
80cb48fd6a | ||
|
|
49c6efbc22 | ||
|
|
4fb1c7a630 | ||
|
|
fd37fdde93 | ||
|
|
66b1d5b7bd | ||
|
|
2bf7a9dd5f | ||
|
|
7bacd8fbca | ||
|
|
b883882a32 | ||
|
|
e5e11b6383 | ||
|
|
01e963ae35 | ||
|
|
90a80c4b63 | ||
|
|
b56f237780 | ||
|
|
4970ae3eb4 | ||
|
|
2e21f084fc | ||
|
|
55513b9ae5 | ||
|
|
07d0062645 | ||
|
|
f4b38af649 | ||
|
|
6e7bcd1243 | ||
|
|
ed3fc5b8b2 | ||
|
|
f5f9157786 | ||
|
|
6cb0cb7f98 | ||
|
|
0210b310b7 | ||
|
|
ce1e44eda4 | ||
|
|
48825e1e59 | ||
|
|
61cfe2d142 | ||
|
|
2d68f4dfd3 | ||
|
|
1e23937aa5 | ||
|
|
aecaf51953 | ||
|
|
da05d6b67d | ||
|
|
99a100ad63 | ||
|
|
bd3bcb4b18 | ||
|
|
534c7864fc | ||
|
|
4bd2eba573 | ||
|
|
563f608255 | ||
|
|
382b5e7e3a | ||
|
|
a503f8ae93 | ||
|
|
e1935fb3fb | ||
|
|
b3763dec3f | ||
|
|
41751d60d2 | ||
|
|
8bd0edca46 | ||
|
|
e2f22b86c7 | ||
|
|
108b5ab225 | ||
|
|
43a238a896 | ||
|
|
daa408e291 | ||
|
|
8aed47dad3 | ||
|
|
0e23c9f6ab | ||
|
|
8fff9afee6 | ||
|
|
ff55318c04 | ||
|
|
41b7957eff | ||
|
|
7e52aadb98 | ||
|
|
69f41c300f | ||
|
|
18c5e0b9a8 | ||
|
|
5230d54551 | ||
|
|
a79be05261 | ||
|
|
9c77296858 | ||
|
|
34da6b64df | ||
|
|
c4481be39f | ||
|
|
f60b6a4869 | ||
|
|
3e02dc550f | ||
|
|
1cf0de395c | ||
|
|
d40b68c004 | ||
|
|
50b9e8c6e6 | ||
|
|
d25f9cd4bd | ||
|
|
bedb98ad9f | ||
|
|
5f37601122 | ||
|
|
796aaed11e | ||
|
|
1da69cfa38 |
@@ -226,14 +226,29 @@ jobs:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
|
||||
|
||||
# Build frontend
|
||||
- run:
|
||||
name: "integration tests"
|
||||
name: "frontend build"
|
||||
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
|
||||
|
||||
|
||||
16
.github/workflows/build-bundle.yml
vendored
16
.github/workflows/build-bundle.yml
vendored
@@ -1,11 +1,11 @@
|
||||
name: Build and Upload Penpot Bundle
|
||||
name: Bundles Builder
|
||||
|
||||
on:
|
||||
# Create bundle from manual action
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
description: 'Name of the branch or ref'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
@@ -22,7 +22,7 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
description: 'Name of the branch or ref'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
@@ -56,10 +56,9 @@ jobs:
|
||||
- name: Extract some useful variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run manage.sh build-bundle from host
|
||||
- name: Build bundle
|
||||
env:
|
||||
BUILD_WASM: ${{ inputs.build_wasm }}
|
||||
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
|
||||
@@ -76,13 +75,6 @@ jobs:
|
||||
zip -r zips/penpot.zip penpot
|
||||
|
||||
- name: Upload Penpot bundle to S3
|
||||
if: github.ref_type == 'branch'
|
||||
run: |
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}-latest.zip
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.commit_hash }}.zip
|
||||
|
||||
- name: Upload Penpot bundle to S3
|
||||
if: github.ref_type == 'tag'
|
||||
run: |
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip
|
||||
|
||||
|
||||
11
.github/workflows/build-develop.yml
vendored
11
.github/workflows/build-develop.yml
vendored
@@ -1,14 +1,21 @@
|
||||
name: DEVELOP - Build and Upload Penpot Bundle
|
||||
name: _DEVELOP
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '16 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-develop-bundle:
|
||||
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
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "develop"
|
||||
|
||||
36
.github/workflows/build-docker-devenv.yml
vendored
Normal file
36
.github/workflows/build-docker-devenv.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
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
|
||||
101
.github/workflows/build-docker.yml
vendored
Normal file
101
.github/workflows/build-docker.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
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
|
||||
11
.github/workflows/build-staging.yml
vendored
11
.github/workflows/build-staging.yml
vendored
@@ -1,14 +1,21 @@
|
||||
name: STAGING - Build and Upload Penpot Bundle
|
||||
name: _STAGING
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '36 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-staging-bundle:
|
||||
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
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "staging"
|
||||
|
||||
19
.github/workflows/build-tag.yml
vendored
19
.github/workflows/build-tag.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: TAG - Build and Upload Penpot Bundle
|
||||
name: _TAG
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,10 +6,25 @@ on:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build-tag-bundle:
|
||||
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 }}
|
||||
|
||||
2
.github/workflows/commit-checker.yml
vendored
2
.github/workflows/commit-checker.yml
vendored
@@ -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|rewind):)\s["A-Z].*[^.]$'
|
||||
flags: 'gm'
|
||||
error: 'Commit should match CONTRIBUTING.md guideline'
|
||||
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
|
||||
|
||||
95
.github/workflows/release.yml
vendored
Normal file
95
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
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 }}
|
||||
52
CHANGES.md
52
CHANGES.md
@@ -1,6 +1,51 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.10.0 (Unreleased)
|
||||
## 2.11.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
|
||||
|
||||
@@ -35,6 +80,7 @@
|
||||
- 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
|
||||
|
||||
@@ -48,7 +94,7 @@
|
||||
- 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)
|
||||
- Misleading affordance in saved versions [Taiga #11887](https://tree.taiga.io/project/penpot/issue/11887)
|
||||
- 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)
|
||||
@@ -143,7 +189,7 @@
|
||||
|
||||
**Penpot Library**
|
||||
|
||||
The initial prototype is completly reworked for provide a more consistent API
|
||||
The initial prototype is completly reworked to 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)
|
||||
|
||||
|
||||
@@ -77,17 +77,14 @@ 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.
|
||||
|
||||
### What’s 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 Penpot’s standardized [design tokens](https://penpot.dev/collaboration/design-tokens) format, you can easily reuse and sync tokens across different platforms, workflows, and disciplines.
|
||||
### 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.
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<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%;">
|
||||
<img src="https://github.com/user-attachments/assets/cce75ad6-f783-473f-8803-da9eb8255fef">
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
org.clojure/clojure {:mvn/version "1.12.2"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-3"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
|
||||
|
||||
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.7.0.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.8.1.RELEASE"}
|
||||
;; Minimal dependencies required by lettuce, we need to include them
|
||||
;; explicitly because clojure dependency management does not support
|
||||
;; yet the BOM format.
|
||||
@@ -28,29 +28,30 @@
|
||||
com.google.guava/guava {:mvn/version "33.4.8-jre"}
|
||||
|
||||
funcool/yetti
|
||||
{:git/tag "v11.4"
|
||||
:git/sha "ce50d42"
|
||||
{:git/tag "v11.6"
|
||||
:git/sha "94dc017"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
com.github.seancorfield/next.jdbc
|
||||
{:mvn/version "1.3.1002"}
|
||||
{:mvn/version "1.3.1070"}
|
||||
|
||||
metosin/reitit-core {:mvn/version "0.9.1"}
|
||||
nrepl/nrepl {:mvn/version "1.3.1"}
|
||||
nrepl/nrepl {:mvn/version "1.4.0"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.7"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
|
||||
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
|
||||
|
||||
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.0"}
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.2"}
|
||||
|
||||
org.jsoup/jsoup {:mvn/version "1.20.1"}
|
||||
org.jsoup/jsoup {:mvn/version "1.21.2"}
|
||||
org.im4java/im4java
|
||||
{:git/tag "1.4.0-penpot-2"
|
||||
:git/sha "e2b3e16"
|
||||
@@ -60,12 +61,12 @@
|
||||
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||
|
||||
dawran6/emoji {:mvn/version "0.1.5"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.3"}
|
||||
dawran6/emoji {:mvn/version "0.2.0"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.4"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.33.8"}}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.33.10"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
@@ -80,12 +81,14 @@
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
|
||||
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
{:main-opts ["-m" "kaocha.runner"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
|
||||
"--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
|
||||
|
||||
:outdated
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
[app.util.blob :as blob]
|
||||
[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]
|
||||
|
||||
@@ -1 +1 @@
|
||||
Invitation to join {{team}}
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”
|
||||
@@ -31,8 +31,7 @@ export PENPOT_FLAGS="\
|
||||
disable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptions \
|
||||
disable-subscriptions-old";
|
||||
enable-subscriptions";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
@@ -78,10 +77,14 @@ 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";
|
||||
|
||||
@@ -24,8 +24,7 @@ export PENPOT_FLAGS="\
|
||||
disable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptions \
|
||||
disable-subscriptions-old";
|
||||
enable-subscriptions";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io]
|
||||
[promesa.exec :as px]))
|
||||
[datoteka.io :as io]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
@@ -476,7 +475,7 @@
|
||||
(vary-meta dissoc ::fmg/migrated))))
|
||||
|
||||
(defn encode-file
|
||||
[{:keys [::wrk/executor] :as cfg} {:keys [id features] :as file}]
|
||||
[cfg {:keys [id features] :as file}]
|
||||
(let [file (if (and (contains? features "fdata/objects-map")
|
||||
(:data file))
|
||||
(fdata/enable-objects-map file)
|
||||
@@ -493,7 +492,7 @@
|
||||
|
||||
(-> file
|
||||
(d/update-when :features into-array)
|
||||
(d/update-when :data (fn [data] (px/invoke! executor #(blob/encode data)))))))
|
||||
(d/update-when :data blob/encode))))
|
||||
|
||||
(defn- file->params
|
||||
[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-worker-threads {:optional true} ::sm/int]
|
||||
[:http-server-max-worker-threads {:optional true} ::sm/int]
|
||||
|
||||
[:telemetry-uri {:optional true} :string]
|
||||
[:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
|
||||
@@ -214,20 +214,21 @@
|
||||
[:media-uri {:optional true} :string]
|
||||
[:assets-path {:optional true} :string]
|
||||
|
||||
;; Legacy, will be removed in 2.5
|
||||
[:netty-io-threads {:optional true} ::sm/int]
|
||||
[:executor-threads {:optional true} ::sm/int]
|
||||
|
||||
;; DEPRECATED
|
||||
[: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-io-threads {:optional true} ::sm/int]]))
|
||||
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]]))
|
||||
|
||||
(defn- parse-flags
|
||||
[config]
|
||||
|
||||
@@ -12,15 +12,14 @@
|
||||
[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]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.worker :as wrk]
|
||||
[promesa.exec :as px]))
|
||||
[app.util.objects-map :as omap.legacy]
|
||||
[app.util.pointer-map :as pmap]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OFFLOAD
|
||||
@@ -38,10 +37,7 @@
|
||||
[file & _opts]
|
||||
(let [update-page
|
||||
(fn [page]
|
||||
(if (and (pmap/pointer-map? page)
|
||||
(not (pmap/loaded? page)))
|
||||
page
|
||||
(update page :objects omap/wrap)))
|
||||
(update page :objects omap/wrap))
|
||||
|
||||
update-data
|
||||
(fn [fdata]
|
||||
@@ -51,6 +47,20 @@
|
||||
(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"
|
||||
@@ -60,7 +70,8 @@
|
||||
(fn [page]
|
||||
(update page :objects
|
||||
(fn [objects]
|
||||
(if (omap/objects-map? objects)
|
||||
(if (or (omap/objects-map? objects)
|
||||
(omap.legacy/objects-map? objects))
|
||||
(update-fn objects)
|
||||
objects)))))
|
||||
fdata))
|
||||
@@ -84,10 +95,10 @@
|
||||
(assoc file :data data)))
|
||||
|
||||
(defn decode-file-data
|
||||
[{:keys [::wrk/executor]} {:keys [data] :as file}]
|
||||
[_system {:keys [data] :as file}]
|
||||
(cond-> file
|
||||
(bytes? data)
|
||||
(assoc :data (px/invoke! executor #(blob/decode data)))))
|
||||
(assoc :data (blob/decode data))))
|
||||
|
||||
(defn load-pointer
|
||||
"A database loader pointer helper"
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
[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]
|
||||
@@ -55,6 +53,8 @@
|
||||
[: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,31 +65,41 @@
|
||||
(assert (sm/check schema:server-params params)))
|
||||
|
||||
(defmethod ig/init-key ::server
|
||||
[_ {:keys [::handler ::router ::host ::port ::wrk/executor] :as cfg}]
|
||||
[_ {:keys [::handler ::router ::host ::port ::mtx/metrics] :as cfg}]
|
||||
(l/info :hint "starting http server" :port port :host host)
|
||||
(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}
|
||||
(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)))
|
||||
|
||||
handler (cond
|
||||
(some? router)
|
||||
(router-handler router)
|
||||
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}
|
||||
|
||||
(some? handler)
|
||||
handler
|
||||
handler
|
||||
(cond
|
||||
(some? router)
|
||||
(router-handler router)
|
||||
|
||||
:else
|
||||
(throw (UnsupportedOperationException. "handler or router are required")))
|
||||
(some? handler)
|
||||
handler
|
||||
|
||||
options (d/without-nils options)
|
||||
server (yt/server handler options)]
|
||||
:else
|
||||
(throw (UnsupportedOperationException. "handler or router are required")))
|
||||
|
||||
server
|
||||
(yt/server handler (d/without-nils options))]
|
||||
|
||||
(assoc cfg ::server (yt/start! server))))
|
||||
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
[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]))
|
||||
|
||||
@@ -40,8 +38,8 @@
|
||||
[_ cfg]
|
||||
(letfn [(handler [request]
|
||||
(let [data (-> request yreq/body slurp)]
|
||||
(px/run! :vthread (partial handle-request cfg data)))
|
||||
{::yres/status 200})]
|
||||
(handle-request cfg data)
|
||||
{::yres/status 200}))]
|
||||
["/sns" {:handler handler
|
||||
:allowed-methods #{:post}}]))
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
::yres/body (yres/stream-body
|
||||
(fn [_ output]
|
||||
(let [channel (sp/chan :buf buf :xf (keep encode))
|
||||
listener (events/start-listener
|
||||
listener (events/spawn-listener
|
||||
channel
|
||||
(partial write! output)
|
||||
(partial pu/close! output))]
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
[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]
|
||||
@@ -148,23 +149,11 @@
|
||||
::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}})
|
||||
:http-server-dispatch-timing
|
||||
{::mdef/name "penpot_http_server_dispatch_timing"
|
||||
::mdef/help "Histogram of dispatch handler"
|
||||
::mdef/labels []
|
||||
::mdef/type :histogram}})
|
||||
|
||||
(def system-config
|
||||
{::db/pool
|
||||
@@ -176,14 +165,12 @@
|
||||
::db/max-size (cf/get :database-max-pool-size 60)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||
|
||||
;; Default thread pool for IO operations
|
||||
::wrk/executor
|
||||
{}
|
||||
;; Default netty IO pool (shared between several services)
|
||||
::wrk/netty-io-executor
|
||||
{:threads (cf/get :netty-io-threads)}
|
||||
|
||||
::wrk/monitor
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::wrk/name "default"}
|
||||
::wrk/netty-executor
|
||||
{:threads (cf/get :executor-threads)}
|
||||
|
||||
:app.migrations/migrations
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
@@ -197,14 +184,19 @@
|
||||
::rds/redis
|
||||
{::rds/uri (cf/get :redis-uri)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
::wrk/netty-executor
|
||||
(ig/ref ::wrk/netty-executor)
|
||||
|
||||
::wrk/netty-io-executor
|
||||
(ig/ref ::wrk/netty-io-executor)}
|
||||
|
||||
::mbus/msgbus
|
||||
{::wrk/executor (ig/ref ::wrk/executor)
|
||||
{::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::rds/redis (ig/ref ::rds/redis)}
|
||||
|
||||
:app.storage.tmp/cleaner
|
||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||
{::wrk/executor (ig/ref ::wrk/netty-executor)}
|
||||
|
||||
::sto.gc-deleted/handler
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
@@ -232,9 +224,10 @@
|
||||
::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)
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||
|
||||
::ldap/provider
|
||||
{:host (cf/get :ldap-host)
|
||||
@@ -312,17 +305,17 @@
|
||||
|
||||
::rpc/climit
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::climit/config (cf/get :rpc-climit-config)
|
||||
::climit/enabled (contains? cf/flags :rpc-climit)}
|
||||
|
||||
:app.rpc/rlimit
|
||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||
{::wrk/executor (ig/ref ::wrk/netty-executor)}
|
||||
|
||||
:app.rpc/methods
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::ldap/provider (ig/ref ::ldap/provider)
|
||||
::sto/storage (ig/ref ::sto/storage)
|
||||
@@ -476,13 +469,14 @@
|
||||
(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/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
::wrk/netty-io-executor
|
||||
(ig/ref ::wrk/netty-io-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)
|
||||
|
||||
@@ -216,8 +216,7 @@
|
||||
(rds/add-listener sconn (create-listener rcv-ch))
|
||||
|
||||
(px/thread
|
||||
{:name "penpot/msgbus/io-loop"
|
||||
:virtual true}
|
||||
{:name "penpot/msgbus"}
|
||||
(try
|
||||
(loop []
|
||||
(let [timeout-ch (sp/timeout-chan 1000)
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
[clojure.java.io :as io]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px])
|
||||
[promesa.core :as p])
|
||||
(:import
|
||||
clojure.lang.MapEntry
|
||||
io.lettuce.core.KeyValue
|
||||
@@ -45,8 +44,10 @@
|
||||
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))
|
||||
|
||||
@@ -111,20 +112,15 @@
|
||||
|
||||
(defmethod ig/expand-key ::redis
|
||||
[k v]
|
||||
(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)))}))
|
||||
{k (-> (d/without-nils v)
|
||||
(assoc ::timeout (ct/duration "10s")))})
|
||||
|
||||
(def ^:private schema:redis-params
|
||||
[:map {:title "redis-params"}
|
||||
::wrk/executor
|
||||
::wrk/netty-io-executor
|
||||
::wrk/netty-executor
|
||||
::mtx/metrics
|
||||
[::uri ::sm/uri]
|
||||
[::worker-threads ::sm/int]
|
||||
[::io-threads ::sm/int]
|
||||
[::timeout ::ct/duration]])
|
||||
|
||||
(defmethod ig/assert-key ::redis
|
||||
@@ -141,17 +137,30 @@
|
||||
|
||||
(defn- initialize-resources
|
||||
"Initialize redis connection resources"
|
||||
[{:keys [::uri ::io-threads ::worker-threads ::wrk/executor ::mtx/metrics] :as params}]
|
||||
[{:keys [::uri ::mtx/metrics ::wrk/netty-io-executor ::wrk/netty-executor] :as params}]
|
||||
|
||||
(l/inf :hint "initialize redis resources"
|
||||
:uri (str uri)
|
||||
:io-threads io-threads
|
||||
:worker-threads worker-threads)
|
||||
:uri (str uri))
|
||||
|
||||
(let [timer (HashedWheelTimer.)
|
||||
resources (.. (DefaultClientResources/builder)
|
||||
(ioThreadPoolSize ^long io-threads)
|
||||
(computationThreadPoolSize ^long worker-threads)
|
||||
(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
|
||||
)))
|
||||
|
||||
(timer ^Timer timer)
|
||||
(build))
|
||||
|
||||
@@ -166,7 +175,7 @@
|
||||
(l/trace :hint "evict connection (cache)" :key key :reason cause)
|
||||
(some-> val d/close!))
|
||||
|
||||
cache (cache/create :executor executor
|
||||
cache (cache/create :executor netty-executor
|
||||
:on-remove on-remove
|
||||
:keepalive "5m")]
|
||||
(reify
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
[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
|
||||
@@ -289,13 +288,9 @@
|
||||
(get-limits cfg)))
|
||||
|
||||
(defn invoke!
|
||||
"Run a function in context of climit.
|
||||
Intended to be used in virtual threads."
|
||||
[{:keys [::executor ::rpc/climit] :as cfg} f params]
|
||||
"Run a function in context of climit."
|
||||
[{:keys [::rpc/climit] :as cfg} f params]
|
||||
(let [f (if climit
|
||||
(let [f (if (some? executor)
|
||||
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
|
||||
f)]
|
||||
(build-exec-chain cfg f))
|
||||
(build-exec-chain cfg f)
|
||||
f)]
|
||||
(f cfg params)))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(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]
|
||||
@@ -62,7 +63,7 @@
|
||||
(ex/raise :type :validation
|
||||
:code :account-without-password
|
||||
:hint "the current account does not have password")
|
||||
(let [result (profile/verify-password cfg password (:password profile))]
|
||||
(let [result (auth/verify-password password (:password profile))]
|
||||
(when (:update result)
|
||||
(l/trc :hint "updating profile password"
|
||||
:id (str (:id profile))
|
||||
@@ -156,7 +157,7 @@
|
||||
(:profile-id tdata)))
|
||||
|
||||
(update-password [conn profile-id]
|
||||
(let [pwd (profile/derive-password cfg password)]
|
||||
(let [pwd (auth/derive-password password)]
|
||||
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
|
||||
nil))]
|
||||
|
||||
@@ -378,7 +379,7 @@
|
||||
(not (contains? cf/flags :email-verification)))
|
||||
params (-> params
|
||||
(assoc :is-active is-active)
|
||||
(update :password #(profile/derive-password cfg %)))
|
||||
(update :password auth/derive-password))
|
||||
profile (->> (create-profile! conn params)
|
||||
(create-profile-rels! conn))]
|
||||
(vary-meta profile assoc :created true))))
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
[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)
|
||||
@@ -94,7 +93,7 @@
|
||||
;; --- Command: import-binfile
|
||||
|
||||
(defn- import-binfile
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id project-id version name file]}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id project-id version name file]}]
|
||||
(let [team (teams/get-team pool
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
@@ -105,13 +104,9 @@
|
||||
(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 (px/invoke! executor (partial bf.v1/import-files! cfg))
|
||||
3 (px/invoke! executor (partial bf.v3/import-files! cfg)))]
|
||||
1 (bf.v1/import-files! cfg)
|
||||
3 (bf.v3/import-files! cfg))]
|
||||
|
||||
(db/update! pool :project
|
||||
{:modified-at (ct/now)}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(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]
|
||||
@@ -14,7 +15,6 @@
|
||||
[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 (profile/derive-password cfg password)
|
||||
:password (derive-password password)
|
||||
:props {}}
|
||||
profile (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(->> (auth/create-profile! conn params)
|
||||
|
||||
@@ -39,8 +39,7 @@
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- FEATURES
|
||||
|
||||
@@ -251,7 +250,7 @@
|
||||
(feat.fmigr/resolve-applied-migrations cfg file))))))
|
||||
|
||||
(defn get-file
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg} id
|
||||
[{:keys [::db/conn] :as cfg} id
|
||||
& {:keys [project-id
|
||||
migrate?
|
||||
include-deleted?
|
||||
@@ -273,13 +272,8 @@
|
||||
::db/remove-deleted (not include-deleted?)
|
||||
::sql/for-update lock-for-update?})
|
||||
(feat.fmigr/resolve-applied-migrations cfg)
|
||||
(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))
|
||||
(feat.fdata/resolve-file-data cfg)
|
||||
(decode-row))
|
||||
|
||||
file (if (and migrate? (fmg/need-migration? file))
|
||||
(migrate-file cfg file options)
|
||||
@@ -342,14 +336,24 @@
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features 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))))
|
||||
(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)))))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-fragment (by id)
|
||||
|
||||
@@ -604,8 +608,16 @@
|
||||
{:components components
|
||||
:variant-ids variant-ids}))
|
||||
|
||||
;;coalesce(string_agg(flr.library_file_id::text, ','), '') as library_file_ids
|
||||
(def ^:private sql:team-shared-files
|
||||
"select f.id,
|
||||
"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,
|
||||
f.revn,
|
||||
f.vern,
|
||||
f.data,
|
||||
@@ -618,10 +630,12 @@
|
||||
f.version,
|
||||
f.is_shared,
|
||||
ft.media_id,
|
||||
p.team_id
|
||||
p.team_id,
|
||||
fla.library_file_ids
|
||||
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
|
||||
@@ -665,6 +679,8 @@
|
||||
(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))))))
|
||||
|
||||
@@ -1061,6 +1077,7 @@
|
||||
[: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}
|
||||
@@ -1074,7 +1091,8 @@
|
||||
(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))))
|
||||
(link-file-to-library conn params)
|
||||
(bfc/get-libraries cfg [library-id]))))
|
||||
|
||||
;; --- MUTATION COMMAND: unlink-file-from-library
|
||||
|
||||
|
||||
@@ -112,14 +112,15 @@
|
||||
;; 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))
|
||||
(db/create-array conn "text"))]
|
||||
(let [features (-> features
|
||||
(set/union (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(into-array))]
|
||||
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id (:id team)}
|
||||
|
||||
@@ -37,9 +37,7 @@
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as wrk]
|
||||
[clojure.set :as set]
|
||||
[promesa.exec :as px]))
|
||||
[clojure.set :as set]))
|
||||
|
||||
(declare ^:private get-lagged-changes)
|
||||
(declare ^:private send-notifications!)
|
||||
@@ -160,7 +158,6 @@
|
||||
|
||||
tpoint (ct/tpoint)]
|
||||
|
||||
|
||||
(when (not= (:vern params)
|
||||
(:vern file))
|
||||
(ex/raise :type :validation
|
||||
@@ -183,15 +180,15 @@
|
||||
(set/difference (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(not-empty))]
|
||||
(let [features (->> features
|
||||
(set/union (:features team))
|
||||
(db/create-array conn "text"))]
|
||||
(let [features (-> features
|
||||
(set/union (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(into-array))]
|
||||
(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)
|
||||
@@ -209,7 +206,7 @@
|
||||
Follow the inner implementation to `update-file-data!` function.
|
||||
|
||||
Only intended for internal use on this module."
|
||||
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
|
||||
[{:keys [::db/conn ::timestamp] :as cfg}
|
||||
{:keys [profile-id file team features changes session-id skip-validate] :as params}]
|
||||
|
||||
(let [;; Retrieve the file data
|
||||
@@ -222,15 +219,11 @@
|
||||
|
||||
;; We create a new lexycal scope for clearly delimit the result of
|
||||
;; executing this update file operation and all its side effects
|
||||
(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))))]
|
||||
(let [file (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)
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as-alias wrk]
|
||||
[promesa.exec :as px]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
||||
(def valid-style #{"normal" "italic"})
|
||||
@@ -105,7 +103,7 @@
|
||||
(create-font-variant cfg (assoc params :profile-id profile-id)))))
|
||||
|
||||
(defn create-font-variant
|
||||
[{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}]
|
||||
[{:keys [::sto/storage ::db/conn]} {:keys [data] :as params}]
|
||||
(letfn [(generate-missing! [data]
|
||||
(let [data (media/run {:cmd :generate-fonts :input data})]
|
||||
(when (and (not (contains? data "font/otf"))
|
||||
@@ -157,7 +155,7 @@
|
||||
:otf-file-id (:id otf)
|
||||
:ttf-file-id (:id ttf)}))]
|
||||
|
||||
(let [data (px/invoke! executor (partial generate-missing! data))
|
||||
(let [data (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))))))
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.templates :as tmpl]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as-alias wrk]
|
||||
[promesa.exec :as px]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
;; --- COMMAND: Duplicate File
|
||||
|
||||
@@ -313,15 +311,14 @@
|
||||
|
||||
;; Update the modification date of the all affected projects
|
||||
;; ensuring that the destination project is the most recent one.
|
||||
(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}))
|
||||
(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))))
|
||||
|
||||
nil))
|
||||
|
||||
@@ -396,12 +393,7 @@
|
||||
;; --- COMMAND: Clone Template
|
||||
|
||||
(defn clone-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.
|
||||
[{:keys [::db/pool] :as cfg} {:keys [project-id profile-id] :as params} template]
|
||||
(let [template (tmp/tempfile-from template
|
||||
:prefix "penpot.template."
|
||||
:suffix ""
|
||||
@@ -419,8 +411,8 @@
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
|
||||
|
||||
result (if (= format :binfile-v3)
|
||||
(px/invoke! executor (partial bf.v3/import-files! cfg))
|
||||
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
|
||||
(bf.v3/import-files! cfg)
|
||||
(bf.v1/import-files! cfg))]
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
|
||||
@@ -24,10 +24,8 @@
|
||||
[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]
|
||||
[promesa.exec :as px]))
|
||||
[datoteka.io :as io]))
|
||||
|
||||
(def default-max-file-size
|
||||
(* 1024 1024 10)) ; 10 MiB
|
||||
@@ -153,9 +151,9 @@
|
||||
(assoc ::image (process-main-image info)))))
|
||||
|
||||
(defn- create-file-media-object
|
||||
[{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
|
||||
[{:keys [::sto/storage ::db/conn] :as cfg}
|
||||
{:keys [id file-id is-local name content]}]
|
||||
(let [result (px/invoke! executor (partial process-image content))
|
||||
(let [result (process-image content)
|
||||
image (sto/put-object! storage (::image result))
|
||||
thumb (when-let [params (::thumb result)]
|
||||
(sto/put-object! storage params))]
|
||||
|
||||
@@ -30,16 +30,13 @@
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(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"}
|
||||
@@ -192,7 +189,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 (verify-password cfg old-password (:password profile)))))
|
||||
(not (:valid (auth/verify-password old-password (:password profile)))))
|
||||
(ex/raise :type :validation
|
||||
:code :old-password-not-match))
|
||||
profile))
|
||||
@@ -201,7 +198,7 @@
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}]
|
||||
(when-not (db/read-only? conn)
|
||||
(db/update! conn :profile
|
||||
{:password (derive-password cfg password)}
|
||||
{:password (auth/derive-password password)}
|
||||
{:id id})
|
||||
nil))
|
||||
|
||||
@@ -303,12 +300,11 @@
|
||||
:content-type (:mtype thumb)}))
|
||||
|
||||
(defn upload-photo
|
||||
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file] :as params}]
|
||||
[{:keys [::sto/storage] :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)))
|
||||
|
||||
@@ -548,15 +544,6 @@
|
||||
[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
|
||||
|
||||
@@ -503,7 +503,7 @@
|
||||
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
(set/difference cfeat/no-team-inheritable-features))
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))
|
||||
|
||||
@@ -224,62 +224,112 @@
|
||||
(def ^:private xf:map-email (map :email))
|
||||
|
||||
(defn- create-team-invitations
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
|
||||
(let [emails (set emails)
|
||||
"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)
|
||||
|
||||
join-requests (->> (get-valid-access-request-profiles conn (:id team))
|
||||
(d/index-by :email))
|
||||
;; Case 2: invitations with individual roles (resend invitations style)
|
||||
(some? invitations)
|
||||
invitations
|
||||
|
||||
team-members (into #{} xf:map-email
|
||||
(teams/get-team-members conn (:id team)))
|
||||
:else
|
||||
(throw (ex-info "Invalid parameters: must provide either emails+role or invitations" {})))
|
||||
|
||||
invitations (into #{}
|
||||
(comp
|
||||
;; We don't re-send inviation to
|
||||
;; already existing members
|
||||
(remove team-members)
|
||||
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 %)))
|
||||
;; We don't send invitations to
|
||||
;; join-requested members
|
||||
(remove join-requests)
|
||||
(map (fn [email] (assoc params :email email)))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
(remove #(contains? join-requests (:email %)))
|
||||
(map (fn [{:keys [email role]}]
|
||||
(create-invitation cfg
|
||||
(-> params
|
||||
(assoc :email email)
|
||||
(assoc :role role)))))
|
||||
(remove nil?))
|
||||
invitation-data)]
|
||||
|
||||
;; For requested invitations, do not send invitation emails, add
|
||||
;; the user directly to the team
|
||||
(->> join-requests
|
||||
(filter #(contains? emails (key %)))
|
||||
(map val)
|
||||
(run! (partial add-member-to-team conn profile team role)))
|
||||
(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))
|
||||
|
||||
invitations))
|
||||
|
||||
(def ^:private schema:create-team-invitations
|
||||
[:map {:title "create-team-invitations"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:role types.team/schema:role]
|
||||
[:emails [::sm/set ::sm/email]]])
|
||||
[: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)))))]])
|
||||
|
||||
(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 allow to send a single or multiple invitations to
|
||||
join the team."
|
||||
"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)"
|
||||
{::doc/added "1.17"
|
||||
::doc/module :teams
|
||||
::sm/params schema:create-team-invitations}
|
||||
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
|
||||
[cfg {:keys [::rpc/profile-id team-id role emails] :as params}]
|
||||
(let [perms (teams/get-permissions cfg profile-id team-id)
|
||||
profile (db/get-by-id cfg :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
;; 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)))]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(when (> invitation-count max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
@@ -288,7 +338,7 @@
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id team-id)
|
||||
(assoc ::quotes/incr (count emails))
|
||||
(assoc ::quotes/incr invitation-count)
|
||||
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
|
||||
{::quotes/id ::quotes/profiles-per-team}))
|
||||
|
||||
@@ -304,7 +354,12 @@
|
||||
(-> params
|
||||
(assoc :profile profile)
|
||||
(assoc :team team)
|
||||
(assoc :emails emails)))]
|
||||
;; 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)))]
|
||||
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(ns app.srepl.cli
|
||||
"PREPL API for external usage (CLI or ADMIN)"
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.auth :refer [derive-password]]
|
||||
[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 (cmd.profile/derive-password system password)
|
||||
(let [password (derive-password password)
|
||||
params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
@@ -74,7 +74,7 @@
|
||||
(assoc :fullname fullname)
|
||||
|
||||
(some? password)
|
||||
(assoc :password (auth/derive-password password))
|
||||
(assoc :password (derive-password password))
|
||||
|
||||
(some? is-active)
|
||||
(assoc :is-active is-active))]
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
(defmethod exec-command "derive-password"
|
||||
[{:keys [password]}]
|
||||
(auth/derive-password password))
|
||||
(derive-password password))
|
||||
|
||||
(defmethod exec-command "authenticate"
|
||||
[{:keys [token]}]
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
[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]
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
|
||||
(defn get-legacy-backend
|
||||
[]
|
||||
(let [name (cf/get :assets-storage-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")
|
||||
(case name
|
||||
:assets-fs :fs
|
||||
:assets-s3 :s3
|
||||
|
||||
@@ -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,12 +87,11 @@
|
||||
|
||||
(def ^:private schema:config
|
||||
[:map {:title "s3-backend-config"}
|
||||
::wrk/executor
|
||||
::wrk/netty-io-executor
|
||||
[::region {:optional true} :keyword]
|
||||
[::bucket {:optional true} ::sm/text]
|
||||
[::prefix {:optional true} ::sm/text]
|
||||
[::endpoint {:optional true} ::sm/uri]
|
||||
[::io-threads {:optional true} ::sm/int]])
|
||||
[::endpoint {:optional true} ::sm/uri]])
|
||||
|
||||
(defmethod ig/expand-key ::backend
|
||||
[k v]
|
||||
@@ -110,6 +109,7 @@
|
||||
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)
|
||||
(px/run! close-fn)))
|
||||
(close-fn)))
|
||||
|
||||
(def ^:private schema:backend
|
||||
[:map {:title "s3-backend"}
|
||||
@@ -198,19 +198,16 @@
|
||||
(Region/of (name region)))
|
||||
|
||||
(defn- build-s3-client
|
||||
[{:keys [::region ::endpoint ::io-threads ::wrk/executor]}]
|
||||
[{:keys [::region ::endpoint ::wrk/netty-io-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)
|
||||
(.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder)
|
||||
(.numberOfThreads (int thr-num))))
|
||||
(.eventLoopGroup (SdkEventLoopGroup/create netty-io-executor))
|
||||
(.connectionAcquisitionTimeout default-timeout)
|
||||
(.connectionTimeout default-timeout)
|
||||
(.readTimeout default-timeout)
|
||||
@@ -262,7 +259,7 @@
|
||||
(.close ^InputStream input))))
|
||||
|
||||
(defn- make-request-body
|
||||
[executor content]
|
||||
[counter content]
|
||||
(let [size (impl/get-size content)]
|
||||
(reify
|
||||
AsyncRequestBody
|
||||
@@ -272,16 +269,19 @@
|
||||
(^void subscribe [_ ^Subscriber subscriber]
|
||||
(let [delegate (AsyncRequestBody/forBlockingInputStream (long size))
|
||||
input (io/input-stream content)]
|
||||
(px/run! executor (partial write-input-stream delegate input))
|
||||
|
||||
(px/thread-call (partial write-input-stream delegate input)
|
||||
{:name (str "penpot/storage/" (.getAndIncrement ^AtomicLong counter))})
|
||||
|
||||
(.subscribe ^BlockingInputStreamAsyncRequestBody delegate
|
||||
^Subscriber subscriber))))))
|
||||
|
||||
(defn- put-object
|
||||
[{:keys [::client ::bucket ::prefix ::wrk/executor]} {:keys [id] :as object} content]
|
||||
[{:keys [::client ::bucket ::prefix ::counter]} {: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 executor content)
|
||||
rbody (make-request-body counter content)
|
||||
request (.. (PutObjectRequest/builder)
|
||||
(bucket bucket)
|
||||
(contentType mtype)
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
[_ cfg]
|
||||
(fs/create-dir default-tmp-dir)
|
||||
(px/fn->thread (partial io-loop cfg)
|
||||
{:name "penpot/storage/tmp-cleaner" :virtual true}))
|
||||
{:name "penpot/storage/tmp-cleaner"}))
|
||||
|
||||
(defmethod ig/halt-key! ::cleaner
|
||||
[_ thread]
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
(sp/put! channel [type data])
|
||||
nil)))
|
||||
|
||||
(defn start-listener
|
||||
(defn spawn-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 (start-listener *channel* on-event (constantly nil))]
|
||||
(let [listener (spawn-listener *channel* on-event (constantly nil))]
|
||||
(try
|
||||
(f)
|
||||
(finally
|
||||
|
||||
@@ -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" :virtual false))))
|
||||
(px/fn->thread dispatcher :name "penpot/worker-dispatcher"))))
|
||||
|
||||
(defmethod ig/halt-key! ::wrk/dispatcher
|
||||
[_ thread]
|
||||
|
||||
@@ -7,97 +7,79 @@
|
||||
(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
|
||||
java.util.concurrent.ThreadPoolExecutor))
|
||||
io.netty.channel.nio.NioEventLoopGroup
|
||||
io.netty.util.concurrent.DefaultEventExecutorGroup
|
||||
java.util.concurrent.ExecutorService
|
||||
java.util.concurrent.ThreadFactory))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(sm/register!
|
||||
{:type ::wrk/executor
|
||||
:pred #(instance? ThreadPoolExecutor %)
|
||||
:pred #(instance? ExecutorService %)
|
||||
:type-properties
|
||||
{:title "executor"
|
||||
:description "Instance of ThreadPoolExecutor"}})
|
||||
: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"}})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; EXECUTOR
|
||||
;; IO Executor
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(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/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/halt-key! ::wrk/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
|
||||
[_ instance]
|
||||
(px/shutdown! instance))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MONITOR
|
||||
;; IO Offload Executor
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- get-stats
|
||||
[^ThreadPoolExecutor executor]
|
||||
{:active (.getPoolSize ^ThreadPoolExecutor executor)
|
||||
:running (.getActiveCount ^ThreadPoolExecutor executor)
|
||||
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
|
||||
(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"))
|
||||
|
||||
(defmethod ig/expand-key ::wrk/monitor
|
||||
[k v]
|
||||
{k (-> (d/without-nils v)
|
||||
(assoc ::interval (ct/duration "2s")))})
|
||||
(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/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))
|
||||
(defmethod ig/halt-key! ::wrk/netty-executor
|
||||
[_ instance]
|
||||
(px/shutdown! instance))
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
(defn- start-thread!
|
||||
[{:keys [::rds/redis ::id ::queue ::wrk/tenant] :as cfg}]
|
||||
(px/thread
|
||||
{:name (format "penpot/worker/runner:%s" id)}
|
||||
{:name (str "penpot/worker-runner/" 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 %))
|
||||
(map #(assoc cfg ::id (str queue "/" %)))
|
||||
(map start-thread!))))))
|
||||
|
||||
(defmethod ig/halt-key! ::wrk/runner
|
||||
|
||||
@@ -113,7 +113,6 @@
|
||||
: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
|
||||
|
||||
@@ -7,25 +7,25 @@
|
||||
org.clojure/clojurescript {:mvn/version "1.12.42"}
|
||||
|
||||
;; Logging
|
||||
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.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.slf4j/slf4j-api {:mvn/version "2.0.17"}
|
||||
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
|
||||
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.40"}
|
||||
|
||||
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.18.0"}
|
||||
metosin/malli {:mvn/version "0.19.1"}
|
||||
|
||||
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 "0.13.1"}
|
||||
integrant/integrant {:mvn/version "1.0.0"}
|
||||
|
||||
funcool/tubax {:mvn/version "2021.05.20-0"}
|
||||
funcool/cuerdas {:mvn/version "2025.06.16-414"}
|
||||
@@ -47,7 +47,7 @@
|
||||
org.la4j/la4j {:mvn/version "0.6.0"}
|
||||
|
||||
;; exception printing
|
||||
fipp/fipp {:mvn/version "0.6.27"}
|
||||
fipp/fipp {:mvn/version "0.6.29"}
|
||||
|
||||
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.1.5"}
|
||||
thheller/shadow-cljs {:mvn/version "3.2.0"}
|
||||
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 {:git/tag "v0.10.9" :git/sha "e405aac"}}
|
||||
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
|
||||
@@ -50,6 +50,13 @@
|
||||
(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)
|
||||
@@ -75,6 +82,40 @@
|
||||
(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)
|
||||
@@ -144,13 +185,15 @@
|
||||
(.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})]
|
||||
`(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))))))
|
||||
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)))))))
|
||||
|
||||
(defn wrap
|
||||
[data]
|
||||
@@ -160,7 +203,7 @@
|
||||
|
||||
(defn allocate
|
||||
[size]
|
||||
#?(:clj (let [buffer (ByteBuffer/allocate (int size))]
|
||||
#?(:clj (let [buffer (ByteBuffer/allocate (unchecked-int size))]
|
||||
(.order buffer ByteOrder/LITTLE_ENDIAN))
|
||||
:cljs (new js/DataView (new js/ArrayBuffer size))))
|
||||
|
||||
@@ -181,6 +224,14 @@
|
||||
(.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
|
||||
@@ -208,3 +259,18 @@
|
||||
[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)))
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"styles/v2"
|
||||
"layout/grid"
|
||||
"plugins/runtime"
|
||||
"tokens/numeric-input"
|
||||
"design-tokens/v1"
|
||||
"text-editor/v2"
|
||||
"render-wasm/v1"
|
||||
@@ -67,11 +68,6 @@
|
||||
"design-tokens/v1"
|
||||
"variants/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
|
||||
;; persist on file features field but can be permanently enabled on
|
||||
@@ -80,13 +76,20 @@
|
||||
#{"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/objects-map"
|
||||
"fdata/pointer-map"})
|
||||
#{"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"})
|
||||
|
||||
;; This is a set of features that does not require an explicit
|
||||
;; migration like components/v2 or the migration is not mandatory to
|
||||
@@ -97,6 +100,7 @@
|
||||
"design-tokens/v1"
|
||||
"fdata/shape-data-type"
|
||||
"fdata/path-data"
|
||||
"tokens/numeric-input"
|
||||
"variants/v1"}
|
||||
(into frontend-only-features)
|
||||
(into backend-only-features)))
|
||||
@@ -121,6 +125,7 @@
|
||||
: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
|
||||
@@ -222,8 +227,6 @@
|
||||
: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
|
||||
|
||||
@@ -323,7 +323,7 @@
|
||||
[:main-instance-page ::sm/uuid]]]
|
||||
|
||||
[:mod-component
|
||||
[:map {:title "ModCompoenentChange"}
|
||||
[:map {:title "ModComponentChange"}
|
||||
[:type [:= :mod-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
|
||||
@@ -366,9 +366,33 @@
|
||||
[:type [:= :del-typography]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
[:update-active-token-themes
|
||||
[:map {:title "UpdateActiveTokenThemes"}
|
||||
[:type [:= :update-active-token-themes]]
|
||||
[: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
|
||||
|
||||
[:set-token
|
||||
[:map {:title "SetTokenChange"}
|
||||
[:type [:= :set-token]]
|
||||
[:set-id ::sm/uuid]
|
||||
[:token-id ::sm/uuid]
|
||||
[:attrs [:maybe ctob/schema:token-attrs]]]]
|
||||
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
[:type [:= :set-token-set]]
|
||||
[:id ::sm/uuid]
|
||||
[:attrs [:maybe ctob/schema:token-set-attrs]]]]
|
||||
|
||||
[:set-token-theme
|
||||
[:map {:title "SetTokenThemeChange"}
|
||||
[:type [:= :set-token-theme]]
|
||||
[:id ::sm/uuid]
|
||||
[:attrs [:maybe ctob/schema:token-theme-attrs]]]]
|
||||
|
||||
[:set-active-token-themes
|
||||
[:map {:title "SetActiveTokenThemes"}
|
||||
[:type [:= :set-active-token-themes]]
|
||||
[:theme-paths [:set :string]]]]
|
||||
|
||||
[:rename-token-set-group
|
||||
@@ -393,39 +417,6 @@
|
||||
[: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]]]]
|
||||
|
||||
[:set-tokens-lib
|
||||
[:map {:title "SetTokensLib"}
|
||||
[:type [:= :set-tokens-lib]]
|
||||
[:tokens-lib ::sm/any]]]
|
||||
|
||||
[: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]]
|
||||
@@ -978,64 +969,63 @@
|
||||
[data {:keys [id]}]
|
||||
(ctyl/delete-typography data id))
|
||||
|
||||
;; -- Tokens
|
||||
;; -- Design 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-name token-id token]}]
|
||||
[data {:keys [set-id token-id attrs]}]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
(cond
|
||||
(not token)
|
||||
(ctob/delete-token-from-set lib' set-name token-id)
|
||||
(not attrs)
|
||||
(ctob/delete-token lib' set-id token-id)
|
||||
|
||||
(not (ctob/get-token-in-set lib' set-name token-id))
|
||||
(ctob/add-token-in-set lib' set-name (ctob/make-token token))
|
||||
(not (ctob/get-token lib' set-id token-id))
|
||||
(ctob/add-token lib' set-id (ctob/make-token attrs))
|
||||
|
||||
:else
|
||||
(ctob/update-token-in-set lib' set-name token-id (fn [prev-token]
|
||||
(ctob/make-token (merge prev-token token)))))))))
|
||||
(ctob/update-token lib' set-id token-id
|
||||
(fn [prev-token]
|
||||
(ctob/make-token (merge prev-token attrs)))))))))
|
||||
|
||||
(defmethod process-change :set-token-set
|
||||
[data {:keys [set-name group? token-set]}]
|
||||
[data {:keys [id attrs]}]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
(cond
|
||||
(not token-set)
|
||||
(if group?
|
||||
(ctob/delete-set-group lib' set-name)
|
||||
(ctob/delete-set lib' set-name))
|
||||
(not attrs)
|
||||
(ctob/delete-set lib' id)
|
||||
|
||||
(not (ctob/get-set lib' set-name))
|
||||
(ctob/add-set lib' token-set)
|
||||
(not (ctob/get-set lib' id))
|
||||
(ctob/add-set lib' (ctob/make-token-set attrs))
|
||||
|
||||
:else
|
||||
(ctob/update-set lib' set-name (fn [_] token-set)))))))
|
||||
(ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs))))))))
|
||||
|
||||
(defmethod process-change :set-token-theme
|
||||
[data {:keys [group theme-name theme]}]
|
||||
[data {:keys [id attrs]}]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
(cond
|
||||
(not theme)
|
||||
(ctob/delete-theme lib' group theme-name)
|
||||
(not attrs)
|
||||
(ctob/delete-theme lib' id)
|
||||
|
||||
(not (ctob/get-theme lib' group theme-name))
|
||||
(ctob/add-theme lib' (ctob/make-token-theme theme))
|
||||
(not (ctob/get-theme lib' id))
|
||||
(ctob/add-theme lib' (ctob/make-token-theme attrs))
|
||||
|
||||
:else
|
||||
(ctob/update-theme lib'
|
||||
group theme-name
|
||||
id
|
||||
(fn [prev-token-theme]
|
||||
(ctob/make-token-theme (merge prev-token-theme theme)))))))))
|
||||
(ctob/make-token-theme (merge prev-token-theme attrs)))))))))
|
||||
|
||||
(defmethod process-change :update-active-token-themes
|
||||
(defmethod process-change :set-active-token-themes
|
||||
[data {:keys [theme-paths]}]
|
||||
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
|
||||
(ctob/set-active-themes theme-paths))))
|
||||
@@ -1059,7 +1049,7 @@
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/move-set-group from-path to-path before-path before-group))))
|
||||
|
||||
;; === Base font size
|
||||
;; === Design Tokens configuration
|
||||
|
||||
(defmethod process-change :set-base-font-size
|
||||
[data {:keys [base-font-size]}]
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
[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]))
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.datafy :refer [datafy]]))
|
||||
|
||||
;; Auxiliary functions to help create a set of changes (undo + redo)
|
||||
;; TODO: this is a duplicate schema
|
||||
@@ -717,6 +718,7 @@
|
||||
(reduce resize-parent changes all-parents)))
|
||||
|
||||
;; Library changes
|
||||
|
||||
(defn add-color
|
||||
[changes color]
|
||||
(-> changes
|
||||
@@ -798,160 +800,6 @@
|
||||
(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))
|
||||
@@ -1081,6 +929,144 @@
|
||||
: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)
|
||||
@@ -1163,15 +1149,3 @@
|
||||
[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))))
|
||||
|
||||
@@ -692,129 +692,9 @@
|
||||
(walk/postwalk process-form data)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SHAPES ORGANIZATION (PATH MANAGEMENT)
|
||||
;; SHAPES ORGANIZATION
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(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 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 ""]))
|
||||
|
||||
(defn get-frame-objects
|
||||
"Retrieves a new objects map only with the objects under frame-id (with frame-id)"
|
||||
[objects frame-id]
|
||||
|
||||
@@ -6,14 +6,29 @@
|
||||
|
||||
(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-child-all-parents-index objects (vals objects)))
|
||||
(generate-index {} objects uuid/zero []))
|
||||
|
||||
([objects shapes]
|
||||
(let [shape->entry
|
||||
@@ -24,24 +39,25 @@
|
||||
(defn create-clip-index
|
||||
"Retrieves the mask information for an object"
|
||||
[objects parents-index]
|
||||
(let [retrieve-clips
|
||||
(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
|
||||
(fn [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)
|
||||
(into [] xform parents))]
|
||||
|
||||
(: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))))
|
||||
(d/update-vals parents-index populate-with-clips)))
|
||||
|
||||
@@ -320,6 +320,31 @@
|
||||
(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 _]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[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]
|
||||
@@ -47,6 +48,8 @@
|
||||
: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
|
||||
@@ -305,6 +308,28 @@
|
||||
"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]
|
||||
@@ -382,6 +407,7 @@
|
||||
(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)
|
||||
@@ -399,7 +425,8 @@
|
||||
;; 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-component-ref shape file page libraries)
|
||||
(check-ref-is-head shape file page libraries))
|
||||
(run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-main-not-root
|
||||
@@ -417,6 +444,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)))
|
||||
@@ -484,7 +512,7 @@
|
||||
(report-error :variant-bad-name
|
||||
(str/ffmt "Variant % has an invalid name" (:id shape))
|
||||
shape file page))
|
||||
(when-not (= (:name parent) (cfh/merge-path-item (:path component) (:name component)))
|
||||
(when-not (= (:name parent) (cpn/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))
|
||||
@@ -543,7 +571,7 @@
|
||||
;; mains can't be nested into mains
|
||||
(if (or (= context :not-component) (= context :main-top))
|
||||
(report-error :nested-main-not-allowed
|
||||
"Nested main component only allowed inside other component"
|
||||
"Component main not allowed inside other component"
|
||||
shape file page)
|
||||
(check-shape-main-root-nested shape file page libraries))
|
||||
|
||||
@@ -608,6 +636,20 @@
|
||||
(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]
|
||||
@@ -615,6 +657,8 @@
|
||||
(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))
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
:tiered-file-data-storage
|
||||
:token-units
|
||||
:token-base-font-size
|
||||
:token-color
|
||||
:token-typography-types
|
||||
:token-typography-composite
|
||||
:transit-readable-response
|
||||
|
||||
@@ -88,8 +88,11 @@
|
||||
([shape]
|
||||
(get-shape-filter-bounds shape false))
|
||||
([shape ignore-shadow-margin?]
|
||||
(if (and (cfh/svg-raw-shape? shape)
|
||||
(not= :svg (dm/get-in shape [:content :tag])))
|
||||
(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)))))
|
||||
(dm/get-prop shape :selrect)
|
||||
(let [filters (shape->filters shape)
|
||||
blur-value (or (-> shape :blur :value) 0)
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
[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]
|
||||
@@ -221,36 +222,42 @@
|
||||
#?(: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)
|
||||
(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#))))))
|
||||
(emit-log (delay ~props) ~cause ~context ~logger ~level ~sync?))))
|
||||
|
||||
#?(:clj
|
||||
(defn slf4j-log-handler
|
||||
@@ -276,7 +283,8 @@
|
||||
(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))
|
||||
header (str/concat "%c" (level->name level) " [" logger "] ")
|
||||
ts (ct/format-inst (ct/now) "kk:mm:ss.SSSS")
|
||||
header (str/concat "%c" (level->name level) " " ts " [" logger "] ")
|
||||
message (str/concat header "%c" @message)]
|
||||
|
||||
(js/console.group message hstyles mstyles)
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
[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]
|
||||
@@ -985,7 +987,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] (cfh/parse-path-name new-name)]
|
||||
(let [[path name] (cpn/split-group-name new-name)]
|
||||
(-> changes
|
||||
(pcb/with-library-data library-data)
|
||||
(pcb/update-component id #(assoc % :path path :name name)))))
|
||||
@@ -1733,6 +1735,17 @@
|
||||
[(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.
|
||||
@@ -1773,6 +1786,8 @@
|
||||
(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)
|
||||
@@ -1796,7 +1811,6 @@
|
||||
(= :content attr)
|
||||
(touched attr-group))
|
||||
|
||||
|
||||
skip-operations?
|
||||
(or (= (get origin-shape attr) (get dest-shape attr))
|
||||
(and (touched attr-group)
|
||||
@@ -2222,7 +2236,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] (cfh/parse-path-name name)
|
||||
[path name] (cpn/split-group-name name)
|
||||
|
||||
[root-shape updated-shapes]
|
||||
(ctn/convert-shape-in-component root objects file-id)
|
||||
@@ -2514,9 +2528,10 @@
|
||||
frames)))
|
||||
|
||||
(defn- duplicate-variant
|
||||
[changes library component base-pos parent-id page-id]
|
||||
[changes library component base-pos parent page-id into-new-variant?]
|
||||
(let [component-page (ctpl/get-page (:data library) (:main-instance-page component))
|
||||
component-shape (dm/get-in component-page [:objects (:main-instance-id component)])
|
||||
objects (:objects component-page)
|
||||
component-shape (get 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)
|
||||
@@ -2526,11 +2541,27 @@
|
||||
new-component-id
|
||||
{:apply-changes-local-library? true
|
||||
:delta delta
|
||||
:new-variant-id parent-id
|
||||
:page-id page-id})]
|
||||
: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)))]
|
||||
|
||||
[shape
|
||||
(-> changes
|
||||
(pcb/change-parent parent-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))]))
|
||||
|
||||
|
||||
(defn generate-duplicate-component-change
|
||||
@@ -2542,11 +2573,13 @@
|
||||
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
|
||||
@@ -2559,29 +2592,42 @@
|
||||
frame-id)]
|
||||
[shape changes])
|
||||
|
||||
[_shape changes]
|
||||
(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))
|
||||
|
||||
(generate-instantiate-component changes
|
||||
objects
|
||||
file-id
|
||||
component-id
|
||||
pos
|
||||
page
|
||||
libraries
|
||||
main-id
|
||||
parent-id
|
||||
frame-id
|
||||
ids-map
|
||||
{})))]
|
||||
[_shape changes]
|
||||
(cond
|
||||
(nil? component)
|
||||
(restore-component)
|
||||
|
||||
(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
|
||||
{}))]
|
||||
changes))
|
||||
|
||||
(defn generate-duplicate-shape-change
|
||||
@@ -2740,7 +2786,8 @@
|
||||
|
||||
changes (-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects all-objects))
|
||||
(pcb/with-objects all-objects)
|
||||
(pcb/with-library-data library-data))
|
||||
changes
|
||||
(->> shapes
|
||||
(reduce #(generate-duplicate-shape-change %1
|
||||
|
||||
@@ -185,15 +185,17 @@
|
||||
interactions)))
|
||||
(vals objects))
|
||||
|
||||
id-to-delete? (set ids-to-delete)
|
||||
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))
|
||||
(->> (: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))
|
||||
|
||||
|
||||
all-parents
|
||||
|
||||
@@ -17,18 +17,16 @@
|
||||
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 [prev-active-token-themes (ctob/get-active-theme-paths tokens-lib)
|
||||
active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
|
||||
(let [active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
|
||||
|
||||
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))]
|
||||
hidden-theme (ctob/get-hidden-theme tokens-lib)
|
||||
hidden-theme' (-> (some-> hidden-theme
|
||||
(ctob/set-sets active-token-set-names))
|
||||
(update-theme-fn))]
|
||||
(-> changes
|
||||
(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))))
|
||||
(pcb/set-active-token-themes #{(ctob/get-theme-path hidden-theme')})
|
||||
(pcb/set-token-theme (ctob/get-id hidden-theme)
|
||||
hidden-theme'))))
|
||||
|
||||
(defn generate-toggle-token-set
|
||||
"Toggle a token set at `set-name` in `tokens-lib` without modifying a
|
||||
@@ -139,3 +137,12 @@
|
||||
(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)))
|
||||
@@ -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] (cfh/parse-path-name new-name)]
|
||||
[cpath cname] (cpn/split-group-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)
|
||||
|
||||
add-name? (not= (:name component) container-name)
|
||||
component-full-name (cpn/merge-path-item (:path component) (:name component))
|
||||
add-name? (not= component-full-name 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] (cfh/parse-path-name (:name variant-container))
|
||||
[cpath cname] (cpn/split-group-name (:name variant-container))
|
||||
container-name (:name variant-container)
|
||||
|
||||
create-new-properties
|
||||
|
||||
134
common/src/app/common/path_names.cljc
Normal file
134
common/src/app/common/path_names.cljc
Normal file
@@ -0,0 +1,134 @@
|
||||
;; 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 ""]))
|
||||
@@ -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])
|
||||
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double not-empty])
|
||||
#?(:cljs (:require-macros [app.common.schema.generators]))
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
@@ -146,3 +146,5 @@
|
||||
|
||||
(def any
|
||||
(tg/one-of [text boolean double int keyword]))
|
||||
|
||||
(def not-empty tg/not-empty)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[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]
|
||||
@@ -36,7 +37,7 @@
|
||||
|
||||
updated-root (first updated-shapes) ; Can't use new-root because it has a new id
|
||||
|
||||
[path name] (cfh/parse-path-name (:name updated-root))]
|
||||
[path name] (cpn/split-group-name (:name updated-root))]
|
||||
(thi/set-id! label (:component-id updated-root))
|
||||
|
||||
(ctf/update-file-data
|
||||
@@ -72,6 +73,10 @@
|
||||
[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]
|
||||
|
||||
@@ -108,7 +108,8 @@
|
||||
page (if (some? page-label)
|
||||
(:id (get-page file page-label))
|
||||
(current-page-id file))
|
||||
libraries (or libraries {})]
|
||||
libraries (or libraries
|
||||
{(:id file) file})]
|
||||
|
||||
(ctf/dump-tree file page libraries params)))
|
||||
|
||||
|
||||
@@ -28,12 +28,10 @@
|
||||
(ctf/update-file-data file #(update % :tokens-lib f)))
|
||||
|
||||
(defn get-token
|
||||
[file set-name token-id]
|
||||
[file set-id token-id]
|
||||
(let [tokens-lib (:tokens-lib (:data file))]
|
||||
(when tokens-lib
|
||||
(-> tokens-lib
|
||||
(ctob/get-set set-name)
|
||||
(ctob/get-token token-id)))))
|
||||
(ctob/get-token tokens-lib set-id token-id))))
|
||||
|
||||
(defn token-data-eq?
|
||||
"Compare token data without comparing unstable fields."
|
||||
|
||||
@@ -130,7 +130,6 @@
|
||||
ms-or-obj
|
||||
|
||||
(integer? ms-or-obj)
|
||||
|
||||
(Duration/ofMillis ms-or-obj)
|
||||
|
||||
:else
|
||||
@@ -433,4 +432,4 @@
|
||||
#?(:cljs
|
||||
(extend-protocol cljs.core/IEncodeJS
|
||||
js/Date
|
||||
(-clj->js [x] x)))
|
||||
(-clj->js [x] x)))
|
||||
|
||||
@@ -145,9 +145,12 @@
|
||||
(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."
|
||||
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"
|
||||
[attr]
|
||||
(or (get sync-attrs attr)
|
||||
(= :shape-ref attr)
|
||||
(= :applied-tokens attr)))
|
||||
|
||||
(defn instance-root?
|
||||
@@ -217,19 +220,16 @@
|
||||
(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,6 +310,22 @@
|
||||
: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)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.common.types.container
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
@@ -77,11 +76,8 @@
|
||||
|
||||
(defn get-shape
|
||||
[container shape-id]
|
||||
|
||||
(assert (check-container container))
|
||||
(assert (uuid? shape-id)
|
||||
"expected valid uuid for `shape-id`")
|
||||
|
||||
(-> container
|
||||
(get :objects)
|
||||
(get shape-id)))
|
||||
@@ -494,29 +490,40 @@
|
||||
all-main?
|
||||
(every? ctk/main-instance? top-children)
|
||||
|
||||
ascendants (cfh/get-parents-with-self objects parent-id)
|
||||
any-main-ascendant (some ctk/main-instance? ascendants)
|
||||
any-variant-container-ascendant (some ctk/is-variant-container? ascendants)
|
||||
|
||||
get-variant-id (fn [shape]
|
||||
(when (:component-id shape)
|
||||
(-> (get-component-from-shape shape libraries)
|
||||
:variant-id)))
|
||||
|
||||
descendants (mapcat #(cfh/get-children-with-self objects %) children-ids)
|
||||
any-variant-container-descendant (some ctk/is-variant-container? descendants)
|
||||
descendants-variant-ids-set (->> descendants
|
||||
(map get-variant-id)
|
||||
set)
|
||||
any-main-descendant
|
||||
(some
|
||||
(fn [shape]
|
||||
(some ctk/main-instance? (cfh/get-children-with-self objects (:id shape))))
|
||||
children)
|
||||
children)]
|
||||
|
||||
;; Are all the top-children a main-instance of a cutted component?
|
||||
all-comp-cut?
|
||||
(when all-main?
|
||||
(->> top-children
|
||||
(map #(ctkl/get-component (dm/get-in libraries [(:component-file %) :data])
|
||||
(:component-id %)
|
||||
true))
|
||||
(every? :deleted)))]
|
||||
(if (or no-changes?
|
||||
(and (not (invalid-structure-for-component? objects parent children pasting? libraries))
|
||||
;; If we are moving into a main component, no descendant can be main
|
||||
(or (nil? any-main-descendant) (not (ctk/main-instance? parent)))
|
||||
;; If we are moving into a variant-container, all the items should be main
|
||||
;; so if we are pasting, only allow main instances that are cut-and-pasted
|
||||
(or (not (ctk/is-variant-container? parent))
|
||||
(and (not pasting?) all-main?)
|
||||
all-comp-cut?)))
|
||||
;; If we are moving (not pasting) into a main component, no descendant can be main
|
||||
(or pasting? (nil? any-main-descendant) (not (ctk/main-instance? parent)))
|
||||
;; Don't allow variant-container inside variant container nor main
|
||||
(or (not any-variant-container-descendant)
|
||||
(and (not any-variant-container-ascendant) (not any-main-ascendant)))
|
||||
;; If the parent is a variant-container, all the items should be main
|
||||
(or (not (ctk/is-variant-container? parent)) all-main?)
|
||||
;; If we are pasting, the parent can't be a "brother" of any of the pasted items,
|
||||
;; so not have the same variant-id of any descendant
|
||||
(or (not pasting?)
|
||||
(not (ctk/is-variant? parent))
|
||||
(not (contains? descendants-variant-ids-set (:variant-id parent))))))
|
||||
[parent-id (get-frame parent-id)]
|
||||
(recur (:parent-id parent) objects children pasting? libraries))))))
|
||||
|
||||
|
||||
521
common/src/app/common/types/objects_map.cljc
Normal file
521
common/src/app/common/types/objects_map.cljc
Normal file
@@ -0,0 +1,521 @@
|
||||
;; 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.types.objects-map
|
||||
"Implements a specialized map-like data structure for store an UUID =>
|
||||
OBJECT mappings. The main purpose of this data structure is be able
|
||||
to serialize it on fressian as byte-array and have the ability to
|
||||
decode each field separatelly without the need to decode the whole
|
||||
map from the byte-array.
|
||||
|
||||
It works transparently, so no aditional dynamic vars are needed. It
|
||||
only works by reference equality and the hash-code is calculated
|
||||
properly from each value."
|
||||
|
||||
(:require
|
||||
#?(:clj [app.common.fressian :as fres])
|
||||
#?(:clj [clojure.data.json :as json])
|
||||
[app.common.transit :as t]
|
||||
[clojure.core :as c]
|
||||
[clojure.core.protocols :as cp])
|
||||
#?(:clj
|
||||
(:import
|
||||
clojure.lang.Murmur3
|
||||
clojure.lang.RT
|
||||
java.util.Iterator)))
|
||||
|
||||
#?(:clj (set! *warn-on-reflection* true))
|
||||
|
||||
(declare create)
|
||||
(declare ^:private do-compact)
|
||||
|
||||
(defprotocol IObjectsMap
|
||||
(^:no-doc compact [this])
|
||||
(^:no-doc get-data [this] "retrieve internal data")
|
||||
(^:no-doc -hash-for-key [this key] "retrieve a hash for a key"))
|
||||
|
||||
#?(:cljs
|
||||
(deftype ObjectsMapEntry [key omap]
|
||||
c/IMapEntry
|
||||
(-key [_] key)
|
||||
(-val [_] (get omap key))
|
||||
|
||||
c/IHash
|
||||
(-hash [_]
|
||||
(-hash-for-key omap key))
|
||||
|
||||
c/IEquiv
|
||||
(-equiv [this other]
|
||||
(and (c/map-entry? other)
|
||||
(= (key this)
|
||||
(key other))
|
||||
(= (val this)
|
||||
(val other))))
|
||||
|
||||
c/ISequential
|
||||
c/ISeqable
|
||||
(-seq [this]
|
||||
(cons key (lazy-seq (cons (c/-val this) nil))))
|
||||
|
||||
c/ICounted
|
||||
(-count [_] 2)
|
||||
|
||||
c/IIndexed
|
||||
(-nth [node n]
|
||||
(cond (== n 0) key
|
||||
(== n 1) (c/-val node)
|
||||
:else (throw (js/Error. "Index out of bounds"))))
|
||||
|
||||
(-nth [node n not-found]
|
||||
(cond (== n 0) key
|
||||
(== n 1) (c/-val node)
|
||||
:else not-found))
|
||||
|
||||
c/ILookup
|
||||
(-lookup [node k]
|
||||
(c/-nth node k nil))
|
||||
(-lookup [node k not-found]
|
||||
(c/-nth node k not-found))
|
||||
|
||||
c/IFn
|
||||
(-invoke [node k]
|
||||
(c/-nth node k))
|
||||
|
||||
(-invoke [node k not-found]
|
||||
(c/-nth node k not-found))
|
||||
|
||||
c/IPrintWithWriter
|
||||
(-pr-writer [this writer opts]
|
||||
(c/pr-sequential-writer
|
||||
writer
|
||||
(fn [item w _]
|
||||
(c/-write w (pr-str item)))
|
||||
"[" ", " "]"
|
||||
opts
|
||||
this)))
|
||||
|
||||
:clj
|
||||
(deftype ObjectsMapEntry [key omap]
|
||||
clojure.lang.IMapEntry
|
||||
(key [_] key)
|
||||
(getKey [_] key)
|
||||
|
||||
(val [_]
|
||||
(get omap key))
|
||||
(getValue [_]
|
||||
(get omap key))
|
||||
|
||||
clojure.lang.Indexed
|
||||
(nth [node n]
|
||||
(cond
|
||||
(== n 0) key
|
||||
(== n 1) (val node)
|
||||
:else (throw (IllegalArgumentException. "Index out of bounds"))))
|
||||
|
||||
(nth [node n not-found]
|
||||
(cond
|
||||
(== n 0) key
|
||||
(== n 1) (val node)
|
||||
:else not-found))
|
||||
|
||||
clojure.lang.IPersistentCollection
|
||||
(empty [_] [])
|
||||
(count [_] 2)
|
||||
(seq [this]
|
||||
(cons key (lazy-seq (cons (val this) nil))))
|
||||
(cons [this item]
|
||||
(.cons ^clojure.lang.IPersistentCollection (vec this) item))
|
||||
|
||||
clojure.lang.IHashEq
|
||||
(hasheq [_]
|
||||
(-hash-for-key omap key))))
|
||||
|
||||
#?(:cljs
|
||||
(deftype ObjectMapIterator [iterator omap]
|
||||
Object
|
||||
(hasNext [_]
|
||||
(.hasNext ^js iterator))
|
||||
|
||||
(next [_]
|
||||
(let [entry (.next iterator)]
|
||||
(ObjectsMapEntry. (key entry) omap)))
|
||||
|
||||
(remove [_]
|
||||
(js/Error. "Unsupported operation")))
|
||||
|
||||
:clj
|
||||
(deftype ObjectsMapIterator [^Iterator iterator omap]
|
||||
Iterator
|
||||
(hasNext [_]
|
||||
(.hasNext iterator))
|
||||
|
||||
(next [_]
|
||||
(let [entry (.next iterator)]
|
||||
(ObjectsMapEntry. (key entry) omap)))))
|
||||
|
||||
#?(:cljs
|
||||
(deftype ObjectsMap [metadata cache
|
||||
^:mutable data
|
||||
^:mutable modified
|
||||
^:mutable hash]
|
||||
Object
|
||||
(toString [this]
|
||||
(pr-str* this))
|
||||
(equiv [this other]
|
||||
(c/-equiv this other))
|
||||
(keys [this]
|
||||
(c/es6-iterator (keys this)))
|
||||
(entries [this]
|
||||
(c/es6-entries-iterator (seq this)))
|
||||
(values [this]
|
||||
(es6-iterator (vals this)))
|
||||
(has [this k]
|
||||
(c/contains? this k))
|
||||
(get [this k not-found]
|
||||
(c/-lookup this k not-found))
|
||||
(forEach [this f]
|
||||
(run! (fn [[k v]] (f v k)) this))
|
||||
|
||||
cp/Datafiable
|
||||
(datafy [_]
|
||||
{:data data
|
||||
:cache cache
|
||||
:modified modified
|
||||
:hash hash})
|
||||
|
||||
IObjectsMap
|
||||
(compact [this]
|
||||
(when modified
|
||||
(do-compact data cache
|
||||
(fn [data']
|
||||
(set! (.-modified this) false)
|
||||
(set! (.-data this) data'))))
|
||||
this)
|
||||
|
||||
(get-data [this]
|
||||
(compact this)
|
||||
data)
|
||||
|
||||
(-hash-for-key [this key]
|
||||
(if (c/-contains-key? cache key)
|
||||
(c/-hash (c/-lookup cache key))
|
||||
(c/-hash (c/-lookup this key))))
|
||||
|
||||
c/IWithMeta
|
||||
(-with-meta [this new-meta]
|
||||
(if (identical? new-meta meta)
|
||||
this
|
||||
(ObjectsMap. new-meta
|
||||
cache
|
||||
data
|
||||
modified
|
||||
hash)))
|
||||
|
||||
c/IMeta
|
||||
(-meta [_] metadata)
|
||||
|
||||
c/ICloneable
|
||||
(-clone [this]
|
||||
(compact this)
|
||||
(ObjectsMap. metadata {} data false nil))
|
||||
|
||||
c/IIterable
|
||||
(-iterator [this]
|
||||
(c/seq-iter this))
|
||||
|
||||
c/ICollection
|
||||
(-conj [this entry]
|
||||
(cond
|
||||
(map-entry? entry)
|
||||
(c/-assoc this (c/-key entry) (c/-val entry))
|
||||
|
||||
(vector? entry)
|
||||
(c/-assoc this (c/-nth entry 0) (c/-nth entry 1))
|
||||
|
||||
:else
|
||||
(loop [ret this es (seq entry)]
|
||||
(if (nil? es)
|
||||
ret
|
||||
(let [e (first es)]
|
||||
(if (vector? e)
|
||||
(recur (c/-assoc ret (c/-nth e 0) (c/-nth e 1))
|
||||
(next es))
|
||||
(throw (js/Error. "conj on a map takes map entries or seqables of map entries"))))))))
|
||||
|
||||
c/IEmptyableCollection
|
||||
(-empty [_]
|
||||
(create))
|
||||
|
||||
c/IEquiv
|
||||
(-equiv [this other]
|
||||
(equiv-map this other))
|
||||
|
||||
c/IHash
|
||||
(-hash [this]
|
||||
(when-not hash
|
||||
(set! hash (hash-unordered-coll this)))
|
||||
hash)
|
||||
|
||||
c/ISeqable
|
||||
(-seq [this]
|
||||
(->> (keys data)
|
||||
(map (fn [id] (new ObjectsMapEntry id this)))
|
||||
(seq)))
|
||||
|
||||
c/ICounted
|
||||
(-count [_]
|
||||
(c/-count data))
|
||||
|
||||
c/ILookup
|
||||
(-lookup [this k]
|
||||
(or (c/-lookup cache k)
|
||||
(if (c/-contains-key? data k)
|
||||
(let [v (c/-lookup data k)
|
||||
v (t/decode-str v)]
|
||||
(set! (.-cache this) (c/-assoc cache k v))
|
||||
v)
|
||||
(do
|
||||
(set! (.-cache this) (assoc cache key nil))
|
||||
nil))))
|
||||
|
||||
(-lookup [this k not-found]
|
||||
(if (c/-contains-key? data k)
|
||||
(c/-lookup this k)
|
||||
not-found))
|
||||
|
||||
c/IAssociative
|
||||
(-assoc [_ k v]
|
||||
(ObjectsMap. metadata
|
||||
(c/-assoc cache k v)
|
||||
(c/-assoc data k nil)
|
||||
true
|
||||
nil))
|
||||
|
||||
(-contains-key? [_ k]
|
||||
(c/-contains-key? data k))
|
||||
|
||||
c/IFind
|
||||
(-find [this k]
|
||||
(when (c/-contains-key? data k)
|
||||
(new ObjectsMapEntry k this)))
|
||||
|
||||
c/IMap
|
||||
(-dissoc [_ k]
|
||||
(ObjectsMap. metadata
|
||||
(c/-dissoc cache k)
|
||||
(c/-dissoc data k)
|
||||
true
|
||||
nil))
|
||||
|
||||
c/IKVReduce
|
||||
(-kv-reduce [this f init]
|
||||
(c/-kv-reduce data
|
||||
(fn [init k _]
|
||||
(f init k (c/-lookup this k)))
|
||||
init))
|
||||
|
||||
c/IFn
|
||||
(-invoke [this k]
|
||||
(c/-lookup this k))
|
||||
(-invoke [this k not-found]
|
||||
(c/-lookup this k not-found))
|
||||
|
||||
c/IPrintWithWriter
|
||||
(-pr-writer [this writer opts]
|
||||
(c/pr-sequential-writer
|
||||
writer
|
||||
(fn [item w _]
|
||||
(c/-write w (pr-str (c/-key item)))
|
||||
(c/-write w \space)
|
||||
(c/-write w (pr-str (c/-val item))))
|
||||
"#penpot/objects-map {" ", " "}"
|
||||
opts
|
||||
(seq this))))
|
||||
|
||||
:clj
|
||||
(deftype ObjectsMap [metadata cache
|
||||
^:unsynchronized-mutable data
|
||||
^:unsynchronized-mutable modified
|
||||
^:unsynchronized-mutable hash]
|
||||
|
||||
Object
|
||||
(hashCode [this]
|
||||
(.hasheq ^clojure.lang.IHashEq this))
|
||||
|
||||
cp/Datafiable
|
||||
(datafy [_]
|
||||
{:data data
|
||||
:cache cache
|
||||
:modified modified
|
||||
:hash hash})
|
||||
|
||||
IObjectsMap
|
||||
(compact [this]
|
||||
(locking this
|
||||
(when modified
|
||||
(do-compact data cache
|
||||
(fn [data']
|
||||
(set! (.-modified this) false)
|
||||
(set! (.-data this) data')))))
|
||||
this)
|
||||
|
||||
(get-data [this]
|
||||
(compact this)
|
||||
data)
|
||||
|
||||
(-hash-for-key [this key]
|
||||
(if (contains? cache key)
|
||||
(c/hash (get cache key))
|
||||
(c/hash (get this key))))
|
||||
|
||||
json/JSONWriter
|
||||
(-write [this writter options]
|
||||
(json/-write (into {} this) writter options))
|
||||
|
||||
clojure.lang.IHashEq
|
||||
(hasheq [this]
|
||||
(when-not hash
|
||||
(set! hash (Murmur3/hashUnordered this)))
|
||||
hash)
|
||||
|
||||
clojure.lang.Seqable
|
||||
(seq [this]
|
||||
(RT/chunkIteratorSeq (.iterator ^Iterable this)))
|
||||
|
||||
java.lang.Iterable
|
||||
(iterator [this]
|
||||
(ObjectsMapIterator. (.iterator ^Iterable data) this))
|
||||
|
||||
clojure.lang.IPersistentCollection
|
||||
(equiv [this other]
|
||||
(and (instance? ObjectsMap other)
|
||||
(= (count this) (count other))
|
||||
(reduce-kv (fn [_ id _]
|
||||
(let [this-val (get this id)
|
||||
other-val (get other id)
|
||||
result (= this-val other-val)]
|
||||
(or result
|
||||
(reduced false))))
|
||||
true
|
||||
data)))
|
||||
|
||||
clojure.lang.IPersistentMap
|
||||
(cons [this o]
|
||||
(if (map-entry? o)
|
||||
(assoc this (key o) (val o))
|
||||
(if (vector? o)
|
||||
(assoc this (nth o 0) (nth o 1))
|
||||
(throw (UnsupportedOperationException. "invalid arguments to cons")))))
|
||||
|
||||
(empty [_]
|
||||
(create))
|
||||
|
||||
(containsKey [_ key]
|
||||
(.containsKey ^clojure.lang.IPersistentMap data key))
|
||||
|
||||
(entryAt [this key]
|
||||
(ObjectsMapEntry. this key))
|
||||
|
||||
(valAt [this key]
|
||||
(or (get cache key)
|
||||
(locking this
|
||||
(if (contains? data key)
|
||||
(let [value (get data key)
|
||||
value (t/decode-str value)]
|
||||
(set! (.-cache this) (assoc cache key value))
|
||||
value)
|
||||
(do
|
||||
(set! (.-cache this) (assoc cache key nil))
|
||||
nil)))))
|
||||
|
||||
(valAt [this key not-found]
|
||||
(if (.containsKey ^clojure.lang.IPersistentMap data key)
|
||||
(.valAt this key)
|
||||
not-found))
|
||||
|
||||
(assoc [_ key val]
|
||||
(ObjectsMap. metadata
|
||||
(assoc cache key val)
|
||||
(assoc data key nil)
|
||||
true
|
||||
nil))
|
||||
|
||||
|
||||
(assocEx [_ _ _]
|
||||
(throw (UnsupportedOperationException. "method not implemented")))
|
||||
|
||||
(without [_ key]
|
||||
(ObjectsMap. metadata
|
||||
(dissoc cache key)
|
||||
(dissoc data key)
|
||||
true
|
||||
nil))
|
||||
|
||||
clojure.lang.Counted
|
||||
(count [_]
|
||||
(count data))))
|
||||
|
||||
#?(:cljs (es6-iterable ObjectsMap))
|
||||
|
||||
|
||||
(defn- do-compact
|
||||
[data cache update-fn]
|
||||
(let [new-data
|
||||
(persistent!
|
||||
(reduce-kv (fn [data id obj]
|
||||
(if (nil? obj)
|
||||
(assoc! data id (t/encode-str (get cache id)))
|
||||
data))
|
||||
(transient data)
|
||||
data))]
|
||||
(update-fn new-data)
|
||||
nil))
|
||||
|
||||
(defn from-data
|
||||
[data]
|
||||
(ObjectsMap. {} {}
|
||||
data
|
||||
false
|
||||
nil))
|
||||
|
||||
(defn objects-map?
|
||||
[o]
|
||||
(instance? ObjectsMap o))
|
||||
|
||||
(defn create
|
||||
([] (from-data {}))
|
||||
([other]
|
||||
(cond
|
||||
(objects-map? other)
|
||||
(-> other get-data from-data)
|
||||
|
||||
:else
|
||||
(throw #?(:clj (UnsupportedOperationException. "invalid arguments")
|
||||
:cljs (js/Error. "invalid arguments"))))))
|
||||
|
||||
(defn wrap
|
||||
[objects]
|
||||
(if (instance? ObjectsMap objects)
|
||||
objects
|
||||
(->> objects
|
||||
(into (create))
|
||||
(compact))))
|
||||
|
||||
#?(:clj
|
||||
(fres/add-handlers!
|
||||
{:name "penpot/objects-map/v2"
|
||||
:class ObjectsMap
|
||||
:wfn (fn [n w o]
|
||||
(fres/write-tag! w n)
|
||||
(fres/write-object! w (get-data o)))
|
||||
:rfn (fn [r]
|
||||
(-> r fres/read-object! from-data))}))
|
||||
|
||||
(t/add-handlers!
|
||||
{:id "penpot/objects-map/v2"
|
||||
:class ObjectsMap
|
||||
:wfn get-data
|
||||
:rfn from-data})
|
||||
@@ -26,6 +26,27 @@
|
||||
(mu/keys)
|
||||
(into #{})))
|
||||
|
||||
(defn find-token-value-references
|
||||
"Returns set of token references found in `token-value`.
|
||||
|
||||
Used for checking if a token has a reference in the value.
|
||||
Token references are strings delimited by curly braces.
|
||||
E.g.: {foo.bar.baz} -> foo.bar.baz"
|
||||
[token-value]
|
||||
(if (string? token-value)
|
||||
(some->> (re-seq #"\{([^}]*)\}" token-value)
|
||||
(map second)
|
||||
(into #{}))
|
||||
#{}))
|
||||
|
||||
(defn token-value-self-reference?
|
||||
"Check if the token is self referencing with its `token-name` in `token-value`.
|
||||
Simple 1 level check, doesn't account for circular self refernces across multiple tokens."
|
||||
[token-name token-value]
|
||||
(let [token-references (find-token-value-references token-value)
|
||||
self-reference? (get token-references token-name)]
|
||||
self-reference?))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -52,7 +73,24 @@
|
||||
:typography "typography"})
|
||||
|
||||
(def dtcg-token-type->token-type
|
||||
(set/map-invert token-type->dtcg-token-type))
|
||||
(-> (set/map-invert token-type->dtcg-token-type)
|
||||
;; Allow these properties to be imported with singular key names for backwards compability
|
||||
(assoc "fontWeight" :font-weight
|
||||
"fontSize" :font-size
|
||||
"fontFamily" :font-family)))
|
||||
|
||||
(def composite-token-type->dtcg-token-type
|
||||
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
||||
(Penpot doesn't support `:line-height` token)"
|
||||
(assoc token-type->dtcg-token-type
|
||||
:line-height "lineHeights"))
|
||||
|
||||
(def composite-dtcg-token-type->token-type
|
||||
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
||||
(Penpot doesn't support `:line-height` token)"
|
||||
(assoc dtcg-token-type->token-type
|
||||
"lineHeights" :line-height
|
||||
"lineHeight" :line-height))
|
||||
|
||||
(def token-types
|
||||
(into #{} (keys token-type->dtcg-token-type)))
|
||||
@@ -217,7 +255,8 @@
|
||||
text-case-keys
|
||||
text-decoration-keys
|
||||
font-weight-keys
|
||||
typography-token-keys))
|
||||
typography-token-keys
|
||||
#{:line-height}))
|
||||
|
||||
;; TODO: Created to extract the font-size feature from the typography feature flag.
|
||||
;; Delete this once the typography feature flag is removed.
|
||||
@@ -289,6 +328,7 @@
|
||||
(font-size-keys shape-attr) #{shape-attr :typography}
|
||||
(letter-spacing-keys shape-attr) #{shape-attr :typography}
|
||||
(font-family-keys shape-attr) #{shape-attr :typography}
|
||||
(= :line-height shape-attr) #{:line-height :typography}
|
||||
(= :text-transform shape-attr) #{:text-case :typography}
|
||||
(text-decoration-keys shape-attr) #{shape-attr :typography}
|
||||
(font-weight-keys shape-attr) #{shape-attr :typography}
|
||||
@@ -468,3 +508,30 @@
|
||||
(when (font-weight-values weight)
|
||||
(cond-> {:weight weight}
|
||||
italic? (assoc :style "italic")))))
|
||||
|
||||
(defn typography-composite-token-reference?
|
||||
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
(def tokens-by-input
|
||||
"A map from input name to applicable token for that input."
|
||||
{:width #{:sizing :dimensions}
|
||||
:height #{:sizing :dimensions}
|
||||
:max-width #{:sizing :dimensions}
|
||||
:max-height #{:sizing :dimensions}
|
||||
:x #{:spacing :dimensions}
|
||||
:y #{:spacing :dimensions}
|
||||
:rotation #{:number :rotation}
|
||||
:border-radius #{:border-radius :dimensions}
|
||||
:row-gap #{:spacing :dimensions}
|
||||
:column-gap #{:spacing :dimensions}
|
||||
:horizontal-padding #{:spacing :dimensions}
|
||||
:vertical-padding #{:spacing :dimensions}
|
||||
:sided-paddings #{:spacing :dimensions}
|
||||
:horizontal-margin #{:spacing :dimensions}
|
||||
:vertical-margin #{:spacing :dimensions}
|
||||
:sided-margins #{:spacing :dimensions}
|
||||
:line-height #{:line-height :number}
|
||||
:font-size #{:font-size}
|
||||
:letter-spacing #{:letter-spacing}})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@
|
||||
(ns app.common.types.variant
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.math :as math]
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.schema :as sm]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
(def property-max-length 60)
|
||||
(def value-prefix "Value ")
|
||||
|
||||
|
||||
(defn properties-to-name
|
||||
"Transform the properties into a name, with the values separated by comma"
|
||||
[properties]
|
||||
@@ -59,7 +58,6 @@
|
||||
(remove str/empty?)
|
||||
(str/join ", ")))
|
||||
|
||||
|
||||
(defn next-property-number
|
||||
"Returns the next property number, to avoid duplicates on the property names"
|
||||
[properties]
|
||||
@@ -92,7 +90,7 @@
|
||||
([path properties]
|
||||
(path-to-properties path properties 0))
|
||||
([path properties min-props]
|
||||
(let [cpath (cfh/split-path path)
|
||||
(let [cpath (cpn/split-path path)
|
||||
total-props (max (count cpath) min-props)
|
||||
assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range))
|
||||
;; Add empty strings to the end of cpath to reach the minimum number of properties
|
||||
@@ -100,7 +98,6 @@
|
||||
remaining (drop (count properties) cpath)]
|
||||
(add-new-props assigned remaining))))
|
||||
|
||||
|
||||
(defn properties-map->formula
|
||||
"Transforms a map of properties to a formula of properties omitting the empty ones"
|
||||
[properties]
|
||||
@@ -110,7 +107,6 @@
|
||||
(str name "=" value))))
|
||||
(str/join ", ")))
|
||||
|
||||
|
||||
(defn properties-formula->map
|
||||
"Transforms a formula of properties to a map of properties"
|
||||
[s]
|
||||
@@ -121,7 +117,6 @@
|
||||
{:name (str/trim k)
|
||||
:value (str/trim v)}))))
|
||||
|
||||
|
||||
(defn valid-properties-formula?
|
||||
"Checks if a formula is valid"
|
||||
[s]
|
||||
@@ -138,21 +133,18 @@
|
||||
(let [upd-names (set (map :name upd-props))]
|
||||
(filterv #(not (contains? upd-names (:name %))) prev-props)))
|
||||
|
||||
|
||||
(defn find-properties-to-update
|
||||
"Compares two property maps to find which properties should be updated"
|
||||
[prev-props upd-props]
|
||||
(filterv #(some (fn [prop] (and (= (:name %) (:name prop))
|
||||
(not= (:value %) (:value prop)))) prev-props) upd-props))
|
||||
|
||||
|
||||
(defn find-properties-to-add
|
||||
"Compares two property maps to find which properties should be added"
|
||||
[prev-props upd-props]
|
||||
(let [prev-names (set (map :name prev-props))]
|
||||
(filterv #(not (contains? prev-names (:name %))) upd-props)))
|
||||
|
||||
|
||||
(defn- split-base-name-and-number
|
||||
"Extract the number in parentheses from an item, if present, and return both the base name and the number"
|
||||
[item]
|
||||
@@ -192,7 +184,6 @@
|
||||
:value (:value prop)}))
|
||||
[])))
|
||||
|
||||
|
||||
(defn find-index-for-property-name
|
||||
"Finds the index of a name in a property map"
|
||||
[props name]
|
||||
@@ -318,4 +309,4 @@
|
||||
"Transforms a variant-name (its properties values) into a standard name:
|
||||
the real name of the shape joined by the properties values separated by '/'"
|
||||
[variant]
|
||||
(cfh/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
|
||||
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
#?(:cljs
|
||||
(defn weak-map
|
||||
"Create a WeakMap like instance what uses clojure equality
|
||||
"Create a WeakMap-like instance what uses clojure equality
|
||||
semantics."
|
||||
[]
|
||||
(new wm/WeakEqMap #js {:hash hash :equals =})))
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.test :as t]
|
||||
[common-tests.types.shape-decode-encode-test :refer [json-roundtrip]]))
|
||||
|
||||
|
||||
@@ -446,3 +446,35 @@
|
||||
(t/is (= (count fills') 1))
|
||||
(t/is (= (:fill-color fill') "#fabada"))
|
||||
(t/is (= (:fill-opacity fill') 1))))
|
||||
|
||||
(t/deftest test-detach-copy-in-main
|
||||
(let [;; ==== Setup
|
||||
file (-> (setup-file)
|
||||
(thc/instantiate-component :c-big-board
|
||||
:copy-big-board
|
||||
:children-labels [:copy-h-board-with-ellipse
|
||||
:copy-nested-h-ellipse
|
||||
:copy-nested-ellipse]))
|
||||
|
||||
page (thf/current-page file)
|
||||
|
||||
;; ==== Action
|
||||
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects (:objects page)))
|
||||
page
|
||||
{(:id file) file}
|
||||
(thi/id :nested-h-ellipse))
|
||||
file' (-> (thf/apply-changes file changes)
|
||||
(tho/propagate-component-changes :c-board-with-ellipse)
|
||||
(tho/propagate-component-changes :c-big-board))
|
||||
|
||||
;; ==== Get
|
||||
nested2-h-ellipse (ths/get-shape file' :nested-h-ellipse)
|
||||
copy-nested2-h-ellipse (ths/get-shape file' :copy-nested-h-ellipse)]
|
||||
|
||||
;; ==== Check
|
||||
|
||||
;; When the nested copy inside the main is detached, their copies are unheaded.
|
||||
(t/is (not (ctk/subcopy-head? nested2-h-ellipse)))
|
||||
(t/is (not (ctk/subcopy-head? copy-nested2-h-ellipse)))))
|
||||
@@ -144,20 +144,21 @@
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tht/add-tokens-lib)
|
||||
(tht/update-tokens-lib #(-> %
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set)
|
||||
:name "test-token-set"))
|
||||
(ctob/add-theme (ctob/make-token-theme :name "test-theme"
|
||||
:sets #{"test-token-set"}))
|
||||
(ctob/set-active-themes #{"/test-theme"})
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-sizing)
|
||||
:name "token-sizing"
|
||||
:type :sizing
|
||||
:value 10))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-spacing)
|
||||
:name "token-spacing"
|
||||
:type :spacing
|
||||
:value 30))))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-sizing)
|
||||
:name "token-sizing"
|
||||
:type :sizing
|
||||
:value 10))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-spacing)
|
||||
:name "token-spacing"
|
||||
:type :spacing
|
||||
:value 30))))
|
||||
(tho/add-frame :frame-1
|
||||
:layout :flex ;; TODO: those values come from main.data.workspace.shape_layout/default-layout-params
|
||||
:layout-flex-dir :row ;; it should be good to use it directly, but first it should be moved to common.logic
|
||||
|
||||
@@ -27,65 +27,66 @@
|
||||
(-> (thf/sample-file :file1)
|
||||
(tht/add-tokens-lib)
|
||||
(tht/update-tokens-lib #(-> %
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set)
|
||||
:name "test-token-set"))
|
||||
(ctob/add-theme (ctob/make-token-theme :name "test-theme"
|
||||
:sets #{"test-token-set"}))
|
||||
(ctob/set-active-themes #{"/test-theme"})
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-radius)
|
||||
:name "token-radius"
|
||||
:type :border-radius
|
||||
:value 10))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-rotation)
|
||||
:name "token-rotation"
|
||||
:type :rotation
|
||||
:value 30))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-opacity)
|
||||
:name "token-opacity"
|
||||
:type :opacity
|
||||
:value 0.7))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-stroke-width)
|
||||
:name "token-stroke-width"
|
||||
:type :stroke-width
|
||||
:value 2))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-color)
|
||||
:name "token-color"
|
||||
:type :color
|
||||
:value "#00ff00"))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-dimensions)
|
||||
:name "token-dimensions"
|
||||
:type :dimensions
|
||||
:value 100))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-font-size)
|
||||
:name "token-font-size"
|
||||
:type :font-size
|
||||
:value 24))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-letter-spacing)
|
||||
:name "token-letter-spacing"
|
||||
:type :letter-spacing
|
||||
:value 2))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-font-family)
|
||||
:name "token-font-family"
|
||||
:type :font-family
|
||||
:value ["Helvetica" "Arial" "sans-serif"]))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-sizing)
|
||||
:name "token-sizing"
|
||||
:type :sizing
|
||||
:value 10))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
(ctob/make-token :id (thi/new-id! :token-spacing)
|
||||
:name "token-spacing"
|
||||
:type :spacing
|
||||
:value 30))))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-radius)
|
||||
:name "token-radius"
|
||||
:type :border-radius
|
||||
:value 10))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-rotation)
|
||||
:name "token-rotation"
|
||||
:type :rotation
|
||||
:value 30))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-opacity)
|
||||
:name "token-opacity"
|
||||
:type :opacity
|
||||
:value 0.7))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-stroke-width)
|
||||
:name "token-stroke-width"
|
||||
:type :stroke-width
|
||||
:value 2))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-color)
|
||||
:name "token-color"
|
||||
:type :color
|
||||
:value "#00ff00"))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-dimensions)
|
||||
:name "token-dimensions"
|
||||
:type :dimensions
|
||||
:value 100))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-font-size)
|
||||
:name "token-font-size"
|
||||
:type :font-size
|
||||
:value 24))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-letter-spacing)
|
||||
:name "token-letter-spacing"
|
||||
:type :letter-spacing
|
||||
:value 2))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-font-family)
|
||||
:name "token-font-family"
|
||||
:type :font-family
|
||||
:value ["Helvetica" "Arial" "sans-serif"]))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-sizing)
|
||||
:name "token-sizing"
|
||||
:type :sizing
|
||||
:value 10))
|
||||
(ctob/add-token (thi/id :test-token-set)
|
||||
(ctob/make-token :id (thi/new-id! :token-spacing)
|
||||
:name "token-spacing"
|
||||
:type :spacing
|
||||
:value 30))))
|
||||
(tho/add-frame :frame1
|
||||
:layout :flex ;; TODO: those values come from main.data.workspace.shape_layout/default-layout-params
|
||||
:layout-flex-dir :row ;; it should be good to use it directly, but first it should be moved to common.logic
|
||||
@@ -131,17 +132,17 @@
|
||||
frame1 (ths/get-shape file :frame1)
|
||||
text1 (ths/get-shape file :text1)
|
||||
circle1 (ths/get-shape file :circle1)
|
||||
token-radius (tht/get-token file "test-token-set" (thi/id :token-radius))
|
||||
token-rotation (tht/get-token file "test-token-set" (thi/id :token-rotation))
|
||||
token-opacity (tht/get-token file "test-token-set" (thi/id :token-opacity))
|
||||
token-stroke-width (tht/get-token file "test-token-set" (thi/id :token-stroke-width))
|
||||
token-color (tht/get-token file "test-token-set" (thi/id :token-color))
|
||||
token-dimensions (tht/get-token file "test-token-set" (thi/id :token-dimensions))
|
||||
token-font-size (tht/get-token file "test-token-set" (thi/id :token-font-size))
|
||||
token-letter-spacing (tht/get-token file "test-token-set" (thi/id :token-letter-spacing))
|
||||
token-font-family (tht/get-token file "test-token-set" (thi/id :token-font-family))
|
||||
token-sizing (tht/get-token file "test-token-set" (thi/id :token-sizing))
|
||||
token-spacing (tht/get-token file "test-token-set" (thi/id :token-spacing))
|
||||
token-radius (tht/get-token file (thi/id :test-token-set) (thi/id :token-radius))
|
||||
token-rotation (tht/get-token file (thi/id :test-token-set) (thi/id :token-rotation))
|
||||
token-opacity (tht/get-token file (thi/id :test-token-set) (thi/id :token-opacity))
|
||||
token-stroke-width (tht/get-token file (thi/id :test-token-set) (thi/id :token-stroke-width))
|
||||
token-color (tht/get-token file (thi/id :test-token-set) (thi/id :token-color))
|
||||
token-dimensions (tht/get-token file (thi/id :test-token-set) (thi/id :token-dimensions))
|
||||
token-font-size (tht/get-token file (thi/id :test-token-set) (thi/id :token-font-size))
|
||||
token-letter-spacing (tht/get-token file (thi/id :test-token-set) (thi/id :token-letter-spacing))
|
||||
token-font-family (tht/get-token file (thi/id :test-token-set) (thi/id :token-font-family))
|
||||
token-sizing (tht/get-token file (thi/id :test-token-set) (thi/id :token-sizing))
|
||||
token-spacing (tht/get-token file (thi/id :test-token-set) (thi/id :token-spacing))
|
||||
|
||||
;; ==== Action
|
||||
changes (-> (-> (pcb/empty-changes nil)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.test-helpers.tokens :as tht]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.datafy :refer [datafy]]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
@@ -33,6 +34,7 @@
|
||||
(pcb/with-library-data (:data file))
|
||||
(clt/generate-toggle-token-set (tht/get-tokens-lib file) "foo/bar"))
|
||||
|
||||
_ (prn "changes" changes)
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
@@ -83,127 +85,133 @@
|
||||
|
||||
(t/deftest set-token-theme-test
|
||||
(t/testing "delete token theme"
|
||||
(let [theme-name "foo"
|
||||
group "main"
|
||||
(let [theme-id (uuid/next)
|
||||
file (setup-file #(-> %
|
||||
(ctob/add-theme (ctob/make-token-theme :name theme-name
|
||||
:group group))))
|
||||
(ctob/add-theme (ctob/make-token-theme :id theme-id
|
||||
:name "foo"
|
||||
:group "main"))))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-theme group theme-name nil))
|
||||
(pcb/set-token-theme theme-id nil))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
;; Redo
|
||||
(t/is (nil? (ctob/get-theme redo-lib group theme-name)))
|
||||
(t/is (nil? (ctob/get-theme redo-lib theme-id)))
|
||||
;; Undo
|
||||
(t/is (some? (ctob/get-theme undo-lib group theme-name)))))
|
||||
(t/is (some? (ctob/get-theme undo-lib theme-id)))))
|
||||
|
||||
(t/testing "add token theme"
|
||||
(let [theme-name "foo"
|
||||
group "main"
|
||||
theme (ctob/make-token-theme :name theme-name
|
||||
:group group)
|
||||
(let [theme-id (uuid/next)
|
||||
theme (ctob/make-token-theme :id theme-id
|
||||
:name "foo"
|
||||
:group "main")
|
||||
file (setup-file identity)
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-theme group theme-name theme))
|
||||
(pcb/set-token-theme theme-id theme))
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
;; Redo
|
||||
(t/is (some? (ctob/get-theme redo-lib group theme-name)))
|
||||
(t/is (some? (ctob/get-theme redo-lib theme-id)))
|
||||
;; Undo
|
||||
(t/is (nil? (ctob/get-theme undo-lib group theme-name)))))
|
||||
(t/is (nil? (ctob/get-theme undo-lib theme-id)))))
|
||||
|
||||
(t/testing "update token theme"
|
||||
(let [theme-name "foo"
|
||||
group "main"
|
||||
prev-theme (ctob/make-token-theme :name theme-name
|
||||
:group group)
|
||||
(let [theme-id (uuid/next)
|
||||
prev-theme-name "foo"
|
||||
prev-theme (ctob/make-token-theme :id theme-id
|
||||
:name prev-theme-name
|
||||
:group "main")
|
||||
file (setup-file #(ctob/add-theme % prev-theme))
|
||||
new-theme-name "foo1"
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-theme group new-theme-name prev-theme))
|
||||
(pcb/set-token-theme theme-id (ctob/rename prev-theme new-theme-name)))
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
redo-theme (ctob/get-theme redo-lib theme-id)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
undo-lib (tht/get-tokens-lib undo)
|
||||
undo-theme (ctob/get-theme undo-lib theme-id)]
|
||||
;; Redo
|
||||
(t/is (some? (ctob/get-theme redo-lib group theme-name)))
|
||||
(t/is (nil? (ctob/get-theme redo-lib group new-theme-name)))
|
||||
(t/is (= new-theme-name (ctob/get-name redo-theme)))
|
||||
;; Undo
|
||||
(t/is (some? (ctob/get-theme undo-lib group theme-name)))
|
||||
(t/is (nil? (ctob/get-theme undo-lib group new-theme-name)))))
|
||||
(t/is (= prev-theme-name (ctob/get-name undo-theme)))))
|
||||
|
||||
(t/testing "toggling token theme updates using changes history"
|
||||
(let [theme-name "foo-theme"
|
||||
group "main"
|
||||
(let [theme-id (uuid/next)
|
||||
theme (ctob/make-token-theme :id theme-id
|
||||
:name "foo-theme"
|
||||
:group "main")
|
||||
set-name "bar-set"
|
||||
token-set (ctob/make-token-set :name set-name)
|
||||
theme (ctob/make-token-theme :name theme-name
|
||||
:group group)
|
||||
file (setup-file #(-> %
|
||||
(ctob/add-theme theme)
|
||||
(ctob/add-set token-set)))
|
||||
theme' (assoc theme :sets #{set-name})
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-theme group theme-name theme'))
|
||||
(pcb/set-token-theme theme-id theme'))
|
||||
changed-file (-> file
|
||||
(thf/apply-changes changes)
|
||||
(thf/apply-undo-changes changes)
|
||||
(thf/apply-changes changes))
|
||||
changed-lib (tht/get-tokens-lib changed-file)]
|
||||
(t/is (= #{set-name}
|
||||
(-> changed-lib (ctob/get-theme group theme-name) :sets))))))
|
||||
(-> changed-lib (ctob/get-theme theme-id) :sets))))))
|
||||
|
||||
(t/deftest set-token-test
|
||||
(t/testing "delete token"
|
||||
(let [set-name "foo"
|
||||
set-id (uuid/next)
|
||||
token-id (uuid/next)
|
||||
file (setup-file #(-> %
|
||||
(ctob/add-set (ctob/make-token-set :name set-name))
|
||||
(ctob/add-token-in-set set-name (ctob/make-token {:name "to.delete.color.red"
|
||||
:id token-id
|
||||
:value "red"
|
||||
:type :color}))))
|
||||
(ctob/add-set (ctob/make-token-set :id set-id
|
||||
:name set-name))
|
||||
(ctob/add-token set-id (ctob/make-token {:name "to.delete.color.red"
|
||||
:id token-id
|
||||
:value "red"
|
||||
:type :color}))))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token set-name token-id nil))
|
||||
(pcb/set-token set-id token-id nil))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
(t/is (nil? (ctob/get-token-in-set redo-lib set-name token-id)))
|
||||
(t/is (nil? (ctob/get-token redo-lib set-id token-id)))
|
||||
;; Undo
|
||||
(t/is (some? (ctob/get-token-in-set undo-lib set-name token-id)))))
|
||||
(t/is (some? (ctob/get-token undo-lib set-id token-id)))))
|
||||
|
||||
(t/testing "add token"
|
||||
(let [set-name "foo"
|
||||
set-id (uuid/next)
|
||||
token (ctob/make-token {:name "to.add.color.red"
|
||||
:value "red"
|
||||
:type :color})
|
||||
file (setup-file #(-> % (ctob/add-set (ctob/make-token-set :name set-name))))
|
||||
file (setup-file #(-> % (ctob/add-set (ctob/make-token-set :id set-id
|
||||
:name set-name))))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token set-name (:id token) token))
|
||||
(pcb/set-token set-id (:id token) token))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
(t/is (= token (ctob/get-token-in-set redo-lib set-name (:id token))))
|
||||
(t/is (= token (ctob/get-token redo-lib set-id (:id token))))
|
||||
;; Undo
|
||||
(t/is (nil? (ctob/get-token-in-set undo-lib set-name (:id token))))))
|
||||
(t/is (nil? (ctob/get-token undo-lib set-id (:id token))))))
|
||||
|
||||
(t/testing "update token"
|
||||
(let [set-name "foo"
|
||||
set-id (uuid/next)
|
||||
prev-token (ctob/make-token {:name "to.update.color.red"
|
||||
:value "red"
|
||||
:type :color})
|
||||
@@ -211,27 +219,29 @@
|
||||
(assoc :name "color.red.changed")
|
||||
(assoc :value "blue"))
|
||||
file (setup-file #(-> %
|
||||
(ctob/add-set (ctob/make-token-set :name set-name))
|
||||
(ctob/add-token-in-set set-name prev-token)))
|
||||
(ctob/add-set (ctob/make-token-set :id set-id
|
||||
:name set-name))
|
||||
(ctob/add-token set-id prev-token)))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token set-name (:id prev-token) token))
|
||||
(pcb/set-token set-id (:id prev-token) token))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
(t/is (tht/token-data-eq? token (ctob/get-token-in-set redo-lib set-name (:id token))))
|
||||
(t/is (tht/token-data-eq? token (ctob/get-token redo-lib set-id (:id token))))
|
||||
;; Undo
|
||||
(t/is (tht/token-data-eq? prev-token (ctob/get-token-in-set undo-lib set-name (:id prev-token)))))))
|
||||
(t/is (tht/token-data-eq? prev-token (ctob/get-token undo-lib set-id (:id prev-token)))))))
|
||||
|
||||
(t/deftest set-token-set-test
|
||||
(t/testing "delete token set"
|
||||
(let [set-name "foo"
|
||||
file (setup-file #(ctob/add-set % (ctob/make-token-set :name set-name)))
|
||||
set-id (uuid/next)
|
||||
file (setup-file #(ctob/add-set % (ctob/make-token-set :id set-id :name set-name)))
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-set set-name false nil))
|
||||
(pcb/set-token-set set-id nil))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
@@ -243,11 +253,12 @@
|
||||
|
||||
(t/testing "add token set"
|
||||
(let [set-name "foo"
|
||||
token-set (ctob/make-token-set :name set-name)
|
||||
set-id (uuid/next)
|
||||
token-set (ctob/make-token-set :id set-id :name set-name)
|
||||
file (setup-file identity)
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-set set-name false token-set))
|
||||
(pcb/set-token-set set-id token-set))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
@@ -259,28 +270,26 @@
|
||||
|
||||
(t/testing "update token set"
|
||||
(let [set-name "foo"
|
||||
token-name "bar"
|
||||
token (ctob/make-token {:name token-name
|
||||
:value "red"
|
||||
:type :color})
|
||||
file (setup-file #(-> (ctob/add-set % (ctob/make-token-set :name set-name))
|
||||
(ctob/add-token-in-set set-name token)))
|
||||
prev-token-set (-> file tht/get-tokens-lib (ctob/get-set set-name))
|
||||
set-id (uuid/next)
|
||||
token-set (ctob/make-token-set :id set-id :name set-name)
|
||||
file (setup-file #(-> (ctob/add-set % token-set)))
|
||||
new-set-name "foo1"
|
||||
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data (:data file))
|
||||
(pcb/set-token-set set-name false (ctob/rename prev-token-set new-set-name)))
|
||||
(pcb/set-token-set set-id (ctob/rename token-set new-set-name)))
|
||||
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)]
|
||||
redo-token-set (ctob/get-set redo-lib set-id)
|
||||
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-lib (tht/get-tokens-lib undo)
|
||||
undo-token-set (ctob/get-set undo-lib set-id)]
|
||||
|
||||
(t/is (= (ctob/get-name redo-token-set) new-set-name))
|
||||
;; Undo
|
||||
(t/is (some? (ctob/get-token-in-set undo-lib set-name (:id token))))
|
||||
(t/is (nil? (ctob/get-token-in-set undo-lib new-set-name (:id token))))
|
||||
;; Redo
|
||||
(t/is (nil? (ctob/get-token-in-set redo-lib set-name (:id token))))
|
||||
(t/is (some? (ctob/get-token-in-set redo-lib new-set-name (:id token)))))))
|
||||
(t/is (= (ctob/get-name undo-token-set) set-name)))))
|
||||
|
||||
(t/deftest generate-toggle-token-set-group-test
|
||||
(t/testing "toggling set group with no active sets inside will activate all child sets"
|
||||
@@ -361,13 +370,13 @@
|
||||
:position :top})
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-sets (-> (tht/get-tokens-lib redo)
|
||||
(ctob/get-ordered-set-names))
|
||||
(ctob/get-set-names))
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-sets (-> (tht/get-tokens-lib undo)
|
||||
(ctob/get-ordered-set-names))]
|
||||
(ctob/get-set-names))]
|
||||
(t/is (= ["bar" "foo" "baz"] (vec redo-sets)))
|
||||
(t/testing "undo"
|
||||
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
|
||||
(t/is (= (ctob/get-set-names lib) undo-sets)))))
|
||||
|
||||
(t/testing "at bottom"
|
||||
(let [file (setup-file #(-> %
|
||||
@@ -380,13 +389,13 @@
|
||||
:position :bot})
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-sets (-> (tht/get-tokens-lib redo)
|
||||
(ctob/get-ordered-set-names))
|
||||
(ctob/get-set-names))
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-sets (-> (tht/get-tokens-lib undo)
|
||||
(ctob/get-ordered-set-names))]
|
||||
(ctob/get-set-names))]
|
||||
(t/is (= ["bar" "baz" "foo"] (vec redo-sets)))
|
||||
(t/testing "undo"
|
||||
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
|
||||
(t/is (= (ctob/get-set-names lib) undo-sets)))))
|
||||
|
||||
(t/testing "dropping out of set group"
|
||||
(let [file (setup-file #(-> %
|
||||
@@ -398,13 +407,13 @@
|
||||
:position :top})
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-sets (-> (tht/get-tokens-lib redo)
|
||||
(ctob/get-ordered-set-names))
|
||||
(ctob/get-set-names))
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-sets (-> (tht/get-tokens-lib undo)
|
||||
(ctob/get-ordered-set-names))]
|
||||
(ctob/get-set-names))]
|
||||
(t/is (= ["bar" "foo"] (vec redo-sets)))
|
||||
(t/testing "undo"
|
||||
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
|
||||
(t/is (= (ctob/get-set-names lib) undo-sets)))))
|
||||
|
||||
(t/testing "into set group"
|
||||
(let [file (setup-file #(-> %
|
||||
@@ -416,13 +425,13 @@
|
||||
:position :bot})
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-sets (-> (tht/get-tokens-lib redo)
|
||||
(ctob/get-ordered-set-names))
|
||||
(ctob/get-set-names))
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-sets (-> (tht/get-tokens-lib undo)
|
||||
(ctob/get-ordered-set-names))]
|
||||
(ctob/get-set-names))]
|
||||
(t/is (= ["foo/bar" "foo/foo"] (vec redo-sets)))
|
||||
(t/testing "undo"
|
||||
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
|
||||
(t/is (= (ctob/get-set-names lib) undo-sets)))))
|
||||
|
||||
(t/testing "edge-cases:"
|
||||
(t/testing "prevent overriding set to identical path"
|
||||
@@ -454,13 +463,13 @@
|
||||
:collapsed-paths #{["foo"]}})
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-sets (-> (tht/get-tokens-lib redo)
|
||||
(ctob/get-ordered-set-names))
|
||||
(ctob/get-set-names))
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-sets (-> (tht/get-tokens-lib undo)
|
||||
(ctob/get-ordered-set-names))]
|
||||
(ctob/get-set-names))]
|
||||
(t/is (= ["foo/bar" "foo"] (vec redo-sets)))
|
||||
(t/testing "undo"
|
||||
(t/is (= (ctob/get-ordered-set-names lib) undo-sets))))))))
|
||||
(t/is (= (ctob/get-set-names lib) undo-sets))))))))
|
||||
|
||||
(t/deftest generate-move-token-group-test
|
||||
(t/testing "Ignore dropping set group to the same position"
|
||||
@@ -496,14 +505,14 @@
|
||||
:position :top})
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-sets (-> (tht/get-tokens-lib redo)
|
||||
(ctob/get-ordered-set-names))
|
||||
(ctob/get-set-names))
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-sets (-> (tht/get-tokens-lib undo)
|
||||
(ctob/get-ordered-set-names))]
|
||||
(ctob/get-set-names))]
|
||||
(t/is (= ["bar/bar" "foo/foo" "baz/baz"] (vec redo-sets)))
|
||||
|
||||
(t/testing "undo"
|
||||
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
|
||||
(t/is (= (ctob/get-set-names lib) undo-sets)))))
|
||||
|
||||
(t/testing "to bottom"
|
||||
(let [file (setup-file #(-> %
|
||||
@@ -515,14 +524,14 @@
|
||||
:position :bot})
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-sets (-> (tht/get-tokens-lib redo)
|
||||
(ctob/get-ordered-set-names))
|
||||
(ctob/get-set-names))
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-sets (-> (tht/get-tokens-lib undo)
|
||||
(ctob/get-ordered-set-names))]
|
||||
(ctob/get-set-names))]
|
||||
(t/is (= ["bar" "foo/foo"] (vec redo-sets)))
|
||||
|
||||
(t/testing "undo"
|
||||
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
|
||||
(t/is (= (ctob/get-set-names lib) undo-sets)))))
|
||||
|
||||
(t/testing "into set group"
|
||||
(let [file (setup-file #(-> %
|
||||
@@ -534,13 +543,13 @@
|
||||
:position :bot})
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-sets (-> (tht/get-tokens-lib redo)
|
||||
(ctob/get-ordered-set-names))
|
||||
(ctob/get-set-names))
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
undo-sets (-> (tht/get-tokens-lib undo)
|
||||
(ctob/get-ordered-set-names))]
|
||||
(ctob/get-set-names))]
|
||||
(t/is (= ["bar/foo/foo" "bar/bar"] (vec redo-sets)))
|
||||
(t/testing "undo"
|
||||
(t/is (= (ctob/get-ordered-set-names lib) undo-sets))))
|
||||
(t/is (= (ctob/get-set-names lib) undo-sets))))
|
||||
|
||||
(t/testing "edge-cases:"
|
||||
(t/testing "prevent overriding set to identical path"
|
||||
|
||||
@@ -1,19 +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 common-tests.pages-helpers-test
|
||||
(:require
|
||||
[app.common.files.helpers :as cfh]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest parse-path-name
|
||||
(t/is (= ["foo" "bar"] (cfh/parse-path-name "foo/bar")))
|
||||
(t/is (= ["" "foo"] (cfh/parse-path-name "foo")))
|
||||
(t/is (= ["" "foo"] (cfh/parse-path-name "/foo")))
|
||||
(t/is (= ["" ""] (cfh/parse-path-name "")))
|
||||
(t/is (= ["" ""] (cfh/parse-path-name nil))))
|
||||
|
||||
33
common/test/common_tests/path_names_test.cljc
Normal file
33
common/test/common_tests/path_names_test.cljc
Normal file
@@ -0,0 +1,33 @@
|
||||
;; 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 common-tests.path-names-test
|
||||
(:require
|
||||
[app.common.path-names :as cpn]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest split-group-name
|
||||
(t/is (= ["foo" "bar"] (cpn/split-group-name "foo/bar")))
|
||||
(t/is (= ["" "foo"] (cpn/split-group-name "foo")))
|
||||
(t/is (= ["" "foo"] (cpn/split-group-name "/foo")))
|
||||
(t/is (= ["" ""] (cpn/split-group-name "")))
|
||||
(t/is (= ["" ""] (cpn/split-group-name nil))))
|
||||
|
||||
(t/deftest split-and-join-path
|
||||
(let [name "group/subgroup/name"
|
||||
path (cpn/split-path name :separator "/")
|
||||
name' (cpn/join-path path :separator "/" :with-spaces? false)]
|
||||
(t/is (= (first path) "group"))
|
||||
(t/is (= (second path) "subgroup"))
|
||||
(t/is (= (nth path 2) "name"))
|
||||
(t/is (= name' name))))
|
||||
|
||||
(t/deftest split-and-join-path-with-spaces
|
||||
(let [name "group / subgroup / name"
|
||||
path (cpn/split-path name :separator "/")]
|
||||
(t/is (= (first path) "group"))
|
||||
(t/is (= (second path) "subgroup"))
|
||||
(t/is (= (nth path 2) "name"))))
|
||||
@@ -30,7 +30,7 @@
|
||||
[common-tests.logic.swap-as-override-test]
|
||||
[common-tests.logic.token-test]
|
||||
[common-tests.media-test]
|
||||
[common-tests.pages-helpers-test]
|
||||
[common-tests.path-names-test]
|
||||
[common-tests.record-test]
|
||||
[common-tests.schema-test]
|
||||
[common-tests.svg-path-test]
|
||||
@@ -41,6 +41,7 @@
|
||||
[common-tests.types.components-test]
|
||||
[common-tests.types.fill-test]
|
||||
[common-tests.types.modifiers-test]
|
||||
[common-tests.types.objects-map-test]
|
||||
[common-tests.types.path-data-test]
|
||||
[common-tests.types.shape-decode-encode-test]
|
||||
[common-tests.types.shape-interactions-test]
|
||||
@@ -81,7 +82,7 @@
|
||||
'common-tests.logic.swap-as-override-test
|
||||
'common-tests.logic.token-test
|
||||
'common-tests.media-test
|
||||
'common-tests.pages-helpers-test
|
||||
'common-tests.path-names-test
|
||||
'common-tests.record-test
|
||||
'common-tests.schema-test
|
||||
'common-tests.svg-path-test
|
||||
@@ -90,9 +91,10 @@
|
||||
'common-tests.time-test
|
||||
'common-tests.types.absorb-assets-test
|
||||
'common-tests.types.components-test
|
||||
'common-tests.types.modifiers-test
|
||||
'common-tests.types.path-data-test
|
||||
'common-tests.types.fill-test
|
||||
'common-tests.types.modifiers-test
|
||||
'common-tests.types.objects-map-test
|
||||
'common-tests.types.path-data-test
|
||||
'common-tests.types.shape-decode-encode-test
|
||||
'common-tests.types.shape-interactions-test
|
||||
'common-tests.types.tokens-lib-test
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"fonts": {
|
||||
"string-font-family": {
|
||||
"$value": "Arial, Helvetica, sans-serif",
|
||||
"$type": "fontFamilies",
|
||||
"$description": "A font family defined as a string"
|
||||
},
|
||||
"array-font-family": {
|
||||
"$value": ["Inter", "system-ui", "sans-serif"],
|
||||
"$type": "fontFamilies",
|
||||
"$description": "A font family defined as an array"
|
||||
},
|
||||
"single-font-family": {
|
||||
"$value": "Georgia",
|
||||
"$type": "fontFamilies"
|
||||
},
|
||||
"complex-font-family": {
|
||||
"$value": "Times New Roman, serif",
|
||||
"$type": "fontFamilies"
|
||||
},
|
||||
"font-with-spaces": {
|
||||
"$value": "Source Sans Pro, Arial, sans-serif",
|
||||
"$type": "fontFamilies"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"test": {
|
||||
"typo": {
|
||||
"$value": {
|
||||
"fontWeight": "100",
|
||||
"fontSize": "16px",
|
||||
"letterSpacing": "0.1em"
|
||||
},
|
||||
"$type": "typography"
|
||||
},
|
||||
"typo2": {
|
||||
"$value": "{typo}",
|
||||
"$type": "typography"
|
||||
},
|
||||
"font-weight": {
|
||||
"$value": "200",
|
||||
"$type": "fontWeights"
|
||||
},
|
||||
"typo-to-single": {
|
||||
"$value": "{font-weight}",
|
||||
"$type": "typography"
|
||||
},
|
||||
"test-empty": {
|
||||
"$value": {},
|
||||
"$type": "typography"
|
||||
},
|
||||
"font-size": {
|
||||
"$value": "18px",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"typo-complex": {
|
||||
"$value": {
|
||||
"fontWeight": "bold",
|
||||
"fontSize": "24px",
|
||||
"letterSpacing": "0.05em",
|
||||
"lineHeights": "100%",
|
||||
"fontFamilies": ["Arial", "sans-serif"],
|
||||
"textCase": "uppercase"
|
||||
},
|
||||
"$type": "typography",
|
||||
"$description": "A complex typography token"
|
||||
},
|
||||
"typo-with-string-font-family": {
|
||||
"$value": {
|
||||
"fontWeight": "600",
|
||||
"fontSize": "20px",
|
||||
"fontFamilies": "Roboto, Helvetica, sans-serif"
|
||||
},
|
||||
"$type": "typography",
|
||||
"$description": "Typography token with string font family"
|
||||
}
|
||||
}
|
||||
}
|
||||
133
common/test/common_tests/types/objects_map_test.cljc
Normal file
133
common/test/common_tests/types/objects_map_test.cljc
Normal file
@@ -0,0 +1,133 @@
|
||||
;; 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 common-tests.types.objects-map-test
|
||||
(:require
|
||||
#?(:clj [app.common.fressian :as fres])
|
||||
[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.common.transit :as transit]
|
||||
[app.common.types.objects-map :as omap]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.plugins :refer [schema:plugin-data]]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.datafy :refer [datafy]]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest basic-operations
|
||||
(t/testing "assoc"
|
||||
(let [id (uuid/custom 0 1)
|
||||
id' (uuid/custom 0 2)
|
||||
obj (-> (omap/create) (assoc id {:foo 1}))]
|
||||
(t/is (not= id id'))
|
||||
(t/is (not (contains? obj id')))
|
||||
(t/is (contains? obj id))))
|
||||
|
||||
(t/testing "assoc-with-non-uuid-keys"
|
||||
(let [obj (-> (omap/create)
|
||||
(assoc :a {:foo 1})
|
||||
(assoc :b {:bar 1}))]
|
||||
(t/is (not (contains? obj :c)))
|
||||
(t/is (contains? obj :a))
|
||||
(t/is (contains? obj :b))))
|
||||
|
||||
(t/testing "dissoc"
|
||||
(let [id (uuid/custom 0 1)
|
||||
obj (-> (omap/create) (assoc id {:foo 1}))]
|
||||
(t/is (contains? obj id))
|
||||
(let [obj (dissoc obj id)]
|
||||
(t/is (not (contains? obj id))))))
|
||||
|
||||
(t/testing "seq"
|
||||
(let [id (uuid/custom 0 1)
|
||||
obj (-> (omap/create) (assoc id 1))]
|
||||
(t/is (contains? obj id))
|
||||
(let [[entry] (seq obj)]
|
||||
(t/is (map-entry? entry))
|
||||
(t/is (= (key entry) id))
|
||||
(t/is (= (val entry) 1)))))
|
||||
|
||||
(t/testing "cons & count"
|
||||
(let [obj (into (omap/create) [[uuid/zero 1]])]
|
||||
(t/is (contains? obj uuid/zero))
|
||||
(t/is (= 1 (count obj)))
|
||||
(t/is (omap/objects-map? obj))))
|
||||
|
||||
(t/testing "wrap"
|
||||
(let [obj1 (omap/wrap {})
|
||||
tmp (omap/create)
|
||||
obj2 (omap/wrap tmp)]
|
||||
(t/is (omap/objects-map? obj1))
|
||||
(t/is (omap/objects-map? obj2))
|
||||
(t/is (identical? tmp obj2))
|
||||
(t/is (= 0 (count obj1)))
|
||||
(t/is (= 0 (count obj2))))))
|
||||
|
||||
(t/deftest internal-state
|
||||
(t/testing "modified & compact"
|
||||
(let [obj (-> (omap/create)
|
||||
(assoc :a 1)
|
||||
(assoc :b 2))]
|
||||
(t/is (= 2 (count obj)))
|
||||
(t/is (-> obj datafy :modified))
|
||||
(let [obj (omap/compact obj)]
|
||||
(t/is (not (-> obj datafy :modified))))))
|
||||
|
||||
(t/testing "create from other"
|
||||
(let [obj1 (-> (omap/create)
|
||||
(assoc :a {:foo 1})
|
||||
(assoc :b {:bar 2}))
|
||||
obj2 (omap/create obj1)]
|
||||
|
||||
(t/is (not (identical? obj1 obj2)))
|
||||
(t/is (= obj1 obj2))
|
||||
(t/is (= (hash obj1) (hash obj2)))
|
||||
(t/is (= (get obj1 :a) (get obj2 :a)))
|
||||
(t/is (= (get obj1 :b) (get obj2 :b))))))
|
||||
|
||||
(t/deftest creation-and-duplication
|
||||
(smt/check!
|
||||
(smt/for [data (->> (sg/map-of (sg/uuid) (sg/generator cts/schema:shape))
|
||||
(sg/not-empty))]
|
||||
(let [obj1 (omap/wrap data)
|
||||
obj2 (omap/create obj1)]
|
||||
(and (= (hash obj1) (hash obj2))
|
||||
(= obj1 obj2))))
|
||||
{:num 100}))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest fressian-encode-decode
|
||||
(smt/check!
|
||||
(smt/for [data (->> (sg/map-of (sg/uuid) (sg/generator cts/schema:shape))
|
||||
(sg/not-empty)
|
||||
(sg/fmap omap/wrap)
|
||||
(sg/fmap (fn [o] {:objects o})))]
|
||||
|
||||
(let [res (-> data fres/encode fres/decode)]
|
||||
(and (contains? res :objects)
|
||||
(omap/objects-map? (:objects res))
|
||||
(= res data))))
|
||||
{:num 100})))
|
||||
|
||||
(t/deftest transit-encode-decode
|
||||
(smt/check!
|
||||
(smt/for [data (->> (sg/map-of (sg/uuid) (sg/generator cts/schema:shape))
|
||||
(sg/not-empty)
|
||||
(sg/fmap omap/wrap)
|
||||
(sg/fmap (fn [o] {:objects o})))]
|
||||
(let [res (-> data transit/encode-str transit/decode-str)]
|
||||
;; (app.common.pprint/pprint data)
|
||||
;; (app.common.pprint/pprint res)
|
||||
(and (every? (fn [[k v]]
|
||||
(= v (get-in data [:objects k])))
|
||||
(:objects res))
|
||||
(omap/objects-map? (:objects data))
|
||||
(omap/objects-map? (:objects res)))))
|
||||
{:num 100}))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -113,12 +113,12 @@ RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
case "${ARCH}" in \
|
||||
aarch64|arm64) \
|
||||
ESUM='6f8725d186d05c627176db9c46c732a6ef3ba41d9e9b3775c4727fc8ac642bb2'; \
|
||||
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_aarch64_linux_hotspot_24.0.2_12.tar.gz'; \
|
||||
ESUM='b60eb9d54c97ba4159547834a98cc5d016281dd2b3e60e7475cba4911324bcb4'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_aarch64.tar.gz'; \
|
||||
;; \
|
||||
amd64|x86_64) \
|
||||
ESUM='aea1cc55e51cf651c85f2f00ad021603fe269c4bb6493fa97a321ad770c9b096'; \
|
||||
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_x64_linux_hotspot_24.0.2_12.tar.gz'; \
|
||||
ESUM='164d901e5a240b8c18516f5ab55bc11fc9689ab6e829045aea8467356dcdb340'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_x64.tar.gz'; \
|
||||
;; \
|
||||
*) \
|
||||
echo "Unsupported arch: ${ARCH}"; \
|
||||
@@ -183,8 +183,8 @@ RUN set -eux; \
|
||||
|
||||
FROM base AS setup-utils
|
||||
|
||||
ENV CLJKONDO_VERSION=2025.01.16 \
|
||||
BABASHKA_VERSION=1.12.207 \
|
||||
ENV CLJKONDO_VERSION=2025.07.28 \
|
||||
BABASHKA_VERSION=1.12.208 \
|
||||
CLJFMT_VERSION=0.13.1
|
||||
|
||||
RUN set -ex; \
|
||||
@@ -310,6 +310,7 @@ RUN set -ex; \
|
||||
fonts-wqy-zenhei \
|
||||
fonts-tlwg-loma-otf \
|
||||
fonts-freefont-ttf \
|
||||
poppler-utils \
|
||||
\
|
||||
libasound2t64 \
|
||||
libatk-bridge2.0-0t64 \
|
||||
|
||||
@@ -39,7 +39,7 @@ tmux new-window -t penpot:3 -n 'exporter'
|
||||
tmux select-window -t penpot:3
|
||||
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
||||
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
|
||||
tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter
|
||||
tmux send-keys -t penpot 'yarn run watch' enter
|
||||
|
||||
tmux split-window -v
|
||||
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
||||
|
||||
@@ -87,14 +87,9 @@ RUN set -ex; \
|
||||
apt-get -qq upgrade; \
|
||||
apt-get -qqy --no-install-recommends install \
|
||||
ca-certificates \
|
||||
curl \
|
||||
fontconfig \
|
||||
fontforge \
|
||||
python3 \
|
||||
python3-tabulate \
|
||||
tzdata \
|
||||
woff-tools \
|
||||
woff2 \
|
||||
\
|
||||
libfontconfig1 \
|
||||
libfreetype6 \
|
||||
libglib2.0-0 \
|
||||
@@ -113,6 +108,11 @@ RUN set -ex; \
|
||||
libxml2 \
|
||||
libzip4t64 \
|
||||
libzstd1 \
|
||||
python3 \
|
||||
python3-tabulate \
|
||||
tzdata \
|
||||
woff-tools \
|
||||
woff2 \
|
||||
; \
|
||||
find tmp/usr/share/zoneinfo/* -type d ! -name 'Etc' |xargs rm -rf; \
|
||||
rm -rf /var/lib /var/cache; \
|
||||
@@ -126,7 +126,9 @@ RUN set -ex; \
|
||||
COPY --from=build /opt/jre /opt/jre
|
||||
COPY --from=build /opt/node /opt/node
|
||||
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
|
||||
COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-backend/"
|
||||
ADD --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/backend/
|
||||
|
||||
USER penpot:penpot
|
||||
WORKDIR /opt/penpot/backend
|
||||
|
||||
@@ -89,7 +89,8 @@ RUN set -eux; \
|
||||
mkdir -p /opt/penpot; \
|
||||
chown -R penpot:penpot /opt/penpot;
|
||||
|
||||
ADD --chown=penpot:penpot ./bundle-exporter/ /opt/penpot/exporter
|
||||
ARG BUNDLE_PATH="./bundle-exporter/"
|
||||
ADD --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/exporter/
|
||||
|
||||
WORKDIR /opt/penpot/exporter
|
||||
USER penpot:penpot
|
||||
|
||||
@@ -6,14 +6,18 @@ USER root
|
||||
RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
mkdir -p /opt/data/assets; \
|
||||
chown -R penpot:penpot /opt/data;
|
||||
chown -R penpot:penpot /opt/data; \
|
||||
mkdir -p /etc/nginx/overrides/http.d/; \
|
||||
mkdir -p /etc/nginx/overrides/server.d/; \
|
||||
mkdir -p /etc/nginx/overrides/location.d/;
|
||||
|
||||
ADD ./bundle-frontend/ /var/www/app/
|
||||
ARG BUNDLE_PATH="./bundle-frontend/"
|
||||
ADD $BUNDLE_PATH /var/www/app/
|
||||
ADD ./files/config.js /var/www/app/js/config.js
|
||||
ADD ./files/nginx.conf /etc/nginx/nginx.conf.template
|
||||
ADD ./files/nginx-proxies.conf /etc/nginx/nginx-proxies.conf
|
||||
ADD ./files/resolvers.conf /etc/nginx/overrides.d/resolvers.conf.template
|
||||
ADD ./files/nginx.conf.template /tmp/nginx.conf.template
|
||||
ADD ./files/nginx-resolvers.conf.template /tmp/resolvers.conf.template
|
||||
ADD ./files/nginx-mime.types /etc/nginx/mime.types
|
||||
ADD ./files/nginx-external-locations.conf /etc/nginx/overrides/location.d/external-locations.conf
|
||||
ADD ./files/nginx-entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chown -R 1001:0 /var/cache/nginx; \
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
#########################################
|
||||
|
||||
if [[ $PENPOT_FLAGS == *"enable-air-gapped-conf"* ]]; then
|
||||
export INCLUDE_PROXIES=""
|
||||
rm /etc/nginx/overrides/location.d/external-locations.conf;
|
||||
export PENPOT_FLAGS="$PENPOT_FLAGS disable-google-fonts-provider disable-dashboard-templates-section"
|
||||
else
|
||||
export INCLUDE_PROXIES="include /etc/nginx/nginx-proxies.conf;"
|
||||
fi
|
||||
|
||||
#########################################
|
||||
@@ -33,14 +31,13 @@ update_flags /var/www/app/js/config.js
|
||||
|
||||
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
|
||||
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
||||
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
|
||||
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
|
||||
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
|
||||
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
|
||||
export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-$PENPOT_DEFAULT_INTERNAL_RESOLVER}
|
||||
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
|
||||
|
||||
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE,\$INCLUDE_PROXIES" \
|
||||
< /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
|
||||
envsubst "\$PENPOT_INTERNAL_RESOLVER" \
|
||||
< /etc/nginx/overrides.d/resolvers.conf.template > /etc/nginx/overrides.d/resolvers.conf
|
||||
< /tmp/resolvers.conf.template > /etc/nginx/overrides/http.d/resolvers.conf
|
||||
|
||||
exec "$@";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
worker_processes auto;
|
||||
pid /tmp/nginx.pid;
|
||||
include /etc/nginx/modules-enabled/*.conf;
|
||||
include /etc/nginx/overrides/main.d/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 2048;
|
||||
# multi_accept on;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
@@ -33,6 +33,11 @@ http {
|
||||
error_log /dev/stderr;
|
||||
access_log /dev/stdout;
|
||||
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
send_timeout 300s;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
@@ -41,7 +46,7 @@ http {
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
||||
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
|
||||
@@ -57,7 +62,14 @@ http {
|
||||
proxy_cache_valid any 48h;
|
||||
proxy_cache_key "$host$request_uri";
|
||||
|
||||
include /etc/nginx/overrides.d/*.conf;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
include /etc/nginx/overrides/http.d/*.conf;
|
||||
|
||||
server {
|
||||
listen 8080 default_server;
|
||||
@@ -66,13 +78,6 @@ http {
|
||||
client_max_body_size $PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE;
|
||||
charset utf-8;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
etag off;
|
||||
|
||||
root /var/www/app/;
|
||||
@@ -119,12 +124,10 @@ http {
|
||||
|
||||
location /api {
|
||||
proxy_pass $PENPOT_BACKEND_URI/api;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /readyz {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass $PENPOT_BACKEND_URI$request_uri;
|
||||
}
|
||||
|
||||
@@ -134,8 +137,10 @@ http {
|
||||
proxy_pass $PENPOT_BACKEND_URI/ws/notifications;
|
||||
}
|
||||
|
||||
include /etc/nginx/overrides/server.d/*.conf;
|
||||
|
||||
location / {
|
||||
$INCLUDE_PROXIES
|
||||
include /etc/nginx/overrides/location.d/*.conf;
|
||||
|
||||
location ~ ^/js/config.js$ {
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
@@ -234,7 +234,7 @@ Use variables from `frontend/src/app/main/ui/ds/spacing.scss`. These are predefi
|
||||
For fixed dimensions (e.g., modals' widths) defined by design and not layout-driven, use or define variables in `frontend/src/app/main/ui/ds/_sizes.scss`. To use them:
|
||||
|
||||
```scss
|
||||
@use "../_sizes.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
```
|
||||
Note: Since these values haven't been semantically defined yet, we’re temporarily using SASS variables instead of named CSS custom properties.
|
||||
|
||||
@@ -242,7 +242,7 @@ Note: Since these values haven't been semantically defined yet, we’re temporar
|
||||
Use border thickness variables from `frontend/src/app/main/ui/ds/_borders.scss`. To import:
|
||||
|
||||
```scss
|
||||
@use "../_borders.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
```
|
||||
|
||||
Avoid using sass variables defined on `frontend/resources/styles/common/refactor/spacing.scss` that are deprecated.
|
||||
@@ -314,7 +314,7 @@ When applying typography in SCSS, use the proper mixin from the Design System.
|
||||
|
||||
✅ **DO: Use the DS mixin**
|
||||
```scss
|
||||
@use "../ds/typography.scss" as t;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
.class {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
@@ -122,7 +122,7 @@ desc: Discover Penpot's free user guide! Learn the interface, workspace basics,
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Teams:</strong> A team allows you to collaborate with other Penpot users. Team members are allowed to work with any project or file within the team depending on their permissions. Members with admin permissions can also invite other members. Create or join as many teams as you need with different groups of people.</li>
|
||||
<li><strong>Teams:</strong> A team allows you to collaborate with other Penpot users. Team members are allowed to work with any project or file within the team depending on their permissions. Members with admin permissions can also invite other members. <a href="/user-guide/teams/#teams-management">Create or join as many teams as you need</a> with different groups of people.</li>
|
||||
<li><strong>Search:</strong> If you are looking for a specific file just type its title at the search box.</li>
|
||||
<li><strong>Projects:</strong> A project allows you to group design files. It works pretty much like a folder in a file system. You can create as many projects as you need. If you are going to work with more people in a project, you should create it inside a team.</li>
|
||||
<li><strong>Drafts:</strong> The drafts section is where you can find the design files that are not inside any project.</li>
|
||||
@@ -200,4 +200,4 @@ desc: Discover Penpot's free user guide! Learn the interface, workspace basics,
|
||||
<img src="/img/interface/viewmode-light.webp" alt="Penpot's view mode" />
|
||||
</a>
|
||||
<figcaption>Penpot's view mode in light mode</figcaption>
|
||||
</figure>
|
||||
</figure>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user