Compare commits

...

103 Commits

Author SHA1 Message Date
Alejandro Alonso
ed5875f29a Merge pull request #7154 from penpot/niwinz-staging-bug-1
🐛 Fix incorrect show request-access dialog on not-found on viewer
2025-08-22 09:19:47 +02:00
Andrey Antukh
ad38a21053 🐛 Fix incorrect show request-access dialog on not-found on viewer
When a user is not-authenticated
2025-08-20 13:35:20 +02:00
Andrey Antukh
adffac4eec Merge remote-tracking branch 'origin/main' into staging 2025-08-20 12:49:31 +02:00
Yamila Moreno
73dfe12ec9 📚 Update k8s documentation 2025-08-20 09:04:25 +02:00
Eva Marco
ff2e845f2c 🐛 Fix double click on set name input (#7096) 2025-08-13 09:23:53 +02:00
Alejandro Alonso
8e0a6e4123 🐛 Fix auto height is fixed in the HTML inspect tab for text elements (#7078) 2025-08-11 09:07:43 +02:00
Marina López
0131cd6f8b Display the total price of the subscription and the cap amount (#7088) 2025-08-11 09:07:24 +02:00
Andrey Antukh
288a7b21d6 Merge tag '2.9.0-RC8' 2025-08-08 09:47:42 +02:00
andrés gonzález
32bd08533d 💄 Remove slide about overrides in the release notes (#7086) 2025-08-08 09:46:40 +02:00
Yamila Moreno
c1aae12327 📎 Improve gh actions 2025-08-07 18:08:25 +02:00
Yamila Moreno
23a6f4b7c1 📎 Improve gh actions 2025-08-07 18:07:47 +02:00
Andrey Antukh
133e6e1e68 Merge tag '2.9.0-RC7' 2025-08-07 16:30:30 +02:00
Andrey Antukh
6abd045273 🐛 Add missing generator for token-set file change operation (#7080)
* 🐛 Add missing generator for token-set file change operation

* 🐛 Use ::sm/any instead of :any for on get-file-data-for-thumbnail rpc method

Mainly because :any will use a very generic generator that can generate
instances of Character that are not directly serializable to JSON
2025-08-07 12:36:14 +02:00
Marina López
778a608854 🐛 Fix tooltip for icon plans from team dropdown (#7075) 2025-08-07 07:43:49 +02:00
Marina López
a76a9fae41 🐛 Fix an unused translation (#7074) 2025-08-06 13:28:02 +02:00
Andrey Antukh
f7cfbdd229 🐛 Comment the problematic migration 2025-08-05 22:05:52 +02:00
Andrey Antukh
e28d2842f6 🐛 Revert the revert of orientation detection on media
This reverts commit 515cbf7bef.
2025-08-05 22:03:09 +02:00
Andrey Antukh
ccc3ca0948 Disable virtual threads on http server 2025-08-05 20:34:47 +02:00
Andrey Antukh
515cbf7bef 🐛 Revert orientation detection on media 2025-08-05 19:30:01 +02:00
Andrey Antukh
c320cbc47b 🐛 Revert to semaphore based climit impl 2025-08-05 19:17:35 +02:00
Andrey Antukh
46969585ed Disable native buffers usage on xnio
A temporal change for investigate native memory leak
2025-08-04 22:13:08 +02:00
Andrey Antukh
47882c5419 Add missing parameter on climit instance creation 2025-08-04 19:53:56 +02:00
andrés gonzález
019d5e083a 💄 Change copys at the 2.9 release slides (#7063) 2025-08-04 19:53:50 +02:00
Andrey Antukh
85f6cf32ae 🐛 Several bugfixes (#7062)
* 🐛 Fix incorrect status validation on subscription internal api

* 🐛 Make the shortcuts overwritting optional
2025-08-04 13:54:29 +02:00
Marina López
ded8e39e73 🐛 Fix hidden button in subscribe modal when there is a large number of teams (#7061) 2025-08-04 13:16:58 +02:00
Andrey Antukh
e730200873 🐛 Fix pinned project ordering on dashboard sidebar (#7060) 2025-08-04 12:07:19 +02:00
Francis Santiago
4501d13961 📚 Clarify OpenShift requirements (#6937)
* 📚 Clarify OpenShift requirements

* 📚 Remove the click for expanding
2025-08-01 16:26:04 +02:00
Juan de la Cruz
baa1cfb2f8 🎉 Add 2.9 release slides (#7019) 2025-08-01 14:59:11 +02:00
Eva Marco
905699d15a Add info to apply-token events (#7050) 2025-08-01 14:00:30 +02:00
Eva Marco
fe53869308 🐛 Fix small details on number token application (#7051) 2025-08-01 13:52:09 +02:00
Andrey Antukh
50076bac83 Merge remote-tracking branch 'origin/main' into staging 2025-08-01 13:10:52 +02:00
Brandon Currell
44bc4b7fa4 🐳 Add missing package in the exporter Docker image (#7026)
penpot-exporter requires poppler-utils for exporting to a PDF, but it is missing.
Added the package to the Dockerfile in the RUN section where dependencies are
being installed.

Signed-off-by: Brandon Currell <brandon+git@currell.pw>
2025-08-01 13:00:51 +02:00
Yamila Moreno
5c14f486d7 🐳 Update Imagemagick version 2025-08-01 12:49:11 +02:00
Andrey Antukh
0cbd980b68 🎉 Add imagemagick docker image build scripts (#6925)
* 🎉 Add imagemagick docker image build scripts

* 📎 Add PR feedback changes
2025-08-01 12:48:47 +02:00
Eva Marco
95dda2b1af 🐛 Fix stroke width token application (#7039) 2025-07-31 14:59:48 +02:00
Andrey Antukh
5170872961 Merge pull request #7031 from penpot/eva-fix-export-button-width
🐛 Fix export button width on inspect tab
2025-07-31 12:25:03 +02:00
Andrey Antukh
871ca68e1e 📎 Allow revert commits on github commit checker 2025-07-31 12:14:29 +02:00
Andrey Antukh
0ab896fc76 Revert " Highlight first font in font selector search, apply on Enter/click"
This reverts commit e62567d09e.
2025-07-31 12:14:29 +02:00
Andrey Antukh
6a4b548457 Revert "🐛 Fix font selector highlight inconsistency (#6990)"
This reverts commit 708a40bff1.
2025-07-31 12:14:29 +02:00
Eva Marco
695a399941 🐛 Fix export button width on inspect tab 2025-07-31 09:30:46 +02:00
Eva Marco
a32463fada 🐛 Fix tooltip position after several shows and hides (#7022) 2025-07-31 09:00:05 +02:00
Eva Marco
5d44c88988 🐛 Fix token pill not showing position application on dimension token type (#7018) 2025-07-30 14:24:10 +02:00
Andrey Antukh
ce87d797d1 Merge pull request #7014 from penpot/niwinz-staging-regression-3
🐛 Fix several issues related to font/text related tokens
2025-07-30 12:25:28 +02:00
Andrey Antukh
7fde1436e1 🐛 Add missing styles to the empty node on editor-v1 2025-07-30 11:45:39 +02:00
Andrey Antukh
e1c5a32fcb 💄 Fix indentation style on generate-unapply-tokens 2025-07-30 11:45:19 +02:00
Andrey Antukh
b262e6a46f 🐛 Fix incorrect condition on checking text shape attrs 2025-07-30 11:44:07 +02:00
Andrey Antukh
2e726b62c3 📎 Update changelog 2025-07-29 20:07:05 +02:00
Andrey Antukh
02acd81c2c 🐛 Add missing profile prop to access style component (#7007)
* 💄 Fix request-access component style

* 🐛 Add missing profile prop to access style component
2025-07-29 16:04:15 +02:00
Andrey Antukh
bae2de75ff Merge branch 'main' into staging 2025-07-29 15:21:58 +02:00
Andrey Antukh
b68c426cd1 🐛 Fix exception on fills menu when binary-fills flag is active
And multiple shapes are selected.
2025-07-29 15:10:32 +02:00
Andrey Antukh
5161ef15bf 🐛 Fix regression on show access request dialog (#7005) 2025-07-29 14:58:02 +02:00
Eva Marco
36d3d94ec9 🐛 Fix X & Y position do not sincronize with tokens (#7004) 2025-07-29 14:32:06 +02:00
Andrey Antukh
17447d7610 Remove restriction of duplicate bindings on mousetrap 2025-07-29 14:14:19 +02:00
andrés gonzález
708a40bff1 🐛 Fix font selector highlight inconsistency (#6990)
* 🐛 Fix font selector highlight inconsistency

*  Add minor performance enhancements

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-07-29 13:12:54 +02:00
Marina López
efaf6573bd 📎 Update monetization texts (#7002) 2025-07-29 12:42:11 +02:00
Yamila Moreno
001bcbce59 Merge pull request #6995 from penpot/yms-update-imagemagick-version
🐳 Update Imagemagick version
2025-07-29 10:58:32 +02:00
Yamila Moreno
c195c07a3f 🐳 Update Imagemagick version 2025-07-29 10:37:11 +02:00
Alejandro Alonso
f5298f51e7 🐛 Fix the context menu always closes after any action (#6944) 2025-07-29 09:50:55 +02:00
Alejandro Alonso
46c440fef2 🐛 Fix remove color button in the gradient editor (#6993) 2025-07-28 17:48:05 +02:00
Alejandro Alonso
e77f8b572a Merge pull request #6953 from penpot/superalex-fix-component-changes-not-propagated
🐛 Fix component changes not propagated
2025-07-28 12:53:37 +02:00
Alejandro Alonso
ade5eecf80 🐛 Fix component changes not propagated 2025-07-28 12:38:09 +02:00
andrés gonzález
97fc7702b8 📚 Improve and clarify 'Hide and lock layers' section (#6975) 2025-07-25 14:53:32 +02:00
andrés gonzález
54fcd58531 📚 Add doc for resizing text (#6974)
* 📚 Add doc for resizing text

* 📚 Update docs for text resizing

Co-authored-by: Madalena Melo <madalena.melo@kaleidos.net>
Signed-off-by: andrés gonzález <andres.gonzalez79@gmail.com>

---------

Signed-off-by: andrés gonzález <andres.gonzalez79@gmail.com>
Co-authored-by: Madalena Melo <madalena.melo@kaleidos.net>
2025-07-25 13:20:52 +02:00
andrés gonzález
b7a8747f00 📚 Add doc for tokens zip file import option (#6973) 2025-07-25 13:20:39 +02:00
Andrey Antukh
d00de7d5a4 Merge pull request #6960 from penpot/xaviju-remove-image-shape-type-fix
🐛 Remove image type from inspect tab panels
2025-07-25 12:49:57 +02:00
andrés gonzález
5ae4dde222 📚 Add font size token doc (#6972) 2025-07-25 12:30:56 +02:00
Xaviju
2fbd4b07e0 🐛 Remove image type from inspect tab panels 2025-07-25 12:08:01 +02:00
Florian Schroedl
58a843ea23 Remove token when applying tyopgraphic asset style 2025-07-24 17:51:53 +02:00
Florian Schroedl
f6b97af148 🐛 Fix spacing menu not available in dimensions token 2025-07-24 15:20:10 +02:00
Alejandro Alonso
76b7287bf1 Merge pull request #6864 from penpot/niwinz-staging-snapshot-migrations
 Add migrations handling on file snapshots
2025-07-24 11:41:17 +02:00
Andrey Antukh
019bc2f183 Add migrations handling on file snapshots 2025-07-24 11:40:54 +02:00
Florian Schroedl
8c96a617be Add test for spacing token application rules 2025-07-24 11:01:49 +02:00
Florian Schroedl
1f15e9b81e Fix spacing token for frame children 2025-07-24 11:01:49 +02:00
Alejandro Alonso
f7627e515a Merge pull request #6876 from penpot/niwinz-develop-minor-changes-logical-deletion
 Change default status filtering for logical deletion
2025-07-24 10:58:49 +02:00
Andrey Antukh
d08c94d5a6 Change default status filtering for logical deletion 2025-07-24 10:43:45 +02:00
Xaviju
01896501c1 🐛 Remove image type from inspect tab panels (#6959) 2025-07-24 09:37:38 +02:00
Andrey Antukh
3f9a1525ca Merge pull request #6954 from penpot/alotor-fix-gradient-stroke
🐛 Fix opacity on stroke gradients
2025-07-24 08:59:02 +02:00
alonso.torres
52c1e227d5 🐛 Fix change from gradient to solid color 2025-07-24 08:58:48 +02:00
alonso.torres
955538b12a 🐛 Fix opacity on stroke gradients 2025-07-24 08:58:46 +02:00
Alonso Torres
8254af27cb 🐛 Fix problem when changing between flex/grid layout (#6949) 2025-07-24 08:54:07 +02:00
Elena Torró
f76391ecbb 🐛 Enable switch to system theme on options menu (#6946) 2025-07-24 08:43:03 +02:00
Andrés Moya
c49e9fbf18 🐛 Fix last migration of token sets (#6957) 2025-07-24 08:42:16 +02:00
Marina López
122701ee7b 🐛 Fix modal submit button for unpaid or canceled subscriptions (#6947) 2025-07-24 08:41:39 +02:00
Andrés Moya
351362bb50 🐛 Fix migration from tokens lib version 1.2 2025-07-23 15:28:53 +02:00
Andrey Antukh
1acf78d57c Merge branch 'main' into staging 2025-07-23 12:09:37 +02:00
Andrey Antukh
523373dfa2 📎 Update .gitignore file 2025-07-23 12:09:15 +02:00
Andrés Moya
f55e7d8165 🐛 Keep shape level groups for token sync later 2025-07-23 12:04:31 +02:00
Andrés Moya
9fdc6be465 🐛 Fix bad touched attributes when applying tokens to text shapes 2025-07-23 12:04:31 +02:00
Alejandro Alonso
9390c1e7be 🐛 Fix "Copy as SVG" generates different code from the Inspect panel (#6945) 2025-07-23 11:46:58 +02:00
Eva Marco
b20b272eae 📚 Update changelog 2025-07-23 09:53:49 +02:00
Alejandro Alonso
d46b519524 🐛 Fix remove color button in the gradient editor (#6942) 2025-07-23 09:04:54 +02:00
Andrey Antukh
4effd375a9 Add several improvements to admin pannel 2025-07-23 08:33:33 +02:00
Andrey Antukh
4e753dc474 💄 Use resolved schemas instead of references
For several schemas on common types
2025-07-23 08:33:28 +02:00
Andrey Antukh
fbf63b98c3 Reuse file data checkers on file validate ns 2025-07-23 08:33:23 +02:00
Marina López
3df557b370 ♻️ Remove the workaround for updating the subscription after subscribing (#6938) 2025-07-23 08:10:20 +02:00
Xaviju
35f3125fff 🐛 Fix null when copying shadow color on inspect tab (#6923)
Co-authored-by: Xavier Julian <xaviju@proton.me>
2025-07-22 14:49:36 +02:00
Francis Santiago
f22aa606ce 📚 Clarify OpenShift requirements (#6937)
* 📚 Clarify OpenShift requirements

* 📚 Remove the click for expanding
2025-07-22 14:05:02 +02:00
David Barragán Merino
9d288486d7 🐛 Subscription current period dates could be null (#6931)
`current-period-start` and `current-period-end` can be null if the invoice has not yet been created in stripe. This happens after the subscription is created, before the webhook is sent.
2025-07-22 12:32:42 +02:00
Pablo Alba
ea5521485a ♻️ Remove redundant flag on text overrides (#6933) 2025-07-22 12:32:24 +02:00
Marina López
f768ffbdad 🐛 Fix wrong behaviour for unpaid or canceled subscriptions (#6932) 2025-07-22 12:31:45 +02:00
Andrey Antukh
4f0d3660de 🎉 Add imagemagick docker image build scripts (#6925)
* 🎉 Add imagemagick docker image build scripts

* 📎 Add PR feedback changes
2025-07-22 11:51:13 +02:00
Andrey Antukh
7ccb742ef3 Merge remote-tracking branch 'origin/develop' into staging 2025-07-21 21:15:54 +02:00
Andrey Antukh
1d550eaa18 Merge remote-tracking branch 'origin/staging' into develop 2025-07-21 21:03:19 +02:00
98 changed files with 1748 additions and 1114 deletions

View File

@@ -1,28 +1,14 @@
name: Build and Upload Penpot Bundles non-prod
name: Build and Upload Penpot Bundles
on:
# Create bundler for every tag
push:
tags:
- '**' # Pattern matched against refs/tags
# Create bundler every hour between 5:00 and 20:00 on working days
schedule:
- cron: '0 5-20 * * 1-5'
# Create bundler from manual action
# Create bundle from manual action
workflow_dispatch:
workflow_call:
inputs:
zip_mode:
# zip_mode defines how the build artifacts are packaged:
# - 'individual': creates one ZIP file per component (frontend, backend, exporter)
# - 'all': creates a single ZIP containing all components
# - null: for the rest of cases (non-manual events)
description: 'Bundle packaging mode'
required: false
default: 'individual'
type: choice
options:
- individual
- all
gh_ref:
description: 'Name of the branch'
type: string
required: true
jobs:
build-bundles:
@@ -38,15 +24,15 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Extract somer useful variables
- name: Extract some useful variables
id: vars
run: |
echo "commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "gh_branch=${{ github.base_ref || github.ref_name }}" >> $GITHUB_OUTPUT
# Set up Docker Buildx for multi-arch build
- name: Set up Docker Buildx
- name: Set up Docker Buildx for multi-arch build
uses: docker/setup-buildx-action@v3
- name: Run manage.sh build-bundle from host
@@ -57,73 +43,22 @@ jobs:
mkdir zips
mv bundles penpot
- name: Create zip bundles for zip_mode == 'all'
if: ${{ github.event.inputs.zip_mode == 'all' }}
- name: Create zip bundles
run: |
echo "📦 Packaging Penpot 'all' bundles..."
zip -r zips/penpot-all-bundles.zip penpot
echo "📦 Packaging Penpot bundles..."
zip -r zips/penpot.zip penpot
- name: Create zip bundles for zip_mode != 'all'
if: ${{ github.event.inputs.zip_mode != 'all' }}
- name: Upload Penpot bundle to S3
run: |
echo "📦 Packaging Penpot 'individual' bundles..."
zip -r zips/penpot-frontend.zip penpot/frontend
zip -r zips/penpot-backend.zip penpot/backend
zip -r zips/penpot-exporter.zip penpot/exporter
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_branch}}-latest.zip
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.commit_hash }}.zip
- name: Upload unified 'all' bundle
if: ${{ github.event.inputs.zip_mode == 'all' }}
uses: actions/upload-artifact@v4
with:
name: penpot-all-bundles
path: zips/penpot-all-bundles.zip
- name: Upload individual bundles
if: ${{ github.event.inputs.zip_mode != 'all' }}
uses: actions/upload-artifact@v4
with:
name: penpot-individual-bundles
path: |
zips/penpot-frontend.zip
zips/penpot-backend.zip
zips/penpot-exporter.zip
- name: Upload unified 'all' bundle to S3
if: ${{ github.event.inputs.zip_mode == 'all' }}
run: |
aws s3 cp zips/penpot-all-bundles.zip s3://${{ secrets.S3_BUCKET }}/penpot-all-bundles-${{ steps.vars.outputs.gh_branch}}.zip
aws s3 cp zips/penpot-all-bundles.zip s3://${{ secrets.S3_BUCKET }}/penpot-all-bundles-${{ steps.vars.outputs.commit_hash }}.zip
- name: Upload 'individual' bundles to S3
if: ${{ github.event.inputs.zip_mode != 'all' }}
run: |
for name in penpot-frontend penpot-backend penpot-exporter; do
aws s3 cp zips/${name}.zip s3://${{ secrets.S3_BUCKET }}/${name}-${{ steps.vars.outputs.gh_branch }}-latest.zip
aws s3 cp zips/${name}.zip s3://${{ secrets.S3_BUCKET }}/${name}-${{ steps.vars.outputs.commit_hash }}.zip
done
- name: Notify Mattermost about automatic bundles
if: github.event_name == 'pull_request'
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
TEXT: |
📦 *Penpot bundle automatically generated*
📄 PR: ${{ github.event.pull_request.title }}
🔁 From: \`${{ github.head_ref }}\` to \`{{ github.base_ref }}\`
*[PENPOT] Error during the execution of the job*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_branch}}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Notify Mattermost about manual bundles
if: github.event_name == 'workflow_dispatch'
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
TEXT: |
📦 *Penpot bundle manually generated*
📄 Triggered from branch: `${{ github.ref_name}}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Print artifact summary URL
run: |
echo "📦 Artifacts available at:"
echo "🔗 https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"

12
.github/workflows/build-develop.yml vendored Normal file
View File

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

12
.github/workflows/build-staging.yml vendored Normal file
View File

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

View File

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

6
.gitignore vendored
View File

@@ -41,6 +41,7 @@
/backend/resources/public/assets
/backend/resources/public/media
/backend/target/
/backend/experiments
/bundle*
/cd.md
/clj-profiler/
@@ -51,9 +52,6 @@
/exporter/target
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/cypress/fixtures/validuser.json
/frontend/cypress/videos/*/
/frontend/cypress/videos/*/
/frontend/dist/
/frontend/npm-debug.log
/frontend/out/
@@ -70,6 +68,8 @@
/vendor/svgclean/bundle*.js
/web
/library/target/
/library/*.zip
/external
clj-profiler/
node_modules

View File

@@ -25,8 +25,11 @@
- Hide bounding box while editing visual effects [Taiga #11576](https://tree.taiga.io/project/penpot/issue/11576)
- Improved text layer resizing: Allow double-click on text bounding box to set auto-width/auto-height [Taiga #11577](https://tree.taiga.io/project/penpot/issue/11577)
- Improve text layer auto-resize: auto-width switches to auto-height on horizontal resize, and only switches to fixed on vertical resize [Taiga #11578](https://tree.taiga.io/project/penpot/issue/11578)
- Highlight first font in font selector search. Apply only on Enter or click. [Taiga #11579](https://tree.taiga.io/project/penpot/issue/11579)
- Add the ability to show login dialog on profile settings [Github #6871](https://github.com/penpot/penpot/pull/6871)
- Improve the application of tokens with object specific tokens [Taiga #10209](https://tree.taiga.io/project/penpot/us/10209)
- Add info to apply-token event [Taiga #11710](https://tree.taiga.io/project/penpot/task/11710)
- Fix double click on set name input [Taiga #11747](https://tree.taiga.io/project/penpot/issue/11747)
### :bug: Bugs fixed
@@ -40,14 +43,32 @@
- Keep color data when copying from info tab into CSS [Taiga #11144](https://tree.taiga.io/project/penpot/issue/11144)
- Update HSL values to modern syntax as defined in W3C CSS Color Module Level 4 [Taiga #11144](https://tree.taiga.io/project/penpot/issue/11144)
- Fix main component receives focus and is selected when using 'Show Main Component' [Taiga #11402](https://tree.taiga.io/project/penpot/issue/11402)
- Fix UI theme selection from main menu [Taiga #11567](https://tree.taiga.io/project/penpot/issue/11567)
- Fix duplicating pages with mainInstance shapes nested inside groups [Taiga #10774](https://tree.taiga.io/project/penpot/issue/10774)
- Fix ESC key not closing Add/Manage Libraries modal [Taiga #11523](https://tree.taiga.io/project/penpot/issue/11523)
- Fix copying a shadow color from info tab [Taiga #11211](https://tree.taiga.io/project/penpot/issue/11211)
- Fix remove color button in the gradient editor [Taiga #11623](https://tree.taiga.io/project/penpot/issue/11623)
- Fix "Copy as SVG" generates different code from the Inspect panel [Taiga #11519](https://tree.taiga.io/project/penpot/issue/11519)
- Fix overriden tokens in text copies are not preserved [Taiga #11486](https://tree.taiga.io/project/penpot/issue/11486)
- Fix problem when changing between flex/grid layout [Taiga #11625](https://tree.taiga.io/project/penpot/issue/11625)
- Fix opacity on stroke gradients [Taiga #11646](https://tree.taiga.io/project/penpot/issue/11646)
- Fix change from gradient to solid color [Taiga #11648](https://tree.taiga.io/project/penpot/issue/11648)
- Fix the context menu always closes after any action [Taiga #11624](https://tree.taiga.io/project/penpot/issue/11624)
- Fix X & Y position do not sincronize with tokens [Taiga #11617](https://tree.taiga.io/project/penpot/issue/11617)
- Fix tooltip position after first time [Taiga #11688](https://tree.taiga.io/project/penpot/issue/11688)
- Fix inconsistent ordering of pinned projects on dashboard sidebar [Taiga #11674](https://tree.taiga.io/project/penpot/issue/11674)
- Fix export button width on inspect tab [Taiga #11394](https://tree.taiga.io/project/penpot/issue/11394)
- Fix stroke width token application [Taiga #11724](https://tree.taiga.io/project/penpot/issue/11724)
- Fix number token application on shape [Taiga #11331](https://tree.taiga.io/project/penpot/task/11331)
- Fix auto height is fixed in the HTML inspect tab for text elements [Taiga #11680](https://tree.taiga.io/project/penpot/task/11680)
## 2.8.1 (Unreleased)
## 2.8.1
### :bug: Bugs fixed
- Fix unexpected exception on processing old texts [Github #6889](https://github.com/penpot/penpot/pull/6889)
- Fix error on inspect tab when selecting multiple shapes [Taiga #11655](https://tree.taiga.io/project/penpot/issue/11655)
- Fix missing package for the penport_exporter Docker image [GitHub #7205](https://github.com/penpot/penpot/issues/7025)
## 2.8.0

View File

@@ -17,38 +17,6 @@ Debug Main Page
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
</fieldset>
<fieldset>
<legend>Download file data:</legend>
<desc>Given an FILE-ID, downloads the file data as file. The file data is encoded using transit.</desc>
<form method="get" action="/dbg/file/data">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="submit" name="download" value="Download" />
<input type="submit" name="clone" value="Clone" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Upload File Data:</legend>
<desc>Create a new file on your draft projects using the file downloaded from the previous section.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/data">
<div class="row">
<input type="file" name="file" value="" />
</div>
<div class="row">
<label>Import with same id?</label>
<input type="checkbox" name="reuseid" />
</div>
<div class="row">
<input type="submit" value="Upload" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Profile Management</legend>
<form method="post" action="/dbg/actions/resend-email-verification">
@@ -81,6 +49,50 @@ Debug Main Page
</section>
<section class="widget">
<fieldset>
<legend>Download RAW file data:</legend>
<desc>Given an FILE-ID, downloads the file AS-IS (no validation
checks, just exports the file data and related objects in raw)
<br/>
<br/>
<b>WARNING: this operation does not performs any checks</b>
</desc>
<form method="get" action="/dbg/actions/file-raw-export-import">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="submit" name="download" value="Download" />
<input type="submit" name="clone" value="Clone" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Upload File Data:</legend>
<desc>Create a new file on your draft projects using the file downloaded from the previous section.
<br/>
<br/>
<b>WARNING: this operation does not performs any checks</b>
</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/actions/file-raw-export-import">
<div class="row">
<input type="file" name="file" value="" />
</div>
<div class="row">
<label>Import with same id?</label>
<input type="checkbox" name="reuseid" />
</div>
<div class="row">
<input type="submit" value="Upload" />
</div>
</form>
</fieldset>
</section>
<section class="widget">
<fieldset>
<legend>Export binfile:</legend>
@@ -88,7 +100,7 @@ Debug Main Page
the related libraries in a single custom formatted binary
file.</desc>
<form method="get" action="/dbg/file/export">
<form method="get" action="/dbg/actions/file-export">
<div class="row set-of-inputs">
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
@@ -116,7 +128,7 @@ Debug Main Page
<legend>Import binfile:</legend>
<desc>Import penpot file in binary format.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/import">
<form method="post" enctype="multipart/form-data" action="/dbg/actions/file-import">
<div class="row">
<input type="file" name="file" value="" />
</div>
@@ -130,79 +142,27 @@ Debug Main Page
<section class="widget">
<fieldset>
<legend>Reset file version</legend>
<desc>Allows reset file data version to a specific number/</desc>
<form method="post" action="/dbg/actions/reset-file-version">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="number" style="width:100px" name="version" placeholder="version" value="32" />
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
</section>
<section class="widget">
<h2>Feature Flags</h2>
<fieldset>
<legend>Enable</legend>
<legend>Feature Flags for Team</legend>
<desc>Add a feature flag to a team</desc>
<form method="post" action="/dbg/actions/add-team-feature">
<form method="post" action="/dbg/actions/handle-team-features">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
<select type="text" style="width:100px" name="feature">
{% for feature in supported-features %}
<option value="{{feature}}">{{feature}}</option>
{% endfor %}
</select>
</div>
<div class="row">
<label for="check-feature">Skip feature check</label>
<input id="check-feature" type="checkbox" name="skip-check" />
<br />
<small>
Do not check if the feature is supported
</small>
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Disable</legend>
<desc>Remove a feature flag from a team</desc>
<form method="post" action="/dbg/actions/remove-team-feature">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
<select style="width:100px" name="action">
<option value="">Action...</option>
<option value="show">Show</option>
<option value="enable">Enable</option>
<option value="disable">Disable</option>
</select>
</div>
<div class="row">

View File

@@ -7,7 +7,9 @@ penpot - error list
{% block content %}
<nav>
<div class="title">
<h1>Error reports (last 200)</h1>
<h1>Error reports (last 200)
<a href="/dbg">[GO BACK]</a>
</h1>
</div>
</nav>
<main class="horizontal-list">

View File

@@ -155,7 +155,7 @@
(defn decode-file
"A general purpose file decoding function that resolves all external
pointers, run migrations and return plain vanilla file map"
[cfg {:keys [id] :as file}]
[cfg {:keys [id] :as file} & {:keys [migrate?] :or {migrate? true}}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(let [file (->> file
(feat.fmigr/resolve-applied-migrations cfg)
@@ -168,7 +168,7 @@
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(update :data assoc :id id)
(fmg/migrate-file libs)))))
(cond-> migrate? (fmg/migrate-file libs))))))
(defn get-file
"Get file, resolve all features and apply migrations.

View File

@@ -37,3 +37,9 @@
{::db/return-keys false
::sql/on-conflict-do-nothing true})
(db/get-update-count))))
(defn reset-migrations!
"Replace file migrations"
[conn {:keys [id] :as file}]
(db/delete! conn :file-migration {:file-id id})
(upsert-migrations! conn file))

View File

@@ -10,18 +10,19 @@
[app.config :as cf]
[app.util.time :as dt]))
(def ^:private canceled-status
#{"canceled" "unpaid"})
(defn get-deletion-delay
"Calculate the next deleted-at for a resource (file, team, etc) in function
of team settings"
[team]
(if-let [subscription (get team :subscription)]
(if-let [{:keys [type status]} (get team :subscription)]
(cond
(and (= (:type subscription) "unlimited")
(= (:status subscription) "active"))
(and (= "unlimited" type) (not (contains? canceled-status status)))
(dt/duration {:days 30})
(and (= (:type subscription) "enterprise")
(= (:status subscription) "active"))
(and (= "enterprise" type) (not (contains? canceled-status status)))
(dt/duration {:days 90})
:else

View File

@@ -25,6 +25,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]
@@ -63,15 +64,16 @@
(assert (sm/check schema:server-params params)))
(defmethod ig/init-key ::server
[_ {:keys [::handler ::router ::host ::port] :as cfg}]
[_ {:keys [::handler ::router ::host ::port ::wrk/executor] :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 :virtual
:xnio/dispatch executor
:ring/compat :ring2
:socket/backlog 4069}

View File

@@ -15,9 +15,11 @@
[app.common.features :as cfeat]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.features.file-migrations :as feat.fmig]
[app.http.session :as session]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.files-create :refer [create-file]]
@@ -50,26 +52,26 @@
{::yres/status 200
::yres/headers {"content-type" "text/html"}
::yres/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {:version (:full cf/version)}))})
(tmpl/render {:version (:full cf/version)
:supported-features cfeat/supported-features}))})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE CHANGES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn prepare-response
[body]
(let [headers {"content-type" "application/transit+json"}]
{::yres/status 200
::yres/body body
::yres/headers headers}))
(defn- get-resolved-file
[cfg file-id]
(some-> (bfc/get-file cfg file-id :migrate? false)
(update :data blob/encode)))
(defn prepare-download-response
[body filename]
(let [headers {"content-disposition" (str "attachment; filename=" filename)
"content-type" "application/octet-stream"}]
{::yres/status 200
::yres/body body
::yres/headers headers}))
(defn prepare-download
[file filename]
{::yres/status 200
::yres/headers
{"content-disposition" (str "attachment; filename=" filename ".json")
"content-type" "application/octet-stream"}
::yres/body
(t/encode file {:type :json-verbose})})
(def sql:retrieve-range-of-changes
"select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn")
@@ -77,45 +79,51 @@
(def sql:retrieve-single-change
"select revn, changes, data from file_change where file_id=? and revn = ?")
(defn- retrieve-file-data
[{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}]
(defn- download-file-data
[cfg {:keys [params ::session/profile-id] :as request}]
(let [file-id (some-> params :file-id parse-uuid)
revn (some-> params :revn parse-long)
filename (str file-id)]
(when-not file-id
(ex/raise :type :validation
:code :missing-arguments))
(let [data (if (integer? revn)
(some-> (db/exec-one! pool [sql:retrieve-single-change file-id revn]) :data)
(some-> (db/get-by-id pool :file file-id) :data))]
(when-not data
(ex/raise :type :not-found
:code :enpty-data
:hint "empty response"))
(if-let [file (get-resolved-file cfg file-id)]
(cond
(contains? params :download)
(prepare-download-response data filename)
(prepare-download file filename)
(contains? params :clone)
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [profile (profile/get-profile conn profile-id)
project-id (:default-project-id profile)
file (-> (create-file cfg {:id (uuid/next)
:name (str "Cloned: " (:name file))
:features (:features file)
:project-id project-id
:profile-id profile-id})
(assoc :data (:data file))
(assoc :migrations (:migrations file)))]
(db/run! pool (fn [{:keys [::db/conn] :as cfg}]
(create-file cfg {:id file-id
:name (str "Cloned file: " filename)
:project-id project-id
:profile-id profile-id})
(db/update! conn :file
{:data data}
{:id file-id})
{::yres/status 201
::yres/body "OK CREATED"})))
(feat.fmig/reset-migrations! conn file)
(db/update! conn :file
{:data (:data file)}
{:id (:id file)}
{::db/return-keys false})
{::yres/status 201
::yres/body "OK CLONED"})))
:else
(prepare-response (blob/decode data))))))
(ex/raise :type :validation
:code :invalid-params
:hint "invalid button"))
(ex/raise :type :not-found
:code :enpty-data
:hint "empty response"))))
(defn- is-file-exists?
[pool id]
@@ -123,81 +131,61 @@
(-> (db/exec-one! pool [sql id]) :exists)))
(defn- upload-file-data
[{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}]
[{:keys [::db/pool] :as cfg} {:keys [::session/profile-id params] :as request}]
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)
data (some-> params :file :path io/read*)]
file (some-> params :file :path io/read* t/decode)]
(if (and data project-id)
(let [fname (str "Imported file *: " (dt/now))
(if (and file project-id)
(let [fname (str "Imported: " (:name file) "(" (dt/now) ")")
reuse-id? (contains? params :reuseid)
file-id (or (and reuse-id? (ex/ignoring (-> params :file :filename parse-uuid)))
(uuid/next))]
(if (and reuse-id? file-id
(is-file-exists? pool file-id))
(do
(db/update! pool :file
{:data data
:deleted-at nil}
{:id file-id})
{::yres/status 200
::yres/body "OK UPDATED"})
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(db/update! conn :file
{:data (:data file)
:features (into-array (:features file))
:deleted-at nil}
{:id file-id}
{::db/return-keys false})
(feat.fmig/reset-migrations! conn file)
{::yres/status 200
::yres/body "OK UPDATED"}))
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [file (-> (create-file cfg {:id file-id
:name fname
:features (:features file)
:project-id project-id
:profile-id profile-id})
(assoc :data (:data file))
(assoc :migrations (:migrations file)))]
(db/run! pool (fn [{:keys [::db/conn] :as cfg}]
(create-file cfg {:id file-id
:name fname
:project-id project-id
:profile-id profile-id})
(db/update! conn :file
{:data data}
{:id file-id})
{:data (:data file)}
{:id file-id}
{::db/return-keys false})
(feat.fmig/reset-migrations! conn file)
{::yres/status 201
::yres/body "OK CREATED"}))))
::yres/body "OK CREATED"})))))
{::yres/status 500
::yres/body "ERROR"})))
(ex/raise :type :validation
:code :invalid-params
:hint "invalid file uploaded"))))
(defn file-data-handler
(defn raw-export-import-handler
[cfg request]
(case (yreq/method request)
:get (retrieve-file-data cfg request)
:get (download-file-data cfg request)
:post (upload-file-data cfg request)
(ex/raise :type :http
:code :method-not-found)))
(defn file-changes-handler
[{:keys [::db/pool]} {:keys [params] :as request}]
(letfn [(retrieve-changes [file-id revn]
(if (str/includes? revn ":")
(let [[start end] (->> (str/split revn #":")
(map str/trim)
(map parse-long))]
(some->> (db/exec! pool [sql:retrieve-range-of-changes file-id start end])
(map :changes)
(map blob/decode)
(mapcat identity)
(vec)))
(if-let [revn (parse-long revn)]
(let [item (db/exec-one! pool [sql:retrieve-single-change file-id revn])]
(some-> item :changes blob/decode vec))
(ex/raise :type :validation :code :invalid-arguments))))]
(let [file-id (some-> params :id parse-uuid)
revn (or (some-> params :revn parse-long) "latest")
filename (str file-id)]
(when (or (not file-id) (not revn))
(ex/raise :type :validation
:code :invalid-arguments
:hint "missing arguments"))
(let [data (retrieve-changes file-id revn)]
(if (contains? params :download)
(prepare-download-response data filename)
(prepare-response data))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ERROR BROWSER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -430,49 +418,49 @@
::yres/body "OK"}))
(defn- add-team-feature
[{:keys [params] :as request}]
(let [team-id (some-> params :team-id d/parse-uuid)
feature (some-> params :feature str)
(defn- handle-team-features
[cfg {:keys [params] :as request}]
(let [team-id (some-> params :team-id d/parse-uuid)
feature (some-> params :feature str)
action (some-> params :action)
skip-check (contains? params :skip-check)]
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(when (nil? team-id)
(ex/raise :type :validation
:code :invalid-team-id
:hint "provided invalid team id"))
(srepl/enable-team-feature! team-id feature :skip-check skip-check)
(if (= action "show")
(let [team (db/run! cfg teams/get-team-info {:id team-id})]
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
::yres/body (apply str "Team features:\n"
(->> (:features team)
(map (fn [feature]
(str "- " feature "\n")))))})
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
::yres/body "OK"}))
(do
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(defn- remove-team-feature
[{:keys [params] :as request}]
(let [team-id (some-> params :team-id d/parse-uuid)
feature (some-> params :feature str)
skip-check (contains? params :skip-check)]
(cond
(= action "enable")
(srepl/enable-team-feature! team-id feature :skip-check skip-check)
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(= action "disable")
(srepl/disable-team-feature! team-id feature :skip-check skip-check)
(when (nil? team-id)
(ex/raise :type :validation
:code :invalid-team-id
:hint "provided invalid team id"))
:else
(ex/raise :type :validation
:code :invalid-action
:hint (str "invalid action: " action)))
(srepl/disable-team-feature! team-id feature :skip-check skip-check)
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
::yres/body "OK"}))
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
::yres/body "OK"}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OTHER SMALL VIEWS/HANDLERS
@@ -525,6 +513,25 @@
(ex/raise :type :authentication
:code :only-admins-allowed)))))})
(def errors
(letfn [(handle-error [cause]
(when-let [data (ex-data cause)]
(when (= :validation (:type data))
(str "Error: " (or (:hint data) (ex-message cause)) "\n"))))]
{:name ::errors
:compile
(fn [& _params]
(fn [handler]
(fn [request]
(try
(handler request)
(catch Throwable cause
(let [body (or (handle-error cause)
(ex/format-throwable cause))]
{::yres/status 400
::yres/headers {"content-type" "text/plain"}
::yres/body body}))))))}))
(defmethod ig/assert-key ::routes
[_ params]
(assert (db/pool? (::db/pool params)) "expected a valid database pool")
@@ -540,15 +547,14 @@
["/changelog" {:handler (partial changelog-handler cfg)}]
["/error/:id" {:handler (partial error-handler cfg)}]
["/error" {:handler (partial error-list-handler cfg)}]
["/actions/resend-email-verification"
{:handler (partial resend-email-notification cfg)}]
["/actions/reset-file-version"
{:handler (partial reset-file-version cfg)}]
["/actions/add-team-feature"
{:handler (partial add-team-feature)}]
["/actions/remove-team-feature"
{:handler (partial remove-team-feature)}]
["/file/export" {:handler (partial export-handler cfg)}]
["/file/import" {:handler (partial import-handler cfg)}]
["/file/data" {:handler (partial file-data-handler cfg)}]
["/file/changes" {:handler (partial file-changes-handler cfg)}]]])
["/actions" {:middleware [[errors]]}
["/resend-email-verification"
{:handler (partial resend-email-notification cfg)}]
["/reset-file-version"
{:handler (partial reset-file-version cfg)}]
["/handle-team-features"
{:handler (partial handle-team-features cfg)}]
["/file-export" {:handler (partial export-handler cfg)}]
["/file-import" {:handler (partial import-handler cfg)}]
["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}]]]])

View File

@@ -231,7 +231,8 @@
::http/router (ig/ref ::http/router)
::http/io-threads (cf/get :http-server-io-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)}
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
::wrk/executor (ig/ref ::wrk/executor)}
::ldap/provider
{:host (cf/get :ldap-host)

View File

@@ -438,7 +438,10 @@
:fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")}
{:name "0139-mod-file-change-table.sql"
:fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}])
:fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}
{:name "0140-mod-file-change-table.sql"
:fn (mg/resource "app/migrations/sql/0140-mod-file-change-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@@ -0,0 +1,2 @@
ALTER TABLE file_change
ADD COLUMN migrations text[];

View File

@@ -178,12 +178,12 @@
(measure metrics mlabels stats nil)
(log "enqueued" req-id stats limit-id limit-label limit-params nil))
(px/invoke! limiter (fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure metrics mlabels stats elapsed)
(log "acquired" req-id stats limit-id limit-label limit-params elapsed)
(handler))))
(pbh/invoke! limiter (fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure metrics mlabels stats elapsed)
(log "acquired" req-id stats limit-id limit-label limit-params elapsed)
(handler))))
(catch ExceptionInfo cause
(let [{:keys [type code]} (ex-data cause)]

View File

@@ -7,7 +7,6 @@
(ns app.rpc.commands.files-create
(:require
[app.binfile.common :as bfc]
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.common.types.file :as ctf]
@@ -41,9 +40,7 @@
:or {is-shared false revn 0 create-page true}
:as params}]
(dm/assert!
"expected a valid connection"
(db/connection? conn))
(assert (db/connection? conn) "expected a valid connection")
(binding [pmap/*tracked* (pmap/create-tracked)
cfeat/*current* features]

View File

@@ -8,6 +8,7 @@
(:require
[app.binfile.common :as bfc]
[app.common.exceptions :as ex]
[app.common.files.migrations :as fmg]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
@@ -15,6 +16,7 @@
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata]
[app.features.file-migrations :refer [reset-migrations!]]
[app.main :as-alias main]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
@@ -27,6 +29,13 @@
[app.util.time :as dt]
[cuerdas.core :as str]))
(defn decode-row
[{:keys [migrations] :as row}]
(when row
(cond-> row
(some? migrations)
(assoc :migrations (db/decode-pgarray migrations)))))
(def sql:get-file-snapshots
"WITH changes AS (
SELECT id, label, revn, created_at, created_by, profile_id
@@ -74,10 +83,7 @@
(assert (#{:system :user :admin} created-by)
"expected valid keyword for created-by")
(let [conn
(db/get-connection cfg)
created-by
(let [created-by
(name created-by)
deleted-at
@@ -101,12 +107,15 @@
(blob/encode (:data file))
features
(db/encode-pgarray (:features file) conn "text")]
(into-array (:features file))
(l/debug :hint "creating file snapshot"
:file-id (str (:id file))
:id (str snapshot-id)
:label label)
migrations
(into-array (:migrations file))]
(l/dbg :hint "creating file snapshot"
:file-id (str (:id file))
:id (str snapshot-id)
:label label)
(db/insert! cfg :file-change
{:id snapshot-id
@@ -114,6 +123,7 @@
:data data
:version (:version file)
:features features
:migrations migrations
:profile-id profile-id
:file-id (:id file)
:label label
@@ -159,7 +169,17 @@
{:file-id file-id
:id snapshot-id}
{::db/for-share true})
(feat.fdata/resolve-file-data cfg))]
(feat.fdata/resolve-file-data cfg)
(decode-row))
;; If snapshot has tracked applied migrations, we reuse them,
;; if not we take a safest set of migrations as starting
;; point. This is because, at the time of implementing
;; snapshots, migrations were not taken into account so we
;; need to make this backward compatible in some way.
file (assoc file :migrations
(or (:migrations snapshot)
(fmg/generate-migrations-from-version 67)))]
(when-not snapshot
(ex/raise :type :not-found
@@ -180,12 +200,16 @@
:label (:label snapshot)
:snapshot-id (str (:id snapshot)))
;; If the file was already offloaded, on restring the snapshot
;; we are going to replace the file data, so we need to touch
;; the old referenced storage object and avoid possible leaks
;; If the file was already offloaded, on restoring the snapshot we
;; are going to replace the file data, so we need to touch the old
;; referenced storage object and avoid possible leaks
(when (feat.fdata/offloaded? file)
(sto/touch-object! storage (:data-ref-id file)))
;; In the same way, on reseting the file data, we need to restore
;; the applied migrations on the moment of taking the snapshot
(reset-migrations! conn file)
(db/update! conn :file
{:data (:data snapshot)
:revn (inc (:revn file))
@@ -253,7 +277,7 @@
:deleted-at nil}
{:id snapshot-id}
{::db/return-keys true})
(dissoc :data :features)))
(dissoc :data :features :migrations)))
(defn- get-snapshot
"Get a minimal snapshot from database and lock for update"

View File

@@ -185,7 +185,7 @@
[:map {:title "PartialFile"}
[:id ::sm/uuid]
[:revn {:min 0} ::sm/int]
[:page :any]])
[:page [:map-of :keyword ::sm/any]]])
(sv/defmethod ::get-file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used

View File

@@ -78,9 +78,10 @@
(defn decode-row
[{:keys [features subscription] :as row}]
(cond-> row
(some? features) (assoc :features (db/decode-pgarray features #{}))
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription))))
(when row
(cond-> row
(some? features) (assoc :features (db/decode-pgarray features #{}))
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription)))))
;; FIXME: move
@@ -461,11 +462,12 @@
;; --- COMMAND QUERY: get-team-info
(defn- get-team-info
(defn get-team-info
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
(db/get* conn :team
{:id id}
{::sql/columns [:id :is-default]}))
(-> (db/get* conn :team
{:id id}
{::sql/columns [:id :is-default :features]})
(decode-row)))
(sv/defmethod ::get-team-info
"Retrieve minimal team info by its ID."

View File

@@ -186,7 +186,7 @@
"canceled"
"incomplete"
"incomplete_expired"
"pass_due"
"past_due"
"paused"
"trialing"
"unpaid"]]
@@ -205,9 +205,8 @@
[:trial-start [:maybe ::sm/timestamp]]
[:cancel-at [:maybe ::sm/timestamp]]
[:canceled-at [:maybe ::sm/timestamp]]
[:current-period-end ::sm/timestamp]
[:current-period-start ::sm/timestamp]
[:current-period-end [:maybe ::sm/timestamp]]
[:current-period-start [:maybe ::sm/timestamp]]
[:cancel-at-period-end :boolean]
[:cancellation-details

View File

@@ -30,7 +30,7 @@
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2025.06.16-414"}
funcool/promesa
{:git/sha "f52f58cfacf62f59eab717e2637f37729d0cc383"
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"
:git/url "https://github.com/funcool/promesa"}
funcool/datoteka

View File

@@ -9,17 +9,16 @@
data resources."
(:refer-clojure :exclude [read-string hash-map merge name update-vals
parse-double group-by iteration concat mapcat
parse-uuid max min regexp?])
parse-uuid max min regexp? array?])
#?(:cljs
(:require-macros [app.common.data]))
(:require
#?(:cljs [cljs.core :as c]
:clj [clojure.core :as c])
#?(:cljs [cljs.reader :as r]
:clj [clojure.edn :as r])
#?(:cljs [goog.array :as garray])
[app.common.math :as mth]
[clojure.core :as c]
[clojure.set :as set]
[cuerdas.core :as str]
[linked.map :as lkm]
@@ -167,6 +166,15 @@
;; Data Structures Access & Manipulation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn array?
[o]
#?(:cljs
(c/array? o)
:clj
(if (some? o)
(.isArray (class o))
false)))
(defn not-empty?
[coll]
(boolean (seq coll)))

View File

@@ -418,7 +418,14 @@
[:type [:= :set-token-set]]
[:set-name :string]
[:group? :boolean]
[:token-set [:maybe [:fn ctob/token-set?]]]]]
;; 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"}

View File

@@ -81,7 +81,7 @@
(update :migrations set/union diff)
(vary-meta assoc ::migrated (not-empty diff)))))
(defn- generate-migrations-from-version
(defn generate-migrations-from-version
"A function that generates new format migration from the old,
version based migration system"
[version]
@@ -1618,4 +1618,4 @@
"0007-clear-invalid-strokes-and-fills-v2"
"0008-fix-library-colors-v4"
"0009-clean-library-colors"
"0009-add-partial-text-touched-flags"]))
#_"0009-add-partial-text-touched-flags"]))

View File

@@ -650,26 +650,12 @@
(check-component component file)
(deref *errors*)))
(def ^:private valid-fdata?
"Structural validation of file data using defined schema"
(sm/lazy-validator ::ctf/data))
(def ^:private get-fdata-explain
"Get schema explain data for file data"
(sm/lazy-explainer ::ctf/data))
(defn validate-file-schema!
"Validates the file itself, without external dependencies, it
performs the schema checking and some semantical validation of the
content."
[{:keys [id data] :as file}]
(when-not (valid-fdata? data)
(ex/raise :type :validation
:code :schema-validation
:hint (str/ffmt "invalid file data structure found on file '%'" id)
:file-id id
::sm/explain (get-fdata-explain data)))
file)
[file]
(update file :data ctf/check-file-data))
(defn validate-file!
"Validate full referential integrity and semantic coherence on file data.
@@ -683,7 +669,6 @@
:file-id (:id file)
:details errors)))
(declare compare-slots)
;; Optional check to look for missing swap slots.

View File

@@ -1665,27 +1665,62 @@
:shapes all-parents})]))))
(defn- text-partial-change-value
[touched-content untouched-content touched]
(cond
(touched :text-content-structure-same-attrs)
(if (touched :text-content-attribute)
;; Both structure and attrs has been touched, keep the
(defn- text-change-value
[touched-content ;; The :content of the copy text before updating
untouched-content ;; The :content of the main component
touched]
(let [main-comps-diff (cttx/get-diff-type touched-content untouched-content)
diff-structure? (contains? main-comps-diff :text-content-structure)
touched-attrs (cttx/get-first-paragraph-text-attrs touched-content)
;; Have touched content an uniform style?
thed-unif-style? (cttx/equal-attrs? touched-content touched-attrs)
untouched-attrs (cttx/get-first-paragraph-text-attrs untouched-content)
;; Have untouched content an uniform style?
untched-unif-style? (cttx/equal-attrs? untouched-content untouched-attrs)]
(cond
;; Both text and attrs has been touched, keep the
;; touched-content
(and (touched :text-content-text) (touched :text-content-attribute))
touched-content
;; Keep the touched-content structure and texts, update
;; its attrs to make them like the untouched-content
(cttx/copy-attrs-keys touched-content (cttx/get-first-paragraph-text-attrs untouched-content)))
(touched :text-content-text)
;; Keep the texts touched in touched-content, so copy the
;; texts from touched-content into untouched-content
(cttx/copy-text-keys touched-content untouched-content)
(touched :text-content-structure)
;; Special case for adding or removing paragraphs:
;; If the structure has been touched, but the attrs don't,
;; and both have uniform attributes, we keep the touched-content structure and
;; texts, updating its attrs to make them like the untouched-content
(if (and (not (touched :text-content-attribute)) thed-unif-style? untched-unif-style?)
(cttx/copy-attrs-keys touched-content untouched-attrs)
;; In other case, we keep the touched content
touched-content)
(touched :text-content-text)
;; Keep the texts touched in touched-content, so copy the
;; texts from touched-content into untouched-content
(cttx/copy-text-keys touched-content untouched-content)
(touched :text-content-attribute)
;; The untouched content has a different structure, but the touched content had't
;; touched the structure
(if diff-structure?
;; If both have uniform attributes, we keep the untouched-content structure and
;; texts, updating its attrs to make them like the touched-content
(if (and thed-unif-style? untched-unif-style?)
(cttx/copy-attrs-keys untouched-content touched-attrs)
;; In other case, we keep the touched content
touched-content)
;; Keep the attrs touched in touched-content, so copy the
;; texts from untouched-content into touched-content
(cttx/copy-text-keys untouched-content touched-content))
;; Nothing is touched
:else
untouched-content)))
(touched :text-content-attribute)
;; Keep the attrs touched in touched-content, so copy the
;; texts from untouched-content into touched-content
(cttx/copy-text-keys untouched-content touched-content)))
(defn- add-update-attr-operations
[attr dest-shape roperations uoperations attr-val]
@@ -1700,34 +1735,6 @@
[(conj roperations roperation)
(conj uoperations uoperation)]))
(defn- is-text-partial-change?
"Check if the attr update is a text partial change"
[untouched-shape touched-shape]
(let [touched (get touched-shape :touched #{})
partial-text-keys [:text-content-attribute :text-content-text]
active-keys (filter touched partial-text-keys)
untouched-content (:content untouched-shape)
untouched-attrs (cttx/get-first-paragraph-text-attrs untouched-content)
eq-untouched-attrs? (cttx/equal-attrs? untouched-content untouched-attrs)]
(and
(or
;; One and only one of the keys is pressent
(= 1 (count active-keys))
(and
(not (touched :text-content-attribute))
(touched :text-content-structure-same-attrs)))
(or
;; Both has the same structure
(cttx/equal-structure? untouched-content (:content touched-shape))
;; The origin and destiny have different structures, but each have the same attrs
;; for all the items on its content tree
(and
eq-untouched-attrs?
(touched :text-content-structure-same-attrs))))))
(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.
@@ -1784,13 +1791,13 @@
;; and attrs (bold, font, etc) are in the same attr :content.
;; If only one of them is touched, we want to adress this case and
;; only update the untouched one
text-partial-change?
(when (and
omit-touched?
(cfh/text-shape? origin-shape)
(= :content attr)
(touched attr-group))
(is-text-partial-change? origin-shape dest-shape))
text-content-change?
(and
omit-touched?
(cfh/text-shape? origin-shape)
(= :content attr)
(touched attr-group))
skip-operations?
(or (= (get origin-shape attr) (get dest-shape attr))
@@ -1799,7 +1806,7 @@
;; When it is a text-partial-change, we should generate operations
;; even when omit-touched? is true, but updating only the text or
;; the attributes, omiting the other part
(not text-partial-change?)))
(not text-content-change?)))
attr-val (when-not skip-operations?
(cond
@@ -1808,10 +1815,10 @@
reset-pos-data?
nil
text-partial-change?
(text-partial-change-value (:content dest-shape)
(:content origin-shape)
touched)
text-content-change?
(text-change-value (:content dest-shape)
(:content origin-shape)
touched)
:else
(get origin-shape attr)))
@@ -1824,7 +1831,7 @@
;; On a text-partial-change, we want to force a position-data reset
;; so it's calculated again
[roperations uoperations]
(if (and text-partial-change? (not skip-operations?))
(if (and text-content-change? (not skip-operations?))
(add-update-attr-operations :position-data dest-shape roperations uoperations nil)
[roperations uoperations])

View File

@@ -7,10 +7,12 @@
(ns app.common.logic.shapes
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.geom.shapes :as gsh]
[app.common.logic.variant-properties :as clvp]
[app.common.text :as ct]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.pages-list :as ctpl]
@@ -21,9 +23,12 @@
[app.common.uuid :as uuid]
[clojure.set :as set]))
(def text-typography-attrs (set ct/text-typography-attrs))
(defn- generate-unapply-tokens
"When updating attributes that have a token applied, we must unapply it, because the value
of the attribute now has been given directly, and does not come from the token."
of the attribute now has been given directly, and does not come from the token.
When applying a typography asset style we also unapply any typographic tokens."
[changes objects changed-sub-attr]
(let [new-objects (pcb/get-objects changes)
mod-obj-changes (->> (:redo-changes changes)
@@ -32,29 +37,38 @@
text-changed-attrs
(fn [shape]
(let [new-shape (get new-objects (:id shape))
attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))]
attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))
;; Unapply token when applying typography asset style
attrs (if (seq (set/intersection text-typography-attrs attrs))
(into attrs cto/typography-keys)
attrs)]
(apply set/union (map cto/shape-attr->token-attrs attrs))))
check-attr (fn [shape changes attr]
(let [tokens (get shape :applied-tokens {})
token-attrs (if (or (not= (:type shape) :text) (not= attr :content))
(cto/shape-attr->token-attrs attr changed-sub-attr)
(text-changed-attrs shape))]
(if (some #(contains? tokens %) token-attrs)
(pcb/update-shapes changes [(:id shape)] #(cto/unapply-token-id % token-attrs))
changes)))
check-attr
(fn [shape changes attr]
(let [shape-id (dm/get-prop shape :id)
tokens (get shape :applied-tokens {})
token-attrs (if (and (cfh/text-shape? shape) (= attr :content))
(text-changed-attrs shape)
(cto/shape-attr->token-attrs attr changed-sub-attr))]
check-shape (fn [changes mod-obj-change]
(let [shape (get objects (:id mod-obj-change))
xf (comp (filter #(= (:type %) :set))
(map :attr))
attrs (into [] xf (:operations mod-obj-change))]
(reduce (partial check-attr shape)
changes
attrs)))]
(reduce check-shape
changes
mod-obj-changes)))
(if (some #(contains? tokens %) token-attrs)
(pcb/update-shapes changes [shape-id] #(cto/unapply-token-id % token-attrs))
changes)))
check-shape
(fn [changes mod-obj-change]
(let [shape (get objects (:id mod-obj-change))
attrs (into []
(comp (filter #(= (:type %) :set))
(map :attr))
(:operations mod-obj-change))]
(reduce (partial check-attr shape)
changes
attrs)))]
(reduce check-shape changes mod-obj-changes)))
(defn generate-update-shapes
[changes ids update-fn objects {:keys [attrs changed-sub-attr ignore-tree ignore-touched with-objects?]}]

View File

@@ -292,7 +292,7 @@
(fix-gradients)
(assoc :text text))))
(split-texts [text styles]
(split-texts [text styles data]
(let [cpoints (text->code-points text)
children (->> (parse-draft-styles styles)
(build-style-index (count cpoints))
@@ -301,7 +301,7 @@
(mapv #(extract-text cpoints %)))]
(cond-> children
(empty? children)
(conj {:text ""}))))
(conj (assoc data :text "")))))
(build-paragraph [block]
(let [key (get block :key)
@@ -312,7 +312,7 @@
(-> data
(assoc :key key)
(assoc :type "paragraph")
(assoc :children (split-texts text styles)))))]
(assoc :children (split-texts text styles data)))))]
{:type "root"
:children

View File

@@ -15,7 +15,7 @@
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.pages-list :as ctpl]
[app.common.types.plugins :as ctpg]
[app.common.types.plugins :refer [schema:plugin-data]]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as cttx]
@@ -30,21 +30,22 @@
(def valid-container-types
#{:page :component})
(sm/register!
^{::sm/type ::container}
[:map
[:id ::sm/uuid]
[:type {:optional true}
[::sm/one-of valid-container-types]]
[:name :string]
[:path {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::sm/inst]
[:objects {:optional true}
[:map-of {:gen/max 10} ::sm/uuid :map]]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:container
(sm/register!
^{::sm/type ::container}
[:map
[:id ::sm/uuid]
[:type {:optional true}
[::sm/one-of valid-container-types]]
[:name :string]
[:path {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::sm/inst]
[:objects {:optional true}
[:map-of {:gen/max 10} ::sm/uuid :map]]
[:plugin-data {:optional true} schema:plugin-data]]))
(def check-container
(sm/check-fn ::container))
(sm/check-fn schema:container))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
@@ -517,15 +518,31 @@
;; --- SHAPE UPDATE
(defn- get-token-groups
"Get the sync attrs groups that are affected by changes in applied tokens.
If any token has been applied or unapplied in the shape, calculate the corresponding
attributes and get the groups. If some of the attributes are to be applied in the
content nodes of a text shape, also return the content groups (only for attributes,
so the text is not touched)."
[shape new-applied-tokens]
(let [old-applied-tokens (d/nilv (:applied-tokens shape) #{})
changed-token-attrs (filter #(not= (get old-applied-tokens %) (get new-applied-tokens %))
ctt/all-keys)
changed-groups (into #{}
(comp (map ctt/token-attr->shape-attr)
(map #(get ctk/sync-attrs %))
(filter some?))
changed-token-attrs)]
(let [old-applied-tokens (d/nilv (:applied-tokens shape) #{})
changed-token-attrs (filter #(not= (get old-applied-tokens %) (get new-applied-tokens %))
ctt/all-keys)
text-shape? (= (:type shape) :text)
attrs-in-text-content? (some #(ctt/attrs-in-text-content %)
changed-token-attrs)
changed-groups (into #{}
(comp (map ctt/token-attr->shape-attr)
(map #(get ctk/sync-attrs %))
(filter some?))
changed-token-attrs)
changed-groups (if (and text-shape?
(d/not-empty? changed-groups)
attrs-in-text-content?)
(conj changed-groups :content-group :text-content-attribute)
changed-groups)]
changed-groups))
(defn set-shape-attr

View File

@@ -23,9 +23,9 @@
[app.common.types.container :as ctn]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.plugins :as ctpg]
[app.common.types.plugins :refer [schema:plugin-data]]
[app.common.types.shape-tree :as ctst]
[app.common.types.tokens-lib :as ctl]
[app.common.types.tokens-lib :refer [schema:tokens-lib]]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
@@ -61,13 +61,13 @@
[:map-of {:gen/max 5} ::sm/uuid ctc/schema:library-color])
(def schema:components
[:map-of {:gen/max 5} ::sm/uuid ::ctn/container])
[:map-of {:gen/max 5} ::sm/uuid ctn/schema:container])
(def schema:typographies
[:map-of {:gen/max 2} ::sm/uuid ::cty/typography])
[:map-of {:gen/max 2} ::sm/uuid cty/schema:typography])
(def schema:pages-index
[:map-of {:gen/max 5} ::sm/uuid ::ctp/page])
[:map-of {:gen/max 5} ::sm/uuid ctp/schema:page])
(def schema:options
[:map {:title "FileOptions"}
@@ -82,8 +82,8 @@
[:colors {:optional true} schema:colors]
[:components {:optional true} schema:components]
[:typographies {:optional true} schema:typographies]
[:plugin-data {:optional true} ::ctpg/plugin-data]
[:tokens-lib {:optional true} ::ctl/tokens-lib]])
[:plugin-data {:optional true} schema:plugin-data]
[:tokens-lib {:optional true} schema:tokens-lib]])
(def schema:file
"A schema for validate a file data structure; data is optional

View File

@@ -53,10 +53,10 @@
[:name :string]
[:index {:optional true} ::sm/int]
[:objects schema:objects]
[:default-grids {:optional true} ::ctg/default-grids]
[:default-grids {:optional true} ctg/schema:default-grids]
[:flows {:optional true} schema:flows]
[:guides {:optional true} schema:guides]
[:plugin-data {:optional true} ::ctpg/plugin-data]
[:plugin-data {:optional true} ctpg/schema:plugin-data]
[:background {:optional true} ctc/schema:hex-color]
[:comment-thread-positions {:optional true}

View File

@@ -0,0 +1,20 @@
;; 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.stroke
(:require
[app.common.colors :as clr]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def default-stroke
{:stroke-alignment :inner
:stroke-style :solid
:stroke-color clr/black
:stroke-opacity 1
:stroke-width 1})

View File

@@ -83,21 +83,12 @@
compare them, and returns a set with the type of differences.
The possibilities are
:text-content-text
:text-content-attribute,
:text-content-structure
:text-content-structure-same-attrs."
:text-content-attribute
:text-content-structure"
[a b]
(let [diff-type (compare-text-content a b
{:text-cb (fn [acc] (conj acc :text-content-text))
:attribute-cb (fn [acc _] (conj acc :text-content-attribute))})]
(if-not (contains? diff-type :text-content-structure)
diff-type
(let [;; get attrs of the first paragraph of the first paragraph-set
attrs (get-first-paragraph-text-attrs a)]
(if (and (equal-attrs? a attrs)
(equal-attrs? b attrs))
#{:text-content-structure :text-content-structure-same-attrs}
diff-type)))))
(compare-text-content a b
{:text-cb (fn [acc] (conj acc :text-content-text))
:attribute-cb (fn [acc _] (conj acc :text-content-attribute))}))
(defn get-diff-attrs
"Given two content text structures, conformed by maps and vectors,

View File

@@ -29,20 +29,20 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:color "color"
:dimensions "dimension"
:font-size "fontSizes"
{:boolean "boolean"
:border-radius "borderRadius"
:color "color"
:dimensions "dimension"
:font-size "fontSizes"
:letter-spacing "letterSpacing"
:number "number"
:opacity "opacity"
:other "other"
:rotation "rotation"
:sizing "sizing"
:spacing "spacing"
:string "string"
:stroke-width "strokeWidth"})
:number "number"
:opacity "opacity"
:other "other"
:rotation "rotation"
:sizing "sizing"
:spacing "spacing"
:string "string"
:stroke-width "strokeWidth"})
(def dtcg-token-type->token-type
(set/map-invert token-type->dtcg-token-type))
@@ -92,19 +92,32 @@
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:spacing
(def ^:private schema:spacing-gap
[:map
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]])
(def ^:private schema:spacing-padding
[:map
[:p1 {:optional true} token-name-ref]
[:p2 {:optional true} token-name-ref]
[:p3 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]])
(def ^:private schema:spacing-margin
[:map
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]])
(def ^:private schema:spacing
(reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin]))
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:dimensions
@@ -115,6 +128,15 @@
(def dimensions-keys (schema-keys schema:dimensions))
(def ^:private schema:axis
[:map
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
(def axis-keys (schema-keys schema:axis))
(def ^:private schema:rotation
[:map
[:rotation {:optional true} token-name-ref]])
@@ -152,6 +174,7 @@
opacity-keys
spacing-keys
dimensions-keys
axis-keys
rotation-keys
typography-keys
number-keys))
@@ -203,7 +226,8 @@
(opacity-keys shape-attr) #{shape-attr}
(spacing-keys shape-attr) #{shape-attr}
(rotation-keys shape-attr) #{shape-attr}
(number-keys shape-attr) #{shape-attr})))
(number-keys shape-attr) #{shape-attr}
(axis-keys shape-attr) #{shape-attr})))
(defn token-attr->shape-attr
[token-attr]
@@ -263,6 +287,13 @@
[attributes token-type]
(seq (appliable-attrs attributes token-type)))
;; Token attrs that are set inside content blocks of text shapes, instead
;; at the shape level.
(def attrs-in-text-content
(set/union
typography-keys
#{:fill}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS IN SHAPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -167,6 +167,11 @@
(get-tokens [_] "return an ordered sequence of all tokens in the set")
(get-tokens-map [_] "return a map of tokens in the set, indexed by token-name"))
;; TODO: this structure is temporary. It's needed to be able to migrate TokensLib
;; from 1.2 to 1.3 when TokenSet datatype was changed to a deftype. This should
;; be removed after migrations are consolidated.
(defrecord TokenSetLegacy [id name description modified-at tokens])
(deftype TokenSet [id name description modified-at tokens]
#?@(:clj [clojure.lang.IDeref
(deref [_] {:id id
@@ -181,6 +186,10 @@
:modified-at modified-at
:tokens tokens})])
#?@(:clj
[json/JSONWriter
(-write [this writter options] (json/-write (deref this) writter options))])
#?@(:cljs [cljs.core/IEncodeJS
(-clj->js [_] (js-obj "id" (clj->js id)
"name" (clj->js name)
@@ -255,6 +264,10 @@
[o]
(instance? TokenSet o))
(defn token-set-legacy?
[o]
(instance? TokenSetLegacy o))
(def schema:token-set-attrs
[:map {:title "TokenSet"}
[:id ::sm/uuid]
@@ -283,7 +296,9 @@
(declare make-token-set)
(def schema:token-set
(sm/required-keys schema:token-set-attrs))
[:schema {:gen/gen (->> (sg/generator schema:token-set-attrs)
(sg/fmap #(make-token-set %)))}
(sm/required-keys schema:token-set-attrs)])
(sm/register! ::token-set schema:token-set) ;; need to register for the recursive schema of token-sets
@@ -1717,10 +1732,11 @@ Will return a value that matches this schema:
migrate-sets-node
(fn recurse [node]
(if (token-set? node)
(assoc node
:id (uuid/next)
:tokens (d/update-vals (:tokens node) migrate-token))
(if (token-set-legacy? node)
(make-token-set
(assoc node
:id (uuid/next)
:tokens (d/update-vals (:tokens node) migrate-token)))
(d/update-vals node recurse)))
sets
@@ -1748,6 +1764,26 @@ Will return a value that matches this schema:
(->TokensLib sets themes active-themes))))
#?(:clj
(defn- read-tokens-lib-v1-3
"Reads the tokens lib data structure and removes the TokenSetLegacy data type,
needed for a temporary migration step."
[r]
(let [sets (fres/read-object! r)
themes (fres/read-object! r)
active-themes (fres/read-object! r)
migrate-sets-node
(fn recurse [node]
(if (token-set-legacy? node)
(make-token-set node)
(d/update-vals node recurse)))
sets
(d/update-vals sets migrate-sets-node)]
(->TokensLib sets themes active-themes))))
#?(:clj
(defn- write-tokens-lib
[n w ^TokensLib o]
@@ -1776,6 +1812,11 @@ Will return a value that matches this schema:
(make-token obj)))}
{:name "penpot/token-set/v1"
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(map->TokenSetLegacy obj)))}
{:name "penpot/token-set/v2"
:class TokenSet
:wfn (fn [n w o]
(fres/write-tag! w n 1)
@@ -1803,8 +1844,11 @@ Will return a value that matches this schema:
{:name "penpot/tokens-lib/v1.2"
:rfn read-tokens-lib-v1-2}
;; CURRENT TOKENS LIB READER & WRITTER
{:name "penpot/tokens-lib/v1.3"
:rfn read-tokens-lib-v1-3}
;; CURRENT TOKENS LIB READER & WRITTER
{:name "penpot/tokens-lib/v1.4"
:class TokensLib
:wfn write-tokens-lib
:rfn read-tokens-lib}))

View File

@@ -103,7 +103,7 @@
file' (thf/apply-changes file changes)
copy-child' (ths/get-shape file' :copy-child)]
(t/is (= #{:content-group :text-content-structure :text-content-structure-same-attrs} (:touched copy-child')))))
(t/is (= #{:content-group :text-content-structure} (:touched copy-child')))))
(t/deftest test-text-copy-changed-structure-diff-attrs
(let [;; ==== Setup

View File

@@ -39,7 +39,7 @@
(t/is (= #{:text-content-attribute} diff-attr))
(t/is (= #{:text-content-text :text-content-attribute} diff-both))
(t/is (= #{:text-content-structure} diff-structure))
(t/is (= #{:text-content-structure :text-content-structure-same-attrs} diff-structure-same-attrs))))
(t/is (= #{:text-content-structure} diff-structure-same-attrs))))
(t/deftest test-get-diff-attrs

View File

@@ -0,0 +1,91 @@
FROM ubuntu:24.04
LABEL maintainer="Penpot <docker@penpot.app>"
ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8' \
DEBIAN_FRONTEND=noninteractive \
TZ=Etc/UTC
ARG IMAGEMAGICK_VERSION=7.1.1-47
RUN set -e; \
apt-get -qq update; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
autoconf \
binutils \
build-essential \
ca-certificates \
curl \
libfftw3-dev \
libheif-dev \
libjpeg-dev \
liblcms2-dev \
libltdl-dev \
liblzma-dev \
libopenexr-dev \
libpng-dev \
librsvg2-dev \
libtiff-dev \
libtool \
libwebp-dev \
libzip-dev \
libzstd-dev \
pkg-config \
; \
curl -LfsSo /tmp/magick.tar.gz https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IMAGEMAGICK_VERSION}.tar.gz; \
mkdir -p /tmp/magick; \
cd /tmp/magick; \
tar -xf /tmp/magick.tar.gz --strip-components=1; \
./configure --prefix=/opt/imagick; \
make -j 2; \
make install; \
rm -rf /opt/imagick/lib/libMagick++*; \
rm -rf /opt/imagick/include; \
rm -rf /opt/imagick/share; \
apt-get -qqy --autoremove purge \
autoconf \
binutils \
build-essential \
ca-certificates \
curl \
libfftw3-dev \
libheif-dev \
libjpeg-dev \
liblcms2-dev \
libltdl-dev \
liblzma-dev \
libopenexr-dev \
libpng-dev \
librsvg2-dev \
libtiff-dev \
libtool\
libwebp-dev \
libzip-dev \
libzstd-dev \
pkg-config \
;\
apt-get -qqy --no-install-recommends install \
libfontconfig1 \
libfreetype6 \
libglib2.0-0 \
libgomp1 \
libheif1 \
libjpeg-turbo8 \
liblcms2-2 \
libopenexr-3-1-30 \
libopenjp2-7 \
libpng16-16 \
librsvg2-2 \
libtiff6 \
libwebp7 \
libwebpdemux2 \
libwebpmux3 \
libxml2 \
libzip4t64 \
libzstd1 \
;\
apt-get -qqy clean; \
rm -rf /var/lib/apt/lists/*;
ENTRYPOINT ["/opt/imagick/bin/magick"]

View File

@@ -6,37 +6,18 @@ ENV LANG='C.UTF-8' \
JAVA_HOME="/opt/jdk" \
DEBIAN_FRONTEND=noninteractive \
NODE_VERSION=v22.16.0 \
IMAGEMAGICK_VERSION=7.1.1-47 \
TZ=Etc/UTC
RUN set -ex; \
apt-get -qq update; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
curl \
ca-certificates \
binutils \
build-essential autoconf libtool pkg-config \
libltdl-dev \
libpng-dev libjpeg-dev libtiff-dev libwebp-dev libopenexr-dev libfftw3-dev \
libzip-dev \
liblcms2-dev liblzma-dev libzstd-dev \
libheif-dev librsvg2-dev \
ca-certificates \
curl \
; \
rm -rf /var/lib/apt/lists/*
RUN set -eux; \
curl -LfsSo /tmp/magick.tar.gz https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IMAGEMAGICK_VERSION}.tar.gz; \
mkdir -p /tmp/magick; \
cd /tmp/magick; \
tar -xf /tmp/magick.tar.gz --strip-components=1; \
./configure --prefix=/opt/imagick; \
make -j 2; \
make install; \
rm -rf /opt/imagick/lib/libMagick++*; \
rm -rf /opt/imagick/include; \
rm -rf /opt/imagick/share;
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
@@ -105,33 +86,33 @@ RUN set -ex; \
apt-get -qq update; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
tzdata \
ca-certificates \
fontconfig \
woff-tools \
woff2 \
fontforge \
python3 \
python3-tabulate \
fontforge \
tzdata \
woff-tools \
woff2 \
\
libpng16-16 \
libfontconfig1 \
libfreetype6 \
libglib2.0-0 \
libgomp1 \
libheif1 \
libjpeg-turbo8 \
liblcms2-2 \
libopenexr-3-1-30 \
libopenjp2-7 \
libpng16-16 \
librsvg2-2 \
libtiff6 \
libwebp7 \
libopenexr-3-1-30 \
libfreetype6 \
libfontconfig1 \
libglib2.0-0 \
libxml2 \
liblcms2-2 \
libheif1 \
libopenjp2-7 \
libzstd1 \
librsvg2-2 \
libgomp1 \
libwebpmux3 \
libwebpdemux2 \
libwebpmux3 \
libxml2 \
libzip4t64 \
libzstd1 \
; \
find tmp/usr/share/zoneinfo/* -type d ! -name 'Etc' |xargs rm -rf; \
rm -rf /var/lib /var/cache; \
@@ -144,7 +125,7 @@ RUN set -ex; \
COPY --from=build /opt/jre /opt/jre
COPY --from=build /opt/node /opt/node
COPY --from=build /opt/imagick /opt/imagick
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
USER penpot:penpot

View File

@@ -39,6 +39,7 @@ RUN set -ex; \
fonts-wqy-zenhei \
fonts-tlwg-loma-otf \
fonts-freefont-ttf \
poppler-utils \
\
libasound2t64 \
libatk-bridge2.0-0t64 \

View File

@@ -97,6 +97,16 @@ file itself, which you can use as a basis for creating your own settings.
You can also consult the list of parameters on the
<a href="https://artifacthub.io/packages/helm/penpot/penpot#parameters" target="_blank">ArtifactHub page of the project</a>.
### Using OpenShift?
If you are deploying Penpot on OpenShift, we recommend following the specific guidelines provided in our Penpot-helm documentation:
<a href="https://artifacthub.io/packages/helm/penpot/penpot#-openshift-requirements" target="_blank">`Installing the chart with OpenShift requirements`</a>
Make sure to review the section **OpenShift Requirements** for important security and compatibility considerations.
### Using Rancher?
If you are deploying Penpot on Rancher, we recommend following the specific guidelines provided in the official documentation:
<a href="https://docs.apps.rancher.io/reference-guides/penpot/" target="_blank">Reference guides / Penpot</a>.
## Upgrade Penpot

View File

@@ -205,6 +205,10 @@ title: 10· Design Tokens
<h4>Y Position (dimension)</h4>
<p>The Y property specifies the position of the element on the Y axis of the canvas.</p>
<h3 id="design-tokens-font-size">Font Size</h3>
<p>Font size tokens allow you to define and standardize font-size values across your design system. These tokens can be applied to the <strong>font-size</strong> property in text layers, ensuring consistent typography throughout your designs.</p>
<p class="advice">Font size token values are always computed as <strong>px</strong> (pixels).</p>
<h3 id="design-tokens-opacity">Opacity</h3>
<p>Opacity tokens allow you to define the opacity of a layer, ranging from fully opaque to fully transparent.</p>
<p>Opacity tokens can be applied to any design element that supports transparency. You can use any decimal value between 0 and 1 to set varying levels of opacity or you can use any value between 0 and 100 with <strong>`%`</strong> sign at the end of the value. For example, you can use <strong>45%</strong> which would resolve to <strong>.45</strong>.</p>
@@ -378,7 +382,15 @@ title: 10· Design Tokens
</ol>
<h3 id="design-tokens-import-options">Import Options</h3>
<h4>Single file</h4>
<h4>ZIP file</h4>
<p>You can import tokens from a <strong>.zip</strong> file. This file can either contain a single JSON file or a folder structure with multiple files. The ZIP import option provides flexibility for organizing your tokens before importing them into Penpot.</p>
<ul>
<li>If the ZIP contains a single JSON file, it will be imported as a single set of tokens.</li>
<li>If the ZIP contains a folder structure, each file and folder will be interpreted as separate token sets, following the same rules as the multifile import.</li>
</ul>
<h4>Single JSON file</h4>
<p>You can import a JSON file comprising all tokens, token sets and token themes.</p>
<p>When importing a single file, the first-level keys of the json file will be interpreted as the set name.</p>

View File

@@ -34,7 +34,13 @@ desc: Master layer basics with Penpot's user guide! Learn to create, manipulate,
<p>Layers are displayed from the bottom to the top of the layer stack, with layers above on the stack being shown on top in the image.</p>
<h2 id="hide-lock">Hide and lock layers</h2>
<p>Click on the eye icon to change the visibility of a layer. Click on the lock icon to lock or unlock a layer. A locked layer can not be modified.</p>
<h3>Hide and show layers</h3>
<p>You can control the visibility of any layer by clicking the eye icon next to it in the Layers panel. When a layer is hidden, it will not appear on the canvas, but you can still select it in the Layers panel, move its order, or modify its properties. The eye icon always indicates whether a layer is visible or hidden, making it easy to manage complex designs.</p>
<h3>Lock and unlock layers</h3>
<p>Locking a layer helps prevent accidental changes or movement on the canvas. When a layer is locked, it cannot be moved or edited directly in the canvas area. However, you can still select a locked layer in the Layers panel and adjust its properties, such as color, effects, or name. The lock icon next to the layers name shows its locked status, helping you keep your design organized and protected.</p>
<figure>
<video title="Layers hide and lock" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-hide-lock.webp" height="auto">
<source src="/img/layers/layers-hide-lock.mp4" type="video/mp4">

View File

@@ -142,6 +142,11 @@ a design.</p>
<source src="/img/objects/text-create.mp4" type="video/mp4">
</video>
</figure>
<p><strong>Tips for resizing</strong></p>
<ul>
<li>Double-click on the right side of the bounding box to set the resize setting to auto-width.</li>
<li>Double-click on the bottom side of the bounding box to set the resize setting to auto-height.</li>
</ul>
<h3>Edit and style text content</h3>
<p>Press <kbd>Enter</kbd> with a text layer selected to start editing the text content. You can style parts of the text content as rich text.</p>
<figure>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -79,14 +79,29 @@
(defprotocol Event
(-data [_] "Get event data"))
(defn- coerce-to-string
[v]
(cond
(keyword? v)
(name v)
(string? v)
v
(nil? v)
nil
:else
(str v)))
(def ^:private xf:coerce-to-string
(keep coerce-to-string))
(defn- simplify-props
"Removes complex data types from props."
[data]
(reduce-kv (fn [data k v]
(cond
(map? v) (assoc data k :placeholder/map)
(vector? v) (assoc data k :placeholder/vec)
(set? v) (assoc data k :placeholder/set)
(vector? v) (assoc data k (into [] xf:coerce-to-string v))
(set? v) (assoc data k (into [] xf:coerce-to-string v))
(coll? v) (assoc data k :placeholder/coll)
(fn? v) (assoc data k :placeholder/fn)
(nil? v) (dissoc data k)

View File

@@ -80,8 +80,7 @@
(cb/format-code style-type)))
markup-code
(-> (cg/generate-markup-code objects markup-type [shape])
(cb/format-code markup-type))]
(cg/generate-formatted-markup-code objects markup-type [shape])]
(update-preview-window
preview

View File

@@ -20,7 +20,6 @@
[app.plugins.register :as plugins.register]
[app.util.i18n :as i18n :refer [tr]]
[app.util.storage :as storage]
[app.util.theme :as theme]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -159,9 +158,6 @@
(update-in state [:profile :theme]
(fn [current]
(let [current (cond
(= current "system")
(theme/get-system-theme)
;; NOTE: this is a workaround for
;; the old data on the database
;; where whe have `default` value
@@ -172,7 +168,8 @@
current)]
(case current
"dark" "light"
"light" "dark"
"light" "system"
"system" "dark"
; Failsafe for missing data
"dark")))))

View File

@@ -148,17 +148,17 @@
(defn- bind!
[shortcuts]
(let [msbind (fn [command callback type]
(->> shortcuts
(remove #(:disabled (second %)))
(run! (fn [[key {:keys [command fn type overwrite]}]]
(let [callback (wrap-cb key fn)
undefined (js* "(void 0)")
commands (if (vector? command)
(into-array command)
#js [command])]
(if type
(mousetrap/bind command callback type)
(mousetrap/bind command callback)))]
(->> shortcuts
(remove #(:disabled (second %)))
(run! (fn [[key {:keys [command fn type]}]]
(let [callback (wrap-cb key fn)]
(if (vector? command)
(run! #(msbind % callback type) command)
(msbind command callback type))))))))
(mousetrap/bind commands callback type overwrite)
(mousetrap/bind commands callback undefined overwrite)))))))
(defn- reset!
([]

View File

@@ -346,8 +346,8 @@
(gsh/translate-to-frame % (get objects parent-frame-id)))
shapes (mapv maybe-translate selected)
svg (svg/generate-markup objects shapes)]
(wapi/write-to-clipboard svg)))))
svg-formatted (svg/generate-formatted-markup objects shapes)]
(wapi/write-to-clipboard svg-formatted)))))
(defn copy-selected-css
[]

View File

@@ -1080,7 +1080,11 @@
(fn [state]
(-> state
(assoc :type :color)
(dissoc :editing-stop :stops :gradient)))))))
(dissoc :editing-stop :stops :gradient)))))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (update-colorpicker-color {} false)))))
(defn activate-colorpicker-gradient
[type]

View File

@@ -1095,15 +1095,24 @@
(when (seq (:redo-changes changes))
(rx/of (dch/commit-changes changes)))
(when-not (empty? updated-frames)
(rx/merge
(rx/of (ptk/data-event :layout/update {:ids (map :id updated-frames) :undo-group undo-group}))
(->> (rx/from updated-frames)
(rx/mapcat
(fn [shape]
(rx/of
(dwt/clear-thumbnail file-id (:page-id shape) (:id shape) "frame")
(when-not (= (:frame-id shape) uuid/zero)
(dwt/clear-thumbnail file-id (:page-id shape) (:frame-id shape) "frame"))))))))
(let [frames-by-page (->> updated-frames
(group-by :page-id))]
(rx/merge
;; Emit one layout/update event for each page
(rx/from
(map (fn [[page-id frames]]
(ptk/data-event :layout/update
{:page-id page-id
:ids (map :id frames)
:undo-group undo-group}))
frames-by-page))
(->> (rx/from updated-frames)
(rx/mapcat
(fn [shape]
(rx/of
(dwt/clear-thumbnail file-id (:page-id shape) (:id shape) "frame")
(when-not (= (:frame-id shape) uuid/zero)
(dwt/clear-thumbnail file-id (:page-id shape) (:frame-id shape) "frame")))))))))
(when (not= file-id library-id)
;; When we have just updated the library file, give some time for the

View File

@@ -131,11 +131,12 @@
;; they are process together. It will get a better performance.
(rx/buffer-time 100)
(rx/filter #(d/not-empty? %))
(rx/map
(rx/mapcat
(fn [data]
(let [page-id (->> data (keep :page-id) first)
ids (reduce #(into %1 (:ids %2)) #{} data)]
(update-layout-positions {:page-id page-id :ids ids}))))
(->> (group-by :page-id data)
(map (fn [[page-id items]]
(let [ids (reduce #(into %1 (:ids %2)) #{} items)]
(update-layout-positions {:page-id page-id :ids ids})))))))
(rx/take-until stopper))))))
(defn finalize-shape-layout

View File

@@ -11,6 +11,7 @@
[app.common.text :as txt]
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr]
[app.common.types.stroke :as cts]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.common.types.typography :as cty]
@@ -31,88 +32,6 @@
(declare token-properties)
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
(defn apply-token
"Apply `attributes` that match `token` for `shape-ids`.
Optionally remove attributes from `attributes-to-remove`,
this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set."
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
(ptk/reify ::apply-token
ptk/WatchEvent
(watch [_ state _]
;; We do not allow to apply tokens while text editor is open.
(when (empty? (get state :workspace-editor-state))
(when-let [tokens (some-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (sd/resolve-tokens tokens)
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
objects (dsh/lookup-page-objects state)
shape-ids (or (->> (select-keys objects shape-ids)
(filter (fn [[_ shape]]
(ctt/any-appliable-attr? attributes (:type shape))))
(keys))
[])
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token)]
(rx/of
(st/emit! (ptk/event ::ev/event {::ev/name "apply-tokens"}))
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes shape-ids (fn [shape]
(cond-> shape
attributes-to-remove
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
:always
(update :applied-tokens merge tokenized-attributes))))
(when on-update-shape
(on-update-shape resolved-value shape-ids attributes))
(dwu/commit-undo-transaction undo-id)))))))))))
(defn unapply-token
"Removes `attributes` that match `token` for `shape-ids`.
Doesn't update shape attributes."
[{:keys [attributes token shape-ids] :as _props}]
(ptk/reify ::unapply-token
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
(update shape :applied-tokens remove-token))))))))
(defn toggle-token
[{:keys [token shapes]}]
(ptk/reify ::on-toggle-token
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [attributes all-attributes on-update-shape]}
(get token-properties (:type token))
unapply-tokens?
(cft/shapes-token-applied? token shapes (or all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or all-attributes attributes)
:token token
:shape-ids shape-ids}))
(rx/of
(apply-token {:attributes attributes
:token token
:shape-ids shape-ids
:on-update-shape on-update-shape})))))))
;; Events to update the value of attributes with applied tokens ---------------------------------------------------------
;; (note that dwsh/update-shapes function returns an event)
@@ -173,8 +92,10 @@
(when (number? value)
(dwsh/update-shapes shape-ids
(fn [shape]
(when (seq (:strokes shape))
(assoc-in shape [:strokes 0 :stroke-width] value)))
(if (seq (:strokes shape))
(assoc-in shape [:strokes 0 :stroke-width] value)
(let [stroke (assoc cts/default-stroke :stroke-width value)]
(assoc shape :strokes [stroke]))))
{:reg-objects? true
:ignore-touched true
:page-id page-id
@@ -332,53 +253,157 @@
(dwsl/update-layout-child shape-ids props {:ignore-touched true
:page-id page-id}))))))))
(defn generate-text-shape-update
[txt-attrs shape-ids page-id]
(let [update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(-> node
(d/txt-merge txt-attrs)
(cty/remove-typography-from-node)))]
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id})))
(defn update-line-height
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(let [update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(-> node
(d/txt-merge {:line-height value})
(cty/remove-typography-from-node)))]
(when (number? value)
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id})))))
(when (number? value)
(generate-text-shape-update {:line-height value} shape-ids page-id))))
(defn update-letter-spacing
([value shape-ids attributes] (update-letter-spacing value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(let [update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(-> node
(d/txt-merge {:letter-spacing (str value)})
(cty/remove-typography-from-node)))]
(when (number? value)
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id})))))
(when (number? value)
(generate-text-shape-update {:letter-spacing (str value)} shape-ids page-id))))
(defn update-font-size
([value shape-ids attributes] (update-font-size value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(let [update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(-> node
(d/txt-merge {:font-size (str value)})
(cty/remove-typography-from-node)))]
(when (number? value)
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id})))))
(when (number? value)
(generate-text-shape-update {:font-size (str value)} shape-ids page-id))))
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
(defn apply-token
"Apply `attributes` that match `token` for `shape-ids`.
Optionally remove attributes from `attributes-to-remove`,
this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set."
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
(ptk/reify ::apply-token
ptk/WatchEvent
(watch [_ state _]
;; We do not allow to apply tokens while text editor is open.
(when (empty? (get state :workspace-editor-state))
(when-let [tokens (some-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (sd/resolve-tokens tokens)
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
objects (dsh/lookup-page-objects state)
selected-shapes (select-keys objects shape-ids)
shape-ids (or (->> selected-shapes
(filter (fn [[_ shape]]
(or
(and (ctsl/any-layout-immediate-child? objects shape)
(some ctt/spacing-margin-keys attributes))
(ctt/any-appliable-attr? attributes (:type shape)))))
(keys))
[])
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token)
type (:type token)]
(rx/of
(st/emit! (ev/event {::ev/name "apply-tokens"
:type type
:applyed-to attributes}))
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes shape-ids (fn [shape]
(cond-> shape
attributes-to-remove
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
:always
(update :applied-tokens merge tokenized-attributes))))
(when on-update-shape
(on-update-shape resolved-value shape-ids attributes))
(dwu/commit-undo-transaction undo-id)))))))))))
(defn apply-spacing-token
"Handles edge-case for spacing token when applying token via toggle button.
Splits out `shape-ids` into seperate default actions:
- Layouts take the `default` update function
- Shapes inside layout will only take margin"
[{:keys [token shapes]}]
(ptk/reify ::apply-spacing-token
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
{:keys [attributes on-update-shape]}
(get token-properties (:type token))
{:keys [other frame-children]}
(group-by #(if (ctsl/any-layout-immediate-child? objects %) :frame-children :other) shapes)]
(rx/of
(apply-token {:attributes attributes
:token token
:shape-ids (map :id other)
:on-update-shape on-update-shape})
(apply-token {:attributes ctt/spacing-margin-keys
:token token
:shape-ids (map :id frame-children)
:on-update-shape update-layout-item-margin}))))))
(defn unapply-token
"Removes `attributes` that match `token` for `shape-ids`.
Doesn't update shape attributes."
[{:keys [attributes token shape-ids] :as _props}]
(ptk/reify ::unapply-token
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
(update shape :applied-tokens remove-token))))))))
(defn toggle-token
[{:keys [token shapes]}]
(ptk/reify ::on-toggle-token
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [attributes all-attributes on-update-shape]}
(get token-properties (:type token))
unapply-tokens?
(cft/shapes-token-applied? token shapes (or all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or all-attributes attributes)
:token token
:shape-ids shape-ids}))
(rx/of
(case (:type token)
:spacing
(apply-spacing-token {:token token
:shapes shapes})
(apply-token {:attributes attributes
:token token
:shape-ids shape-ids
:on-update-shape on-update-shape}))))))))
;; Map token types to different properties used along the cokde ---------------------------------------------
@@ -444,6 +469,7 @@
ctt/spacing-keys
ctt/sizing-keys
ctt/border-radius-keys
ctt/axis-keys
ctt/stroke-width-keys)
:on-update-shape update-shape-dimensions
:modal {:key :tokens/dimensions

View File

@@ -34,7 +34,6 @@
[app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.timers :as ts]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
@@ -196,7 +195,7 @@
:on-edit on-edit-open
:on-menu-close on-menu-close}]]))
(mf/defc sidebar-search
(mf/defc sidebar-search*
[{:keys [search-term team-id] :as props}]
(let [search-term (or search-term "")
focused? (mf/use-state false)
@@ -586,8 +585,8 @@
:data-testid "delete-team"}
(tr "dashboard.delete-team")])]))
(mf/defc sidebar-team-switch
[{:keys [team profile] :as props}]
(mf/defc sidebar-team-switch*
[{:keys [team profile]}]
(let [teams (mf/deref refs/teams)
teams-without-default (into {} (filter (fn [[_ v]] (= false (:is-default v))) teams))
team-ids (map #(str "teams-selector-" %) (keys teams-without-default))
@@ -606,10 +605,6 @@
(when (get-in team [:permissions :is-owner])
"teams-options-delete-team")]
;; _ (prn "--------------- sidebar-team-switch")
;; _ (app.common.pprint/pprint teams)
handle-show-team-click
(fn [event]
(dom/stop-propagation event)
@@ -716,8 +711,7 @@
:profile profile}]]]))
(mf/defc sidebar-content*
{::mf/private true
::mf/props :obj}
{::mf/private true}
[{:keys [projects profile section team project search-term default-project] :as props}]
(let [default-project-id
(get default-project :id)
@@ -730,6 +724,7 @@
drafts? (and (= section :dashboard-files)
(= (:id project) default-project-id))
container (mf/use-ref nil)
overflow* (mf/use-state false)
overflow? (deref overflow*)
@@ -740,13 +735,14 @@
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)
(ts/schedule-on-idle
(fn []
(when-let [projects-title (dom/get-element "dashboard-projects-title")]
(dom/set-attribute! projects-title "tabindex" "0")
(dom/focus! projects-title)
(dom/set-attribute! projects-title "tabindex" "-1")))))))
(st/emit!
(dcm/go-to-dashboard-recent :team-id team-id)
(ts/schedule-on-idle
(fn []
(when-let [projects-title (dom/get-element "dashboard-projects-title")]
(dom/set-attribute! projects-title "tabindex" "0")
(dom/focus! projects-title)
(dom/set-attribute! projects-title "tabindex" "-1")))))))
go-fonts
(mf/use-fn
@@ -756,14 +752,17 @@
go-fonts-with-key
(mf/use-fn
(mf/deps team)
#(st/emit! (dcm/go-to-dashboard-fonts :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [font-title (dom/get-element "dashboard-fonts-title")]
(when font-title
(dom/set-attribute! font-title "tabindex" "0")
(dom/focus! font-title)
(dom/set-attribute! font-title "tabindex" "-1")))))))
(fn []
(st/emit!
(dcm/go-to-dashboard-fonts :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [font-title (dom/get-element "dashboard-fonts-title")]
(when font-title
(dom/set-attribute! font-title "tabindex" "0")
(dom/focus! font-title)
(dom/set-attribute! font-title "tabindex" "-1"))))))))
go-drafts
(mf/use-fn
(mf/deps team-id default-project-id)
@@ -785,39 +784,43 @@
go-libs
(mf/use-fn
(mf/deps team-id)
#(st/emit! (dcm/go-to-dashboard-libraries :team-id team-id)))
(fn [] (st/emit! (dcm/go-to-dashboard-libraries :team-id team-id))))
go-libs-with-key
(mf/use-fn
(mf/deps team-id)
#(st/emit! (dcm/go-to-dashboard-libraries :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [libs-title (dom/get-element "dashboard-libraries-title")]
(when libs-title
(dom/set-attribute! libs-title "tabindex" "0")
(dom/focus! libs-title)
(dom/set-attribute! libs-title "tabindex" "-1")))))))
pinned-projects
(->> projects
(remove :is-default)
(filter :is-pinned))]
(fn []
(st/emit!
(dcm/go-to-dashboard-libraries :team-id team-id)
(ts/schedule-on-idle
(fn []
(let [libs-title (dom/get-element "dashboard-libraries-title")]
(when libs-title
(dom/set-attribute! libs-title "tabindex" "0")
(dom/focus! libs-title)
(dom/set-attribute! libs-title "tabindex" "-1"))))))))
(mf/use-layout-effect
(mf/deps pinned-projects)
(fn []
(let [dom (mf/ref-val container)
client-height (obj/get dom "clientHeight")
scroll-height (obj/get dom "scrollHeight")]
(reset! overflow* (> scroll-height client-height)))))
pinned-projects
(mf/with-memo [projects]
(->> projects
(remove :is-default)
(filter :is-pinned)
(sort-by :name)
(not-empty)))]
(mf/with-layout-effect [pinned-projects]
(let [node (mf/ref-val container)
client-height (.-clientHeight ^js node)
scroll-height (.-scrollHeight ^js node)]
(reset! overflow* (> scroll-height client-height))))
[:*
[:div {:class (stl/css-case :sidebar-content true)
:ref container}
[:& sidebar-team-switch {:team team :profile profile}]
[:> sidebar-team-switch* {:team team :profile profile}]
[:& sidebar-search {:search-term search-term
:team-id (:id team)}]
[:> sidebar-search* {:search-term search-term
:team-id (:id team)}]
[:div {:class (stl/css :sidebar-content-section)}
[:ul {:class (stl/css :sidebar-nav)}
@@ -861,7 +864,7 @@
:data-testid "pinned-projects"}
[:div {:class (stl/css :sidebar-section-title)}
(tr "labels.pinned-projects")]
(if (seq pinned-projects)
(if (some? pinned-projects)
[:ul {:class (stl/css :sidebar-nav :pinned-projects)}
(for [item pinned-projects]
[:& sidebar-project
@@ -876,7 +879,6 @@
[:div {:class (stl/css-case :separator true :overflow-separator overflow?)}]]))
(mf/defc profile-section*
{::mf/props :obj}
[{:keys [profile team]}]
(let [show* (mf/use-state false)
show (deref show*)

View File

@@ -61,7 +61,7 @@
[:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.your-subscription")
:top-description (tr "subscription.dashboard.power-up.professional.top-title")
:bottom-description (tr "subscription.dashboard.power-up.professional.bottom-description", subscription-href)
:bottom-description (tr "subscription.dashboard.power-up.professional.bottom", subscription-href)
:has-dropdown true}]
"unlimited"
@@ -75,7 +75,7 @@
[:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.your-subscription")
:top-description (tr "subscription.dashboard.power-up.unlimited-plan")
:bottom-description (tr "subscription.dashboard.power-up.unlimited.bottom-description", subscription-href)
:bottom-description (tr "subscription.dashboard.power-up.unlimited.bottom-text", subscription-href)
:has-dropdown true}])
"enterprise"
@@ -125,7 +125,11 @@
(mf/defc menu-team-icon*
[{:keys [subscription-type]}]
[:span {:class (stl/css :subscription-icon) :data-testid "subscription-icon"}
[:span {:class (stl/css :subscription-icon)
:title (if (= subscription-type "unlimited")
(tr "subscription.dashboard.power-up.unlimited-plan")
(tr "subscription.dashboard.power-up.enterprise-plan"))
:data-testid "subscription-icon"}
(case subscription-type
"unlimited" i/character-u
"enterprise" i/character-e)])

View File

@@ -154,12 +154,17 @@
the dom with the result."
[tooltip placement origin-brect offset]
(show-popover tooltip)
(let [tooltip-brect (dom/get-bounding-rect tooltip)
(let [saved-height (dom/get-data tooltip "height")
saved-width (dom/get-data tooltip "width")
tooltip-brect (dom/get-bounding-rect tooltip)
tooltip-brect (assoc tooltip-brect :height (or saved-height (:height tooltip-brect)) :width (or saved-width (:width tooltip-brect)))
window-size (dom/get-window-size)]
(when-let [[placement placement-rect] (find-matching-placement placement tooltip-brect origin-brect window-size offset)]
(let [height (if (or (= placement "right") (= placement "left"))
(- (:height placement-rect) arrow-height)
(:height placement-rect))]
(dom/set-data! tooltip "height" (:height tooltip-brect))
(dom/set-data! tooltip "width" (:width tooltip-brect))
(dom/set-css-property! tooltip "block-size" (dm/str height "px"))
(dom/set-css-property! tooltip "inset-block-start" (dm/str (:top placement-rect) "px"))
(dom/set-css-property! tooltip "inset-inline-start" (dm/str (:left placement-rect) "px")))

View File

@@ -27,13 +27,12 @@
[rumext.v2 :as mf]))
(def type->options
{:multiple [:fill :stroke :image :text :shadow :blur :layout-element]
{:multiple [:fill :stroke :text :shadow :blur :layout-element]
:frame [:visibility :geometry :fill :stroke :shadow :blur :layout :layout-element]
:group [:visibility :geometry :svg :layout-element]
:rect [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:circle [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:path [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:image [:visibility :image :geometry :fill :stroke :shadow :blur :svg :layout-element]
:text [:visibility :geometry :text :shadow :blur :stroke :layout-element]
:variant [:variant :geometry :fill :stroke :shadow :blur :layout :layout-element]})

View File

@@ -13,6 +13,7 @@
[app.main.ui.components.title-bar :refer [inspect-title-bar*]]
[app.main.ui.inspect.attributes.common :refer [color-row]]
[app.util.code-gen.style-css :as css]
[app.util.code-gen.style-css-formats :refer [format-color]]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -22,8 +23,18 @@
(defn- shadow-copy-data [shadow]
(css/shadow->css shadow))
(defn- copy-color-data
"Converts a fill object to CSS color string in the specified format."
[color format]
(format-color color {:format format}))
(mf/defc shadow-block [{:keys [shadow]}]
(let [color-format (mf/use-state :hex)]
(let [color-format (mf/use-state :hex)
color-format* (deref color-format)
on-change-format
(mf/use-fn
(fn [format]
(reset! color-format format)))]
[:div {:class (stl/css :attributes-shadow-block)}
[:div {:class (stl/css :shadow-row)}
[:div {:class (stl/css :global/attr-label)} (->> shadow :style d/name (str "workspace.options.shadow-options.") (tr))]
@@ -42,7 +53,8 @@
[:& color-row {:color (:color shadow)
:format @color-format
:on-change-format #(reset! color-format %)}]]))
:copy-data (copy-color-data (:color shadow) color-format*)
:on-change-format on-change-format}]]))
(mf/defc shadow-panel [{:keys [shapes]}]
(let [shapes (->> shapes (filter has-shadow?))]

View File

@@ -7,10 +7,8 @@
(ns app.main.ui.inspect.attributes.text
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[app.common.text :as txt]
[app.common.types.color :as ctc]
[app.main.fonts :as fonts]
@@ -20,6 +18,7 @@
[app.main.ui.components.title-bar :refer [inspect-title-bar*]]
[app.main.ui.formats :as fmt]
[app.main.ui.inspect.attributes.common :refer [color-row]]
[app.util.code-gen.style-css-formats :refer [format-color]]
[app.util.color :as uc]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
@@ -38,13 +37,6 @@
(get-in state [:viewer-libraries file-id :data :typographies]))]
#(l/derived get-library st/state)))
(defn alpha->hex [alpha]
(-> (mth/round (* 255 alpha))
(js/Number)
(.toString 16)
(.toUpperCase)
(.padStart 2 "0")))
(defn- copy-style-data
[style & properties]
(->> properties
@@ -58,35 +50,13 @@
"background-clip: text;"
"color: transparent;"))
(defn- format-solid-color
"returns a CSS color string based on the provided color and format."
[color format]
(let [color-value (:color color)
opacity (:opacity color 1)
has-opacity? (not (= 1 opacity))]
(case format
:rgba
(let [[r g b a] (cc/hex->rgba color-value opacity)]
(str "color: rgba(" (cc/format-rgba [r g b a]) ");"))
:hex
(str "color: " color-value
(when has-opacity? (alpha->hex opacity)) ";")
:hsla
(let [[h s l a] (cc/hex->hsla color-value opacity)]
(str "color: hsla(" (cc/format-hsla [h s l a]) ");"))
;; Default fallback
(str "color: " color-value ";"))))
(defn- copy-color-data
"Converts a fill object to CSS color string in the specified format."
[fill format]
(let [color (ctc/fill->color fill)]
(if-let [gradient (:gradient color)]
(format-gradient-css gradient)
(format-solid-color color format))))
(format-color color {:format format}))))
(mf/defc typography-block
[{:keys [text style]}]

View File

@@ -149,8 +149,7 @@
(mf/use-memo
(mf/deps markup-type shapes images-data)
(fn []
(-> (cg/generate-markup-code objects markup-type shapes)
(cb/format-code markup-type))))
(cg/generate-formatted-markup-code objects markup-type shapes)))
on-markup-copied
(mf/use-fn

View File

@@ -101,5 +101,5 @@
@extend .button-secondary;
@include uppercaseTitleTipography;
height: $s-32;
width: $s-252;
width: 100%;
}

View File

@@ -35,6 +35,7 @@
[app.main.ui.releases.v2-6]
[app.main.ui.releases.v2-7]
[app.main.ui.releases.v2-8]
[app.main.ui.releases.v2-9]
[app.util.object :as obj]
[app.util.timers :as tm]
[rumext.v2 :as mf]))
@@ -99,4 +100,4 @@
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "2.8")))
(rc/render-release-notes (assoc params :version "2.9")))

View File

@@ -0,0 +1,112 @@
;; 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.main.ui.releases.v2-9
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
(defmethod c/render-release-notes "2.9"
[{:keys [slide klass next finish navigate version]}]
(mf/html
(case slide
:start
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.9-slide-0.jpg"
:class (stl/css :start-image)
:border "0"
:alt "Penpot 2.9 is here!"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Whats new in Penpot?"]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :features-block)}
[:span {:class (stl/css :feature-title)}
"Penpot 2.9 is out!"]
[:p {:class (stl/css :feature-content)}
"We're keeping the momentum going with another exciting round of improvements and features!"]
[:p {:class (stl/css :feature-content)}
"This release brings major progress in Design Token management (including our very first typography token!), smarter text overrides for components, and a rich collection of quality-of-life enhancements."]
[:p {:class (stl/css :feature-content)}
"Lets dive in!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click next} "Continue"]]]]]]
0
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.9-font-size.gif"
:class (stl/css :start-image)
:border "0"
:alt "New typography token type"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"New typography token type"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"You can now define and manage font size tokens right from the Design Tokens panel. This is just the first of many typography token types to come. Font family token is next!"]
[:p {:class (stl/css :feature-content)}
"And theres more progress on Tokens, including support for importing multiple token files via .zip, and smarter token visibility, only showing the relevant tokens for each layer type."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 2}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
1
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.9-qol.gif"
:class (stl/css :start-image)
:border "0"
:alt "Quality-of-life galore"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"Quality-of-life galore"]]
[:div {:class (stl/css :feature)}
[:p {:class (stl/css :feature-content)}
"This release packs dozens of small yet impactful usability improvements, including enhanced UX writing (thanks to community contributions!), a new visual indicator for comments directly in the design space, a reorganized dashboard sidebar, improved text resizing behavior, and much more."]
[:p {:class (stl/css :feature-content)}
"As always, we've squashed plenty of bugs and made underlying performance improvements to keep everything running smoothly."]]
[:div {:class (stl/css :navigation)}
[:& c/navigation-bullets
{:slide slide
:navigate navigate
:total 2}]
[:button {:on-click finish
:class (stl/css :next-btn)} "Let's go"]]]]]])))

View File

@@ -0,0 +1,102 @@
// 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
@import "refactor/common-refactor.scss";
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: $s-324 1fr;
height: $s-500;
width: $s-888;
border-radius: $br-8;
background-color: var(--modal-background-color);
border: $s-2 solid var(--modal-border-color);
}
.start-image {
width: $s-324;
border-radius: $br-8 0 0 $br-8;
}
.modal-content {
padding: $s-40;
display: grid;
grid-template-rows: auto 1fr $s-32;
gap: $s-24;
a {
color: var(--button-primary-background-color-rest);
}
}
.modal-header {
display: grid;
gap: $s-8;
}
.version-tag {
@include flexCenter;
@include headlineSmallTypography;
height: $s-32;
width: $s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: $br-8;
}
.modal-title {
@include headlineLargeTypography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: $s-16;
width: $s-440;
}
.feature {
display: flex;
flex-direction: column;
gap: $s-8;
}
.feature-title {
@include bodyLargeTypography;
color: var(--modal-title-foreground-color);
}
.feature-content {
@include bodyMediumTypography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
@include bodyMediumTypography;
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
gap: $s-8;
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend .button-primary;
width: $s-100;
justify-self: flex-end;
grid-area: button;
}

View File

@@ -6,7 +6,6 @@
[app.main.data.auth :as da]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.router :as rt]
@@ -62,7 +61,7 @@
(mf/defc subscribe-management-dialog
{::mf/register modal/components
::mf/register-as :management-dialog}
[{:keys [subscription-type teams subscribe-to-trial]}]
[{:keys [subscription-type current-subscription teams subscribe-to-trial]}]
(let [subscription-name (if subscribe-to-trial
(if (= subscription-type "unlimited")
@@ -127,12 +126,15 @@
(tr "subscription.settings.management.dialog.no-teams")])
(when (and
(or (= subscription-type "professional") (= subscription-type "unlimited"))
(or (and (= subscription-type "professional") (contains? #{"unlimited" "enterprise"} (:type current-subscription)))
(and (= subscription-type "unlimited") (= (:type current-subscription) "enterprise")))
(not (contains? #{"unpaid" "canceled"} (:status current-subscription)))
(not subscribe-to-trial))
[:div {:class (stl/css :modal-text)}
(tr "subscription.settings.management.dialog.downgrade")])
(if (and (= subscription-type "unlimited") subscribe-to-trial)
(if (and (= subscription-type "unlimited")
(or subscribe-to-trial (contains? #{"unpaid" "canceled"} (:status current-subscription))))
[:& fm/form {:on-submit subscribe-to-unlimited
:class (stl/css :seats-form)
:form form}
@@ -148,7 +150,16 @@
:class (stl/css :input-field)}]]
[:div {:class (stl/css :editors-cost)}
[:span {:class (stl/css :modal-text-small)}
(tr "subscription.settings.management.dialog.price-month" (or (get-in @form [:clean-data :min-members]) 0))]
(when (> (get-in @form [:clean-data :min-members]) 25)
[:> i18n/tr-html*
{:class (stl/css :modal-text-cap)
:tag-name "span"
:content (tr "subscription.settings.management.dialog.price-month" "175")}])
[:> i18n/tr-html*
{:class (stl/css-case :text-strikethrough (> (get-in @form [:clean-data :min-members]) 25))
:tag-name "span"
:content (tr "subscription.settings.management.dialog.price-month"
(* 7 (or (get-in @form [:clean-data :min-members]) 0)))}]]
[:span {:class (stl/css :modal-text-small)}
(tr "subscription.settings.management.dialog.payment-explanation")]]]
@@ -161,7 +172,7 @@
:on-click handle-close-dialog}]
[:> fm/submit-button*
{:label (tr "subscription.settings.start-trial")
{:label (if subscribe-to-trial (tr "subscription.settings.start-trial") (tr "labels.continue"))
:class (stl/css :primary-button)}]]]]
[:div {:class (stl/css :modal-footer)}
@@ -177,7 +188,9 @@
:type "button"
:value (if subscribe-to-trial (tr "subscription.settings.start-trial") (tr "labels.continue"))
:on-click (if subscribe-to-trial subscribe-to-enterprise handle-accept-dialog)}]]])]]]))
:on-click (if (or subscribe-to-trial
(contains? #{"unpaid" "canceled"} (:status current-subscription)))
subscribe-to-enterprise handle-accept-dialog)}]]])]]]))
(mf/defc subscription-success-dialog
{::mf/register modal/components
@@ -232,6 +245,9 @@
(or (= params-subscription "subscribed-to-penpot-unlimited")
(= params-subscription "subscribed-to-penpot-enterprise"))
success-modal-is-trial?
(-> route :params :query :trial)
subscription
(-> profile :props :subscription)
@@ -269,12 +285,13 @@
open-subscription-modal
(mf/use-fn
(mf/deps teams)
(fn [subscription-type]
(fn [subscription-type current-subscription]
(st/emit! (ev/event {::ev/name "open-subscription-modal"
::ev/origin "settings:in-app"}))
(st/emit!
(modal/show :management-dialog
{:subscription-type subscription-type
:current-subscription current-subscription
:teams teams :subscribe-to-trial (not subscription)}))))]
(mf/with-effect []
@@ -285,7 +302,7 @@
(mf/with-effect []
(dom/set-html-title (tr "subscription.labels")))
(mf/with-effect [authenticated? show-subscription-success-modal? show-trial-subscription-modal? subscription]
(mf/with-effect [authenticated? show-subscription-success-modal? show-trial-subscription-modal? success-modal-is-trial? subscription]
(when ^boolean authenticated?
(cond
^boolean show-trial-subscription-modal?
@@ -297,6 +314,7 @@
{:subscription-type (if (= params-subscription "subscription-to-penpot-unlimited")
"unlimited"
"enterprise")
:current-subscription subscription
:teams teams
:subscribe-to-trial (not subscription)})
(rt/nav :settings-subscription {} {::rt/replace true}))
@@ -305,14 +323,12 @@
(st/emit!
(modal/show :subscription-success
{:subscription-name (if (= params-subscription "subscribed-to-penpot-unlimited")
(tr "subscription.settings.unlimited-trial")
(tr "subscription.settings.enterprise-trial"))})
(du/update-profile-props {:subscription
(-> subscription
(assoc :type (if (= params-subscription "subscribed-to-penpot-unlimited")
"unlimited"
"enterprise"))
(assoc :status "trialing"))})
(if (= success-modal-is-trial? "true")
(tr "subscription.settings.unlimited-trial")
(tr "subscription.settings.unlimited"))
(if (= success-modal-is-trial? "true")
(tr "subscription.settings.enterprise-trial")
(tr "subscription.settings.enterprise")))})
(rt/nav :settings-subscription {} {::rt/replace true})))))
[:section {:class (stl/css :dashboard-section)}
@@ -327,7 +343,7 @@
[:> plan-card* {:card-title (tr "subscription.settings.professional")
:benefits [(tr "subscription.settings.professional.projects-files"),
(tr "subscription.settings.professional.teams-editors"),
(tr "subscription.settings.professional.storage")]}]
(tr "subscription.settings.professional.storage-autosave")]}]
"unlimited"
(if subscription-is-trial?
@@ -336,7 +352,7 @@
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.unlimited.teams"),
(tr "subscription.settings.unlimited.bill"),
(tr "subscription.settings.unlimited.storage")]
(tr "subscription.settings.unlimited.storage-autosave")]
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
@@ -348,7 +364,7 @@
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
:benefits [(tr "subscription.settings.unlimited.teams"),
(tr "subscription.settings.unlimited.bill"),
(tr "subscription.settings.unlimited.storage")]
(tr "subscription.settings.unlimited.storage-autosave")]
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments
:editors (-> profile :props :subscription :quantity)}])
@@ -358,22 +374,22 @@
[:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial")
:card-title-icon i/character-e
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.enterprise.support"),
(tr "subscription.settings.enterprise.security"),
(tr "subscription.settings.enterprise.logs")]
:benefits [(tr "subscription.settings.enterprise.unlimited-storage"),
(tr "subscription.settings.enterprise.capped-bill"),
(tr "subscription.settings.enterprise.autosave")]
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments}]
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
:card-title-icon i/character-e
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.enterprise.support"),
(tr "subscription.settings.enterprise.security"),
(tr "subscription.settings.enterprise.logs")]
:benefits [(tr "subscription.settings.enterprise.unlimited-storage"),
(tr "subscription.settings.enterprise.capped-bill"),
(tr "subscription.settings.enterprise.autosave")]
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments}]))
[:div {:class (stl/css :membership-container)}
(when subscribed-since
(when (and subscribed-since (not= subscription-type "professional"))
[:div {:class (stl/css :membership)}
[:span {:class (stl/css :subscription-member)} i/crown]
[:span {:class (stl/css :membership-date)}
@@ -392,7 +408,7 @@
:price-period (tr "subscription.settings.price-editor-month")
:benefits [(tr "subscription.settings.professional.projects-files"),
(tr "subscription.settings.professional.teams-editors"),
(tr "subscription.settings.professional.storage")]
(tr "subscription.settings.professional.storage-autosave")]
:cta-text (tr "subscription.settings.subscribe")
:cta-link #(open-subscription-modal "professional")
:cta-text-with-icon (tr "subscription.settings.more-information")
@@ -406,9 +422,9 @@
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.unlimited.teams"),
(tr "subscription.settings.unlimited.bill"),
(tr "subscription.settings.unlimited.storage")]
(tr "subscription.settings.unlimited.storage-autosave")]
:cta-text (if subscription (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free"))
:cta-link #(open-subscription-modal "unlimited")
:cta-link #(open-subscription-modal "unlimited" subscription)
:cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page}])
@@ -417,11 +433,11 @@
:card-title-icon i/character-e
:price-value "$950"
:price-period (tr "subscription.settings.price-organization-month")
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.enterprise.support"),
(tr "subscription.settings.enterprise.security"),
(tr "subscription.settings.enterprise.logs")]
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
:benefits [(tr "subscription.settings.enterprise.unlimited-storage"),
(tr "subscription.settings.enterprise.capped-bill"),
(tr "subscription.settings.enterprise.autosave")]
:cta-text (if subscription (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free"))
:cta-link #(open-subscription-modal "enterprise")
:cta-link #(open-subscription-modal "enterprise" subscription)
:cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page}])]]]))

View File

@@ -199,6 +199,20 @@
@include t.use-typography("body-small");
}
.modal-text-cap {
margin-inline-end: var(--sp-s);
}
.text-strikethrough {
text-decoration: line-through;
}
.modal-text-small strong,
.text-strikethrough strong,
.modal-text-cap strong {
font-weight: $fw700;
}
.modal-content,
.modal-end {
color: var(--color-foreground-secondary);
@@ -251,6 +265,8 @@
list-style-type: disc;
margin-inline-start: var(--sp-xl);
margin-block: var(--sp-xxl);
max-height: $s-216;
overflow-y: auto;
}
.input-field {

View File

@@ -103,15 +103,15 @@
(obj/set! attrs "strokeWidth" width)
(when (some? gradient)
(if (some? gradient)
(let [gradient-id (dm/str "stroke-color-gradient-" render-id "-" index)]
(obj/set! attrs "stroke" (str/ffmt "url(#%)" gradient-id))))
(obj/set! attrs "stroke" (str/ffmt "url(#%)" gradient-id)))
(when-not (some? gradient)
(when (some? color)
(obj/set! attrs "stroke" color))
(when (some? opacity)
(obj/set! attrs "strokeOpacity" opacity)))
(obj/set! attrs "stroke" color)))
(when (some? opacity)
(obj/set! attrs "strokeOpacity" opacity))
(when (not= style :svg)
(obj/set! attrs "strokeDasharray" (calculate-dasharray style width)))

View File

@@ -11,6 +11,7 @@
[app.common.data :as d]
[app.common.pprint :as pp]
[app.common.uri :as u]
[app.main.data.auth :refer [is-authenticated?]]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.refs :as refs]
@@ -180,7 +181,7 @@
[:div {:class (stl/css :form-container)}
[:& recovery-sent-page {:email @user-email}]])]]]))
(mf/defc request-dialog
(mf/defc request-dialog*
{::mf/props :obj}
[{:keys [title content button-text on-button-click cancel-text on-close]}]
(let [on-click (or on-button-click on-close)]
@@ -234,45 +235,45 @@
(cond
is-default
[:& request-dialog {:title (tr "not-found.no-permission.project")
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.project")
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(and (some? file-id) (:already-requested requested))
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.file")
:content [(tr "not-found.no-permission.already-requested.or-others.file")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.already-requested.file")
:content [(tr "not-found.no-permission.already-requested.or-others.file")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(:already-requested requested)
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.project")
:content [(tr "not-found.no-permission.already-requested.or-others.project")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.already-requested.project")
:content [(tr "not-found.no-permission.already-requested.or-others.project")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(:sent requested)
[:& request-dialog {:title (tr "not-found.no-permission.done.success")
:content [(tr "not-found.no-permission.done.remember")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.done.success")
:content [(tr "not-found.no-permission.done.remember")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(some? file-id)
[:& request-dialog {:title (tr "not-found.no-permission.file")
:content [(tr "not-found.no-permission.you-can-ask.file")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:> request-dialog* {:title (tr "not-found.no-permission.file")
:content [(tr "not-found.no-permission.you-can-ask.file")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(some? team-id)
[:& request-dialog {:title (tr "not-found.no-permission.project")
:content [(tr "not-found.no-permission.you-can-ask.project")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}])))
[:> request-dialog* {:title (tr "not-found.no-permission.project")
:content [(tr "not-found.no-permission.you-can-ask.project")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}])))
(mf/defc not-found*
[]
@@ -485,31 +486,31 @@
{::mf/props :obj}
[{:keys [data route] :as props}]
(let [type (:type data)
path (:path route)
(let [type (:type data)
path (:path route)
params (:query-params route)
params (:query-params route)
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
;; We store the request access info int this state
info* (mf/use-state nil)
info (deref info*)
info* (mf/use-state nil)
info (deref info*)
loaded? (get info :loaded false)
profile (mf/deref refs/profile)
profile (mf/deref refs/profile)
auth-error?
(= type :authentication)
auth-error? (= type :authentication)
not-found? (= type :not-found)
authenticated?
(is-authenticated? profile)
request-access?
(and
(or (= type :not-found) auth-error?)
(or workspace? dashboard? view?)
(or (:file-id info)
(:team-id info)))]
(or (some? (:file-id info))
(some? (:team-id info))))]
(mf/with-effect [params info]
(when-not (:loaded info)
@@ -518,24 +519,25 @@
(partial reset! info* {:loaded true})))))
(if auth-error?
[:> context-wrapper*
{:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> login-dialog* {}]]
(when loaded?
(if request-access?
[:> context-wrapper* {:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> request-access* {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
:is-workspace workspace?}]]
[:> exception-section* props])))))
(if (or auth-error? not-found?)
(if (not authenticated?)
[:> context-wrapper*
{:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> login-dialog* {}]]
(when (get info :loaded false)
(if request-access?
[:> context-wrapper* {:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> request-access* {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
:profile profile
:is-workspace workspace?}]]
[:> exception-section* props])))
[:> exception-section* props])))

View File

@@ -275,11 +275,8 @@
handle-gradient-remove-stop
(mf/use-fn
(mf/deps state)
(fn [stop]
(when (> (count (:stops state)) 2)
(when-let [index (d/index-of-pred (:stops state) #(= % stop))]
(st/emit! (dc/remove-gradient-stop index))))))
(fn [index]
(st/emit! (dc/remove-gradient-stop index))))
handle-stop-edit-start
(mf/use-fn

View File

@@ -80,10 +80,10 @@
handle-remove-stop
(mf/use-callback
(mf/deps on-remove-stop stop)
(mf/deps on-remove-stop index)
(fn []
(when on-remove-stop
(on-remove-stop stop))))
(on-remove-stop index))))
handle-focus-stop-offset
(mf/use-fn

View File

@@ -24,5 +24,6 @@
{:delete-stop {:tooltip (ds/supr)
:command ["del" "backspace"]
:subsections [:edit]
:overwrite true
:fn #(st/emit! (dwc/remove-gradient-stop))}}))

View File

@@ -59,6 +59,14 @@
on-unmount children is-selected icon disabled value]}]
(let [submenu-ref (mf/use-ref nil)
hovering? (mf/use-ref false)
on-click'
(mf/use-fn
(mf/deps on-click)
(fn [event]
(st/emit! dw/hide-context-menu)
(when on-click (on-click event))))
on-pointer-enter
(mf/use-fn
(fn []
@@ -96,7 +104,7 @@
:disabled disabled
:data-value value
:ref set-dom-node
:on-click on-click
:on-click on-click'
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
[:span
@@ -110,7 +118,7 @@
:disabled disabled
:ref set-dom-node
:data-value value
:on-click on-click
:on-click on-click'
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
[:span {:class (stl/css :title)} title]

View File

@@ -280,8 +280,8 @@
:data-testid "toggle-theme"
:id "file-menu-toggle-theme"}
[:span {:class (stl/css :item-name)}
(case (:theme profile) ;; default = dark -> light -> system -> dark and so on
"default" (tr "workspace.header.menu.toggle-light-theme")
(case (:theme profile) ;; dark -> light -> system -> dark and so on
"dark" (tr "workspace.header.menu.toggle-light-theme")
"light" (tr "workspace.header.menu.toggle-system-theme")
"system" (tr "workspace.header.menu.toggle-dark-theme")
(tr "workspace.header.menu.toggle-light-theme"))]

View File

@@ -310,7 +310,7 @@
:id "align-self-end"}]])
(mf/defc layout-item-menu
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent?}
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent? :is-grid-layout? :is-flex-layout?}
::mf/props :obj}
[{:keys [ids values
^boolean is-layout-child?

View File

@@ -7,9 +7,9 @@
(ns app.main.ui.workspace.sidebar.options.menus.stroke
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.stroke :as cts]
[app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as dc]
[app.main.store :as st]
@@ -157,11 +157,7 @@
on-add-stroke
(fn [_]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/add-stroke ids {:stroke-alignment :inner
:stroke-style :solid
:stroke-color clr/black
:stroke-opacity 1
:stroke-width 1}))
(st/emit! (dc/add-stroke ids cts/default-stroke))
(when (not (some? (seq strokes))) (open-content)))
disable-drag (mf/use-state false)

View File

@@ -112,6 +112,7 @@
recent-fonts (mf/with-memo [state recent-fonts]
(filter-fonts state recent-fonts))
full-size? (boolean (and full-size show-recent))
select-next
@@ -130,13 +131,6 @@
(dom/prevent-default event)
(swap! selected get-prev-font fonts)))
on-select-and-close
(mf/use-fn
(mf/deps on-select on-close)
(fn [font]
(on-select font)
(on-close)))
on-key-down
(mf/use-fn
(mf/deps fonts)
@@ -145,7 +139,7 @@
(kbd/up-arrow? event) (select-prev event)
(kbd/down-arrow? event) (select-next event)
(kbd/esc? event) (on-close)
(kbd/enter? event) (do (on-select-and-close @selected))
(kbd/enter? event) (on-close)
:else (dom/focus! (mf/ref-val input)))))
on-filter-change
@@ -169,6 +163,9 @@
(when-let [index (:index @selected)]
(.scrollToRow ^js inst index))))
(mf/with-effect [@selected]
(on-select @selected))
(mf/with-effect []
(st/emit! (dsc/push-shortcuts :typography {}))
(fn []
@@ -181,10 +178,6 @@
#(let [offset (.getOffsetForRow ^js inst #js {:alignment "center" :index index})]
(.scrollToPosition ^js inst offset)))))
(mf/with-effect [(:term state) fonts]
(when (and (seq fonts) (not= (:id @selected) (:id (first fonts))))
(reset! selected (first fonts))))
[:div {:class (stl/css :font-selector)}
[:div {:class (stl/css-case :font-selector-dropdown true :font-selector-dropdown-full-size full-size?)}
[:div {:class (stl/css :header)}

View File

@@ -2,6 +2,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
@@ -61,6 +62,10 @@
(mf/with-memo [selected objects]
(into [] (keep (d/getf objects)) selected))
is-selected-inside-layout
(mf/with-memo [selected-shapes objects]
(some #(ctsl/any-layout-immediate-child? objects %) selected-shapes))
active-theme-tokens
(mf/with-memo [tokens-lib]
(if tokens-lib
@@ -148,6 +153,7 @@
:is-open (get open-status type false)
:type type
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens'
:tokens tokens}]))
@@ -155,5 +161,6 @@
[:> token-group* {:key (name type)
:type type
:selected-shapes selected-shapes
:is-selected-inside-layout :is-selected-inside-layout
:active-theme-tokens active-theme-tokens'
:tokens []}])]))

View File

@@ -10,6 +10,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cft]
[app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
@@ -34,11 +35,13 @@
(some #(contains? m %) ks))
(defn clean-separators
"Cleans up `:separator` inside of `items`
Will clean consecutive items like `[:separator :separator {}]`
And will return nil for lists consisting only of `:separator` items."
"Cleans up `:separator` inside of `items` with these rules:
- Clean consecutive items like `[:separator :separator {}]`
- Returns nil for lists consisting only of `:separator` items.
- Removes `:separator` at the beginning of the `items`"
[items]
(let [items' (dedupe items)]
(let [items' (->> (dedupe items)
(drop-while #(= % :separator)))]
(when-not (every? #(= % :separator) items')
items')))
@@ -190,7 +193,7 @@
(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes] :as context-data}]
(defn spacing-attribute-actions [{:keys [token selected-shapes allowed-shape-attributes is-selected-inside-layout] :as context-data}]
(let [padding-attr-labels {:p1 "Padding top"
:p2 "Padding right"
:p3 "Padding bottom"
@@ -209,7 +212,9 @@
:m2 "Margin right"
:m3 "Margin bottom"
:m4 "Margin left"}
margin-items (when (key-in-map? allowed-shape-attributes margin-attr-labels)
margin-items (when (or
is-selected-inside-layout
(key-in-map? allowed-shape-attributes margin-attr-labels))
(layout-spacing-items {:token token
:selected-shapes selected-shapes
:all-attr-labels margin-attr-labels
@@ -224,11 +229,13 @@
:hint (tr "workspace.tokens.gaps")
:on-update-shape dwta/update-layout-spacing}
context-data)]
(concat gap-items
(when padding-items [:separator])
padding-items
(when margin-items [:separator])
margin-items)))
(->> (concat
gap-items
[:separator]
padding-items
[:separator]
margin-items)
(clean-separators))))
(defn sizing-attribute-actions [context-data]
(->>
@@ -446,18 +453,30 @@
(mf/defc token-context-menu-tree
[{:keys [width errors] :as mdata}]
(let [objects (mf/deref refs/workspace-page-objects)
(let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
selected-shapes (into [] (keep (d/getf objects)) selected)
token-name (:token-name mdata)
token (mf/deref (refs/workspace-token-in-selected-set token-name))
selected-token-set-name (mf/deref refs/selected-token-set-name)]
token-type (:type token)
selected-token-set-name (mf/deref refs/selected-token-set-name)
selected-shapes
(mf/with-memo [selected objects]
(into [] (keep (d/getf objects)) selected))
is-selected-inside-layout
(mf/with-memo [token-type selected-shapes objects]
(when (contains? #{:spacing :dimensions} token-type)
(some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)))]
[:ul {:class (stl/css :context-list)}
[:& menu-tree {:submenu-offset width
:token token
:errors errors
:selected-token-set-name selected-token-set-name
:selected-shapes selected-shapes}]]))
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout}]]))
(mf/defc token-context-menu
[]

View File

@@ -42,7 +42,7 @@
(mf/defc token-group*
{::mf/private true}
[{:keys [type tokens selected-shapes active-theme-tokens is-open]}]
[{:keys [type tokens selected-shapes is-selected-inside-layout active-theme-tokens is-open]}]
(let [{:keys [modal title]}
(get dwta/token-properties type)
editing-ref (mf/deref refs/workspace-editor-state)
@@ -91,7 +91,7 @@
(mf/deps selected-shapes not-editing?)
(fn [event token]
(dom/stop-propagation event)
(when (and not-editing? (seq selected-shapes))
(when (and not-editing? (seq selected-shapes) (not= (:type token) :number))
(st/emit! (dwta/toggle-token {:token token
:shapes selected-shapes})))))]
@@ -115,6 +115,7 @@
{:key (:name token)
:token token
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
:on-click on-token-pill-click
:on-context-menu on-context-menu}])]])]]))

View File

@@ -21,6 +21,7 @@
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[clojure.set :as set]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -123,7 +124,9 @@
base-title (dm/str "Token: " name "\n"
(tr "workspace.tokens.original-value" value) "\n"
(tr "workspace.tokens.resolved-value" resolved-value))]
(tr "workspace.tokens.resolved-value" resolved-value)
(when (= (:type token) :number)
(dm/str "\n" (tr "workspace.tokens.more-options"))))]
(cond
;; If there are errors, show the appropriate message
@@ -164,17 +167,20 @@
(cft/shapes-applied-all? ids-by-attributes shape-ids attributes)))
(defn attributes-match-selection?
[selected-shapes attrs]
(some (fn [shape]
(ctt/any-appliable-attr? attrs (:type shape)))
selected-shapes))
[selected-shapes attrs & {:keys [selected-inside-layout?]}]
(or
;; Edge-case for allowing margin attribute on shapes inside layout parent
(and selected-inside-layout? (set/subset? ctt/spacing-margin-keys attrs))
(some (fn [shape]
(ctt/any-appliable-attr? attrs (:type shape)))
selected-shapes)))
(def token-types-with-status-icon
#{:color :border-radius :rotation :sizing :dimensions :opacity :spacing :stroke-width})
(mf/defc token-pill*
{::mf/wrap [mf/memo]}
[{:keys [on-click token on-context-menu selected-shapes active-theme-tokens]}]
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
(let [{:keys [name value errors type]} token
has-selected? (pos? (count selected-shapes))
@@ -201,7 +207,7 @@
has-selected?
(not applied?)
(not half-applied?)
(not (attributes-match-selection? selected-shapes attributes)))
(not (attributes-match-selection? selected-shapes attributes {:selected-inside-layout? is-selected-inside-layout})))
;; FIXME: move to context or props
can-edit? (:can-edit (deref refs/permissions))

View File

@@ -59,7 +59,7 @@
:type "text"
:on-blur on-submit
:on-key-down on-key-down
:maxlength "256"
:max-length "256"
:auto-focus true
:placeholder (tr "workspace.tokens.set-edit-placeholder")
:default-value default-value}]))
@@ -210,7 +210,7 @@
(mf/defc sets-tree-set*
[{:keys [id set label tree-depth tree-path tree-index is-selected is-active is-draggable is-editing
on-select on-drop on-toggle on-start-edition on-reset-edition on-edit-submit]}]
on-select on-drop on-toggle on-start-edition on-reset-edition on-edit-submit is-new]}]
(let [set-name (ctob/get-name set)
can-edit? (mf/use-ctx ctx/can-edit?)
@@ -239,7 +239,11 @@
:path tree-path})))))
on-double-click
(mf/use-fn (mf/deps id) #(on-start-edition id))
(mf/use-fn
(mf/deps id is-new)
(fn []
(when-not is-new
(on-start-edition id))))
on-checkbox-click
(mf/use-fn
@@ -392,7 +396,7 @@
:is-editing true
:is-active true
:is-selected true
:is-new true
:tree-path path
:tree-depth depth
:tree-index index
@@ -416,6 +420,7 @@
:tree-path path
:tree-depth depth
:tree-index index
:is-new false
:tree-parent-path parent-path
:on-toggle on-toggle-set
:edition-id edition-id

View File

@@ -410,7 +410,7 @@
:else
(let [objects (u/locate-objects)
shapes (into [] (map u/proxy->shape) shapes)]
(cg/generate-markup-code objects type shapes)))))
(cg/generate-formatted-markup-code objects type shapes)))))
:generateStyle
(fn [shapes options]

View File

@@ -6,6 +6,7 @@
(ns app.util.code-gen
(:require
[app.util.code-beautify :as cb]
[app.util.code-gen.markup-html :as html]
[app.util.code-gen.markup-svg :as svg]
[app.util.code-gen.style-css :as css]))
@@ -18,6 +19,11 @@
"svg" svg/generate-markup)]
(generate-markup objects shapes)))
(defn generate-formatted-markup-code
[objects type shapes]
(let [markup (generate-markup-code objects type shapes)]
(cb/format-code markup type)))
(defn generate-style-code
([objects type root-shapes all-shapes]
(generate-style-code objects type root-shapes all-shapes nil))

View File

@@ -8,6 +8,7 @@
(:require
["react-dom/server" :as rds]
[app.main.render :as render]
[app.util.code-beautify :as cb]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -24,3 +25,8 @@
(->> shapes
(map #(generate-svg objects %))
(str/join "\n")))
(defn generate-formatted-markup
[objects shapes]
(let [markup (generate-markup objects shapes)]
(cb/format-code markup "svg")))

View File

@@ -172,6 +172,7 @@ body {
(format-value property value options))))
(defn format-css-property
"Format a single CSS property in the format 'property: value;'."
[[property value] options]
(when (some? value)
(let [formatted-value (format-css-value property value options)

View File

@@ -53,6 +53,7 @@
:else value))
(defn format-color
"Format a color value to a CSS compatible string based on the given format."
[value options]
(let [format (get options :format :hex)]
(cond

View File

@@ -116,13 +116,17 @@
(let [root? (contains? (:root-shapes options) (:id shape))]
(if (and root? (ctl/any-layout? shape))
:fill
(get-shape-size shape objects :width))))
;; Don't set fixed width for auto-width text shapes
(when-not (and (cfh/text-shape? shape) (= (:grow-type shape) :auto-width))
(get-shape-size shape objects :width)))))
(defmethod get-value :height
[_ shape objects options]
(let [root? (contains? (:root-shapes options) (:id shape))]
(when-not (and root? (ctl/any-layout? shape))
(get-shape-size shape objects :height))))
;; Don't set fixed height for auto-height text shapes
(when-not (and (cfh/text-shape? shape) (= (:grow-type shape) :auto-height))
(get-shape-size shape objects :height)))))
(defmethod get-value :flex-grow
[_ shape _ options]

View File

@@ -450,9 +450,9 @@
(t/testing "token got applied to rect with stroke and shape stroke got updated"
(t/is (= (:stroke-width (:applied-tokens rect-with-stroke')) (:name token-target')))
(t/is (= (get-in rect-with-stroke' [:strokes 0 :stroke-width]) 10)))
(t/testing "token got applied to rect without stroke but shape didnt get updated"
(t/testing "token got applied to rect without stroke and shape stroke got updated"
(t/is (= (:stroke-width (:applied-tokens rect-without-stroke')) (:name token-target')))
(t/is (empty? (:strokes rect-without-stroke')))))))))))
(t/is (= (get-in rect-without-stroke' [:strokes 0 :stroke-width]) 10))))))))))
(t/deftest test-apply-font-size
(t/testing "applies font-size token and updates the text font-size"
@@ -645,6 +645,51 @@
(t/is (= (:r1 (:applied-tokens rect-without-token')) (:name target-token)))
(t/is (= (:r1 (:applied-tokens rect-with-other-token-2')) (:name target-token)))))))))))
(t/deftest test-toggle-spacing-token
(t/testing "applies spacing token only to layouts and layout children"
(t/async
done
(let [spacing-token {:name "spacing.md"
:value "16"
:type :spacing}
file (-> (setup-file-with-tokens)
(ctho/add-frame-with-child :frame-layout :rect-in-layout
{:frame-params {:layout :grid}})
(ctho/add-rect :rect-regular)
(update-in [:data :tokens-lib]
#(ctob/add-token-in-set % "Set A" (ctob/make-token spacing-token))))
store (ths/setup-store file)
frame-layout (cths/get-shape file :frame-layout)
rect-in-layout (cths/get-shape file :rect-in-layout)
rect-regular (cths/get-shape file :rect-regular)
events [(dwta/toggle-token {:token (toht/get-token file "spacing.md")
:shapes [frame-layout rect-in-layout rect-regular]})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-state new-state)
frame-layout' (cths/get-shape file' :frame-layout)
rect-in-layout' (cths/get-shape file' :rect-in-layout)
rect-regular' (cths/get-shape file' :rect-regular)]
(t/testing "frame with layout gets all spacing attributes"
(t/is (= "spacing.md" (:column-gap (:applied-tokens frame-layout'))))
(t/is (= "spacing.md" (:row-gap (:applied-tokens frame-layout'))))
(t/is (= 16 (get-in frame-layout' [:layout-gap :column-gap])))
(t/is (= 16 (get-in frame-layout' [:layout-gap :row-gap]))))
(t/testing "shape inside layout frame gets only margin attributes"
(t/is (= "spacing.md" (:m1 (:applied-tokens rect-in-layout'))))
(t/is (= "spacing.md" (:m2 (:applied-tokens rect-in-layout'))))
(t/is (= "spacing.md" (:m3 (:applied-tokens rect-in-layout'))))
(t/is (= "spacing.md" (:m4 (:applied-tokens rect-in-layout'))))
(t/is (nil? (:column-gap (:applied-tokens rect-in-layout'))))
(t/is (nil? (:row-gap (:applied-tokens rect-in-layout'))))
(t/is (= {:m1 16, :m2 16, :m3 16, :m4 16} (get rect-in-layout' :layout-item-margin))))
(t/testing "regular shape doesn't get spacing attributes"
(t/is (nil? (:applied-tokens rect-regular')))))))))))
(t/deftest test-detach-styles-color
(t/testing "applying a color token to a shape with color styles should detach the styles"
(t/async

View File

@@ -4313,17 +4313,12 @@ msgstr "Please upgrade to match your usage. Contact with the team owner: %s"
msgid "subscription.dashboard.power-up.enterprise-plan"
msgstr "Enterprise plan"
#: src/app/main/ui/dashboard/subscription.cljs:77
#, unused
msgid "subscription.dashboard.power-up.enterprise.description"
msgstr "Advanced security, activity logs, dedicated support and more."
#: src/app/main/ui/dashboard/subscription.cljs:60
#, markdown
msgid "subscription.dashboard.power-up.professional.bottom-description"
msgid "subscription.dashboard.power-up.professional.bottom"
msgstr ""
"Get extra editors and storage, file backup and more with the Unlimited "
"plan[Power up|target:self](%s)"
"Get extra editors and storage, file recovery and more with the Unlimited "
"plan. [Power up|target:self](%s)"
#: src/app/main/ui/dashboard/subscription.cljs:59
msgid "subscription.dashboard.power-up.professional.top-title"
@@ -4357,10 +4352,10 @@ msgstr "Enterprise plan (trial)"
#: src/app/main/ui/dashboard/subscription.cljs:74
#, markdown
msgid "subscription.dashboard.power-up.unlimited.bottom-description"
msgid "subscription.dashboard.power-up.unlimited.bottom-text"
msgstr ""
"Get advanced security, activity logs, dedicated support and more. Take a "
"look to the[Enterprise plan.|target:self](%s)"
"Get extra editors, more backup, unlimited storage and more. "
"[Take a look to the Enterprise plan.|target:self](%s)"
#: src/app/main/ui/dashboard/subscription.cljs:70
#, unused
@@ -4392,11 +4387,6 @@ msgstr "Subscription"
msgid "subscription.settings.add-payment-to-continue"
msgstr "Add a payment method to continue after your trial"
#: src/app/main/ui/settings/subscription.cljs:82, src/app/main/ui/settings/subscription.cljs:115, src/app/main/ui/settings/subscription.cljs:127
#, fuzzy, unused
msgid "subscription.settings.benefits.all-professiona-benefits"
msgstr ""
#: src/app/main/ui/settings/subscription.cljs:247, src/app/main/ui/settings/subscription.cljs:268, src/app/main/ui/settings/subscription.cljs:303, src/app/main/ui/settings/subscription.cljs:317
msgid "subscription.settings.benefits.all-professional-benefits"
msgstr "All Professional plan benefits and:"
@@ -4414,16 +4404,15 @@ msgid "subscription.settings.enterprise-trial"
msgstr "Enterprise (trial)"
#: src/app/main/ui/settings/subscription.cljs:271, src/app/main/ui/settings/subscription.cljs:320
msgid "subscription.settings.enterprise.logs"
msgstr "Activity logs"
msgid "subscription.settings.enterprise.unlimited-storage"
msgstr "Unlimited storage and 90-day autosave versions and file recovery"
#: src/app/main/ui/settings/subscription.cljs:270, src/app/main/ui/settings/subscription.cljs:319
msgid "subscription.settings.enterprise.security"
msgstr "Advanced security"
msgid "subscription.settings.enterprise.autosave"
msgstr "90-day autosave versions and file recovery"
#: src/app/main/ui/settings/subscription.cljs:269, src/app/main/ui/settings/subscription.cljs:318
msgid "subscription.settings.enterprise.support"
msgstr "Dedicated support"
msgid "subscription.settings.enterprise.capped-bill"
msgstr "Capped monthly bill"
#: src/app/main/ui/dashboard/subscription.cljs:114, src/app/main/ui/settings/subscription.cljs:251, src/app/main/ui/settings/subscription.cljs:262, src/app/main/ui/settings/subscription.cljs:272
msgid "subscription.settings.manage-your-subscription"
@@ -4452,8 +4441,10 @@ msgid "subscription.settings.management.dialog.payment-explanation"
msgstr "(No payment will be made now)"
#: src/app/main/ui/settings/subscription.cljs:124
#, markdown
msgid "subscription.settings.management.dialog.price-month"
msgstr "$7 per editor/month x %s"
msgstr ""
"**$%s** per month"
#: src/app/main/ui/settings/subscription.cljs:112
msgid "subscription.settings.management.dialog.select-editors"
@@ -4492,8 +4483,8 @@ msgid "subscription.settings.professional.projects-files"
msgstr "Unlimited projects, files and drafts"
#: src/app/main/ui/settings/subscription.cljs:241, src/app/main/ui/settings/subscription.cljs:292
msgid "subscription.settings.professional.storage"
msgstr "10GB of storage and 7-day autosave versions"
msgid "subscription.settings.professional.storage-autosave"
msgstr "10GB of storage and 7-day autosave versions and file recovery"
#: src/app/main/ui/settings/subscription.cljs:240, src/app/main/ui/settings/subscription.cljs:291
msgid "subscription.settings.professional.teams-editors"
@@ -4537,11 +4528,6 @@ msgstr "You've been supporting us with this plan since: %s"
msgid "subscription.settings.try-it-free"
msgstr "Try it free for 14 days"
#: src/app/main/ui/settings/subscription.cljs:119
#, unused
msgid "subscription.settings.ulimited.try-it-free"
msgstr "Try it free for 14 days"
#: src/app/main/ui/dashboard/subscription.cljs:108, src/app/main/ui/settings/subscription.cljs:56, src/app/main/ui/settings/subscription.cljs:256, src/app/main/ui/settings/subscription.cljs:299
msgid "subscription.settings.unlimited"
msgstr "Unlimited"
@@ -4555,8 +4541,8 @@ msgid "subscription.settings.unlimited.bill"
msgstr "Capped monthly bill"
#: src/app/main/ui/settings/subscription.cljs:250, src/app/main/ui/settings/subscription.cljs:261, src/app/main/ui/settings/subscription.cljs:306
msgid "subscription.settings.unlimited.storage"
msgstr "25GB of storage and 30-day autosave versions and file backup"
msgid "subscription.settings.unlimited.storage-autosave"
msgstr "25GB of storage and 30-day autosave versions and file recovery"
#: src/app/main/ui/settings/subscription.cljs:248, src/app/main/ui/settings/subscription.cljs:259, src/app/main/ui/settings/subscription.cljs:304
msgid "subscription.settings.unlimited.teams"
@@ -7557,6 +7543,10 @@ msgstr "Type '%s' is not supported (%s)\n"
msgid "workspace.tokens.value-not-valid"
msgstr "The value is not valid"
#: src/app/main/ui/workspace/tokens/token_pill.cljs
msgid "workspace.tokens.more-options"
msgstr "Right click to see options"
#: src/app/main/data/workspace/tokens/errors.cljs:61
msgid "workspace.tokens.value-with-units"
msgstr "Invalid value: Units are not allowed."

View File

@@ -4340,16 +4340,11 @@ msgstr "Por favor, mejóralo para adaptarlo a tu uso. Contacta con el "
msgid "subscription.dashboard.power-up.enterprise-plan"
msgstr "Plan Enterprise"
#: src/app/main/ui/dashboard/subscription.cljs:77
#, unused
msgid "subscription.dashboard.power-up.enterprise.description"
msgstr "Seguridad avanzada, registros de actividad, asistencia dedicada y mucho más."
#: src/app/main/ui/dashboard/subscription.cljs:60
#, markdown
msgid "subscription.dashboard.power-up.professional.bottom-description"
msgid "subscription.dashboard.power-up.professional.bottom"
msgstr ""
"Consigue editores y almacenamiento adicionales, copias de seguridad de "
"Consigue editores y almacenamiento adicionales, recuperación de "
"archivos y mucho más con el Plan Unlimited[Mejóralo|target:self](%s)"
#: src/app/main/ui/dashboard/subscription.cljs:59
@@ -4388,10 +4383,10 @@ msgstr "Plan Unlimited"
#: src/app/main/ui/dashboard/subscription.cljs:74
#, markdown
msgid "subscription.dashboard.power-up.unlimited.bottom-description"
msgid "subscription.dashboard.power-up.unlimited.bottom-text"
msgstr ""
"Obtenga seguridad avanzada, registros de actividad, asistencia dedicada y "
"mucho más. Echa un ojo al[Plan Enterprise|target:self](%s)"
"Consigue editores adicionales, copias de seguridad, almacenamiento ilimitado y mucho más. "
"[Echa un ojo al Plan Enterprise|target:self](%s)"
#: src/app/main/ui/dashboard/subscription.cljs:70
#, unused
@@ -4442,16 +4437,15 @@ msgid "subscription.settings.enterprise-trial"
msgstr "Enterprise (prueba)"
#: src/app/main/ui/settings/subscription.cljs:271, src/app/main/ui/settings/subscription.cljs:320
msgid "subscription.settings.enterprise.logs"
msgstr "Registros de actividad"
msgid "subscription.settings.enterprise.unlimited-storage"
msgstr "Almacenamiento ilimitado y versiones de autoguardado de 90 días y recuperación de archivos"
#: src/app/main/ui/settings/subscription.cljs:270, src/app/main/ui/settings/subscription.cljs:319
msgid "subscription.settings.enterprise.security"
msgstr "Seguridad avanzada"
msgid "subscription.settings.enterprise.autosave"
msgstr "Versiones guardadas automáticamente cada 90 días y recuperación de archivos"
#: src/app/main/ui/settings/subscription.cljs:269, src/app/main/ui/settings/subscription.cljs:318
msgid "subscription.settings.enterprise.support"
msgstr "Apoyo específico"
msgid "subscription.settings.enterprise.capped-bill"
msgstr "Factura mensual limitada"
#: src/app/main/ui/dashboard/subscription.cljs:114, src/app/main/ui/settings/subscription.cljs:251, src/app/main/ui/settings/subscription.cljs:262, src/app/main/ui/settings/subscription.cljs:272
msgid "subscription.settings.manage-your-subscription"
@@ -4480,8 +4474,10 @@ msgid "subscription.settings.management.dialog.payment-explanation"
msgstr "(Ahora no se efectuará ningún pago)"
#: src/app/main/ui/settings/subscription.cljs:124
#, markdown
msgid "subscription.settings.management.dialog.price-month"
msgstr "$7 por editor/mes x %s"
msgstr ""
"**$%s** por mes"
#: src/app/main/ui/settings/subscription.cljs:112
msgid "subscription.settings.management.dialog.select-editors"
@@ -4516,8 +4512,8 @@ msgid "subscription.settings.professional.projects-files"
msgstr "Proyectos, archivos y borradores ilimitados"
#: src/app/main/ui/settings/subscription.cljs:241, src/app/main/ui/settings/subscription.cljs:292
msgid "subscription.settings.professional.storage"
msgstr "10 GB de almacenamiento y versiones de autoguardado de 7 días"
msgid "subscription.settings.professional.storage-autosave"
msgstr "10 GB de almacenamiento y versiones de autoguardado de 7 días y recuperación de archivos"
#: src/app/main/ui/settings/subscription.cljs:240, src/app/main/ui/settings/subscription.cljs:291
msgid "subscription.settings.professional.teams-editors"
@@ -4560,11 +4556,6 @@ msgstr "Nos has estado apoyando con este plan desde: %s"
msgid "subscription.settings.try-it-free"
msgstr "Pruébalo gratis durante 14 días"
#: src/app/main/ui/settings/subscription.cljs:119
#, unused
msgid "subscription.settings.ulimited.try-it-free"
msgstr "Pruébalo gratis durante 14 días"
#: src/app/main/ui/dashboard/subscription.cljs:108, src/app/main/ui/settings/subscription.cljs:56, src/app/main/ui/settings/subscription.cljs:256, src/app/main/ui/settings/subscription.cljs:299
msgid "subscription.settings.unlimited"
msgstr "Unlimited"
@@ -4578,10 +4569,10 @@ msgid "subscription.settings.unlimited.bill"
msgstr "Factura mensual limitada"
#: src/app/main/ui/settings/subscription.cljs:250, src/app/main/ui/settings/subscription.cljs:261, src/app/main/ui/settings/subscription.cljs:306
msgid "subscription.settings.unlimited.storage"
msgid "subscription.settings.unlimited.storage-autosave"
msgstr ""
"25 GB de almacenamiento y 30 días de autoguardado de versiones y copias de "
"seguridad de archivos"
"25 GB de almacenamiento y 30 días de autoguardado de versiones y recuperación "
"de archivos"
#: src/app/main/ui/settings/subscription.cljs:248, src/app/main/ui/settings/subscription.cljs:259, src/app/main/ui/settings/subscription.cljs:304
msgid "subscription.settings.unlimited.teams"
@@ -7494,6 +7485,10 @@ msgstr "El tipo '%s' no está soportado (%s)\n"
msgid "workspace.tokens.value-not-valid"
msgstr "El valor no es válido"
#: src/app/main/ui/workspace/tokens/token_pill.cljs
msgid "workspace.tokens.more-options"
msgstr "Click derecho para ver opciones"
#: src/app/main/data/workspace/tokens/errors.cljs:61
msgid "workspace.tokens.value-with-units"
msgstr "Valor no válido: No se permiten unidades."

View File

@@ -821,7 +821,7 @@ function Mousetrap(targetElement) {
* @param {number=} level - what part of the sequence the command is
* @returns void
*/
function _bindSingle(combination, callback, action, sequenceName, level) {
function _bindSingle(combination, callback, action, sequenceName, level, overwrite) {
// store a direct mapped reference for use with Mousetrap.trigger
self._directMap[combination + ':' + action] = callback;
@@ -846,7 +846,9 @@ function Mousetrap(targetElement) {
self._callbacks[info.key] = self._callbacks[info.key] || [];
// remove an existing match if there is one
_getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
if (overwrite) {
_getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
}
// add this call back to the array
// if it is a sequence put it at the beginning
@@ -872,9 +874,9 @@ function Mousetrap(targetElement) {
* @param {string|undefined} action
* @returns void
*/
self._bindMultiple = function(combinations, callback, action) {
self._bindMultiple = function(combinations, callback, action, overwrite) {
for (var i = 0; i < combinations.length; ++i) {
_bindSingle(combinations[i], callback, action);
_bindSingle(combinations[i], callback, action, undefined, undefined, overwrite);
}
};
@@ -899,10 +901,10 @@ function Mousetrap(targetElement) {
* @param {string=} action - 'keypress', 'keydown', or 'keyup'
* @returns void
*/
Mousetrap.prototype.bind = function(keys, callback, action) {
Mousetrap.prototype.bind = function(keys, callback, action, overwrite) {
var self = this;
keys = keys instanceof Array ? keys : [keys];
self._bindMultiple.call(self, keys, callback, action);
self._bindMultiple.call(self, keys, callback, action, overwrite);
return self;
};

119
manage.sh
View File

@@ -7,6 +7,7 @@ export DEVENV_PNAME="penpotdev";
export CURRENT_USER_ID=$(id -u);
export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD);
export IMAGEMAGICK_VERSION=7.1.2-0
# Safe directory to avoid ownership errors with Git
git config --global --add safe.directory /home/penpot/penpot || true
@@ -16,16 +17,23 @@ export JAVA_OPTS=${JAVA_OPTS:-"-Xmx1000m -Xms50m"};
set -e
ARCH=$(uname -m)
if [[ "$ARCH" == "x86_64" || "$ARCH" == "i386" || "$ARCH" == "i686" ]]; then
ARCH="amd64"
elif [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
ARCH="arm64"
else
echo "Unknown architecture $ARCH"
exit -1
fi
function print-current-version {
echo -n "$(git describe --tags --match "*.*.*")";
}
function build-devenv {
set +e;
echo "Building development image $DEVENV_IMGNAME:latest..."
pushd docker/devenv;
function setup-buildx {
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx inspect penpot > /dev/null 2>&1;
@@ -36,19 +44,28 @@ function build-devenv {
docker buildx use penpot;
docker buildx inspect --bootstrap > /dev/null 2>&1;
fi
# docker build -t $DEVENV_IMGNAME:latest .
docker buildx build --platform linux/amd64,linux/arm64 --push -t $DEVENV_IMGNAME:latest .;
docker pull $DEVENV_IMGNAME:latest;
popd;
}
function build-devenv-local {
echo "Building local only development image $DEVENV_IMGNAME:latest..."
function build-devenv {
set +e;
pushd docker/devenv;
docker build -t $DEVENV_IMGNAME:latest .;
if [ "$1" = "--local" ]; then
echo "Build local only $DEVENV_IMGNAME:latest image";
docker build -t $DEVENV_IMGNAME:latest .;
else
echo "Build and push $DEVENV_IMGNAME:latest image";
setup-buildx;
docker buildx build \
--platform linux/amd64,linux/arm64 \
--output type=registry \
-t $DEVENV_IMGNAME:latest .;
docker pull $DEVENV_IMGNAME:latest;
fi
popd;
}
@@ -124,6 +141,32 @@ function run-devenv-isolated-shell {
$DEVENV_IMGNAME:latest sudo -EH -u penpot bash
}
function build-imagemagick-docker-image {
set +e;
echo "Building image penpotapp/imagemagick:$IMAGEMAGICK_VERSION"
pushd docker/imagemagick;
output_option="type=registry";
platform="linux/amd64,linux/arm64";
if [ "$1" = "--local" ]; then
output_option="type=docker";
platform="linux/$ARCH"
fi
setup-buildx;
docker buildx build \
--build-arg IMAGEMAGICK_VERSION=$IMAGEMAGICK_VERSION \
--platform $platform \
--output $output_option \
-t penpotapp/imagemagick:latest \
-t penpotapp/imagemagick:$IMAGEMAGICK_VERSION .;
popd;
}
function build {
echo ">> build start: $1"
local version=$(print-current-version);
@@ -219,21 +262,21 @@ function build-docs-bundle {
echo ">> bundle docs end";
}
function build-frontend-docker-images {
function build-frontend-docker-image {
rsync -avr --delete ./bundles/frontend/ ./docker/images/bundle-frontend/;
pushd ./docker/images;
docker build -t penpotapp/frontend:$CURRENT_BRANCH -t penpotapp/frontend:latest -f Dockerfile.frontend .;
popd;
}
function build-backend-docker-images {
function build-backend-docker-image {
rsync -avr --delete ./bundles/backend/ ./docker/images/bundle-backend/;
pushd ./docker/images;
docker build -t penpotapp/backend:$CURRENT_BRANCH -t penpotapp/backend:latest -f Dockerfile.backend .;
popd;
}
function build-exporter-docker-images {
function build-exporter-docker-image {
rsync -avr --delete ./bundles/exporter/ ./docker/images/bundle-exporter/;
pushd ./docker/images;
docker build -t penpotapp/exporter:$CURRENT_BRANCH -t penpotapp/exporter:latest -f Dockerfile.exporter .;
@@ -246,7 +289,7 @@ function usage {
echo "Options:"
echo "- pull-devenv Pulls docker development oriented image"
echo "- build-devenv Build docker development oriented image"
echo "- build-devenv-local Build a local docker development oriented image"
echo "- build-devenv --local Build a local docker development oriented image"
echo "- create-devenv Create the development oriented docker compose service."
echo "- start-devenv Start the development oriented docker compose service."
echo "- stop-devenv Stops the development oriented docker compose service."
@@ -263,9 +306,9 @@ function usage {
echo "- build-docs-bundle Build docs bundle."
echo ""
echo "- build-docker-images Build all docker images (frontend, backend and exporter)."
echo "- build-frontend-docker-images Build frontend docker images."
echo "- build-backend-docker-images Build backend docker images."
echo "- build-exporter-docker-images Build exporter docker images."
echo "- build-frontend-docker-image Build frontend docker images."
echo "- build-backend-docker-image Build backend docker images."
echo "- build-exporter-docker-image Build exporter docker images."
echo ""
echo "- version Show penpot's version."
}
@@ -281,11 +324,8 @@ case $1 in
;;
build-devenv)
build-devenv ${@:2}
;;
build-devenv-local)
build-devenv-local ${@:2}
shift;
build-devenv $@;
;;
create-devenv)
@@ -339,25 +379,30 @@ case $1 in
build-docs-bundle;
;;
build-imagemagick-docker-image)
shift;
build-imagemagick-docker-image $@;
;;
build-docker-images)
build-frontend-docker-images
build-backend-docker-images
build-exporter-docker-images
build-frontend-docker-image
build-backend-docker-image
build-exporter-docker-image
;;
build-frontend-docker-images)
build-frontend-docker-images
build-frontend-docker-image)
build-frontend-docker-image
;;
build-backend-docker-images)
build-backend-docker-images
build-backend-docker-image)
build-backend-docker-image
;;
build-exporter-docker-images)
build-exporter-docker-images
build-exporter-docker-image)
build-exporter-docker-image
;;
*)
usage
;;
esac
esac