mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed5875f29a | ||
|
|
ad38a21053 | ||
|
|
adffac4eec | ||
|
|
73dfe12ec9 | ||
|
|
ff2e845f2c | ||
|
|
8e0a6e4123 | ||
|
|
0131cd6f8b | ||
|
|
288a7b21d6 | ||
|
|
32bd08533d | ||
|
|
c1aae12327 | ||
|
|
23a6f4b7c1 | ||
|
|
133e6e1e68 | ||
|
|
6abd045273 | ||
|
|
778a608854 | ||
|
|
a76a9fae41 | ||
|
|
f7cfbdd229 | ||
|
|
e28d2842f6 | ||
|
|
ccc3ca0948 | ||
|
|
515cbf7bef | ||
|
|
c320cbc47b | ||
|
|
46969585ed | ||
|
|
47882c5419 | ||
|
|
019d5e083a | ||
|
|
85f6cf32ae | ||
|
|
ded8e39e73 | ||
|
|
e730200873 | ||
|
|
4501d13961 | ||
|
|
baa1cfb2f8 | ||
|
|
905699d15a | ||
|
|
fe53869308 | ||
|
|
50076bac83 | ||
|
|
44bc4b7fa4 | ||
|
|
5c14f486d7 | ||
|
|
0cbd980b68 | ||
|
|
95dda2b1af | ||
|
|
5170872961 | ||
|
|
871ca68e1e | ||
|
|
0ab896fc76 | ||
|
|
6a4b548457 | ||
|
|
695a399941 | ||
|
|
a32463fada | ||
|
|
5d44c88988 | ||
|
|
ce87d797d1 | ||
|
|
7fde1436e1 | ||
|
|
e1c5a32fcb | ||
|
|
b262e6a46f | ||
|
|
2e726b62c3 | ||
|
|
02acd81c2c | ||
|
|
bae2de75ff | ||
|
|
b68c426cd1 | ||
|
|
5161ef15bf | ||
|
|
36d3d94ec9 | ||
|
|
17447d7610 | ||
|
|
708a40bff1 | ||
|
|
efaf6573bd | ||
|
|
001bcbce59 | ||
|
|
c195c07a3f | ||
|
|
f5298f51e7 | ||
|
|
46c440fef2 | ||
|
|
e77f8b572a | ||
|
|
ade5eecf80 | ||
|
|
97fc7702b8 | ||
|
|
54fcd58531 | ||
|
|
b7a8747f00 | ||
|
|
d00de7d5a4 | ||
|
|
5ae4dde222 | ||
|
|
2fbd4b07e0 | ||
|
|
58a843ea23 | ||
|
|
f6b97af148 | ||
|
|
76b7287bf1 | ||
|
|
019bc2f183 | ||
|
|
8c96a617be | ||
|
|
1f15e9b81e | ||
|
|
f7627e515a | ||
|
|
d08c94d5a6 | ||
|
|
01896501c1 | ||
|
|
3f9a1525ca | ||
|
|
52c1e227d5 | ||
|
|
955538b12a | ||
|
|
8254af27cb | ||
|
|
f76391ecbb | ||
|
|
c49e9fbf18 | ||
|
|
122701ee7b | ||
|
|
351362bb50 | ||
|
|
1acf78d57c | ||
|
|
523373dfa2 | ||
|
|
f55e7d8165 | ||
|
|
9fdc6be465 | ||
|
|
9390c1e7be | ||
|
|
b20b272eae | ||
|
|
d46b519524 | ||
|
|
4effd375a9 | ||
|
|
4e753dc474 | ||
|
|
fbf63b98c3 | ||
|
|
3df557b370 | ||
|
|
35f3125fff | ||
|
|
f22aa606ce | ||
|
|
9d288486d7 | ||
|
|
ea5521485a | ||
|
|
f768ffbdad | ||
|
|
4f0d3660de | ||
|
|
7ccb742ef3 | ||
|
|
7bc29c22ed | ||
|
|
1d550eaa18 | ||
|
|
b71ec4bfe0 | ||
|
|
827bbf6a7f | ||
|
|
2db0cc0cbf | ||
|
|
42ef01b339 | ||
|
|
fdaef2be69 | ||
|
|
ae3213f5d4 | ||
|
|
6dfd05fdd1 | ||
|
|
51107c3fc9 | ||
|
|
b6863efb3a | ||
|
|
799bceb8b7 | ||
|
|
9e573128c1 | ||
|
|
1f05511add | ||
|
|
eeee52a738 | ||
|
|
7f53860296 | ||
|
|
16d0077393 | ||
|
|
622fed2f0d | ||
|
|
d22ade3289 | ||
|
|
7febf330ac | ||
|
|
75a50ac1ac | ||
|
|
e62567d09e | ||
|
|
8d80eebeb1 | ||
|
|
ee9a42238d | ||
|
|
758c76d661 | ||
|
|
1dec46cbfa | ||
|
|
ae25d704c1 | ||
|
|
e05f8c0329 | ||
|
|
ce62e11626 | ||
|
|
9f04c2fc1d | ||
|
|
05a405a82d | ||
|
|
3c8c21c378 | ||
|
|
2dbeb884a5 | ||
|
|
931d72b41f | ||
|
|
2e3cdd872c | ||
|
|
55a13c3139 | ||
|
|
f63d1c87e3 | ||
|
|
abbfd44534 | ||
|
|
f772724f9a | ||
|
|
f3abd0f190 | ||
|
|
5d4042c861 | ||
|
|
1fbcec98fb | ||
|
|
abef9f3cf7 | ||
|
|
6f1958f9f2 | ||
|
|
6b2ce86d5f | ||
|
|
0cfd70da2e | ||
|
|
4167faf39d | ||
|
|
90e6e8c5eb | ||
|
|
b40b1fa2e4 | ||
|
|
bb1ec109d8 | ||
|
|
4c21468850 | ||
|
|
40c300fa1a | ||
|
|
77a47e4b2b |
105
.github/workflows/build-bundles.yml
vendored
105
.github/workflows/build-bundles.yml
vendored
@@ -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
12
.github/workflows/build-develop.yml
vendored
Normal 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
12
.github/workflows/build-staging.yml
vendored
Normal 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"
|
||||
2
.github/workflows/commit-checker.yml
vendored
2
.github/workflows/commit-checker.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check Commit Type
|
||||
uses: gsactions/commit-message-checker@v2
|
||||
with:
|
||||
pattern: '^(Merge|:(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
6
.gitignore
vendored
@@ -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
|
||||
|
||||
101
CHANGES.md
101
CHANGES.md
@@ -8,23 +8,67 @@
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Clarify message when inviting existing team members to make it more user-friendly and clear which invitations will be sent. [Taiga #11441](https://tree.taiga.io/project/penpot/issue/11441) by [@iprithvitharun](https://github.com/iprithvitharun)
|
||||
- Update email change confirmation message for clarity and correct grammar. [GitHub #6786](https://github.com/penpot/penpot/issues/6786) by [@iprithvitharun](https://github.com/iprithvitharun)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Add visual indicator for new comments in the workspace [Taiga #11328](https://tree.taiga.io/project/penpot/issue/11328)
|
||||
- On components overrides, separate the content of the text from the rest of properties [Taiga #7434](https://tree.taiga.io/project/penpot/us/7434)
|
||||
- Improve dashboard's sidebar [Taiga #10700](https://tree.taiga.io/project/penpot/us/10700)
|
||||
- Change "Save color" button to primary button [Taiga #9410](https://tree.taiga.io/project/penpot/issue/9410)
|
||||
- Support for exif rotated images [GitHub #6767](https://github.com/penpot/penpot/issues/6767)
|
||||
- Display Blend Mode and Layer Opacity properties in the Inspect tab [Taiga #11283](https://tree.taiga.io/project/penpot/issue/11283)
|
||||
- Provide CSS `mix-blend-mode` property in code editor when present on shape [Taiga #11282](https://tree.taiga.io/project/penpot/issue/11282)
|
||||
- Add the option to import tokens in a .zip file. [Taiga #11378](https://tree.taiga.io/project/penpot/us/11378)
|
||||
- New typography token type - font size token [Taiga #10938](https://tree.taiga.io/project/penpot/us/10938)
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
- Copying font size does not copy the unit [Taiga #11143](https://tree.taiga.io/project/penpot/issue/11143)
|
||||
- Fix text-decoration line-through that displays a wrong property value [Taiga #11145](https://tree.taiga.io/project/penpot/issue/11145)
|
||||
- Fix display error message on register form [Taiga #11444](https://tree.taiga.io/project/penpot/issue/11444)
|
||||
- Fix toggle focus mode did not restore viewport and selection upon exit [GitHub #6280](https://github.com/penpot/penpot/issues/6820)
|
||||
- Fix problem when creating a layout from an existing layout [Taiga #11554](https://tree.taiga.io/project/penpot/issue/11554)
|
||||
- Fix title button from Title Case to Capitalize [Taiga #11476](https://tree.taiga.io/project/penpot/issue/11476)
|
||||
- Fix touchpad swipe leading to navigating back/forth [GitHub #4246](https://github.com/penpot/penpot/issues/4246)
|
||||
- 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
|
||||
|
||||
### :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
|
||||
|
||||
@@ -46,6 +90,7 @@ in future versions. Therefore, **migration from Redis to ValKey is recommended f
|
||||
on-premises instances** that want to keep up to date.
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Add Serbian language [GitHub #5002](https://github.com/penpot/penpot/issues/5002) by [crnobog69](https://github.com/crnobog69)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
@@ -101,7 +146,6 @@ on-premises instances** that want to keep up to date.
|
||||
- Fix copy in error message [GitHub #6615](https://github.com/penpot/penpot/pull/6615)
|
||||
- Fix url on invitation link [Taiga #11284](https://tree.taiga.io/project/penpot/issue/11284)
|
||||
|
||||
|
||||
## 2.7.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -109,7 +153,6 @@ on-premises instances** that want to keep up to date.
|
||||
- Fix incorrect handling of strokes with images on importing files
|
||||
- Fix tokens disappearing after manual additions [Taiga #11063](https://tree.taiga.io/project/penpot/issue/11063)
|
||||
|
||||
|
||||
## 2.7.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -241,7 +284,6 @@ on-premises instances** that want to keep up to date.
|
||||
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
|
||||
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
|
||||
|
||||
|
||||
## 2.5.4
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
@@ -286,7 +328,7 @@ on-premises instances** that want to keep up to date.
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
Although this is not a breaking change, we believe it’s important to highlight it in this
|
||||
Although this is not a breaking change, we believe it's important to highlight it in this
|
||||
section:
|
||||
|
||||
This release includes a fix for an internal bug in Penpot that caused incorrect handling
|
||||
@@ -294,9 +336,9 @@ of media assets (e.g., fill images). The issue has been resolved since version 2
|
||||
no new incorrect references will be generated. However, existing files may still contain
|
||||
incorrect references.
|
||||
|
||||
To address this, we’ve provided a script to correct these references in existing files.
|
||||
To address this, we've provided a script to correct these references in existing files.
|
||||
|
||||
While having incorrect references generally doesn’t result in visible issues, there are
|
||||
While having incorrect references generally doesn't result in visible issues, there are
|
||||
rare cases where it can cause problems. For example, if a component library (containing
|
||||
images) is deleted, and that library is being used in other files, running the FileGC task
|
||||
(responsible for freeing up space and performing logical deletions) could leave those
|
||||
@@ -371,7 +413,6 @@ is a number of cores)
|
||||
- Fix missing methods reference on API Docs
|
||||
- Fix memory usage issue on file-gc asynchronous task (related to snapshots feature)
|
||||
|
||||
|
||||
## 2.4.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -379,7 +420,6 @@ is a number of cores)
|
||||
- Fix error when importing files with touched components [Taiga #9625](https://tree.taiga.io/project/penpot/issue/9625)
|
||||
- Fix problem when changing color libraries [Plugins #184](https://github.com/penpot/penpot-plugins/issues/184)
|
||||
|
||||
|
||||
## 2.4.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -433,7 +473,6 @@ is a number of cores)
|
||||
|
||||
- Add initial documentation for Kubernetes
|
||||
|
||||
|
||||
## 2.3.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -441,7 +480,6 @@ is a number of cores)
|
||||
- Fix unexpected issue on interaction between plugins sandbox and
|
||||
internal impl of promise
|
||||
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -467,7 +505,6 @@ is a number of cores)
|
||||
|
||||
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with constraints buttons [Taiga #8465](https://tree.taiga.io/project/penpot/issue/8465)
|
||||
@@ -507,8 +544,8 @@ is a number of cores)
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- Removed "merge assets" option when exporting ".svg + .json" files. After the components changes the option wasn't
|
||||
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
|
||||
time being.
|
||||
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
|
||||
time being.
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
@@ -524,7 +561,7 @@ time being.
|
||||
freeing up space in the database. It can be enabled with the
|
||||
`enable-enable-tiered-file-data-storage` flag.
|
||||
|
||||
*(On-Premise feature, EXPERIMENTAL).*
|
||||
_(On-Premise feature, EXPERIMENTAL)._
|
||||
|
||||
- **JSON Interoperability for HTTP API** [Taiga #8372](https://tree.taiga.io/project/penpot/us/8372)
|
||||
|
||||
@@ -567,7 +604,7 @@ time being.
|
||||
|
||||
- **Design System**
|
||||
|
||||
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
|
||||
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
|
||||
|
||||
- **Storybook** [Taiga #6329](https://tree.taiga.io/project/penpot/us/6329)
|
||||
|
||||
@@ -622,11 +659,11 @@ time being.
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
|
||||
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix the “search” label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
|
||||
- Fix the "search" label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
|
||||
- Fix pencil loader [Taiga #8348](https://tree.taiga.io/project/penpot/issue/8348)
|
||||
- Fix several issues on the OIDC.
|
||||
- Fix regression on the `email-verification` flag [Taiga #8398](https://tree.taiga.io/project/penpot/issue/8398)
|
||||
@@ -706,22 +743,21 @@ time being.
|
||||
- Fix color palette sorting [Taiga #7458](https://tree.taiga.io/project/penpot/issue/7458)
|
||||
- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671)
|
||||
|
||||
|
||||
## 2.0.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
|
||||
|
||||
|
||||
## 2.0.0 - I Just Can't Get Enough
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- Grid CSS layout [Taiga #4915](https://tree.taiga.io/project/penpot/epic/4915)
|
||||
- UI redesign [Taiga #4958](https://tree.taiga.io/project/penpot/epic/4958)
|
||||
- New components System [Taiga #2662](https://tree.taiga.io/project/penpot/epic/2662)
|
||||
- Swap components [Taiga #1331](https://tree.taiga.io/project/penpot/us/1331)
|
||||
- Images as fill [Taiga #2983](https://tree.taiga.io/project/penpot/us/2983)
|
||||
- Images as fill [Taiga #2983](https://tree.taiga.io/project/penpot/us/2983)
|
||||
- HTML code generation [Taiga #5277](https://tree.taiga.io/project/penpot/us/5277)
|
||||
- Light and dark themes [Taiga #2287](https://tree.taiga.io/project/penpot/us/2287)
|
||||
|
||||
@@ -730,9 +766,9 @@ time being.
|
||||
- New strokes default to inside border [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
|
||||
- Change default z ordering on layers in flex layout. The previous behavior was inconsistent with how HTML works and we changed it to be more consistent. Previous layers that overlapped could be hidden, the fastest way to fix this is changing the z-index property but a better way is to change the order of your layers.
|
||||
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
|
||||
|
||||
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
|
||||
- Hide bounding-box when editing shape (by @VasilevsVV) [#3930](https://github.com/penpot/penpot/pull/3930)
|
||||
- CTRL + "+" to zoom into canvas instead of browser (by @audriu) [#3848](https://github.com/penpot/penpot/pull/3848)
|
||||
- Add dev deps.edn in the project root (by @PEZ) [#3794](https://github.com/penpot/penpot/pull/3794)
|
||||
@@ -741,6 +777,7 @@ time being.
|
||||
- Typo (by StephanEggermont) [#157](https://github.com/penpot/penpot-docs/pull/157)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Send comments with Ctrl+Enter / Cmd + Enter [Taiga #6085](https://tree.taiga.io/project/penpot/issue/6085)
|
||||
- Select through stroke only rectangle [Taiga #5484](https://tree.taiga.io/project/penpot/issue/5484)
|
||||
- Stroke default position [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
|
||||
@@ -808,6 +845,7 @@ time being.
|
||||
- [REDESIGN] Onboarding slides [Taiga #6678](https://tree.taiga.io/project/penpot/us/6678)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix pixelated thumbnails [Github #3681](https://github.com/penpot/penpot/issues/3681), [Github #3661](https://github.com/penpot/penpot/issues/3661)
|
||||
- Fix problem with not applying colors to boards [Github #3941](https://github.com/penpot/penpot/issues/3941)
|
||||
- Fix problem with path editor undoing changes [Github #3998](https://github.com/penpot/penpot/issues/3998)
|
||||
@@ -816,7 +854,7 @@ time being.
|
||||
- Selecting from Color Palette does not work for board when there is no existing fill [Taiga #6464](https://tree.taiga.io/project/penpot/issue/6464)
|
||||
- Color thumbnails are consistently rounded in the inspect code mode [Taiga #5886](https://tree.taiga.io/project/penpot/issue/5886)
|
||||
- Adding vector path points before first point of existing open path not working [Taiga #6593](https://tree.taiga.io/project/penpot/issue/6593)
|
||||
- Some image formats include the extension when importing [Taiga #5485](https://tree.taiga.io/project/penpot/issue/5485)
|
||||
- Some image formats include the extension when importing [Taiga #5485](https://tree.taiga.io/project/penpot/issue/5485)
|
||||
- Gradient color tool doesn't work properly with flipped items [Taiga #6485](https://tree.taiga.io/project/penpot/issue/6485)
|
||||
- [TEXT] Align options are not shown when several text are selected [Taiga #5948](https://tree.taiga.io/project/penpot/issue/5948)
|
||||
- [VIEW MODE] Comments not working properly on multiple pages [Taiga #6281](https://tree.taiga.io/project/penpot/issue/6281)
|
||||
@@ -860,7 +898,7 @@ time being.
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Improve selected colors [Taiga #5805]( https://tree.taiga.io/project/penpot/us/5805)
|
||||
- Improve selected colors [Taiga #5805](https://tree.taiga.io/project/penpot/us/5805)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -895,7 +933,6 @@ time being.
|
||||
- Fix deleted pages comments shown in right sidebar [Taiga #5648](https://tree.taiga.io/project/penpot/us/5648)
|
||||
- Fix tooltip on toggle visibility and toggle lock buttons [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
|
||||
|
||||
|
||||
## 1.19.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -1009,7 +1046,6 @@ time being.
|
||||
|
||||
- Update google fonts catalog (at 2023/07/06) [Taiga #5592](https://tree.taiga.io/project/penpot/issue/5592)
|
||||
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- Update Typography palette order (by @akshay-gupta7) [Github #3156](https://github.com/penpot/penpot/pull/3156)
|
||||
@@ -1163,12 +1199,14 @@ time being.
|
||||
- Fix problem with opacity in imported SVG's [Taiga #4923](https://tree.taiga.io/project/penpot/issue/4923)
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To @ondrejkonec: for contributing to the code with:
|
||||
- Refactor CSS variables [Github #2948](https://github.com/penpot/penpot/pull/2948)
|
||||
|
||||
## 1.17.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix copy and paste very nested inside itself [Taiga #4848](https://tree.taiga.io/project/penpot/issue/4848)
|
||||
- Fix custom fonts not rendered correctly [Taiga #4874](https://tree.taiga.io/project/penpot/issue/4874)
|
||||
- Fix problem with shadows and blur on multiple selection
|
||||
@@ -1201,6 +1239,7 @@ time being.
|
||||
## 1.17.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix components groups items show the component name in list mode [Taiga #4770](https://tree.taiga.io/project/penpot/issue/4770)
|
||||
- Fix typing CMD+Z on MacOS turns the cursor into a Zoom cursor [Taiga #4778](https://tree.taiga.io/project/penpot/issue/4778)
|
||||
- Fix white space on small screens [Taiga #4774](https://tree.taiga.io/project/penpot/issue/4774)
|
||||
@@ -1315,7 +1354,7 @@ time being.
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- Removed the support for v2 internal file data blob format. This
|
||||
- Removed the support for v2 internal file data blob format. This
|
||||
version has never been documented nor set as default value so
|
||||
technically this is not a breaking change because we are removing
|
||||
a "private API".
|
||||
@@ -1420,7 +1459,6 @@ time being.
|
||||
- Fix when ungrouping, the items previously grouped should ALWAYS remain selected [Taiga #4064](https://tree.taiga.io/project/penpot/issue/4064)
|
||||
- Change shortcut for "Clear undo" [#2219](https://github.com/penpot/penpot/issues/2219)
|
||||
|
||||
|
||||
## 1.15.2-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -1504,6 +1542,7 @@ time being.
|
||||
- Fix bringing complete file data when launching the export dialog [Taiga #4006](https://tree.taiga.io/project/penpot/issue/4006)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
## 1.14.2-beta
|
||||
@@ -1544,10 +1583,10 @@ time being.
|
||||
- Prototype connection should be under the rules [Taiga #3384](https://tree.taiga.io/project/penpot/issue/3384)
|
||||
- Fix problem with empty text boxes events [Taiga #3627](https://tree.taiga.io/project/penpot/issue/3627)
|
||||
|
||||
|
||||
## 1.13.5-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix orientation artboard preset not working with differently sized artboards [Taiga #3548](https://tree.taiga.io/project/penpot/issue/3548)
|
||||
- Fix background on export arboards [Taiga #1991](https://tree.taiga.io/project/penpot/issue/1991)
|
||||
|
||||
@@ -1691,6 +1730,7 @@ time being.
|
||||
- Fix problem when resizing a group with texts with auto-width/height [#3171](https://tree.taiga.io/project/penpot/issue/3171)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
## 1.12.4-beta
|
||||
@@ -1708,7 +1748,7 @@ time being.
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix issue with shift+select to deselect shapes [Taiga #3154](https://tree.taiga.io/project/penpot/issue/3154)
|
||||
- Fix issue with drag-select shapes [Taiga #3165](https://tree.taiga.io/project/penpot/issue/3165)
|
||||
- Fix issue with drag-select shapes [Taiga #3165](https://tree.taiga.io/project/penpot/issue/3165)
|
||||
- Fix issue on password persistence after registration process on private instances
|
||||
|
||||
## 1.12.2-beta
|
||||
@@ -1726,7 +1766,6 @@ time being.
|
||||
- Fix length of names in sidebar [Taiga #2962](https://tree.taiga.io/project/penpot/issue/2962)
|
||||
- Fix issues on loki integration
|
||||
|
||||
|
||||
## 1.12.0-beta
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Click to the link below to confirm the change:</div>
|
||||
Click the link below to confirm the change.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -217,8 +217,7 @@
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
If you received this email by mistake, please consider changing your password for security
|
||||
reasons.</div>
|
||||
If you did not request this change, consider changing your password for security reasons.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -2,12 +2,11 @@ Hello {{name|abbreviate:25}}!
|
||||
|
||||
We received a request to change your current email to {{ pending-email }}.
|
||||
|
||||
Click to the link below to confirm the change:
|
||||
Click the link below to confirm the change.
|
||||
|
||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||
|
||||
If you received this email by mistake, please consider changing your password
|
||||
for security reasons.
|
||||
If you did not request this change, consider changing your password for security reasons.
|
||||
|
||||
Enjoy!
|
||||
The Penpot team.
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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)}]]]])
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE file_change
|
||||
ADD COLUMN migrations text[];
|
||||
@@ -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)]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -349,7 +349,7 @@
|
||||
rounded-s (d/format-number (* 100 s) precision)
|
||||
rounded-l (d/format-number (* 100 l) precision)
|
||||
rounded-a (d/format-number a precision)]
|
||||
(str/concat "" rounded-h ", " rounded-s "%, " rounded-l "%, " rounded-a)))
|
||||
(str/concat "" rounded-h " " rounded-s "% " rounded-l "% / " rounded-a)))
|
||||
|
||||
(defn format-rgba
|
||||
[[r g b a]]
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"]))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,33 +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.
|
||||
@@ -1783,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))
|
||||
@@ -1798,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
|
||||
@@ -1807,18 +1815,23 @@
|
||||
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)))
|
||||
|
||||
;; If the final attr-value is the actual value, skip
|
||||
skip-operations? (or skip-operations?
|
||||
(= attr-val (get dest-shape attr)))
|
||||
|
||||
|
||||
;; On a text-partial-change, we want to force a position-data reset
|
||||
;; so it's calculated again
|
||||
[roperations uoperations]
|
||||
(if text-partial-change?
|
||||
(if (and text-content-change? (not skip-operations?))
|
||||
(add-update-attr-operations :position-data dest-shape roperations uoperations nil)
|
||||
[roperations uoperations])
|
||||
|
||||
@@ -1830,6 +1843,93 @@
|
||||
roperations'
|
||||
uoperations')))))))
|
||||
|
||||
|
||||
(defn- switch-text-change-value
|
||||
[prev-content ;; The :content of the text before the switch
|
||||
current-content ;; The :content of the text after the switch (a clean copy)
|
||||
ref-content touched] ;; The :content of the referenced text on the main component
|
||||
;; before the switch
|
||||
(let [;; We need the differences between the contents on the main
|
||||
;; components. current-content is the content of a clean copy,
|
||||
;; so for all effects its the same as the content on its main
|
||||
main-comps-diff (cttx/get-diff-type ref-content current-content)
|
||||
can-keep-text? (not (contains? main-comps-diff :text-content-text))
|
||||
can-keep-attr? (not (contains? main-comps-diff :text-content-attribute))
|
||||
main-diff-structure? (contains? main-comps-diff :text-content-structure)
|
||||
|
||||
current-attrs (cttx/get-first-paragraph-text-attrs current-content)
|
||||
;; Have current content an uniform style?
|
||||
curr-unif-style? (cttx/equal-attrs? current-content current-attrs)
|
||||
prev-attrs (cttx/get-first-paragraph-text-attrs prev-content)
|
||||
;; Have prev content an uniform style?
|
||||
prev-unif-style? (cttx/equal-attrs? prev-content prev-attrs)
|
||||
ref-attrs (cttx/get-first-paragraph-text-attrs ref-content)
|
||||
;; Have ref content an uniform style?
|
||||
ref-unif-style? (cttx/equal-attrs? ref-content ref-attrs)]
|
||||
(cond
|
||||
;; When the main components have a difference in structure
|
||||
;; (different number of paragraph or text entries)
|
||||
main-diff-structure?
|
||||
;; Special case for adding or removing paragraphs:
|
||||
;; If the structure has changed between ref-content and current-content,
|
||||
;; but each one have uniform attributes, and the attrs on the main
|
||||
;; components were equal, we keep the touched-content structure and
|
||||
;; texts, updating its attrs to make them like the current-content
|
||||
(if (and curr-unif-style?
|
||||
ref-unif-style?
|
||||
prev-unif-style?
|
||||
(= ref-attrs current-attrs))
|
||||
(cttx/copy-attrs-keys current-content prev-attrs)
|
||||
;; In any other case of structure change, we discard all
|
||||
;; the overrides and keep the content of the current-shape
|
||||
current-content)
|
||||
|
||||
;; When the main components are equal, we keep the updated
|
||||
;; content from previous-shape as is
|
||||
(and can-keep-text? can-keep-attr?)
|
||||
prev-content
|
||||
|
||||
;; When we can't keep anything, we discard all the
|
||||
;; overrides and keep the content of the current-shape
|
||||
(and (not can-keep-text?) (not can-keep-attr?))
|
||||
current-content
|
||||
|
||||
;; Special case for added or removed paragraphs:
|
||||
;; If the structure has changed on current-content, but it has uniform attributes
|
||||
;; and the previous-content also has uniform attributes, and we can keep the changes
|
||||
;; on the text, we keep the touched-content structure and texts, updating
|
||||
;; its attrs to make them like the current-content
|
||||
(and (touched :text-content-structure)
|
||||
curr-unif-style?
|
||||
prev-unif-style?)
|
||||
(if can-keep-text?
|
||||
(cttx/copy-attrs-keys prev-content current-attrs)
|
||||
(cttx/copy-attrs-keys current-content prev-attrs))
|
||||
|
||||
;; In any other case of structure change, we discard all
|
||||
;; the overrides and keep the content of the current-shape
|
||||
(touched :text-content-structure)
|
||||
current-content
|
||||
|
||||
;; When there is a change on :text-content-text,
|
||||
;; and and we can keep it, we copy the texts from
|
||||
;; previous-shape over the attrs of current-shape
|
||||
(and
|
||||
(touched :text-content-text) can-keep-text?)
|
||||
(cttx/copy-text-keys prev-content current-content)
|
||||
|
||||
;; When there is a change on :text-content-attribute,
|
||||
;; and we can keep it, we copy the texts from current-shape
|
||||
;; over the attrs of previous-shape
|
||||
(and
|
||||
(touched :text-content-attribute) can-keep-attr?)
|
||||
(cttx/copy-text-keys current-content prev-content)
|
||||
|
||||
;; In any other case, we discard all the overrides
|
||||
;; and keep the content of the current-shape
|
||||
:else
|
||||
current-content)))
|
||||
|
||||
(defn update-attrs-on-switch
|
||||
"Copy attributes that have changed in the shape previous to the switch
|
||||
to the current shape (post switch). Used only on variants switch"
|
||||
@@ -1883,14 +1983,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
|
||||
(not skip-operations?)
|
||||
(cfh/text-shape? current-shape)
|
||||
(cfh/text-shape? previous-shape)
|
||||
(= :content attr)
|
||||
(touched attr-group))
|
||||
(is-text-partial-change? current-shape previous-shape))
|
||||
text-change?
|
||||
(and
|
||||
(not skip-operations?)
|
||||
(cfh/text-shape? current-shape)
|
||||
(cfh/text-shape? previous-shape)
|
||||
(= :content attr)
|
||||
(touched attr-group))
|
||||
|
||||
;; position-data is a special case because can be affected by :geometry-group and :content-group
|
||||
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
|
||||
@@ -1902,20 +2001,34 @@
|
||||
(not= (:position-data previous-shape) (:position-data current-shape))
|
||||
(touched :geometry-group))
|
||||
|
||||
attr-val (when-not skip-operations?
|
||||
(cond
|
||||
;; If position data changes and the geometry group is touched
|
||||
;; we need to put to nil so we can regenerate it
|
||||
reset-pos-data?
|
||||
nil
|
||||
attr-val
|
||||
(when-not skip-operations?
|
||||
(cond
|
||||
;; If position data changes and the geometry group is touched
|
||||
;; we need to put to nil so we can regenerate it
|
||||
reset-pos-data?
|
||||
nil
|
||||
|
||||
text-partial-change?
|
||||
(text-partial-change-value (:content previous-shape)
|
||||
(:content current-shape)
|
||||
touched)
|
||||
text-change?
|
||||
(switch-text-change-value (:content previous-shape)
|
||||
(:content current-shape)
|
||||
(:content origin-ref-shape)
|
||||
touched)
|
||||
|
||||
:else
|
||||
(get previous-shape attr)))
|
||||
:else
|
||||
(get previous-shape attr)))
|
||||
|
||||
;; If the final attr-value is the actual value, skip
|
||||
skip-operations? (or skip-operations?
|
||||
(= attr-val (get current-shape attr)))
|
||||
|
||||
|
||||
;; On a text-change, we want to force a position-data reset
|
||||
;; so it's calculated again
|
||||
[roperations uoperations]
|
||||
(if (and (not skip-operations?) text-change?)
|
||||
(add-update-attr-operations :position-data current-shape roperations uoperations nil)
|
||||
[roperations uoperations])
|
||||
|
||||
[roperations' uoperations']
|
||||
(if skip-operations?
|
||||
|
||||
@@ -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?]}]
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logic.libraries :as cll]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.logic.variants :as clv]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.text :as txt]
|
||||
[app.common.types.container :as ctn]
|
||||
@@ -275,26 +278,36 @@
|
||||
|
||||
(defn swap-component
|
||||
"Swap the specified shape by the component specified by component-tag"
|
||||
[file shape component-tag & {:keys [page-label propagate-fn]}]
|
||||
[file shape component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label]}]
|
||||
(let [page (if page-label
|
||||
(thf/get-page file page-label)
|
||||
(thf/current-page file))
|
||||
libraries {(:id file) file}
|
||||
|
||||
[_ _all-parents changes]
|
||||
orig-shapes (when keep-touched? (cfh/get-children-with-self (:objects page) (:id shape)))
|
||||
|
||||
[new-shape _all-parents changes]
|
||||
(cll/generate-component-swap (pcb/empty-changes)
|
||||
(:objects page)
|
||||
shape
|
||||
(:data file)
|
||||
page
|
||||
{(:id file) file}
|
||||
libraries
|
||||
(-> (thc/get-component file component-tag)
|
||||
:id)
|
||||
0
|
||||
nil
|
||||
{}
|
||||
false)
|
||||
(true? keep-touched?))
|
||||
|
||||
changes (if keep-touched?
|
||||
(clv/generate-keep-touched changes new-shape shape orig-shapes page libraries (:data file))
|
||||
changes)
|
||||
|
||||
|
||||
file' (thf/apply-changes file changes)]
|
||||
(when new-shape-label
|
||||
(thi/set-id! new-shape-label (:id new-shape)))
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
file')))
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
(:require
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]))
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.text :as txt]
|
||||
[app.common.types.shape :as cts]))
|
||||
|
||||
(defn add-variant
|
||||
[file variant-label component1-label root1-label component2-label root2-label
|
||||
@@ -37,3 +39,48 @@
|
||||
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "p1v1"} {:name "Property2" :value "p2v1"}]})
|
||||
(thc/make-component component2-label root2-label)
|
||||
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "p1v2"} {:name "Property2" :value "p2v2"}]}))))
|
||||
|
||||
(defn add-variant-with-child
|
||||
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label
|
||||
& {:keys [child1-params child2-params]}]
|
||||
(let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
|
||||
variant-id (thi/id variant-label)]
|
||||
(-> file
|
||||
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
|
||||
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
|
||||
(ths/add-sample-shape child1-label (assoc child1-params :parent-label root1-label))
|
||||
(ths/add-sample-shape child2-label (assoc child2-params :parent-label root2-label))
|
||||
(thc/make-component component1-label root1-label)
|
||||
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]})
|
||||
(thc/make-component component2-label root2-label)
|
||||
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value2"}]}))))
|
||||
|
||||
|
||||
(defn add-variant-with-text
|
||||
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label text1 text2
|
||||
& {:keys [text1-params text2-params]}]
|
||||
(let [text1 (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
|
||||
(txt/change-text text1)
|
||||
(assoc :position-data nil
|
||||
:parent-label root1-label))
|
||||
text2 (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
|
||||
(txt/change-text text2)
|
||||
(assoc :position-data nil
|
||||
:parent-label root2-label))
|
||||
|
||||
file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
|
||||
variant-id (thi/id variant-label)]
|
||||
(-> file
|
||||
|
||||
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
|
||||
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
|
||||
(ths/add-sample-shape child1-label
|
||||
(merge text1
|
||||
text1-params))
|
||||
(ths/add-sample-shape child2-label
|
||||
(merge text2
|
||||
text2-params))
|
||||
(thc/make-component component1-label root1-label)
|
||||
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]})
|
||||
(thc/make-component component2-label root2-label)
|
||||
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value2"}]}))))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -294,8 +295,8 @@
|
||||
([page component library-data position]
|
||||
(make-component-instance page component library-data position {}))
|
||||
([page component library-data position
|
||||
{:keys [main-instance? force-id force-frame-id keep-ids?]
|
||||
:or {main-instance? false force-id nil force-frame-id nil keep-ids? false}}]
|
||||
{:keys [main-instance? force-id force-frame-id keep-ids? force-parent-id]
|
||||
:or {main-instance? false force-id nil force-frame-id nil keep-ids? false force-parent-id nil}}]
|
||||
(let [component-page (ctpl/get-page library-data (:main-instance-page component))
|
||||
|
||||
component-shape (-> (get-shape component-page (:main-instance-id component))
|
||||
@@ -303,7 +304,6 @@
|
||||
(assoc :frame-id uuid/zero)
|
||||
(remove-swap-keep-attrs))
|
||||
|
||||
|
||||
orig-pos (gpt/point (:x component-shape) (:y component-shape))
|
||||
delta (gpt/subtract position orig-pos)
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
|
||||
[new-shape new-shapes _]
|
||||
(ctst/clone-shape component-shape
|
||||
frame-id
|
||||
(or force-parent-id frame-id)
|
||||
(:objects component-page)
|
||||
:update-new-shape update-new-shape
|
||||
:force-id force-id
|
||||
@@ -518,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
20
common/src/app/common/types/stroke.cljc
Normal file
20
common/src/app/common/types/stroke.cljc
Normal 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})
|
||||
@@ -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,
|
||||
@@ -127,7 +118,8 @@
|
||||
entries"
|
||||
[a b]
|
||||
(cond
|
||||
(not= (type a) (type b))
|
||||
(and (not= (type a) (type b))
|
||||
(not (and (map? a) (map? b)))) ;; Sometimes they are both maps but of different subtypes
|
||||
false
|
||||
|
||||
(map? a)
|
||||
|
||||
@@ -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]])
|
||||
@@ -135,6 +157,8 @@
|
||||
|
||||
(def typography-keys (set/union font-size-keys letter-spacing-keys))
|
||||
|
||||
;; TODO: Created to extract the font-size feature from the typography feature flag.
|
||||
;; Delete this once the typography feature flag is removed.
|
||||
(def ff-typography-keys (set/difference typography-keys font-size-keys))
|
||||
|
||||
(def ^:private schema:number
|
||||
@@ -150,6 +174,7 @@
|
||||
opacity-keys
|
||||
spacing-keys
|
||||
dimensions-keys
|
||||
axis-keys
|
||||
rotation-keys
|
||||
typography-keys
|
||||
number-keys))
|
||||
@@ -201,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]
|
||||
@@ -261,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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
#?(:cljs [goog.color :as gcolors])
|
||||
[app.common.colors :as colors]
|
||||
[app.common.data :as d]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest valid-hex-color
|
||||
@@ -52,8 +51,8 @@
|
||||
(t/is (= [1 2 3] (colors/hex->rgb "#010203"))))
|
||||
|
||||
(t/deftest format-hsla
|
||||
(t/is (= "210, 50%, 0.78%, 1" (colors/format-hsla [210.0 0.5 0.00784313725490196 1])))
|
||||
(t/is (= "220, 5%, 30%, 0.8" (colors/format-hsla [220.0 0.05 0.3 0.8]))))
|
||||
(t/is (= "210 50% 0.78% / 1" (colors/format-hsla [210.0 0.5 0.00784313725490196 1])))
|
||||
(t/is (= "220 5% 30% / 0.8" (colors/format-hsla [220.0 0.05 0.3 0.8]))))
|
||||
|
||||
(t/deftest format-rgba
|
||||
(t/is (= "210, 199, 12, 0.08" (colors/format-rgba [210 199 12 0.08])))
|
||||
|
||||
@@ -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
|
||||
|
||||
1125
common/test/common_tests/logic/variants_switch_test.cljc
Normal file
1125
common/test/common_tests/logic/variants_switch_test.cljc
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
91
docker/imagemagick/Dockerfile
Normal file
91
docker/imagemagick/Dockerfile
Normal 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"]
|
||||
@@ -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
|
||||
|
||||
@@ -39,6 +39,7 @@ RUN set -ex; \
|
||||
fonts-wqy-zenhei \
|
||||
fonts-tlwg-loma-otf \
|
||||
fonts-freefont-ttf \
|
||||
poppler-utils \
|
||||
\
|
||||
libasound2t64 \
|
||||
libatk-bridge2.0-0t64 \
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -x
|
||||
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
IMAGE=${1:-backend}
|
||||
|
||||
OUTPUT="type=registry"
|
||||
|
||||
if [ "--local" = "$2" ]; then
|
||||
OUTPUT="type=docker"
|
||||
fi
|
||||
|
||||
ORG=${PENPOT_DOCKER_NAMESPACE:-penpotapp};
|
||||
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64};
|
||||
|
||||
IMAGE=${PENPOT_BUILD_IMAGE:-backend}
|
||||
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64};
|
||||
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64,linux/arm64};
|
||||
VERSION=${PENPOT_BUILD_VERSION:-latest}
|
||||
|
||||
DOCKER_IMAGE="$ORG/$IMAGE";
|
||||
OPTIONS="-t $DOCKER_IMAGE:$VERSION";
|
||||
|
||||
@@ -20,7 +23,7 @@ for element in "${TAGS[@]}"; do
|
||||
done
|
||||
|
||||
docker buildx inspect penpot > /dev/null 2>&1;
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all > /dev/null;
|
||||
|
||||
if [ $? -eq 1 ]; then
|
||||
docker buildx create --name=penpot --use
|
||||
@@ -32,4 +35,5 @@ fi
|
||||
|
||||
unset IFS;
|
||||
|
||||
docker buildx build --platform ${PLATFORM// /,} $OPTIONS -f Dockerfile.$IMAGE "$@" .;
|
||||
shift;
|
||||
docker buildx build --output $OUTPUT --platform ${PLATFORM// /,} $OPTIONS -f Dockerfile.$IMAGE .;
|
||||
|
||||
@@ -28,5 +28,6 @@ Use Docker if you already know the tool, if need full control of the process or
|
||||
and do not want to depend on any external provider, or need to do any special customization.
|
||||
</p>
|
||||
|
||||
Or you can try <a href="#unofficial-self-host-options">other options</a>,
|
||||
offered by Penpot community.
|
||||
Or you can try [other options][1], offered by Penpot community.
|
||||
|
||||
[1]: /technical-guide/getting-started/unofficial-options/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 layer’s 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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
2950
frontend/playwright/data/render-wasm/get-file-text-decoration.json
Normal file
2950
frontend/playwright/data/render-wasm/get-file-text-decoration.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -121,7 +121,6 @@ test("Renders a file with styled texts", async ({ page }) => {
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
||||
test("Renders a file with texts with images", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
@@ -145,6 +144,28 @@ test("Renders a file with texts with images", async ({ page }) => {
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with text decoration", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockFileMediaAsset(
|
||||
[
|
||||
"d6c33e7b-7b64-80f3-8006-78509a3a2d21",
|
||||
],
|
||||
"render-wasm/assets/pattern.png",
|
||||
);
|
||||
await mockGetEmojiFont(workspace);
|
||||
await mockGetJapaneseFont(workspace);
|
||||
|
||||
await workspace.mockGetFile("render-wasm/get-file-text-decoration.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "d6c33e7b-7b64-80f3-8006-785098582f1d",
|
||||
pageId: "d6c33e7b-7b64-80f3-8006-785098582f1e",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with multiple emoji", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
BIN
frontend/resources/images/features/2.9-font-size.gif
Normal file
BIN
frontend/resources/images/features/2.9-font-size.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
BIN
frontend/resources/images/features/2.9-overrides.gif
Normal file
BIN
frontend/resources/images/features/2.9-overrides.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 412 KiB |
BIN
frontend/resources/images/features/2.9-qol.gif
Normal file
BIN
frontend/resources/images/features/2.9-qol.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 222 KiB |
BIN
frontend/resources/images/features/2.9-slide-0.jpg
Normal file
BIN
frontend/resources/images/features/2.9-slide-0.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -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)
|
||||
@@ -245,4 +260,4 @@
|
||||
|
||||
(defn event
|
||||
[props]
|
||||
(ptk/data-event ::events props))
|
||||
(ptk/data-event ::event props))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")))))
|
||||
|
||||
|
||||
@@ -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!
|
||||
([]
|
||||
|
||||
@@ -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
|
||||
[]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
[app.main.data.workspace :as-alias dw]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.notifications :as-alias dwn]
|
||||
[app.main.data.workspace.pages :as-alias dwpg]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.specialized-panel :as dwsp]
|
||||
@@ -700,7 +701,7 @@
|
||||
(fn [page-id shape-id]
|
||||
(rx/merge
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dw/initialize-page))
|
||||
(rx/filter (ptk/type? ::dwpg/initialize-page))
|
||||
(rx/take 1)
|
||||
(rx/observe-on :async)
|
||||
(rx/mapcat (fn [_] (select-and-zoom shape-id))))
|
||||
@@ -1094,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
|
||||
|
||||
@@ -847,3 +847,25 @@
|
||||
(if undo-transation?
|
||||
(rx/of (dwu/commit-undo-transaction undo-id))
|
||||
(rx/empty))))))))
|
||||
|
||||
;; Pure function to determine next grow-type for text layers
|
||||
(defn next-grow-type [current-grow-type resize-direction]
|
||||
(cond
|
||||
(= current-grow-type :fixed)
|
||||
:fixed
|
||||
|
||||
(and (= resize-direction :horizontal)
|
||||
(= current-grow-type :auto-width))
|
||||
:auto-height
|
||||
|
||||
(and (= resize-direction :horizontal)
|
||||
(= current-grow-type :auto-height))
|
||||
:auto-height
|
||||
|
||||
(and (= resize-direction :vertical)
|
||||
(or (= current-grow-type :auto-width)
|
||||
(= current-grow-type :auto-height)))
|
||||
:fixed
|
||||
|
||||
:else
|
||||
current-grow-type))
|
||||
|
||||
@@ -174,12 +174,15 @@
|
||||
add-component-copy
|
||||
(fn [objs id shape]
|
||||
(let [component (ctkl/get-component fdata (:component-id shape))
|
||||
parent-id (when (not= (:parent-id shape) uuid/zero) (:parent-id shape))
|
||||
[new-shape new-shapes]
|
||||
(ctn/make-component-instance page
|
||||
component
|
||||
fdata
|
||||
(gpt/point (:x shape) (:y shape))
|
||||
{:keep-ids? true :force-frame-id (:frame-id shape)})
|
||||
{:keep-ids? true
|
||||
:force-frame-id (:frame-id shape)
|
||||
:force-parent-id parent-id})
|
||||
children (into {} (map (fn [shape] [(:id shape) shape]) new-shapes))
|
||||
objs (assoc objs id new-shape)]
|
||||
(merge objs children)))
|
||||
@@ -201,10 +204,8 @@
|
||||
(assoc :id id)
|
||||
(assoc :objects
|
||||
objects))
|
||||
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-page id page))]
|
||||
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(s/def ::rename-page
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
;; We don't have the fills attribute. It's an old text without color
|
||||
;; so need to be black
|
||||
(and (nil? (:fills node)) (empty? color-attrs))
|
||||
(update :fills conj txt/default-text-attrs)
|
||||
(assoc :fills (:fills txt/default-text-attrs))
|
||||
|
||||
;; Remove duplicates from the fills
|
||||
:always
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
(ntf/show {:content (tr "workspace.tokens.unknown-token-type-message")
|
||||
:detail (->> (for [[token-type tokens] type->tokens]
|
||||
(tr "workspace.tokens.unknown-token-type-section" token-type (count tokens)))
|
||||
(str/join "\n"))
|
||||
(str/join "<br>"))
|
||||
:type :toast
|
||||
:level :info})))
|
||||
|
||||
|
||||
@@ -218,16 +218,23 @@
|
||||
(gpt/add resize-origin displacement)
|
||||
resize-origin)
|
||||
|
||||
;; Determine resize direction for grow-type logic
|
||||
resize-direction (cond
|
||||
(or (= handler :left) (= handler :right)) :horizontal
|
||||
(or (= handler :top) (= handler :bottom)) :vertical
|
||||
:else nil)
|
||||
|
||||
;; Calculate new grow-type for text layers
|
||||
new-grow-type (when (cfh/text-shape? shape)
|
||||
(dwm/next-grow-type (dm/get-prop shape :grow-type) resize-direction))
|
||||
|
||||
;; When the horizontal/vertical scale a flex children with auto/fill
|
||||
;; we change it too fixed
|
||||
change-width?
|
||||
(not (mth/close? (dm/get-prop scalev :x) 1))
|
||||
|
||||
change-height?
|
||||
(not (mth/close? (dm/get-prop scalev :y) 1))
|
||||
|
||||
auto-width-text? (and (cfh/text-shape? shape) (= :auto-width (dm/get-prop shape :grow-type)))
|
||||
auto-height-text? (and (cfh/text-shape? shape) (= :auto-height (dm/get-prop shape :grow-type)))]
|
||||
(not (mth/close? (dm/get-prop scalev :y) 1))]
|
||||
|
||||
(cond-> (ctm/empty)
|
||||
(some? displacement)
|
||||
@@ -242,11 +249,9 @@
|
||||
^boolean change-height?
|
||||
(ctm/change-property :layout-item-v-sizing :fix)
|
||||
|
||||
(and auto-width-text? (or change-width? change-height?))
|
||||
(ctm/change-property :grow-type :fixed)
|
||||
|
||||
(and auto-height-text? change-height?)
|
||||
(ctm/change-property :grow-type :fixed)
|
||||
;; Set grow-type if it should change
|
||||
(and new-grow-type (not= new-grow-type (dm/get-prop shape :grow-type)))
|
||||
(ctm/change-property :grow-type new-grow-type)
|
||||
|
||||
^boolean scale-text
|
||||
(ctm/scale-content (dm/get-prop scalev :x)))))
|
||||
|
||||
@@ -102,23 +102,27 @@
|
||||
(print-trace! error)
|
||||
(print-data! error))))
|
||||
|
||||
;; We receive a explicit authentication error;
|
||||
;; If the uri is for workspace, dashboard or view assign the
|
||||
;; exception for the 'Oops' page. Otherwise this explicitly clears
|
||||
;; all profile data and redirect the user to the login page. This is
|
||||
;; here and not in app.main.errors because of circular dependency.
|
||||
;; We receive a explicit authentication error; If the uri is for
|
||||
;; workspace, dashboard, viewer or settings, then assign the exception
|
||||
;; for show the error page. Otherwise this explicitly clears all
|
||||
;; profile data and redirect the user to the login page. This is here
|
||||
;; and not in app.main.errors because of circular dependency.
|
||||
(defmethod ptk/handle-error :authentication
|
||||
[e]
|
||||
(let [msg (tr "errors.auth.unable-to-login")
|
||||
uri (.-href glob/location)
|
||||
show-oops? (or (str/includes? uri "workspace")
|
||||
(str/includes? uri "dashboard")
|
||||
(str/includes? uri "view"))]
|
||||
(if show-oops?
|
||||
(st/async-emit! (rt/assign-exception e))
|
||||
[error]
|
||||
(let [message (tr "errors.auth.unable-to-login")
|
||||
uri (rt/get-current-href)
|
||||
|
||||
show-error?
|
||||
(or (str/includes? uri "workspace")
|
||||
(str/includes? uri "dashboard")
|
||||
(str/includes? uri "view")
|
||||
(str/includes? uri "settings"))]
|
||||
|
||||
(if show-error?
|
||||
(st/async-emit! (rt/assign-exception error))
|
||||
(do
|
||||
(st/emit! (da/logout))
|
||||
(ts/schedule 500 #(st/emit! (ntf/warn msg)))))))
|
||||
(ts/schedule 500 #(st/emit! (ntf/warn message)))))))
|
||||
|
||||
;; Error that happens on an active business model validation does not
|
||||
;; passes an validation (example: profile can't leave a team). From
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
[app.main.ui.onboarding.team-choice :refer [onboarding-team-modal]]
|
||||
[app.main.ui.releases :refer [release-notes-modal]]
|
||||
[app.main.ui.static :as static]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.theme :as theme]
|
||||
[beicon.v2.core :as rx]
|
||||
@@ -362,6 +363,8 @@
|
||||
;; initialize themes
|
||||
(theme/use-initialize profile)
|
||||
|
||||
(dom/prevent-browser-gesture-navigation!)
|
||||
|
||||
[:& (mf/provider ctx/current-route) {:value route}
|
||||
[:& (mf/provider ctx/current-profile) {:value profile}
|
||||
(if edata
|
||||
|
||||
@@ -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*)
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.dom.normalize-wheel :as nw]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.storage :as storage]
|
||||
@@ -171,41 +172,48 @@
|
||||
(mf/defc templates-section*
|
||||
{::mf/props :obj}
|
||||
[{:keys [default-project-id profile project-id team-id]}]
|
||||
(let [templates (mf/deref builtin-templates)
|
||||
templates (mf/with-memo [templates]
|
||||
(filterv #(and
|
||||
(not= (:id %) "welcome")
|
||||
(not= (:id %) "tutorial-for-beginners")) templates))
|
||||
(let [templates (mf/deref builtin-templates)
|
||||
templates (mf/with-memo [templates]
|
||||
(filterv #(and
|
||||
(not= (:id %) "welcome")
|
||||
(not= (:id %) "tutorial-for-beginners")) templates))
|
||||
|
||||
route (mf/deref refs/route)
|
||||
route-name (get-in route [:data :name])
|
||||
section (if (= route-name :dashboard-files)
|
||||
(if (= project-id default-project-id)
|
||||
"dashboard-drafts"
|
||||
"dashboard-project")
|
||||
(name route-name))
|
||||
route (mf/deref refs/route)
|
||||
route-name (get-in route [:data :name])
|
||||
section (if (= route-name :dashboard-files)
|
||||
(if (= project-id default-project-id)
|
||||
"dashboard-drafts"
|
||||
"dashboard-project")
|
||||
(name route-name))
|
||||
|
||||
collapsed* (mf/use-state
|
||||
#(get storage/global ::collapsed))
|
||||
collapsed (deref collapsed*)
|
||||
collapsed* (mf/use-state
|
||||
#(get storage/global ::collapsed))
|
||||
collapsed (deref collapsed*)
|
||||
|
||||
|
||||
|
||||
can-move (mf/use-state {:left false :right true})
|
||||
can-move (mf/use-state {:left false :right true})
|
||||
|
||||
total (count templates)
|
||||
total (count templates)
|
||||
|
||||
;; We need space for total plus the libraries&templates link
|
||||
content-ref (mf/use-ref)
|
||||
|
||||
move-left (fn [] (dom/scroll-by! (mf/ref-val content-ref) -300 0))
|
||||
move-right (fn [] (dom/scroll-by! (mf/ref-val content-ref) 300 0))
|
||||
content-ref (mf/use-ref)
|
||||
|
||||
on-toggle-collapse
|
||||
(mf/use-fn
|
||||
(fn [_event]
|
||||
(swap! collapsed* not)))
|
||||
|
||||
on-wheel
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(let [event* (nw/normalize-wheel event)
|
||||
deltaY (.-spinY event*)
|
||||
deltaX (.-spinX event*)
|
||||
node (mf/ref-val content-ref)]
|
||||
(when (> (abs deltaY) (abs deltaX))
|
||||
(.scrollBy node #js {:left (* 300 deltaY) :mode "smooth"})))))
|
||||
|
||||
on-scroll
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
@@ -219,16 +227,10 @@
|
||||
:right (> scroll-available client-width)}))))
|
||||
|
||||
on-move-left
|
||||
(mf/use-fn #(move-left))
|
||||
|
||||
on-move-left-key-down
|
||||
(mf/use-fn #(move-left))
|
||||
(mf/use-fn #(dom/scroll-by! (mf/ref-val content-ref) -300 0))
|
||||
|
||||
on-move-right
|
||||
(mf/use-fn #(move-right))
|
||||
|
||||
on-move-right-key-down
|
||||
(mf/use-fn #(move-right))
|
||||
(mf/use-fn #(dom/scroll-by! (mf/ref-val content-ref) 300 0))
|
||||
|
||||
on-import-template
|
||||
(mf/use-fn
|
||||
@@ -236,7 +238,7 @@
|
||||
(fn [template _event]
|
||||
(import-template! template team-id project-id default-project-id section)))]
|
||||
|
||||
(mf/with-effect [content-ref templates]
|
||||
(mf/with-effect [templates]
|
||||
(let [content (mf/ref-val content-ref)]
|
||||
(when (and (some? content) (some? templates))
|
||||
(dom/scroll-to content #js {:behavior "instant" :left 0 :top 0})
|
||||
@@ -258,6 +260,7 @@
|
||||
|
||||
[:div {:class (stl/css :content)
|
||||
:on-scroll on-scroll
|
||||
:on-wheel on-wheel
|
||||
:ref content-ref}
|
||||
|
||||
(for [index (range (count templates))]
|
||||
@@ -279,13 +282,13 @@
|
||||
[:button {:class (stl/css :move-button :move-left)
|
||||
:tab-index (if ^boolean collapsed "-1" "0")
|
||||
:on-click on-move-left
|
||||
:on-key-down on-move-left-key-down}
|
||||
:on-key-down on-move-left}
|
||||
arrow-icon])
|
||||
|
||||
(when (:right @can-move)
|
||||
[:button {:class (stl/css :move-button :move-right)
|
||||
:tab-index (if collapsed "-1" "0")
|
||||
:on-click on-move-right
|
||||
:aria-label (tr "labels.next")
|
||||
:on-key-down on-move-right-key-down}
|
||||
:on-key-down on-move-right
|
||||
:aria-label (tr "labels.next")}
|
||||
arrow-icon])]))
|
||||
|
||||
@@ -71,4 +71,5 @@
|
||||
[:div {:on-click on-toggle-detail}
|
||||
(tr "workspace.notification-pill.detail")]]
|
||||
(when show-detail
|
||||
[:div {:class (stl/css :error-detail-content)} detail])])]))
|
||||
[:div {:class (stl/css :error-detail-content)
|
||||
:dangerouslySetInnerHTML #js {:__html detail}}])])]))
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -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]})
|
||||
|
||||
|
||||
@@ -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?))]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.text :as txt]
|
||||
[app.common.types.color :as types.color]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -18,6 +18,8 @@
|
||||
[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]
|
||||
[okulary.core :as l]
|
||||
@@ -41,6 +43,21 @@
|
||||
(map #(dm/str (d/name %) ": " (get style %) ";"))
|
||||
(str/join "\n")))
|
||||
|
||||
(defn- format-gradient-css
|
||||
"Converts a gradient object to a CSS string."
|
||||
[gradient]
|
||||
(str "background-image: " (uc/gradient->css gradient) ";"
|
||||
"background-clip: text;"
|
||||
"color: transparent;"))
|
||||
|
||||
(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-color color {:format format}))))
|
||||
|
||||
(mf/defc typography-block
|
||||
[{:keys [text style]}]
|
||||
(let [typography-library-ref
|
||||
@@ -57,7 +74,8 @@
|
||||
file-library-workspace (get (mf/deref refs/files) (:typography-ref-file style))
|
||||
typography-external-lib (get-in file-library-workspace [:data :typographies (:typography-ref-id style)])
|
||||
|
||||
color-format (mf/use-state :hex)
|
||||
color-format! (mf/use-state :hex)
|
||||
color-format* (deref color-format!)
|
||||
|
||||
typography (or (get (or typography-library file-typographies-viewer file-typographies-workspace) (:typography-ref-id style)) typography-external-lib)]
|
||||
|
||||
@@ -65,10 +83,10 @@
|
||||
(when (:fills style)
|
||||
(for [[idx fill] (map-indexed vector (:fills style))]
|
||||
[:& color-row {:key idx
|
||||
:format @color-format
|
||||
:color (types.color/fill->color fill)
|
||||
:copy-data (copy-style-data fill :fill-color :fill-color-gradient)
|
||||
:on-change-format #(reset! color-format %)}]))
|
||||
:format color-format*
|
||||
:color (ctc/fill->color fill)
|
||||
:copy-data (copy-color-data fill color-format*)
|
||||
:on-change-format #(reset! color-format! %)}]))
|
||||
|
||||
(when (:typography-ref-id style)
|
||||
[:div {:class (stl/css :text-row)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -101,5 +101,5 @@
|
||||
@extend .button-secondary;
|
||||
@include uppercaseTitleTipography;
|
||||
height: $s-32;
|
||||
width: $s-252;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -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")))
|
||||
|
||||
112
frontend/src/app/main/ui/releases/v2_9.cljs
Normal file
112
frontend/src/app/main/ui/releases/v2_9.cljs
Normal 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)}
|
||||
"What’s 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)}
|
||||
"Let’s 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 there’s 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"]]]]]])))
|
||||
|
||||
102
frontend/src/app/main/ui/releases/v2_9.scss
Normal file
102
frontend/src/app/main/ui/releases/v2_9.scss
Normal 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;
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
@@ -130,7 +131,10 @@
|
||||
(assoc query-params :team-id (:default-team-id profile))))))
|
||||
|
||||
:else
|
||||
(st/emit! (rt/assign-exception {:type :not-found})))))))))
|
||||
(st/emit! (rt/assign-exception {:type :not-found}))))
|
||||
|
||||
(fn [cause]
|
||||
(errors/on-error cause)))))))
|
||||
|
||||
(defn init-routes
|
||||
[]
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
(mf/with-effect [profile]
|
||||
(when (nil? profile)
|
||||
(st/emit! (rt/nav :auth-login))))
|
||||
(st/emit! (rt/assign-exception {:type :authentication}))))
|
||||
|
||||
[:*
|
||||
[:> modal-container*]
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[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]
|
||||
@@ -61,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")
|
||||
@@ -96,7 +96,7 @@
|
||||
handle-accept-dialog (mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
|
||||
::ev/origin "profile"
|
||||
::ev/origin "settings"
|
||||
:section "subscription-management-modal"}))
|
||||
(let [current-href (rt/get-current-href)
|
||||
returnUrl (js/encodeURIComponent current-href)
|
||||
@@ -126,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}
|
||||
@@ -147,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")]]]
|
||||
|
||||
@@ -160,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)}
|
||||
@@ -176,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
|
||||
@@ -212,43 +226,73 @@
|
||||
|
||||
(mf/defc subscription-page*
|
||||
[{:keys [profile]}]
|
||||
(let [route (mf/deref refs/route)
|
||||
params (:params route)
|
||||
params-subscription (:subscription (:query params))
|
||||
show-trial-subscription-modal (or (= params-subscription "subscription-to-penpot-unlimited")
|
||||
(= params-subscription "subscription-to-penpot-enterprise"))
|
||||
show-subscription-success-modal (or (= params-subscription "subscribed-to-penpot-unlimited")
|
||||
(= params-subscription "subscribed-to-penpot-enterprise"))
|
||||
subscription (:subscription (:props profile))
|
||||
subscription-type (get-subscription-type subscription)
|
||||
subscription-is-trial (= (:status subscription) "trialing")
|
||||
teams* (mf/use-state nil)
|
||||
teams (deref teams*)
|
||||
locale (mf/deref i18n/locale)
|
||||
penpot-member (dt/format-date-locale-short (:created-at profile) {:locale locale})
|
||||
subscription-member (dt/format-date-locale-short (:start-date subscription) {:locale locale})
|
||||
go-to-pricing-page (mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "explore-pricing-click" ::ev/origin "settings" :section "subscription"}))
|
||||
(dom/open-new-window "https://penpot.app/pricing")))
|
||||
go-to-payments (mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
|
||||
::ev/origin "profile"
|
||||
:section "subscription"}))
|
||||
(let [current-href (rt/get-current-href)
|
||||
returnUrl (js/encodeURIComponent current-href)
|
||||
href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
|
||||
(st/emit! (rt/nav-raw :href href)))))
|
||||
open-subscription-modal (mf/use-fn
|
||||
(mf/deps teams)
|
||||
(fn [subscription-type]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-modal"
|
||||
::ev/origin "settings:in-app"}))
|
||||
(st/emit!
|
||||
(modal/show :management-dialog
|
||||
{:subscription-type subscription-type
|
||||
:teams teams :subscribe-to-trial (not subscription)}))))]
|
||||
(let [route (mf/deref refs/route)
|
||||
authenticated? (da/is-authenticated? profile)
|
||||
|
||||
teams* (mf/use-state nil)
|
||||
teams (deref teams*)
|
||||
|
||||
locale (mf/deref i18n/locale)
|
||||
|
||||
params-subscription
|
||||
(-> route :params :query :subscription)
|
||||
|
||||
show-trial-subscription-modal?
|
||||
(or (= params-subscription "subscription-to-penpot-unlimited")
|
||||
(= params-subscription "subscription-to-penpot-enterprise"))
|
||||
|
||||
show-subscription-success-modal?
|
||||
(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)
|
||||
|
||||
subscription-type
|
||||
(get-subscription-type subscription)
|
||||
|
||||
subscription-is-trial?
|
||||
(= (:status subscription) "trialing")
|
||||
|
||||
member-since
|
||||
(dt/format-date-locale-short (:created-at profile) {:locale locale})
|
||||
|
||||
subscribed-since
|
||||
(dt/format-date-locale-short (:start-date subscription) {:locale locale})
|
||||
|
||||
go-to-pricing-page
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ev/event {::ev/name "explore-pricing-click"
|
||||
::ev/origin "settings"
|
||||
:section "subscription"}))
|
||||
(dom/open-new-window "https://penpot.app/pricing")))
|
||||
|
||||
go-to-payments
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ev/event {::ev/name "open-subscription-management"
|
||||
::ev/origin "settings"
|
||||
:section "subscription"}))
|
||||
(let [current-href (rt/get-current-href)
|
||||
returnUrl (js/encodeURIComponent current-href)
|
||||
href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
|
||||
(st/emit! (rt/nav-raw :href href)))))
|
||||
|
||||
open-subscription-modal
|
||||
(mf/use-fn
|
||||
(mf/deps teams)
|
||||
(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 []
|
||||
(->> (rp/cmd! :get-owned-teams)
|
||||
@@ -258,33 +302,34 @@
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "subscription.labels")))
|
||||
|
||||
(mf/with-effect [show-trial-subscription-modal subscription]
|
||||
(when show-trial-subscription-modal
|
||||
(st/emit!
|
||||
(ptk/event ::ev/event {::ev/name "open-subscription-modal"
|
||||
::ev/origin "settings:from-pricing-page"})
|
||||
(modal/show :management-dialog
|
||||
{:subscription-type (if (= params-subscription "subscription-to-penpot-unlimited")
|
||||
"unlimited"
|
||||
"enterprise")
|
||||
:teams teams
|
||||
:subscribe-to-trial (not subscription)})
|
||||
(rt/nav :settings-subscription {} {::rt/replace true}))))
|
||||
(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?
|
||||
|
||||
(mf/with-effect [show-subscription-success-modal subscription]
|
||||
(when show-subscription-success-modal
|
||||
(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"))})
|
||||
(rt/nav :settings-subscription {} {::rt/replace true}))))
|
||||
(st/emit!
|
||||
(ptk/event ::ev/event {::ev/name "open-subscription-modal"
|
||||
::ev/origin "settings:from-pricing-page"})
|
||||
(modal/show :management-dialog
|
||||
{: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}))
|
||||
|
||||
^boolean show-subscription-success-modal?
|
||||
(st/emit!
|
||||
(modal/show :subscription-success
|
||||
{:subscription-name (if (= params-subscription "subscribed-to-penpot-unlimited")
|
||||
(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)}
|
||||
[:div {:class (stl/css :dashboard-content)}
|
||||
@@ -298,16 +343,16 @@
|
||||
[:> 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
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited-trial")
|
||||
:card-title-icon i/character-u
|
||||
: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")
|
||||
@@ -319,38 +364,41 @@
|
||||
: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)}])
|
||||
|
||||
"enterprise"
|
||||
(if subscription-is-trial
|
||||
(if subscription-is-trial?
|
||||
[:> 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 subscription-member [:div {:class (stl/css :membership)}
|
||||
[:span {:class (stl/css :subscription-member)} i/crown]
|
||||
[:span {:class (stl/css :membership-date)} (tr "subscription.settings.support-us-since" subscription-member)]])
|
||||
(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)}
|
||||
(tr "subscription.settings.support-us-since" subscribed-since)]])
|
||||
|
||||
[:div {:class (stl/css :membership)}
|
||||
[:span {:class (stl/css :penpot-member)} i/user]
|
||||
[:span {:class (stl/css :membership-date)} (tr "subscription.settings.member-since" penpot-member)]]]]
|
||||
[:span {:class (stl/css :membership-date)}
|
||||
(tr "subscription.settings.member-since" member-since)]]]]
|
||||
|
||||
[:div {:class (stl/css :other-subscriptions)}
|
||||
[:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.other-plans")]
|
||||
@@ -360,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")
|
||||
@@ -374,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}])
|
||||
|
||||
@@ -385,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}])]]]))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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]
|
||||
@@ -69,9 +70,8 @@
|
||||
[:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
|
||||
[:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])
|
||||
|
||||
(mf/defc login-dialog
|
||||
{::mf/props :obj}
|
||||
[{:keys [show-dialog]}]
|
||||
(mf/defc login-dialog*
|
||||
[]
|
||||
(let [current-section (mf/use-state :login)
|
||||
user-email (mf/use-state "")
|
||||
register-token (mf/use-state "")
|
||||
@@ -94,9 +94,7 @@
|
||||
|
||||
success-login
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! show-dialog false)
|
||||
(st/emit! (rt/reload true))))
|
||||
#(st/emit! (rt/reload true)))
|
||||
|
||||
success-register
|
||||
(mf/use-fn
|
||||
@@ -117,7 +115,7 @@
|
||||
(reset! current-section :recovery-email-sent)))
|
||||
|
||||
on-nav-root
|
||||
(mf/use-fn #(st/emit! (rt/nav-root)))]
|
||||
(mf/use-fn #(st/emit! (rt/nav :auth-login {})))]
|
||||
|
||||
[:div {:class (stl/css :overlay)}
|
||||
[:div {:class (stl/css :dialog-login)}
|
||||
@@ -183,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)]
|
||||
@@ -203,11 +201,9 @@
|
||||
[:button {:on-click on-click} button-text]]]]))
|
||||
|
||||
(mf/defc request-access*
|
||||
[{:keys [file-id team-id is-default is-workspace]}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
requested* (mf/use-state {:sent false :already-requested false})
|
||||
[{:keys [file-id team-id is-default is-workspace profile]}]
|
||||
(let [requested* (mf/use-state {:sent false :already-requested false})
|
||||
requested (deref requested*)
|
||||
show-dialog (mf/use-state true)
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
@@ -237,90 +233,47 @@
|
||||
(st/emit! (dcm/create-team-access-request
|
||||
(with-meta params mdata))))))]
|
||||
|
||||
[:*
|
||||
(if (some? file-id)
|
||||
(if is-workspace
|
||||
[:div {:class (stl/css :workspace)}
|
||||
[:div {:class (stl/css :workspace-left)}
|
||||
i/logo-icon
|
||||
[:div
|
||||
[:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
|
||||
[:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
|
||||
[:div {:class (stl/css :workspace-right)}]]
|
||||
(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}]
|
||||
|
||||
[:div {:class (stl/css :viewer)}
|
||||
;; FIXME: the viewer header was never designed to be reused
|
||||
;; from other parts of the application, and this code looks
|
||||
;; like a fast workaround reusing it as-is without a proper
|
||||
;; component adaptation for be able to use it easily it on
|
||||
;; viewer context or static error page context
|
||||
[:& viewer.header/header {:project
|
||||
{:name (tr "not-found.no-permission.project-name")}
|
||||
:index 0
|
||||
:file {:name (tr "not-found.no-permission.penpot-file")}
|
||||
:page nil
|
||||
:frame nil
|
||||
:permissions {:is-logged true}
|
||||
:zoom 1
|
||||
:section :interactions
|
||||
:shown-thumbnails false
|
||||
:interactions-mode nil}]])
|
||||
(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}]
|
||||
|
||||
[:div {:class (stl/css :dashboard)}
|
||||
[:div {:class (stl/css :dashboard-sidebar)}
|
||||
[:> sidebar*
|
||||
{:team nil
|
||||
:projects []
|
||||
:project (:default-project-id profile)
|
||||
:profile profile
|
||||
:section :dashboard-projects
|
||||
:search-term ""}]]])
|
||||
(: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}]
|
||||
|
||||
(when @show-dialog
|
||||
(cond
|
||||
(nil? profile)
|
||||
[:& login-dialog {:show-dialog show-dialog}]
|
||||
(: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}]
|
||||
|
||||
is-default
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.project")
|
||||
: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}]
|
||||
|
||||
(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}]
|
||||
|
||||
(: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}]
|
||||
|
||||
(: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}]
|
||||
|
||||
(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}]
|
||||
|
||||
(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}]))]))
|
||||
(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}])))
|
||||
|
||||
(mf/defc not-found*
|
||||
[]
|
||||
@@ -484,32 +437,80 @@
|
||||
|
||||
[:> internal-error* props])))
|
||||
|
||||
(mf/defc context-wrapper*
|
||||
[{:keys [is-workspace is-dashboard is-viewer profile children]}]
|
||||
[:*
|
||||
(cond
|
||||
is-workspace
|
||||
[:div {:class (stl/css :workspace)}
|
||||
[:div {:class (stl/css :workspace-left)}
|
||||
i/logo-icon
|
||||
[:div
|
||||
[:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
|
||||
[:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
|
||||
[:div {:class (stl/css :workspace-right)}]]
|
||||
|
||||
is-viewer
|
||||
[:div {:class (stl/css :viewer)}
|
||||
;; FIXME: the viewer header was never designed to be reused
|
||||
;; from other parts of the application, and this code looks
|
||||
;; like a fast workaround reusing it as-is without a proper
|
||||
;; component adaptation for be able to use it easily it on
|
||||
;; viewer context or static error page context
|
||||
[:& viewer.header/header {:project
|
||||
{:name (tr "not-found.no-permission.project-name")}
|
||||
:index 0
|
||||
:file {:name (tr "not-found.no-permission.penpot-file")}
|
||||
:page nil
|
||||
:frame nil
|
||||
:permissions {:is-logged true}
|
||||
:zoom 1
|
||||
:section :interactions
|
||||
:shown-thumbnails false
|
||||
:interactions-mode nil}]]
|
||||
|
||||
is-dashboard
|
||||
[:div {:class (stl/css :dashboard)}
|
||||
[:div {:class (stl/css :dashboard-sidebar)}
|
||||
[:> sidebar*
|
||||
{:team nil
|
||||
:projects []
|
||||
:project (:default-project-id profile)
|
||||
:profile profile
|
||||
:section :dashboard-projects
|
||||
:search-term ""}]]])
|
||||
|
||||
children])
|
||||
|
||||
(mf/defc exception-page*
|
||||
{::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)
|
||||
|
||||
auth-error? (= type :authentication)
|
||||
not-found? (= type :not-found)
|
||||
|
||||
authenticated?
|
||||
(is-authenticated? profile)
|
||||
|
||||
request-access?
|
||||
(and
|
||||
(or (= type :not-found)
|
||||
(= type :authentication))
|
||||
(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)
|
||||
@@ -517,11 +518,26 @@
|
||||
(rx/subs! (partial reset! info*)
|
||||
(partial reset! info* {:loaded true})))))
|
||||
|
||||
(when loaded?
|
||||
(if request-access?
|
||||
[:> 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])))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
{:delete-stop {:tooltip (ds/supr)
|
||||
:command ["del" "backspace"]
|
||||
:subsections [:edit]
|
||||
:overwrite true
|
||||
:fn #(st/emit! (dwc/remove-gradient-stop))}}))
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user