Compare commits

..

484 Commits

Author SHA1 Message Date
Alejandro Alonso
24fa4f71ad 📎 Update version.txt file 2023-02-27 10:37:39 +01:00
Andrey Antukh
fa21dc4cf9 📎 Fix tests 2023-02-25 10:35:00 +01:00
Alejandro
2460f36bab Merge pull request #2983 from penpot/niwinz-invitations-fixes
Fix issues with invitation user flow
2023-02-24 15:50:40 +01:00
Andrey Antukh
4d627f8993 🐛 Fix incorrect invitation flow 2023-02-24 15:44:29 +01:00
Andrey Antukh
7771467aa0 🐛 Fix missing member-id field on invitation copy-link 2023-02-24 15:41:15 +01:00
Alejandro
0e97182ef0 Merge pull request #2977 from penpot/niwinz-invitations-1
 Add proper audit log for invitations
2023-02-24 10:57:13 +01:00
Andrey Antukh
f0c0e5e43a Add proper audit log for invitations 2023-02-24 10:28:07 +01:00
Alejandro
475b6ff6e0 Merge pull request #2969 from penpot/alotor-fix-redo-curve-tool
🐛 Fix problem with redo curve drawings
2023-02-22 11:25:40 +01:00
alonso.torres
a1f41c80a2 🐛 Fix problem with redo curve drawings 2023-02-22 10:43:51 +01:00
Eva Marco
4297b6fda8 Merge pull request #2968 from penpot/alotor-bug-width-fill
🐛 Fix problem with align center and width 100%
2023-02-21 16:25:08 +01:00
alonso.torres
28dce3cc8b 🐛 Fix problem with align center and width 100% 2023-02-21 15:44:13 +01:00
Andrey Antukh
3c650ae47e Merge branch 'main' into staging 2023-02-20 13:28:51 +01:00
Alejandro
1806200613 Merge pull request #2947 from penpot/alotor-performance-improvement
 Performance improvement
2023-02-16 09:38:05 +01:00
alonso.torres
ed22e2c6d1 Performance improvement 2023-02-15 15:17:50 +01:00
Alejandro
0487539b23 Merge pull request #2946 from penpot/alotor-bug-new-frame
🐛 Fix problem with new frame inside layout
2023-02-15 13:48:57 +01:00
alonso.torres
fd15ff940f 🐛 Fix problem with new frame inside layout 2023-02-15 13:38:03 +01:00
Alejandro
ece6193260 Merge pull request #2939 from penpot/palba-fix-undo-duplicate-with-alt
Fix duplicate with alt and undo only undo one step
2023-02-15 12:21:00 +01:00
Pablo Alba
813a188e24 🐛 Fix duplicate with alt and undo only undo one step 2023-02-15 12:20:47 +01:00
Alejandro
0f07def536 Merge pull request #2940 from penpot/alotor-layout-improvements
 Add space-evenly option
2023-02-15 12:08:36 +01:00
alonso.torres
490f5f19f1 Add space-evenly option 2023-02-15 12:08:22 +01:00
Alejandro
b3216000fd Merge pull request #2941 from penpot/alotor-fix-frame-opacity
🐛 Fix problem with opacity in frames
2023-02-15 11:57:10 +01:00
Alejandro
2ef3e4b325 Merge pull request #2944 from penpot/alotor-fix-unhandled-error
🐛 Fix crash when resizing frame
2023-02-15 11:49:17 +01:00
alonso.torres
70edd2c290 🐛 Fix crash when resizing frame 2023-02-15 09:59:28 +01:00
alonso.torres
02543b1a4f 🐛 Fix problem with opacity in frames 2023-02-14 17:54:51 +01:00
Alejandro
094556926e Merge pull request #2932 from penpot/eva-change-onboarding-images
💄 Update onboarding images with new style
2023-02-13 16:54:23 +01:00
Andrey Antukh
1ed3b3cf75 📎 Add missing restart policy to some containers
on default compose file
2023-02-10 14:07:12 +01:00
Eva
1637e82018 💄 Update onboarding images with new style 2023-02-10 13:52:53 +01:00
Andrey Antukh
c467d04d50 🐛 Fix permission issue on docker images 2023-02-10 13:37:33 +01:00
Andrey Antukh
8d19c067e8 🐛 Fix incorrect mountpoint on docker compose 2023-02-10 13:23:22 +01:00
Alejandro
a99fb7ada3 Merge pull request #2922 from penpot/palba-fix-middle-button-drags-guides
🐛 Fix middle button panning can drag guides
2023-02-09 14:27:28 +01:00
Alejandro
2f1d1a6c41 Merge pull request #2921 from penpot/eva-fix-invite-members-btn
🐛 Fix invite members text on modal button
2023-02-09 14:23:40 +01:00
Eva
7f963edf9e 🐛 Fix invite members text on modal button 2023-02-09 13:51:43 +01:00
Eva Marco
9c99d86e08 Merge pull request #2927 from penpot/alotor-fix-auto-size
Fix auto size
2023-02-09 13:51:03 +01:00
Eva
6a5bfdd7fb ❤️ Add thanks for ondrejkonec 2023-02-09 13:36:26 +01:00
Ondřej Konečný
a98ba72c12 added width property to avoid shrinking on icons 2023-02-09 13:33:06 +01:00
alonso.torres
ee42dd8b01 🐛 Fix layout on multiple selection 2023-02-09 11:18:37 +01:00
alonso.torres
da209b7507 🐛 Fix problem with auto sizes 2023-02-09 10:41:18 +01:00
Pablo Alba
d49e1f1641 🐛 Fix middle button panning can drag guides 2023-02-09 08:53:42 +01:00
Pablo Alba
8e35ad0f7f Merge pull request #2896 from penpot/eva-bugfixing-6
🐛 Fix paste a frame inside itself
2023-02-08 12:16:09 +01:00
Eva
be3a973d09 🐛 Fix paste a frame inside itself 2023-02-08 12:01:11 +01:00
Andrey Antukh
78aea0f24e 🐛 Fix incorrect props cleaning on auditlog 2023-02-08 10:35:57 +01:00
Andrey Antukh
6e1ce62aad Merge branch 'staging' 2023-02-07 17:06:42 +01:00
Alejandro
070ea135e5 Merge pull request #2919 from penpot/niwinz-docker-oidc-fixes
🐛 Docker & OIDC fixes
2023-02-07 16:56:22 +01:00
Andrey Antukh
5ae1fe5867 📎 Add nano editor to backend docker image 2023-02-07 16:50:58 +01:00
Andrey Antukh
eef2cba976 🐛 Fix incorrect registration flag handling on frontend
registration flag should not prevent include register on the
router because a registration process can be started from oidc
auth process
2023-02-07 16:50:52 +01:00
Andrey Antukh
1c4dcf1574 Add minor improvements to logging on docker images 2023-02-07 15:06:35 +01:00
Andrey Antukh
220b80799d Add more logging to OIDC providers 2023-02-07 14:49:12 +01:00
Alejandro Alonso
22b6d4241d 📎 Update version.txt file 2023-02-07 11:47:50 +01:00
Alejandro
fa02df7106 Merge pull request #2914 from penpot/alotor-small-fixes
Small fixes
2023-02-07 11:23:54 +01:00
Andrey Antukh
5d6462b2a7 🐛 Fix compatibility issues on docker upgrade path 2023-02-07 11:19:46 +01:00
Alejandro
3464842c1e Merge pull request #2917 from penpot/eva-bugfixing-7
🐛 Fix tooltips on left toolbar
2023-02-07 11:18:43 +01:00
Pablo Alba
d74af6ddc1 Revert "🐛 Fix line-height inconsistent"
This reverts commit 3974a4778a.
2023-02-07 11:18:01 +01:00
Alejandro
8cb33dc19c Merge pull request #2908 from penpot/niwinz-bugfix-oidc-autidiscover
🐛 Fix issue with oidc autodiscover
2023-02-07 11:16:00 +01:00
Eva
4912107fcc 🐛 Fix tooltips on left toolbar 2023-02-07 10:03:18 +01:00
alonso.torres
d5c7a6e547 🐛 Fix problem with auto-width and space-around 2023-02-07 00:17:16 +01:00
Andrey Antukh
f1085aadd1 🐛 Fix compatibility issues on docker upgrade path 2023-02-06 19:21:55 +01:00
alonso.torres
ca5b59f102 🐛 Fix sizing when moving shapes into/out of a layout 2023-02-06 17:50:59 +01:00
alonso.torres
a0898fbabd 🐛 Named redis volume 2023-02-06 17:00:42 +01:00
Andrey Antukh
aaf332ed18 🐛 Fix issue with oidc autodiscover 2023-02-06 14:20:57 +01:00
Christian Clauss
b05ca4bb82 🐛 Fix undefined name RuntimeException on manage.py script
Python defines [`RuntimeError`](https://docs.python.org/3.7/library/exceptions.html#RuntimeError)
but it does not define `RuntimeException` so a `NameError` will be raised when any of these lines
are executed.

% `python3 -c "RuntimeException('This is a test...')"`
```
Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'RuntimeException' is not defined
```

% `flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics`
```
./backend/scripts/manage.py:22:15: F821 undefined name 'RuntimeException'
        raise RuntimeException(f"invalid PREPL_URI: {PREPL_URI}")
              ^
./backend/scripts/manage.py:25:15: F821 undefined name 'RuntimeException'
        raise RuntimeException(f"invalid PREPL_URI: {PREPL_URI}")
              ^
./backend/scripts/manage.py:49:23: F821 undefined name 'RuntimeException'
                raise RuntimeException("unexpected response from PREPL")
                      ^
3     F821 undefined name 'RuntimeException'
3
```
2023-02-05 11:19:41 +01:00
Christian Clauss
b46b23b027 🐛 Fix undefined name RuntimeException on manage.py script
Python defines [`RuntimeError`](https://docs.python.org/3.7/library/exceptions.html#RuntimeError)
but it does not define `RuntimeException` so a `NameError` will be raised when any of these lines
are executed.

% `python3 -c "RuntimeException('This is a test...')"`
```
Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'RuntimeException' is not defined
```

% `flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics`
```
./backend/scripts/manage.py:22:15: F821 undefined name 'RuntimeException'
        raise RuntimeException(f"invalid PREPL_URI: {PREPL_URI}")
              ^
./backend/scripts/manage.py:25:15: F821 undefined name 'RuntimeException'
        raise RuntimeException(f"invalid PREPL_URI: {PREPL_URI}")
              ^
./backend/scripts/manage.py:49:23: F821 undefined name 'RuntimeException'
                raise RuntimeException("unexpected response from PREPL")
                      ^
3     F821 undefined name 'RuntimeException'
3
```
2023-02-05 11:19:28 +01:00
Mario Bašić
29c0190b7a 🐛 Add mailcatch to penpot network on docker compose
Without this the backend complains that it cannot connect to the smtp host (when using mailcatcher). The reason is because the mailcatcher is not on the same network as the backend application.
2023-02-05 11:16:34 +01:00
Mario Bašić
f1b09e763e 🐛 Add mailcatch to penpot network on docker compose
Without this the backend complains that it cannot connect to the smtp host (when using mailcatcher). The reason is because the mailcatcher is not on the same network as the backend application.
2023-02-05 11:16:07 +01:00
Alejandro
2e5e772392 Merge pull request #2900 from penpot/alotor-fix-space-around-bounds
🐛 Fix problem with space-around
2023-02-02 19:47:41 +01:00
alonso.torres
ecd4bb54c9 🐛 Fix problem with space-around 2023-02-02 19:31:02 +01:00
Alejandro Alonso
3cfc432c23 Merge remote-tracking branch 'origin/staging' 2023-02-02 18:07:20 +01:00
Alejandro
e426425cb5 Merge pull request #2898 from penpot/alotor-fix-safari-thumbnails
🐛 Temporary deactivate thumbnails in Safari
2023-02-02 17:37:25 +01:00
alonso.torres
3a0cc63fa7 🐛 Temporary deactivate thumbnails in Safari 2023-02-02 17:32:12 +01:00
Alejandro
88a8370e8d Merge pull request #2897 from penpot/alotor-pro-fixes
Fixes
2023-02-02 16:08:30 +01:00
alonso.torres
e8972dd802 🐛 Fix problem with thumbnail updating 2023-02-02 15:34:35 +01:00
alonso.torres
3e52bef6d4 🐛 Fix problem with multiple selection of layout items 2023-02-02 13:37:34 +01:00
alonso.torres
7c215dc11b 🐛 Align-items center/end weren't respected when layout was outside bounds 2023-02-02 13:37:34 +01:00
alonso.torres
48c3e3e00b 🐛 Fix problem with Safari canvas behavior 2023-02-02 13:37:34 +01:00
Alejandro
412dcae01a Merge pull request #2894 from penpot/fix-social-urls
🐛 Fix social links broken
2023-02-02 13:20:25 +01:00
Pablo Alba
cc5f245209 🐛 Fix social links broken 2023-02-02 13:13:24 +01:00
Alejandro
dc4aabe263 Merge pull request #2893 from penpot/palba-fix-can-move-shape-with-lens-zoom
🐛 Fix can move shape with lens zoom active
2023-02-02 13:12:34 +01:00
Pablo Alba
708a8ce27b 🐛 Fix can move shape with lens zoom active 2023-02-02 12:59:02 +01:00
Alejandro
7c1d9ce06f Merge pull request #2892 from penpot/alotor-fix-safari-problem
🐛 Fix problem with thumbnails in safari
2023-02-02 11:36:19 +01:00
Aitor
b0cbf09950 Merge pull request #2885 from penpot/eva-bugfixing-5
Some bug fixing
2023-02-02 11:33:25 +01:00
Aitor
f31bc7457f Merge branch 'staging' into eva-bugfixing-5 2023-02-02 11:31:49 +01:00
alonso.torres
e47ce3235e 🐛 Fix problem with thumbnails in safari 2023-02-02 11:31:10 +01:00
Alejandro
fe76e0fab6 Merge pull request #2884 from penpot/alotor-post-release-fixes
Post release fixes
2023-02-02 11:22:53 +01:00
Alejandro
297ba10e9d Merge pull request #2886 from penpot/hiru-dashboard-names
🐛 Fix weird numeration creating elements in dashboard
2023-02-02 10:51:26 +01:00
Andrés Moya
dd2321a37b 🐛 Fix weird numeration creating elements in dashboard 2023-02-02 10:19:36 +01:00
Eva
f98630a46b 🐛 Fix invitations input on team management and onboarding modal 2023-02-02 09:37:21 +01:00
Eva
82d6ba790c 🐛 Fix button spacing on delete account modal 2023-02-02 09:37:19 +01:00
Eva
575aec209c 🐛 Fix button spacing on delete acount modal 2023-02-02 09:37:00 +01:00
alonso.torres
00e265695c Change parent/children constraint for problematic configurations 2023-02-02 09:18:53 +01:00
alonso.torres
071ac0366c 🐛 Fix problem with max-size 0 2023-02-02 09:18:53 +01:00
alonso.torres
1a2a90f829 🐛 Fix problems with strange file 2023-02-02 09:18:53 +01:00
alonso.torres
028c084b22 🐛 Add limit to growth fill shapes to the bounds of the layout 2023-02-02 09:18:53 +01:00
alonso.torres
e7e80e99bd 🐛 Fix thumbnail not updating when changing pages 2023-02-02 09:18:53 +01:00
alonso.torres
70fa169d0d 🐛 Fix problem with selection on nested boards 2023-02-02 09:18:53 +01:00
Alejandro
6be83fc6d6 Merge pull request #2889 from penpot/palba-fix-commad-z-in-mac
🐛 Fix typing CMD+Z on MacOS turns the cursor into a Zoom cursor
2023-02-01 22:45:26 +01:00
Pablo Alba
1e9ece43d0 🐛 Fix typing CMD+Z on MacOS turns the cursor into a Zoom cursor 2023-02-01 21:56:30 +01:00
Alejandro
965c0d6fa2 Merge pull request #2888 from penpot/hiru-move-readme-images
💄 Move all README images to a subfolder
2023-02-01 20:40:14 +01:00
Andrés Moya
950d5dcc2f 💄 Move all README images to a subfolder 2023-02-01 19:31:12 +01:00
Andrey Antukh
43d034798c Merge branch 'staging' 2023-02-01 18:06:50 +01:00
Andrey Antukh
86712f977d 🐛 Skip unnecesary mutation events from audit log 2023-02-01 18:06:12 +01:00
Alejandro Alonso
707e6c2a33 Merge remote-tracking branch 'origin/staging' 2023-02-01 13:12:34 +01:00
Alejandro
3dfd87eee1 Merge pull request #2883 from penpot/palba-fix-components-groups-names-in-list-mode
🐛 Fix components groups items show the component name in list mode
2023-02-01 10:41:33 +01:00
Alejandro
037ba19e87 Merge pull request #2882 from penpot/niwinz-webhooks-fix
🐛 Fix incorrect state management on webhooks crud
2023-02-01 10:38:33 +01:00
Pablo Alba
cdbab2c098 🐛 Fix components groups items show the component name in list mode 2023-02-01 10:23:51 +01:00
Andrey Antukh
e8ea61ee78 🐛 Fix incorrect state management on webhooks crud 2023-02-01 10:15:25 +01:00
Andrey Antukh
7ab91f68af Merge branch 'staging' 2023-01-31 23:02:22 +01:00
Andrey Antukh
91ececa59e 🐛 Fix backend flags on docker compose sample file 2023-01-31 23:01:58 +01:00
Andrey Antukh
8758723200 Merge pull request #2874 from penpot/niwinz-hotfix-1
🐛 Fix docker images issues in the latest release
2023-01-31 23:00:30 +01:00
Alejandro Alonso
8a968dc081 🐛 Fix upload team image 2023-01-31 22:29:05 +01:00
Andrey Antukh
f8cb505196 🐛 Fix arguments handling on docker/images/build.sh script 2023-01-31 19:48:41 +01:00
Andrey Antukh
14e3439cae 🔥 Remove admin mention from compose, still not working correctly 2023-01-31 19:08:38 +01:00
Andrey Antukh
7dd55c7f9d Revert JRE/JLINK changes from Dockerfile.backend file 2023-01-31 18:50:53 +01:00
Alejandro Alonso
e8e3398a74 🐛 Fix default nginx configuration for docker images 2023-01-31 17:42:28 +01:00
Alejandro Alonso
95cad24c18 Merge remote-tracking branch 'origin/staging' 2023-01-31 13:57:30 +01:00
Alejandro
d31138db72 Merge pull request #2868 from penpot/alotor-fix-layout-problems
Fix layout problems
2023-01-31 12:19:20 +01:00
Alejandro Alonso
2c5f35e192 🐛 Fix penpot.app urls 2023-01-31 12:10:06 +01:00
Alejandro Alonso
5a8f8ba349 🐛 Fix create team and invite 2023-01-31 12:09:13 +01:00
alonso.torres
3fe5cd3752 🐛 Fix problem when resizing layout to zero size 2023-01-31 12:07:17 +01:00
alonso.torres
da60911d81 🐛 Create new layouts without clip content 2023-01-31 12:03:23 +01:00
Alejandro
f4f1f80050 Merge pull request #2864 from penpot/alotor-fix-hug-compacting
🐛 Fix problem with size 100% and auto parent
2023-01-31 10:29:06 +01:00
alonso.torres
18445ea5f4 🐛 Fix problem with size 100% and auto parent 2023-01-31 09:40:01 +01:00
Alejandro
2d28e02742 Merge pull request #2865 from penpot/superalex-fix-onboarding-poll
🐛 Fix onboarding poll
2023-01-31 09:30:32 +01:00
Alejandro Alonso
b0b963fb7c 🐛 Fix onboarding poll 2023-01-31 09:24:50 +01:00
Alejandro Alonso
5cfee13956 🐛 Remove beta word 2023-01-30 17:35:18 +01:00
Alejandro
7271e98df3 Merge pull request #2862 from penpot/niwinz-multipart-encodign-bugfix
⬆️ Update yetti to v9.12
2023-01-30 15:38:44 +01:00
Andrey Antukh
f0386ef7b0 ⬆️ Update yetti to v9.12
Fixes encoding bug on multipart uploads
2023-01-30 15:29:53 +01:00
Alejandro
185cabb2fa Merge pull request #2861 from penpot/alotor-regenerate-empty-thumbnails
🐛 Try to refresh thumbnails on empty stored data in back
2023-01-30 14:05:57 +01:00
alonso.torres
3a19223264 🐛 Try to refresh thumbnails on empty stored data in back 2023-01-30 13:45:56 +01:00
Alejandro Alonso
2c38f31aa9 🐛 Fix clean archived auditlog 2023-01-30 13:11:50 +01:00
Alejandro
a1dcb11261 Merge pull request #2860 from penpot/palba-fix-paste-texts-typos
🐛 Fix copy paste texts with typography assets linked
2023-01-30 12:32:32 +01:00
Pablo Alba
9f8d86a80e 🐛 Fix copy paste texts with typography assets linked 2023-01-30 12:26:31 +01:00
Alejandro Alonso
c59fc87fc4 🐛 Fix styling info at the libraries modal 2023-01-30 12:22:50 +01:00
Alejandro Alonso
3421e6ef57 🐛 Fix viewer misalignment when expanding code tab 2023-01-30 11:54:18 +01:00
Alejandro Alonso
40349c8ece 🐛 Fix line-height inconsistent 2023-01-30 11:44:20 +01:00
Alejandro
5a53376b01 Merge pull request #2859 from penpot/alotor-fix-code-generator-hangs
🐛 Fix problem with code view hanging
2023-01-30 11:18:55 +01:00
Alejandro
d4dfdaff57 Merge pull request #2854 from penpot/palba-fix-incorrect-state-after-undo-page-creation
🐛 Fix incorrect state after undo page creation
2023-01-30 11:14:17 +01:00
Pablo Alba
c7f87d0f26 🐛 Fix incorrect state after undo page creation 2023-01-30 11:13:59 +01:00
alonso.torres
c7954990f0 🐛 Fix problem with code view hanging 2023-01-30 11:13:05 +01:00
Alejandro
fe118819ce Merge pull request #2858 from penpot/info-release-1.17
Info release 1.17
2023-01-30 10:42:39 +01:00
Alejandro
073ec9ea2b Merge pull request #2856 from penpot/alotor-fix-transform-precision
🐛 Fix problem with transform when a coordinate was very close to …
2023-01-30 09:57:50 +01:00
Alejandro
f85a731969 Merge pull request #2855 from penpot/alotor-migration-fix-frame-id
🐛 Add migration to fix problem with frame-id
2023-01-30 09:39:18 +01:00
Alejandro
a3a88d7a0a Merge pull request #2850 from penpot/alotor-fix-frame-overlay
🐛 Fix frame overlays in workspace
2023-01-30 07:19:08 +01:00
Alejandro
1660dd634e Merge pull request #2857 from penpot/palba-fix-resend-invitation-doesnt-reset-expiration
🐛 Fix resend invitation doesn't reset the expiration date
2023-01-30 06:55:47 +01:00
Pablo Alba
6e698110d6 🐛 Fix resend invitation doesn't reset the expiration date 2023-01-27 16:56:19 +01:00
alonso.torres
951c67a2d5 🐛 Fix problem with transform when a coordinate was very close to zero 2023-01-27 16:09:44 +01:00
Alejandro
50b7337b8c Merge pull request #2852 from penpot/eva-bugfixing-4
Eva bugfixing 4
2023-01-27 15:07:20 +01:00
Eva
15e62ff649 🐛 Remove copy all button in fills and strokes 2023-01-27 14:58:53 +01:00
Eva
e7ddd6055f ♻️ Improve a case 2023-01-27 14:53:32 +01:00
Eva
aa3438f800 ♻️ Improve some functions 2023-01-27 14:53:31 +01:00
Eva
a45380a91c 🐛 Fix typo 2023-01-27 14:53:08 +01:00
alonso.torres
86b68aeca4 🐛 Add migration to fix problem with frame-id 2023-01-27 14:43:55 +01:00
alonso.torres
d69d392362 🐛 Fix duplicate frames 2023-01-27 13:41:20 +01:00
Andrés Moya
506c2b8d7b 🔧 Add script to rename some layout attrs in existing files 2023-01-27 13:11:46 +01:00
Eva
b463ebc17b 🐛 Fix layout spec with proper names 2023-01-27 13:11:46 +01:00
alonso.torres
f90fda2c90 🐛 Fix frame overlays in workspace 2023-01-27 11:26:35 +01:00
Eva Marco
87c5aa71a3 Merge pull request #2847 from penpot/superalex-text-weight-inspect-code
🐛 Fix text weight on inspect code
2023-01-27 10:09:11 +01:00
Alejandro Alonso
4f82f6bde4 🐛 Fix text weight on inspect code 2023-01-27 10:00:54 +01:00
Alejandro
545b3860b4 Merge pull request #2844 from penpot/alotor-fix-transparent-thumbnails
🐛 Fix problem with transparent frame thumbnails
2023-01-27 09:29:26 +01:00
alonso.torres
d4921c8eb9 🐛 Fix problem with transparent frame thumbnails 2023-01-27 09:27:18 +01:00
Alejandro Alonso
18652d0b6f 🐛 Fix outline corner radius 2023-01-27 09:26:10 +01:00
Alejandro Alonso
2dbeda1d8f 🐛 Fix outline corner radius 2023-01-27 09:14:11 +01:00
elhombretecla
9422d1e9e2 Fix wording 2023-01-27 07:45:31 +01:00
Alejandro
e0441bc16a Merge pull request #2845 from penpot/palba-text-shortcuts-multi-layer
 Apply text format shortcuts to several layers
2023-01-27 07:03:11 +01:00
Pablo Alba
d7d6166232 Apply text format shortcuts to several layers (even inside groups) 2023-01-26 18:46:16 +01:00
Alejandro
6fd6205634 Merge pull request #2841 from penpot/alotor-polishing-11
Polishing
2023-01-26 16:27:05 +01:00
Eva
7cd6f5ba70 🐛 User icons are not centered 2023-01-26 15:56:21 +01:00
Eva
9cc3cceb06 💄 Change layout flex by flex layout text 2023-01-26 15:56:21 +01:00
Eva
6f6bcd2f7e 💄 Improve warning message css in fonts 2023-01-26 15:56:21 +01:00
Alejandro Alonso
f9f3b3951f 🐛 Fix external borders not considered for thumbnails 2023-01-26 15:43:11 +01:00
Andrés Moya
22ded62000 🐛 Fix paths not flagged as modified when resized 2023-01-26 15:29:20 +01:00
Alejandro Alonso
71d104f768 🐛 Fix fills and strokes on inspect code 2023-01-26 15:18:26 +01:00
alonso.torres
5a36cbceb7 Enter to select children allow for multiselection 2023-01-26 14:27:14 +01:00
Pablo Alba
f2033c46f3 🐛 Fix ctrl+z shows zoom icon 2023-01-26 14:07:21 +01:00
alonso.torres
6b225a10b5 🐛 Fix problem with align and flex layout 2023-01-26 13:44:40 +01:00
alonso.torres
38fe6e856a 🐛 Fix problems with content between/around and auto-width 2023-01-26 12:55:40 +01:00
alonso.torres
1984109436 🐛 Fix problem with change frame groups 2023-01-26 12:55:40 +01:00
alonso.torres
9f9d9277a6 🐛 Fix problem with space-around and auto-width/height 2023-01-26 12:55:40 +01:00
alonso.torres
e041f93680 🐛 Fix space-between preserves gap distances 2023-01-26 12:55:40 +01:00
alonso.torres
2d779a4414 🐛 Fix problem with empty text rendering 2023-01-26 12:55:40 +01:00
Alejandro
21fc9289a6 Merge pull request #2835 from penpot/palba-fix-multiplayer-shadow
🐛 Fix multiuser - "Shadow" element is not updating immediately
2023-01-26 07:33:47 +01:00
Pablo Alba
b40ea3fb2a 🐛 Fix multiuser - "Shadow" element is not updating immediately 2023-01-25 17:48:40 +01:00
Pablo Alba
444e9a3081 Merge pull request #2833 from penpot/hiru-fix-unwanted-popup
🐛 Fix unneeded popup when updating local components
2023-01-25 16:51:29 +01:00
Andrés Moya
f93d305545 🐛 Fix unneeded popup when updating local components 2023-01-25 16:50:41 +01:00
Pablo Alba
09a91c87be Merge pull request #2834 from penpot/superalex-fix-ctrl-c-in-inspect-code
🐛 Fix ctrl+c on inspect code
2023-01-25 16:23:38 +01:00
Alejandro Alonso
e71d569cda 🐛 Fix ctrl+c on inspect code 2023-01-25 16:11:58 +01:00
alonso.torres
a56a9868dc 🐛 Fix error on thumbnail generation 2023-01-25 13:20:06 +01:00
Pablo Alba
a09198b46e 🐛 Fix wrong pop on setup shortcuts 2023-01-25 13:05:03 +01:00
Alejandro
c7e9c658cd Merge pull request #2827 from penpot/eva-flex-bugfixing-2
🐛 Fix missing flex props on code generation
2023-01-25 11:46:34 +01:00
Alejandro
58d7bc5c14 Merge pull request #2831 from penpot/azazeln28-fix-viewer-all-mouse-wheel-issues
Fix all viewer mouse wheel issues
2023-01-25 11:38:31 +01:00
Alejandro
e939db927e Merge pull request #2825 from penpot/palba-text-formatting-shortcuts
🎉 Shortcuts for text formatting
2023-01-25 11:33:17 +01:00
Pablo Alba
efe50479de 🎉 Shortcuts for text formatting 2023-01-25 11:32:59 +01:00
Eva
ea1b3bd058 🐛 Fix missing flex props on code generation 2023-01-25 08:19:33 +01:00
Aitor
4751d7d385 🐛 Fix all viewer mouse wheel issues 2023-01-24 17:44:15 +01:00
Hosted Weblate
bc88e30efa Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2023-01-24 16:17:30 +01:00
Andrés Moya
9623dbfbd6 📚 Validate translations 2023-01-24 16:17:30 +01:00
Andrés Moya
f177de6661 Merge remote-tracking branch 'weblate/develop' into translations 2023-01-24 16:17:30 +01:00
Alejandro
43043e2dc1 Merge pull request #2830 from penpot/alotor-polishing-10
Small bugfixes
2023-01-24 15:53:16 +01:00
alonso.torres
05d21d7d07 🐛 Fix reorder layers with keys not refreshing layout 2023-01-24 15:30:20 +01:00
alonso.torres
02aab37ee7 🐛 Fix bold typefaces in thumbnails 2023-01-24 15:08:58 +01:00
elhombretecla
d3aee1afa3 Add new images 2023-01-24 15:01:18 +01:00
elhombretecla
ac361cdb36 Adds new 1.17 onboarding info 2023-01-24 14:53:12 +01:00
Aitor
7ac6f49c08 Merge pull request #2808 from penpot/superalex-fix-font-vertical-metrics
🐛 Fix font vertical metrics
2023-01-24 14:26:14 +01:00
Alejandro Alonso
d3e11433bf 🐛 Fix font vertical metrics 2023-01-24 14:21:16 +01:00
Pablo Alba
771d1d9194 🐛 Fix double click and lens zoom 2023-01-24 14:19:14 +01:00
Alejandro
4a3a53182b Merge pull request #2797 from penpot/palba-lens-tool
🎉 Zoom lense tool
2023-01-24 13:34:23 +01:00
Pablo Alba
c25cf043fa 🎉 Zoom lense tool 2023-01-24 13:34:04 +01:00
elhombretecla
7440d38c94 Add new login image 2023-01-24 13:19:16 +01:00
Alejandro
a8c0d437ce Merge pull request #2828 from penpot/superalex-update-changes
 Update changes
2023-01-24 12:55:35 +01:00
Alejandro
8d683beae4 Merge pull request #2829 from penpot/fix-safari-thumbnails
🐛 Fix thumbnails for Safari browsers
2023-01-24 12:36:15 +01:00
Alejandro Alonso
4007d8713c Update changes 2023-01-24 12:26:17 +01:00
alonso.torres
ead64a1820 🐛 Fix thumbnails for Safari browsers 2023-01-24 11:56:59 +01:00
Alejandro
88e2a5c56e Merge pull request #2826 from penpot/alotor-thumbnails-performance
Thumbnails performance
2023-01-24 09:59:20 +01:00
alonso.torres
9782d9077f Improved and fixed thumbnail generation 2023-01-24 09:44:56 +01:00
Alejandro
b4c4511d9d Merge pull request #2823 from penpot/alotor-polishing-9
Improved thumbnails behavior
2023-01-23 17:18:53 +01:00
alonso.torres
316b3d4539 🐛 Try to remove cases when the thumbnail could be empty 2023-01-23 14:07:51 +01:00
alonso.torres
1c54e9fa4d Allow debug in for release build 2023-01-23 14:03:28 +01:00
alonso.torres
3d064b804b Improve performance on multiple options 2023-01-23 14:03:02 +01:00
alonso.torres
088a8af345 🐛 Fix problem with empty styles 2023-01-23 11:05:28 +01:00
Andrés Moya
77cd645e25 🔧 Update docker-compose without needing config file 2023-01-23 10:34:00 +01:00
Alejandro
8ee7915c1d Merge pull request #2820 from penpot/eva-typos
🐛 Fix some typos on translation
2023-01-23 09:54:24 +01:00
Eva
ea8755ce24 🐛 Fix some typos on translation 2023-01-23 09:41:16 +01:00
Prithvi Tharun
381aae735d Improves empty state content
Reads in neutral content without alarming the users

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2023-01-23 09:38:52 +01:00
Prithvi Tharun
a4826eddcd Improves empty state content
Better instructions explaining multiple ways people can add files to library. Also, reads in neutral tone withour blaming and alarming the users

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2023-01-23 09:38:52 +01:00
Prithvi Tharun
31e2fff4d4 Improves tooltip
X and Y position tooltips updated to communicate more info

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2023-01-23 09:38:52 +01:00
Alejandro
021c714867 Merge pull request #2817 from penpot/niwinz-docker-and-admin
 Improvements on PREPL
2023-01-23 09:12:06 +01:00
Andrey Antukh
231ac00934 🎉 Add manage.py script for docker images 2023-01-20 17:14:32 +01:00
Andrey Antukh
578ff944a6 📎 Use name instead of pr-str of error type 2023-01-20 16:52:58 +01:00
Andrey Antukh
bf8a514871 Add more flexible prepl api for external tools 2023-01-20 16:52:58 +01:00
Andrey Antukh
8d60b3fc3e 🐛 Add missing hint on duplicate profile exception 2023-01-20 16:52:58 +01:00
Andrey Antukh
8468e7af24 🎉 Add admin example service to default docker compose file 2023-01-20 16:52:58 +01:00
Alejandro
50eee3f597 Merge pull request #2816 from penpot/eva-bugfix-2
Eva polishing
2023-01-20 15:50:59 +01:00
Eva Marco
b9b3fcdb6a Merge pull request #2813 from penpot/superalex-fix-svg-import
🐛 Fix svg import
2023-01-20 14:36:59 +01:00
Eva
f0d74ab63e 🐛 Fix hsla information in viewer 2023-01-20 14:30:17 +01:00
Alejandro
dad5d953ce Merge pull request #2814 from penpot/alotor-polishing-8
Fix some mask issues
2023-01-20 14:04:45 +01:00
Alejandro
f6058aa71e Merge pull request #2815 from penpot/alotor-debug-names
 Add debug tool to display name and id for shapes
2023-01-20 13:57:18 +01:00
alonso.torres
85d56e6057 Add debug tool to display name and id for shapes 2023-01-20 13:47:40 +01:00
Eva
c353d3703b Add some accessibility on shortcuts panel 2023-01-20 12:39:07 +01:00
Andrés Moya
9367788898 Small improvement 2023-01-20 12:04:47 +01:00
Andrés Moya
2b978777d7 🐛 Fix import/export components with boards inside 2023-01-20 12:04:47 +01:00
Eva
2a30c23334 🐛 Fix hidden shapes inside hidden group 2023-01-20 11:15:14 +01:00
Alejandro Alonso
2f188e7fb4 🐛 Fix regression with library svgs 2023-01-20 11:01:22 +01:00
alonso.torres
0743b07667 🐛 Fix problem with mask disappearing in root frame 2023-01-20 10:56:55 +01:00
alonso.torres
f38197b227 Remove restriction to move mask layers 2023-01-20 10:56:55 +01:00
alonso.torres
bc9be7846a 🐛 Fix problem with empty mask on layout 2023-01-20 10:56:55 +01:00
Eva Marco
28114b166c Merge pull request #2812 from penpot/hiru-avoid-bad-nested-component
Avoid creating component with shapes that belong to other one
2023-01-20 09:22:25 +01:00
Alejandro Alonso
be74cd2c7b 🐛 Fix import svgs with currentColor as stroke fill 2023-01-20 09:17:04 +01:00
Eva Marco
b329de6487 Merge pull request #2807 from penpot/hiru-fix-layout-sync
Fix layout attributes sync in components
2023-01-20 09:06:36 +01:00
Alejandro Alonso
9c66998530 🐛 Fix svg import 2023-01-20 07:35:21 +01:00
Eva
8b377ac556 💄 Add entry on changes 2023-01-19 19:33:00 +01:00
Andrés Moya
8c6f07ab65 🐛 Fix right sidebar component options menu 2023-01-19 16:54:30 +01:00
Andrés Moya
dc89610d07 🐛 Avoid creating component with shapes that belong to other one 2023-01-19 16:40:27 +01:00
Eva
40195a4f52 🐛 Fix index on layout created over group 2023-01-19 16:16:22 +01:00
Eva
6a257503ae 🐛 Fix create layout over mask destroy mask 2023-01-19 16:16:22 +01:00
Eva Marco
a3e583d745 Merge pull request #2809 from penpot/hiru-fix-asset-with-board
🐛 Fix display of components with frame root in assets panel
2023-01-19 15:47:52 +01:00
alonso.torres
685a071e87 🐛 Fix rounding error with matrices 2023-01-19 15:47:25 +01:00
Andrey Antukh
73658c47f3 🐛 Fix issues on rlimit module 2023-01-19 15:41:12 +01:00
Andrey Antukh
d98fd76032 🎉 Add namespace with a set of helpers for access throught the BREPL 2023-01-19 15:41:12 +01:00
Andrey Antukh
2fef3dc881 🎉 Add prepl support
And rename the current repl to urepl (user-repl).
2023-01-19 15:41:12 +01:00
Eva Marco
a1a0444cc7 Merge pull request #2806 from penpot/alotor-polishing-7
Changes to paths and fixed reorder
2023-01-19 14:42:51 +01:00
Andrés Moya
792c17fe46 🐛 Fix display of components with frame root in assets panel 2023-01-19 14:18:55 +01:00
Andrés Moya
77d71abb5d 🐛 Sync correctly all layout attributes 2023-01-19 13:44:28 +01:00
alonso.torres
75d6e21af8 Show tools on path creation. Change snap while drawing 2023-01-19 13:39:21 +01:00
alonso.torres
0632111e96 🐛 Fix reorder layers with keys 2023-01-19 13:39:21 +01:00
Eva Marco
fe77ef4438 Merge pull request #2802 from penpot/alotor-polishing-6
Polishing after confluence test
2023-01-19 12:55:59 +01:00
alonso.torres
e7ac7ff7fb 🐛 Fix problem with disappearing titles 2023-01-19 11:51:53 +01:00
alonso.torres
d78ad30e23 Enter on containers selects children 2023-01-19 11:51:53 +01:00
alonso.torres
4b5caf5fb9 Shift+move ignores snap-pixel on the axis moving 2023-01-19 11:51:53 +01:00
alonso.torres
4e1eb2d6e9 🐛 Fix problem when flipping a mask 2023-01-19 11:51:53 +01:00
alonso.torres
ab7683f1e3 Improved text layout handling 2023-01-19 11:51:53 +01:00
alonso.torres
89371e10d1 🐛 Fix problem moving shapes inside a group inside a layout 2023-01-19 11:51:53 +01:00
alonso.torres
9fd6c65d93 🐛 Fix problem with empty paths 2023-01-19 11:51:53 +01:00
Eva
1f9c89fb32 🐛 Fix add svg did not update layout 2023-01-19 11:31:46 +01:00
Eva
61e83d7e01 🐛 Add selected colors on rightbar when selecting a board 2023-01-19 11:31:46 +01:00
Eva
a1a3d09998 🐛 Remove numbers in layer creation 2023-01-19 11:31:46 +01:00
Eva
de7a1d34c0 🐛 Fix board preset size selector css 2023-01-19 11:31:46 +01:00
Eva
f93d0e1c4d 🐛 Fix snap to pixel on path creation and edit 2023-01-19 11:31:46 +01:00
Eva Marco
c5d8d77070 Merge pull request #2799 from penpot/alotor-polishing-5
Polishing
2023-01-18 11:38:40 +01:00
alonso.torres
c18d3c66a8 Changes to snap to pixel 2023-01-18 11:04:24 +01:00
alonso.torres
0d96b5b798 🐛 Fix problems with mask elements modifiers when moving child 2023-01-18 11:04:24 +01:00
alonso.torres
24f45fafbf 🐛 Fix problem with disappearing frame titles 2023-01-18 11:04:13 +01:00
Alejandro
ca8df3a8d8 Merge pull request #2788 from penpot/niwinz-bugfix-binfile-import-storage-features
🐛 Properly handle storage features on binfile import
2023-01-18 09:52:51 +01:00
Eva
d14f4c5c4a 🐛 Fix open color palette from colorpicker 2023-01-17 23:28:02 +01:00
Eva
f6ff80a3d4 🐛 Fix Apply library colors to gradient points 2023-01-17 23:28:02 +01:00
Eva
b2d8f807f9 🐛 Fix alignment inside dropdown 2023-01-17 23:28:02 +01:00
Eva
03b3b441b5 🐛 Fix create mask inside layout 2023-01-17 23:28:02 +01:00
Andrey Antukh
523539e403 🐛 Properly handle storage features on binfile import 2023-01-17 10:14:30 +01:00
Alejandro
3280a6853e Merge pull request #2784 from penpot/palba-fix-undo-in-multiplayer
🐛 Fix undo corner case in multiplayer
2023-01-17 09:32:52 +01:00
Eva
fb060cb806 🐛 Fix padding values and add tooltip 2023-01-16 18:24:48 +01:00
Eva
8892cebb6f 🐛 Fix create layout flex around component 2023-01-16 18:24:48 +01:00
Eva
6fb97e54a9 🐛 Fix context menu when two elements where selected 2023-01-16 18:24:48 +01:00
Eva Marco
1c3470ca53 Merge pull request #2785 from penpot/azazeln28-fix-mousewheel-on-viewer-inspector
🐛 Fix mouse wheel on viewer inspector
2023-01-16 18:00:52 +01:00
alonso.torres
0ae42be851 🐛 Fix pipeline for thumbnails 2023-01-16 17:42:17 +01:00
alonso.torres
ff6f0b2744 🐛 Fix debugging util 2023-01-16 17:33:28 +01:00
Aitor
a3a2ab1ecd 🐛 Fix mouse wheel on viewer inspector 2023-01-16 17:30:18 +01:00
Eva Marco
01ba68fd6f Merge pull request #2786 from penpot/alotor-fix-text-loop
Fix problem with text hanging the application
2023-01-16 17:14:01 +01:00
alonso.torres
1ab669cc7b 🐛 Fix problems with texts looping 2023-01-16 17:05:34 +01:00
Pablo Alba
ab421ac3f9 🐛 Fix undo corner case in multiplayer 2023-01-16 11:43:42 +01:00
Pablo Alba
0faa0b21a4 Merge pull request #2776 from penpot/superalex-justify-text-is-stretched
🐛 Fix justify text is stretched
2023-01-16 07:09:24 +01:00
Alejandro Alonso
4ca6a89e6f 🐛 Fix justify text is stretched 2023-01-13 16:55:00 +01:00
Eva Marco
ab5fd68689 Merge pull request #2777 from penpot/alotor-polishing-4
Polishing
2023-01-13 14:58:38 +01:00
Alejandro
275eb993ce Merge pull request #2775 from penpot/niwinz-bugfixes-1
🐛 Several backend bugfixes
2023-01-13 14:33:46 +01:00
Alejandro
88143cfb8b Merge pull request #2778 from penpot/palba-filter-gfonts-variants
🐛 Filter gfonts variants for figma exporter plugin
2023-01-13 14:33:32 +01:00
alonso.torres
5f0f3abeae 🐛 Fix problem when dropping indices in flipped frames 2023-01-13 14:23:11 +01:00
alonso.torres
b203c87dbb 🐛 Fix problem with rotated texts inside flex layout 2023-01-13 14:23:11 +01:00
alonso.torres
7a796bc83f 🐛 Fix problem with thumbnails when duplicating artboards 2023-01-13 14:23:11 +01:00
alonso.torres
196e193281 🐛 Fix error message when the thumbnail fails to be added 2023-01-13 14:23:11 +01:00
alonso.torres
d0a15cda96 🐛 Fix issue when mirroring artboard 2023-01-13 14:23:11 +01:00
Pablo Alba
c3733ed2e1 🐛 Filter gfonts variants for figma exporter plugin 2023-01-13 14:19:57 +01:00
Pablo Alba
379623d629 Merge pull request #2773 from penpot/azazeln28-fix-library-list-spacing
🐛 Fix library list spacing
2023-01-13 13:32:13 +01:00
Pablo Alba
cb2553a8ca Merge pull request #2772 from penpot/azazeln28-fix-cannot-move-vertical-scroll-in-viewer
🐛 Fixes vertical scroll in viewer (code mode)
2023-01-13 13:30:10 +01:00
Aitor
1b7ea6ed53 Adds method to-fixed to math
Changes matrix/toString to use `to-fixed`
2023-01-13 11:43:50 +01:00
Eva
57a569a07a 🐛 Fix alignment on justify content space between 2023-01-13 11:43:05 +01:00
Andrey Antukh
a5006b1687 🐛 Remove www-form encoding from webhooks
It is broken by design, so we just do not support it
2023-01-13 10:34:56 +01:00
Aitor
24dc40a1b0 🐛 Fix library list spacing 2023-01-13 10:34:02 +01:00
Andrey Antukh
b4fc39f73c 📎 Disable quotes by default 2023-01-13 10:23:00 +01:00
Andrey Antukh
095dc2ad11 Do not merge path params into params
makes conflict with possible params coming from user
2023-01-13 10:19:39 +01:00
Andrey Antukh
fcbbe8e5c7 🐛 Fix incorrect logging context setup 2023-01-13 10:19:39 +01:00
Andrey Antukh
bafe3ec087 Revert some changes related to admin that are no longer necessary 2023-01-13 10:19:39 +01:00
Aitor
5d44d75465 🐛 Fixes vertical scroll in viewer (code mode) 2023-01-12 16:57:15 +01:00
Alejandro
44102050ee Merge pull request #2768 from penpot/palba-more-undo-tuning
🐛 Fix undo ungroup (shift+g) scrambles positions
2023-01-12 13:31:59 +01:00
Eva Marco
cae436f365 Merge pull request #2766 from penpot/alotor-polishing-3
Polishing
2023-01-12 13:24:04 +01:00
alonso.torres
e6d80e34b9 Drawing shapes over flex layout checks its drop index 2023-01-12 13:18:38 +01:00
Alejandro
fbec07bd48 Merge pull request #2767 from penpot/azazeln28-fix-ignore-file-library-sync-status
🐛 Fix ignore file library sync status
2023-01-12 13:13:54 +01:00
Pablo Alba
a555028ee2 🐛 Fix undo ungroup (shift+g) scrambles positions 2023-01-12 12:54:07 +01:00
Aitor
d91e8c349e 🐛 Fix ignore file library sync status
- Adds missing `update` of `:features` using `db/decode-pgarray`
2023-01-12 12:38:26 +01:00
alonso.torres
abe26007d7 🐛 Fix problem with measures for frames 2023-01-12 12:35:20 +01:00
alonso.torres
2da421bb7a 🐛 Fix error on path editing 2023-01-12 12:35:20 +01:00
alonso.torres
7d48b86e46 ❇️ Debugging panel for development 2023-01-12 12:35:20 +01:00
alonso.torres
28663b5ff6 Improved performance of svg paths 2023-01-12 12:29:02 +01:00
Alejandro Alonso
651d4f794b 🐛 Fix copy paste line break 2023-01-12 12:28:05 +01:00
alonso.torres
58aa6b3666 🐛 Fix problem with gap-row/gap-column 2023-01-12 12:17:26 +01:00
Eva
131c2f331e 🐛 Fix gap 2023-01-12 12:17:26 +01:00
Eva
8df861faaa 🐛 Fix some undo while actions in flex 2023-01-12 12:17:26 +01:00
Eva
4f81f9636a ♻️ Change uuid/next for js/Symbol on undo-id for transactions 2023-01-12 12:17:26 +01:00
Eva
31dfdf51c9 🐛 Fix some errors on flex layout 2023-01-12 12:17:26 +01:00
Pablo Alba
acf51ea744 💄 Use css color variable instead of fixed value 2023-01-12 09:21:57 +01:00
Pablo Alba
a54f5484e8 🎉 Added a tag to mark read only mode (now on pages) 2023-01-11 17:25:49 +01:00
Pablo Alba
3a8486f4b0 🐛 Fix share prototype styles on hover: color and tooltip 2023-01-11 17:16:43 +01:00
Pablo Alba
43c3d67521 🐛 Fix max height in library dialog 2023-01-11 17:10:56 +01:00
Aitor Moreno
4b2d82e100 :fix: react warning duplicate keys 2023-01-11 15:10:53 +01:00
Aitor Moreno
f2fd380979 Improves matrix serialization 2023-01-11 15:03:18 +01:00
Alejandro
984187037c Merge pull request #2757 from penpot/palba-fix-expanded-code
🐛 Fix switching tabs produces strange behaviour when "expanded" v…
2023-01-11 12:55:33 +01:00
Alejandro
173e5da98e Merge pull request #2759 from penpot/palba-read-only-tag
🎉 Added a tag to mark read only mode
2023-01-11 12:53:10 +01:00
Pablo Alba
2ab3ed9ab4 🎉 Added a tag to mark read only mode 2023-01-11 12:06:42 +01:00
Aitor
74e4273549 Merge pull request #2754 from penpot/superalex-fix-move-boards-with-comments
🐛 Fix move boards with comments
2023-01-11 10:28:04 +01:00
Pablo Alba
12392a4038 🐛 Fix switching tabs produces strange behaviour when "expanded" view mode was used for Code 2023-01-10 17:19:45 +01:00
Pablo Alba
987b7f44f4 🐛 Fix layer orders messed up on move, group, reparent and undo 2023-01-10 16:45:08 +01:00
Alejandro
3480d6979b Merge pull request #2755 from penpot/eva-fix-icon
🐛 Fix pin icon in project
2023-01-10 15:55:23 +01:00
Eva
9ca1efc128 🐛 Fix pin icon in project 2023-01-10 14:26:52 +01:00
Alejandro Alonso
81a95d362c 🐛 Fix move boards with comments 2023-01-10 13:55:41 +01:00
Eva Marco
a7dfda515b Merge pull request #2753 from penpot/alotor-polishing-2
Fix problems with text positons
2023-01-10 11:26:49 +01:00
alonso.torres
b5c1199f4d 🐛 Fix problem with texts randomly moving 2023-01-10 11:02:40 +01:00
alonso.torres
4aa8baa129 Add debug history overlay 2023-01-10 11:02:40 +01:00
Alejandro
553f2f5576 Merge pull request #2748 from penpot/eva-workspace-visual-changes
Bugfixing
2023-01-10 10:35:32 +01:00
Alejandro
b132837432 Merge pull request #2743 from penpot/niwinz-enhancements-2
🐛 Bugfixes
2023-01-10 09:56:45 +01:00
Eva
36bc276d93 🐛 Fix outline in color type selector on view mode 2023-01-10 09:40:24 +01:00
Eva Marco
35aa391129 Merge pull request #2749 from penpot/hiru-fix-ghost-sync
🐛 Fix ghost shapes after sync groups in components
2023-01-09 16:20:20 +01:00
Eva
2c2755b35e 🐛 Fix allow names selection in dashboard 2023-01-09 16:06:00 +01:00
Andrés Moya
bedaef961b 🐛 Fix ghost shapes after sync groups in components 2023-01-09 14:21:29 +01:00
Eva
fe7f4004f1 🐛 Fix scape key to clear focus on input 2023-01-09 13:01:15 +01:00
Eva
eef42acf79 🐛 Fix component list on update 2023-01-09 13:01:15 +01:00
Eva
937713311e 🐛 Fix some visual errors 2023-01-09 13:01:15 +01:00
Alejandro
94fc067286 Merge pull request #2744 from penpot/alotor-fix-svg-upload
🐛 Fix problem uploading svg with style tag
2023-01-09 12:43:32 +01:00
Alejandro
ae6ea7744e Merge pull request #2738 from penpot/alotor-polishing
Polishing
2023-01-09 12:35:43 +01:00
Andrey Antukh
f628955a15 🐛 Set the same tenant default on backend and exporter 2023-01-09 11:57:13 +01:00
Andrey Antukh
6cdf696fc4 🐛 Fix issues on ldap provider and rpc method 2023-01-09 11:57:13 +01:00
alonso.torres
c42ef7c5b0 🐛 Fix problem uploading svg with style tag 2023-01-09 11:56:02 +01:00
Andrey Antukh
853be27780 🐛 Fix issues on database logger 2023-01-09 11:43:26 +01:00
Andrey Antukh
b235d3f0f2 Improve update-file webhook batching
make it per user
2023-01-09 11:43:02 +01:00
Alejandro
04dc9f7881 Merge pull request #2736 from penpot/superalex-fix-text-sync-hotfix
🐛 Fix text content sync and touched detection in shape displacement
2023-01-09 11:35:59 +01:00
alonso.torres
1fdf09a692 🐛 Fix problem with snap-pixel for very big shapes 2023-01-09 11:06:09 +01:00
alonso.torres
c2e0b18f26 🐛 Fix problem with thumbnails uploading 2023-01-09 11:05:31 +01:00
alonso.torres
672cfa4ecc 🐛 Fix problem when forcing persistence on screen change 2023-01-05 16:20:37 +01:00
alonso.torres
c459c56f37 Improved performance of snap to distances 2023-01-05 16:20:37 +01:00
Andrés Moya
0863a96f93 🐛 Fix text content sync and touched detection in shape displacement 2023-01-05 13:26:33 +01:00
Andrey Antukh
97a884018f Move media mutations to commands 2023-01-05 13:23:57 +01:00
Andrey Antukh
1718f49a90 💄 Fix code consistency issues on comments rpc methods
related to the ::rpc/profile-id usage.
2023-01-05 13:23:57 +01:00
Alejandro
2c1fb1424c Merge pull request #2734 from penpot/hiru-fix-text-sync
🐛 Fix text content sync and touched detection in shape displacement
2023-01-05 12:13:30 +01:00
Andrés Moya
5e1cabc857 🐛 Fix text content sync and touched detection in shape displacement 2023-01-05 10:23:34 +01:00
Andrés Moya
6f72ea0530 💄 Clean up CHANGES.md (staging) 2023-01-05 09:57:10 +01:00
Alejandro
c2d8c1994c Merge pull request #2671 from penpot/hiru-sync-groups
🐛 Fix group/ungroup shapes inside a component
2023-01-05 09:21:58 +01:00
Andrés Moya
985d5cc20c 🐛 Fix group/ungroup shapes inside a component 2023-01-05 09:21:42 +01:00
Alejandro
a0364e8835 Merge pull request #2728 from penpot/eva-bugfixin-flex
🐛 Fix some bugs reported on flex feedback
2023-01-05 08:54:55 +01:00
Alejandro
b273bd44c5 Merge pull request #2733 from penpot/alotor-performance
Performance enhancements
2023-01-05 07:39:57 +01:00
Alejandro
ec2fff31a0 Merge pull request #2732 from penpot/palba-create-group-form-enter
🐛 Fix confirm group name with enter doesn't work in assets modal
2023-01-05 07:07:58 +01:00
Pablo Alba
53a8718e8d 🐛 Fix confirm group name with enter doesn't work in assets modal 2023-01-05 07:07:43 +01:00
Alejandro
216a43cc43 Merge pull request #2731 from penpot/superalex-fix-enter-events-hotfix
🐛 Fix enter events
2023-01-05 07:02:34 +01:00
alonso.torres
10439934d4 Use the function hypot for distances 2023-01-04 16:21:41 +01:00
alonso.torres
84e9f69213 Improved text rendering performance 2023-01-04 16:12:12 +01:00
alonso.torres
837b52aea1 Improved performand for hug content in layout 2023-01-04 16:12:12 +01:00
alonso.torres
98698cf2db Improved modifiers lens 2023-01-04 16:12:12 +01:00
alonso.torres
d5ab0eea1a Removed reflow in viewport 2023-01-04 16:12:12 +01:00
Pablo Alba
333acacbbf Merge pull request #2730 from penpot/superalex-fix-enter-events
🐛 Fix enter events
2023-01-04 14:37:15 +01:00
Eva
598959cd3f 🐛 Fix some bugs reported on flex feedback 2023-01-04 14:00:13 +01:00
Alejandro Alonso
05431cc757 🐛 Fix enter events 2023-01-04 13:23:05 +01:00
Alejandro Alonso
f56b8be33d 🐛 Fix enter events 2023-01-04 13:09:39 +01:00
alonso.torres
644854a651 Reorder layers through keys in flex layout 2023-01-04 09:24:47 +01:00
alonso.torres
e926b11fef Changes to the margin-item and min/max width/height 2023-01-04 09:24:42 +01:00
alonso.torres
40da1c302a Support hidden elements in flex layout 2023-01-04 09:24:35 +01:00
Alejandro Alonso
b5e53b57d1 🎉 Dynamic alignment only in sight 2023-01-02 14:12:14 +01:00
Alejandro
e8d561ac7f Merge pull request #2724 from penpot/hiru-fix-component-board
Avoid extra group when creating a component from a board
2023-01-02 13:52:14 +01:00
Alejandro
cf87c54ed4 Merge pull request #2726 from penpot/palba-error-import-file
🐛 Fix export/import svg + json format isn't working
2023-01-02 13:48:14 +01:00
Pablo Alba
3ce1540331 🐛 Fix export/import svg + json format isn't working 2023-01-02 13:36:49 +01:00
Alejandro
cda2dade95 Merge pull request #2725 from penpot/alotor-bug-fixing
Bug fixing
2023-01-02 11:56:51 +01:00
Andrés Moya
baf4dfdecc 🐛 Allow to create component from frame 2023-01-02 11:33:58 +01:00
alonso.torres
ade13d3bca 🐛 Fix problem with auto-height text resize 2023-01-02 11:25:53 +01:00
alonso.torres
ff9b2090cf 🐛 Fix problem with shapes moving randomly 2023-01-02 11:25:53 +01:00
alonso.torres
733b35dd53 ⬆️ Updated potok dependency 2023-01-02 11:25:53 +01:00
Alejandro
466e018411 Merge pull request #2700 from penpot/palba-orphans-st
🐛 Add function to reparent orphan shapes
2023-01-02 11:16:22 +01:00
alonso.torres
32d39c35e4 🐛 Fix problem with flipped shapes 2023-01-02 11:12:59 +01:00
Alejandro
5f77df1996 Merge pull request #2713 from penpot/palba-show-color-name-inspect
🐛 Show color name on inspect
2023-01-02 10:35:36 +01:00
Alejandro
24538add3f Merge pull request #2723 from penpot/eva-a11y-bugfixing
🐛 Fix some visual errors
2023-01-02 10:33:23 +01:00
Eva
407831ffd1 🐛 Fix some visual errors 2023-01-02 09:46:42 +01:00
Alejandro
379997f9db Merge pull request #2716 from penpot/palba-fix-internal-error-set-text-style
🐛 Fix internal error at setting text style
2023-01-02 09:41:48 +01:00
Alejandro
b1d99232a9 Merge pull request #2718 from penpot/niwinz-bugfixes
🐛 Bugfixes
2023-01-02 09:23:39 +01:00
Alejandro Alonso
7e21d827c9 🐛 Fix duplicate frame issues 2023-01-02 08:52:45 +01:00
Alejandro
443d8b21c1 Merge pull request #2719 from penpot/niwinz-quotes
🎉 Quotes & Soft Quotes
2023-01-02 08:13:14 +01:00
Andrey Antukh
e372e8ba3e 🐛 Fix s3 client issues with s3 compatible services 2022-12-31 16:37:42 +01:00
Andrey Antukh
27451b9796 ♻️ Refactor comments RPC methods and add tests 2022-12-31 12:00:57 +01:00
Andrey Antukh
73a3e0c0ae 🎉 Add usage quotes 2022-12-31 11:22:36 +01:00
Andrey Antukh
d68be0869b Improve error report on point constructor 2022-12-31 11:11:22 +01:00
Andrey Antukh
7a8b0e710b Improve trace reporting on unhandled exception 2022-12-31 11:11:17 +01:00
Andrey Antukh
3b61a7dd91 🐛 Fix incorrect arguments to process-changes 2022-12-31 11:11:13 +01:00
Andrey Antukh
941aa6ad5d 🔥 Remove unused configuration attrs 2022-12-31 09:22:57 +01:00
Pablo Alba
42b69df671 🐛 Fix internal error at setting text style 2022-12-30 13:34:47 +01:00
Pablo Alba
4442246e08 Merge pull request #2714 from penpot/superalex-fix-ignoring-boolean-operations-on-ctrl-click-actions
🐛 Fix ignore booleans on ctrl + click selection
2022-12-30 12:11:30 +01:00
Pablo Alba
d1dbc3850d 🐛 Show color name on inspect 2022-12-30 11:59:46 +01:00
Alejandro Alonso
ed4a5f6c60 🐛 Fix ignore booleans on ctrl + click selection 2022-12-30 11:33:03 +01:00
Alejandro
0144939f34 Merge pull request #2702 from penpot/palba-fix-boolean-shapes-color
🐛 Fix incorrect color in properties of multiple bool shapes
2022-12-30 08:06:48 +01:00
Pablo Alba
ede07e4f44 🐛 Fix incorrect color in properties of multiple bool shapes 2022-12-30 08:05:26 +01:00
Alejandro
b2c55c79a4 Merge pull request #2710 from penpot/palba-convert-current-color-in-svg
🐛 Fix "currentColor" is not converted when importing SVG
2022-12-30 07:45:05 +01:00
Pablo Alba
0b2ffbe1fa 🐛 Fix "currentColor" is not converted when importing SVG 2022-12-30 07:44:47 +01:00
Alejandro
ebfe651b7d Merge pull request #2711 from penpot/palba-create-component-menu
🐛 Fix missing create component menu for frames
2022-12-30 07:40:20 +01:00
Pablo Alba
dac11d1606 🐛 Fix missing create component menu for frames 2022-12-30 07:28:27 +01:00
Alejandro
c8bd1e89d6 Merge pull request #2708 from penpot/palba-fix-selection-stroke-on-multiple-fonts
🐛 Fix selection stroke missing in properties of multiple texts
2022-12-30 07:25:50 +01:00
Alejandro
8111db1110 Merge pull request #2709 from penpot/eva-bugfixing-css
🐛 Fix some visual errors
2022-12-29 14:25:30 +01:00
Eva
0a8dfde0a2 🐛 Fix some visual errors 2022-12-29 14:20:02 +01:00
Pablo Alba
9f6a3cbc23 🐛 Fix selection stroke missing in properties of multiple texts 2022-12-29 13:53:35 +01:00
Alejandro
6592456085 Merge pull request #2707 from penpot/alotor-remove-handoff-references
Remove handoff references
2022-12-29 13:14:55 +01:00
alonso.torres
3bbf632121 ♻️ Changed handoff for inspect everywhere 2022-12-29 13:06:16 +01:00
Alejandro
104059a7b1 Merge pull request #2703 from penpot/palba-add-loading-icon-shared-libraries
🐛 Fix missing loading icon on shared libraries
2022-12-29 11:56:00 +01:00
Pablo Alba
f75af88877 🐛 Fix missing loading icon on shared libraries 2022-12-29 11:47:31 +01:00
Alejandro Alonso
d4360be96e 🐛 Fix guides inside frames issues 2022-12-29 09:50:04 +01:00
Andrey Antukh
dcf95a7502 ⬇️ Downgrade promesa library
Causes incorect release build for some unknown reasons
2022-12-28 13:15:35 +01:00
Pablo Alba
4fc3f316e0 🐛 Add function to reparent orphan shapes 2022-12-28 12:49:07 +01:00
Alejandro
83c8e7f03a Merge pull request #2699 from penpot/alotor-bugfixes
Alotor bugfixes
2022-12-28 11:53:57 +01:00
alonso.torres
074864a6bf 🐛 Fix problem when drawing boxes won't detect mouse-up 2022-12-28 11:48:38 +01:00
alonso.torres
aed7f0ad43 🐛 Fix problem when moving texts with keyboard 2022-12-28 11:48:36 +01:00
alonso.torres
cd2df41e87 🐛 Fix problems with transparent frames thumbnails 2022-12-28 11:48:14 +01:00
alonso.torres
00fbfd6e9e 🐛 Fix problem when moving shape inside frame 2022-12-28 11:48:14 +01:00
alonso.torres
93726cf8fe 🐛 Fix wrong interaction between comments and panning modes 2022-12-28 11:48:14 +01:00
Andrey Antukh
1dc6464974 🐛 Fix unexpected behavior of font-variant query
Missing coersion of team-id parameter
2022-12-28 11:30:27 +01:00
Alejandro
81cebb2aa8 Merge pull request #2693 from penpot/palba-fix-non-persistent-display-type-st
🐛 Fix display type of component library not persistent
2022-12-28 11:23:47 +01:00
Pablo Alba
6c8144a18a 🐛 Fix display type of component library not persistent 2022-12-28 11:15:42 +01:00
Alejandro Alonso
47bf758ad7 🐛 Fix guides spec failure 2022-12-28 11:14:51 +01:00
Alejandro
13cfe56301 Merge pull request #2698 from penpot/palba-interaction-bad-positioning-when-fixed
🐛 Fix bad element positioning on interaction with fixed scroll
2022-12-28 11:02:23 +01:00
Pablo Alba
33f7cec933 🐛 Fix bad element positioning on interaction with fixed scroll 2022-12-28 10:57:45 +01:00
Alejandro
1f00d91dd7 Merge pull request #2694 from penpot/niwinz-enhancements-6
🐛 Bugfixes
2022-12-28 09:57:07 +01:00
Andrey Antukh
c1a8437b6d Merge pull request #2697 from penpot/palba-outline-rounded-rect
🐛 Fix show outline with rounded corners on rects
2022-12-28 09:33:01 +01:00
Pablo Alba
5cb3aa5dbc 🐛 Fix show outline with rounded corners on rects 2022-12-28 09:23:11 +01:00
Andrey Antukh
de72dc5769 🐛 Fix race conditions issues on concurrent edition 2022-12-28 09:10:06 +01:00
Andrey Antukh
b827037f90 📎 Add experimental label on admin image on compose file 2022-12-28 09:10:06 +01:00
Andrey Antukh
60fb3f3d0e 🐛 Fix storage/pointer-map support issues on thumbnails and libs loading 2022-12-28 09:10:06 +01:00
Andrey Antukh
84fd952471 Improve storage/* features support on srepl helpers 2022-12-28 09:10:06 +01:00
Andrey Antukh
e37fc00351 🐛 Fix pointer-map support issue on get-team-shared-files RPC method 2022-12-28 09:10:06 +01:00
Andrey Antukh
4164c8f012 ⬆️ Update dependencies 2022-12-28 09:10:06 +01:00
Alejandro
c86af68349 Merge pull request #2695 from penpot/alotor-safari-fix-texts
🐛 Fix problems with Safari texts
2022-12-28 07:16:05 +01:00
alonso.torres
4302ab05e4 🐛 Fix problems with Safari texts 2022-12-27 15:22:28 +01:00
Alejandro
777e2fb0a3 Merge pull request #2692 from penpot/palba-fix-style-team-invite
🐛 Fix style for team invite in deutsch
2022-12-27 12:49:05 +01:00
Alejandro Alonso
f7412ccbd7 📎 Fix version number and changelog 2022-12-27 12:05:42 +01:00
Alejandro
fe11b37b8f Merge pull request #2686 from penpot/niwinz-enhancements-5
General enhancements & Bugfixes
2022-12-27 11:58:22 +01:00
Pablo Alba
c469bd5757 🐛 Fix style for team invite in deutsch 2022-12-27 09:10:19 +01:00
Andrey Antukh
7d817eb080 🎉 Add new version of docker-compose.yaml
That not depends on config.env file and has better examples
2022-12-22 16:42:45 +01:00
Andrey Antukh
2840cb893e 🎉 Add login-with-password flag
As replacement to `login` flag
2022-12-22 16:42:45 +01:00
Andrey Antukh
7f5491f45b 💄 Add minor cosmetic changes to manage.sh script 2022-12-22 16:42:45 +01:00
Andrey Antukh
ef9dcf391d 🐛 Fix slow exit of exporter docker container 2022-12-22 16:42:45 +01:00
Andrey Antukh
81ecb26f8b Make exporter docker image run on non-root user 2022-12-22 16:42:45 +01:00
Andrey Antukh
35fd3ce150 Make backend docker image run on non-root user 2022-12-22 16:42:45 +01:00
Andrey Antukh
68d2afc75d Add missing type hints on backend code 2022-12-22 16:42:45 +01:00
Andrey Antukh
d094eb3595 Use custom jre for backend docker image
Reduces in 230M the image size.
Also updates JRE.
2022-12-22 16:42:45 +01:00
Andrey Antukh
f0d4ad4b20 🎉 Add local docker image building to manage.sh 2022-12-22 16:42:45 +01:00
Andrey Antukh
b929564fa7 ♻️ Add admin facilities on the code base
- Fix bugs related to orphan teams on profile deletion
- Separate session based profile-id param from api user provided
2022-12-22 16:42:45 +01:00
Andrey Antukh
53d9b547c3 🐛 Fix several rpc events not emiting webhooks 2022-12-22 16:42:45 +01:00
Andrey Antukh
50c17e1261 🐛 Fix invitation link translations issues 2022-12-22 16:42:45 +01:00
Andrey Antukh
a113a64554 🐛 Fix invitation link validation issue 2022-12-22 16:42:41 +01:00
382 changed files with 25398 additions and 12848 deletions

View File

@@ -1,8 +1,38 @@
# CHANGELOG
## 1.17.2
## :rocket: Next (1.17)
### :bug: Bugs fixed
### :boom: Breaking changes & Deprecations
- Fix invite members button text [Taiga #4794](https://tree.taiga.io/project/penpot/issue/4794)
- Fix problem with opacity in frames [Taiga #4795](https://tree.taiga.io/project/penpot/issue/4795)
- Fix correct behaviour for space-around and added space-evenly option
- Fix duplicate with alt and undo only undo one step [Taiga #4746](https://tree.taiga.io/project/penpot/issue/4746)
- Fix problem creating frames inside layout [Taiga #4844](https://tree.taiga.io/project/penpot/issue/4844)
## 1.17.2
### :bug: Bugs fixed
- Fix paste board inside itself [Taiga #4775](https://tree.taiga.io/project/penpot/issue/4775)
- Fix middle button panning can drag guides [Taiga #4266](https://tree.taiga.io/project/penpot/issue/4266)
### :heart: Community contributions by (Thank you!)
- To @ondrejkonec: for some code contributions on this release.
## 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)
- Fix button spacing on delete acount modal [Taiga #4762](https://tree.taiga.io/project/penpot/issue/4762)
- Fix invitations input on team management and onboarding modal [Taiga #4760](https://tree.taiga.io/project/penpot/issue/4760)
- Fix weird numeration creating new elements in dashboard [Taiga #4755](https://tree.taiga.io/project/penpot/issue/4755)
- Fix can move shape with lens zoom active [Taiga #4787](https://tree.taiga.io/project/penpot/issue/4787)
- Fix social links broken [Taiga #4759](https://tree.taiga.io/project/penpot/issue/4759)
- Fix tooltips on left toolbar [Taiga #4793](https://tree.taiga.io/project/penpot/issue/4793)
## 1.17.0
### :sparkles: New features
@@ -10,6 +40,18 @@
- Better overlays interactions on boards inside boards [Taiga #4386](https://tree.taiga.io/project/penpot/us/4386)
- Show board miniature in manual overlay setting [Taiga #4475](https://tree.taiga.io/project/penpot/issue/4475)
- Handoff visual improvements [Taiga #3124](https://tree.taiga.io/project/penpot/us/3124)
- Dynamic alignment only in sight [Github 1971](https://github.com/penpot/penpot/issues/1971)
- Add some accessibility to shortcut panel [Taiga #4713](https://tree.taiga.io/project/penpot/issue/4713)
- Add shortcuts for text editing [Taiga #2052](https://tree.taiga.io/project/penpot/us/2052)
- Second level boards treated as groups in terms of selection [Taiga #4269](https://tree.taiga.io/project/penpot/us/4269)
- Performance improvements both for backend and frontend
- Accessibility improvements for login area [Taiga #4353](https://tree.taiga.io/project/penpot/us/4353)
- Outbound webhooks [Taiga #4577](https://tree.taiga.io/project/penpot/us/4577)
- Add copy invitation link to the invitation options [Taiga #4213](https://tree.taiga.io/project/penpot/us/4213)
- Dynamic alignment only in sight [Taiga #3537](https://tree.taiga.io/project/penpot/us/3537)
- Improve naming of layers [Taiga #4036](https://tree.taiga.io/project/penpot/us/4036)
- Add zoom lense [Taiga #4691](https://tree.taiga.io/project/penpot/us/4691)
- Detect potential problems with custom font vertical metrics [Taiga #4697](https://tree.taiga.io/project/penpot/us/4697)
### :bug: Bugs fixed
@@ -22,12 +64,51 @@
- Fix adding an extra page on import [Taiga #4543](https://tree.taiga.io/project/penpot/task/4543)
- Fix unable to select text at assets inputs in firefox [Taiga #4572](https://tree.taiga.io/project/penpot/issue/4572)
- Fix component sync when converting to path [Taiga #3642](https://tree.taiga.io/project/penpot/issue/3642)
### :arrow_up: Deps updates
- Fix style for team invite in deutsch [Taiga #4614](https://tree.taiga.io/project/penpot/issue/4614)
- Fix problem with text edition in Safari [Taiga #4046](https://tree.taiga.io/project/penpot/issue/4046)
- Fix show outline with rounded corners on rects [Taiga #4053](https://tree.taiga.io/project/penpot/issue/4053)
- Fix wrong interaction between comments and panning modes [Taiga #4297](https://tree.taiga.io/project/penpot/issue/4297)
- Fix bad element positioning on interaction with fixed scroll [Github #2660](https://github.com/penpot/penpot/issues/2660)
- Fix display type of component library not persistent [Taiga #4512](https://tree.taiga.io/project/penpot/issue/4512)
- Fix problem when moving texts with keyboard [#2690](https://github.com/penpot/penpot/issues/2690)
- Fix problem when drawing boxes won't detect mouse-up [Taiga #4618](https://tree.taiga.io/project/penpot/issue/4618)
- Fix missing loading icon on shared libraries [Taiga #4148](https://tree.taiga.io/project/penpot/issue/4148)
- Fix selection stroke missing in properties of multiple texts [Taiga #4048](https://tree.taiga.io/project/penpot/issue/4048)
- Fix missing create component menu for frames [Github #2670](https://github.com/penpot/penpot/issues/2670)
- Fix "currentColor" is not converted when importing SVG [Github 2276](https://github.com/penpot/penpot/issues/2276)
- Fix incorrect color in properties of multiple bool shapes [Taiga #4355](https://tree.taiga.io/project/penpot/issue/4355)
- Fix pressing the enter key gives you an internal error [Github 2675](https://github.com/penpot/penpot/issues/2675) [Github 2577](https://github.com/penpot/penpot/issues/2577)
- Fix confirm group name with enter doesn't work in assets modal [Taiga #4506](https://tree.taiga.io/project/penpot/issue/4506)
- Fix group/ungroup shapes inside a component [Taiga #4052](https://tree.taiga.io/project/penpot/issue/4052)
- Fix wrong update of text in components [Taiga #4646](https://tree.taiga.io/project/penpot/issue/4646)
- Fix problem with SVG imports with style [#2605](https://github.com/penpot/penpot/issues/2605)
- Fix ghost shapes after sync groups in components [Taiga #4649](https://tree.taiga.io/project/penpot/issue/4649)
- Fix layer orders messed up on move, group, reparent and undo [Github #2672](https://github.com/penpot/penpot/issues/2672)
- Fix max height in library dialog [Github #2335](https://github.com/penpot/penpot/issues/2335)
- Fix undo ungroup (shift+g) scrambles positions [Taiga #4674](https://tree.taiga.io/project/penpot/issue/4674)
- Fix justified text is stretched [Github #2539](https://github.com/penpot/penpot/issues/2539)
- Fix mousewheel on viewer inspector [Taiga #4221](https://tree.taiga.io/project/penpot/issue/4221)
- Fix path edition activated on boards [Taiga #4105](https://tree.taiga.io/project/penpot/issue/4105)
- Fix hidden layers inside groups become visible after the group visibility is changed[Taiga #4710](https://tree.taiga.io/project/penpot/issue/4710)
- Fix format of HSLA color on viewer [Taiga #4393](https://tree.taiga.io/project/penpot/issue/4393)
- Fix some typos [Taiga #4724](https://tree.taiga.io/project/penpot/issue/4724)
- Fix ctrl+c for inspect code [Taiga #4739](https://tree.taiga.io/project/penpot/issue/4739)
- Fix text in custom font is not at the expected position at export [Taiga #4394](https://tree.taiga.io/project/penpot/issue/4394)
- Fix unneeded popup when updating local components [Taiga #4430](https://tree.taiga.io/project/penpot/issue/4430)
- Fix multiuser - "Shadow" element is not updating immediately [Taiga #4709](https://tree.taiga.io/project/penpot/issue/4709)
- Fix paths not flagged as modified when resized [Taiga #4742](https://tree.taiga.io/project/penpot/issue/4742)
- Fix resend invitation doesn't reset the expiration date [Taiga #4741](https://tree.taiga.io/project/penpot/issue/4741)
- Fix incorrect state after undo page creation [Taiga #4690](https://tree.taiga.io/project/penpot/issue/4690)
- Fix copy paste texts with typography assets linked [Taiga #4750](https://tree.taiga.io/project/penpot/issue/4750)
### :heart: Community contributions by (Thank you!)
- To @iprithvitharun: let's make UX Writing contributions in Open Source a trend!
## 1.16.2-beta
### :bug: Bugs fixed
- Fix strage cursor behaviour after clicking viewport with text pool [Github #2447](https://github.com/penpot/penpot/issues/2447)
## 1.16.1-beta
@@ -97,7 +178,6 @@
- Fix grid not syncing immediately in multiuser [Taiga #4339](https://tree.taiga.io/project/penpot/issue/4339)
- Fix custom font upload fails silently for unsupported formats [Taiga #4279](https://tree.taiga.io/project/penpot/issue/4280)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
- To @andrewzhurov for many code contributions on this release.

View File

@@ -50,7 +50,7 @@ Being web based, Penpot is not dependent on operating systems or local installat
Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work.
<p align="center">
<img src="https://penpot.app/images/open-source.png" alt="Open Source">
<img src="https://penpot.app/images/readme/open-source.png" alt="Open Source">
</p>
@@ -93,7 +93,7 @@ You will find the following categories:
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
<p align="center">
<img src="https://penpot.app/images/cross-teams.webp" alt="Community">
<img src="https://penpot.app/images/readme/cross-teams.webp" alt="Community">
</p>
## Contributing ##

View File

@@ -1,12 +1,12 @@
{:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.11.1"}
org.clojure/core.async {:mvn/version "1.5.648"}
org.clojure/core.async {:mvn/version "1.6.673"}
;; Logging
org.zeromq/jeromq {:mvn/version "0.5.2"}
org.zeromq/jeromq {:mvn/version "0.5.3"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-4"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-5"}
org.clojure/data.fressian {:mvn/version "1.0.0"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
@@ -18,18 +18,18 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.2.1.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.2.2.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti
{:git/tag "v9.11"
:git/sha "6f9197a"
{:git/tag "v9.12"
:git/sha "51646d8"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.834"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
metosin/reitit-core {:mvn/version "0.5.18"}
org.postgresql/postgresql {:mvn/version "42.5.0"}
org.postgresql/postgresql {:mvn/version "42.5.1"}
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
io.whitfin/siphash {:mvn/version "2.0.0"}
@@ -37,9 +37,9 @@
buddy/buddy-hashers {:mvn/version "1.8.158"}
buddy/buddy-sign {:mvn/version "3.4.333"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.1"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.2"}
org.jsoup/jsoup {:mvn/version "1.15.1"}
org.jsoup/jsoup {:mvn/version "1.15.3"}
org.im4java/im4java
{:git/tag "1.4.0-penpot-2"
:git/sha "e2b3e16"
@@ -51,11 +51,12 @@
integrant/integrant {:mvn/version "0.8.0"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.11.3"}
markdown-clj/markdown-clj {:mvn/version "1.11.4"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.17.278"}}
software.amazon.awssdk/s3 {:mvn/version "2.19.8"}
}
:paths ["src" "resources" "target/classes"]
:aliases
@@ -69,8 +70,10 @@
mockery/mockery {:mvn/version "RELEASE"}}
:extra-paths ["test" "dev"]}
:build
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.9.0" :git/sha "8c93e0c"}}
:ns-default build}
:test

View File

@@ -48,8 +48,8 @@
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
</mj-social>
</mj-column>
</mj-section>

View File

@@ -41,8 +41,8 @@
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
</mj-social>
</mj-column>
</mj-section>

View File

@@ -50,8 +50,8 @@
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
</mj-social>
</mj-column>
</mj-section>

View File

@@ -47,8 +47,8 @@
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://instagram.com/penpotapp/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/uxbox" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
</mj-social>
</mj-column>
</mj-section>

View File

@@ -103,9 +103,9 @@
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -129,9 +129,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -143,7 +143,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -157,9 +157,9 @@
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -211,9 +211,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -225,7 +225,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -239,9 +239,9 @@
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
@@ -257,9 +257,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -271,7 +271,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -285,9 +285,9 @@
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -301,7 +301,7 @@
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -321,7 +321,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -341,7 +341,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -361,7 +361,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -370,7 +370,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://instagram.com/penpotapp/" target="_blank">
<a href="https://www.instagram.com/penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
@@ -381,7 +381,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -390,7 +390,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
<a href="https://tree.taiga.io/project/penpot" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
@@ -401,7 +401,7 @@
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
@@ -411,9 +411,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -425,7 +425,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -439,9 +439,9 @@
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -457,9 +457,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>

View File

@@ -103,9 +103,9 @@
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -129,9 +129,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -143,7 +143,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -157,9 +157,9 @@
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -201,9 +201,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -215,7 +215,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -229,9 +229,9 @@
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
@@ -247,9 +247,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -261,7 +261,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -275,9 +275,9 @@
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -291,7 +291,7 @@
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -311,7 +311,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -331,7 +331,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -351,7 +351,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -360,7 +360,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://instagram.com/penpotapp/" target="_blank">
<a href="https://www.instagram.com/penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
@@ -371,7 +371,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -380,7 +380,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
<a href="https://tree.taiga.io/project/penpot" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
@@ -391,7 +391,7 @@
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
@@ -401,9 +401,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -415,7 +415,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -429,9 +429,9 @@
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -447,9 +447,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>

View File

@@ -103,9 +103,9 @@
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -129,9 +129,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -143,7 +143,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -157,9 +157,9 @@
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -206,9 +206,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -220,7 +220,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -234,9 +234,9 @@
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
@@ -252,9 +252,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -266,7 +266,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -280,9 +280,9 @@
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -296,7 +296,7 @@
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -316,7 +316,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -336,7 +336,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -356,7 +356,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -365,7 +365,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://instagram.com/penpotapp/" target="_blank">
<a href="https://www.instagram.com/penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
@@ -376,7 +376,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -385,7 +385,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
<a href="https://tree.taiga.io/project/penpot" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
@@ -396,7 +396,7 @@
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
@@ -406,9 +406,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -420,7 +420,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -434,9 +434,9 @@
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -452,9 +452,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>

View File

@@ -103,9 +103,9 @@
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -129,9 +129,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -143,7 +143,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -157,9 +157,9 @@
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -201,9 +201,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -215,7 +215,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -229,9 +229,9 @@
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
@@ -247,9 +247,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -261,7 +261,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -275,9 +275,9 @@
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -291,7 +291,7 @@
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -311,7 +311,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -331,7 +331,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -351,7 +351,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -360,7 +360,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://instagram.com/penpotapp/" target="_blank">
<a href="https://www.instagram.com/penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
@@ -371,7 +371,7 @@
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
@@ -380,7 +380,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/uxbox" target="_blank">
<a href="https://tree.taiga.io/project/penpot" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
@@ -391,7 +391,7 @@
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
@@ -401,9 +401,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
@@ -415,7 +415,7 @@
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
@@ -429,9 +429,9 @@
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
@@ -447,9 +447,9 @@
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>

View File

@@ -12,10 +12,11 @@ cp ../CHANGES.md target/classes/changelog.md;
clojure -T:build jar;
mv target/penpot.jar target/dist/penpot.jar
cp resources/log4j2.xml target/dist/log4j2.xml
cp scripts/run.template.sh target/dist/run.sh;
cp scripts/manage.template.sh target/dist/manage.sh;
cp scripts/manage.py target/dist/manage.py
chmod +x target/dist/run.sh;
chmod +x target/dist/manage.sh;
chmod +x target/dist/manage.py
# Prefetch
bb ./scripts/prefetch-templates.clj resources/app/onboarding.edn builtin-templates/

167
backend/scripts/manage.py Executable file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
#
# 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 argparse
import json
import socket
import sys
from getpass import getpass
from urllib.parse import urlparse
PREPL_URI = "tcp://localhost:6063"
def get_prepl_conninfo():
uri_data = urlparse(PREPL_URI)
if uri_data.scheme != "tcp":
raise RuntimeError(f"invalid PREPL_URI: {PREPL_URI}")
if not isinstance(uri_data.netloc, str):
raise RuntimeError(f"invalid PREPL_URI: {PREPL_URI}")
host, port = uri_data.netloc.split(":", 2)
if port is None:
port = 6063
if isinstance(port, str):
port = int(port)
return host, port
def send_eval(expr):
host, port = get_prepl_conninfo()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
s.send(expr.encode("utf-8"))
s.send(b":repl/quit\n\n")
with s.makefile() as f:
result = json.load(f)
tag = result.get("tag", None)
if tag != "ret":
raise RuntimeError("unexpected response from PREPL")
return result.get("val", None), result.get("exception", None)
def encode(val):
return json.dumps(json.dumps(val))
def print_error(res):
for error in res["via"]:
print("ERR:", error["message"])
break
def run_cmd(params):
expr = "(app.srepl.ext/run-json-cmd {})".format(encode(params))
res, failed = send_eval(expr)
if failed:
print_error(res)
sys.exit(-1)
return res
def create_profile(fullname, email, password):
params = {
"cmd": "create-profile",
"params": {
"fullname": fullname,
"email": email,
"password": password
}
}
res = run_cmd(params)
print(f"Created: {res['email']} / {res['id']}")
def update_profile(email, fullname, password, is_active):
params = {
"cmd": "update-profile",
"params": {
"email": email,
"fullname": fullname,
"password": password,
"is_active": is_active
}
}
res = run_cmd(params)
if res is True:
print(f"Updated")
else:
print(f"No profile found with email {email}")
def derive_password(password):
params = {
"cmd": "derive-password",
"params": {
"password": password,
}
}
res = run_cmd(params)
print(f"Derived password: \"{res}\"")
available_commands = [
"create-profile",
"update-profile",
"derive-password"
]
parser = argparse.ArgumentParser(
description=(
"Penpot Command Line Interface (CLI)"
)
)
parser.add_argument("-V", "--version", action="version", version="Penpot CLI %%develop%%")
parser.add_argument("action", action="store", choices=available_commands)
parser.add_argument("-n", "--fullname", help="Fullname", action="store")
parser.add_argument("-e", "--email", help="Email", action="store")
parser.add_argument("-p", "--password", help="Password", action="store")
parser.add_argument("-c", "--connect", help="Connect to PREPL", action="store", default="tcp://localhost:6063")
args = parser.parse_args()
PREPL_URI = args.connect
if args.action == "create-profile":
email = args.email
password = args.password
fullname = args.fullname
if email is None:
email = input("Email: ")
if fullname is None:
fullname = input("Fullname: ")
if password is None:
password = getpass("Password: ")
create_profile(fullname, email, password)
elif args.action == "update-profile":
email = args.email
password = args.password
if email is None:
email = input("Email: ")
if password is None:
password = getpass("Password: ")
update_profile(email, None, password, None)
elif args.action == "derive-password":
password = args.password
if password is None:
password = getpass("Password: ")
derive_password(password)

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
set +e
JAVA_CMD=$(type -p java)
set -e
if [[ ! -n "$JAVA_CMD" ]]; then
if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
JAVA_CMD="$JAVA_HOME/bin/java"
else
>&2 echo "Couldn't find 'java'. Please set JAVA_HOME."
exit 1
fi
fi
if [ -f ./environ ]; then
source ./environ
fi
exec $JAVA_CMD $JVM_OPTS -jar penpot.jar -m app.cli.manage "$@"

View File

@@ -2,7 +2,21 @@
export PENPOT_HOST=devenv
export PENPOT_TENANT=dev
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp enable-webhooks"
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-backend-asserts \
enable-audit-log \
enable-transit-readable-response \
enable-demo-users \
disable-secure-session-cookies \
enable-smtp \
enable-prepl-server \
enable-urepl-server \
enable-rpc-climit \
enable-rpc-rlimit \
enable-soft-rpc-rlimit \
enable-webhooks \
enable-access-tokens";
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot"
# export PENPOT_DATABASE_USERNAME="penpot"

View File

@@ -18,5 +18,7 @@ if [ -f ./environ ]; then
source ./environ
fi
export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow $JVM_OPTS"
set -x
exec $JAVA_CMD $JVM_OPTS "$@" -jar penpot.jar -m app.main

View File

@@ -41,15 +41,18 @@
(reduce-kv clojure.string/replace s replacements))
(defn- search-user
[{:keys [conn attrs base-dn] :as cfg} email]
(let [query (replace-several (:query cfg) ":username" email)
[{:keys [::conn base-dn] :as cfg} email]
(let [query (replace-several (:query cfg) ":username" email)
attrs [(:attrs-username cfg)
(:attrs-email cfg)
(:attrs-fullname cfg)]
params {:filter query
:sizelimit 1
:attributes attrs}]
(first (ldap/search conn base-dn params))))
(defn- retrieve-user
[{:keys [conn] :as cfg} {:keys [email password]}]
[{:keys [::conn] :as cfg} {:keys [email password]}]
(when-let [{:keys [dn] :as user} (search-user cfg email)]
(when (ldap/bind? conn dn password)
{:fullname (get user (-> cfg :attrs-fullname keyword))
@@ -66,7 +69,7 @@
(defn authenticate
[cfg params]
(with-open [conn (connect cfg)]
(when-let [user (-> (assoc cfg :conn conn)
(when-let [user (-> (assoc cfg ::conn conn)
(retrieve-user params))]
(when-not (s/valid? ::info-data user)
(let [explain (s/explain-str ::info-data user)]
@@ -100,17 +103,6 @@
:host (:host cfg) :port (:port cfg) :cause cause)
nil))))
(defn- prepare-attributes
[cfg]
(assoc cfg :attrs [(:attrs-username cfg)
(:attrs-email cfg)
(:attrs-fullname cfg)]))
(defmethod ig/init-key ::provider
[_ cfg]
(when (:enabled? cfg)
(some-> cfg try-connectivity prepare-attributes)))
(s/def ::enabled? ::us/boolean)
(s/def ::host ::cf/ldap-host)
(s/def ::port ::cf/ldap-port)
@@ -124,8 +116,7 @@
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname)
(s/def ::attrs-username ::cf/ldap-attrs-username)
(defmethod ig/pre-init-spec ::provider
[_]
(s/def ::provider-params
(s/keys :opt-un [::host ::port
::ssl ::tls
::enabled?
@@ -135,3 +126,14 @@
::attrs-email
::attrs-username
::attrs-fullname]))
(s/def ::provider
(s/nilable ::provider-params))
(defmethod ig/pre-init-spec ::provider
[_]
(s/spec ::provider))
(defmethod ig/init-key ::provider
[_ cfg]
(when (:enabled? cfg)
(try-connectivity cfg)))

View File

@@ -50,7 +50,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- discover-oidc-config
[cfg {:keys [::base-uri] :as opts}]
[cfg {:keys [base-uri] :as opts}]
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
response (ex/try! (http/req! cfg
{:method :get :uri (str discovery-uri)}
@@ -64,10 +64,17 @@
nil)
(= 200 (:status response))
(let [data (json/decode (:body response))]
{:token-uri (get data :token_endpoint)
:auth-uri (get data :authorization_endpoint)
:user-uri (get data :userinfo_endpoint)})
(let [data (json/decode (:body response))
token-uri (get data :token_endpoint)
auth-uri (get data :authorization_endpoint)
user-uri (get data :userinfo_endpoint)]
(l/debug :hint "oidc uris discovered"
:token-uri token-uri
:auth-uri auth-uri
:user-uri user-uri)
{:token-uri token-uri
:auth-uri auth-uri
:user-uri user-uri})
:else
(do
@@ -110,7 +117,7 @@
(if-let [opts (prepare-oidc-opts cfg)]
(do
(l/info :hint "provider initialized"
:provider :oidc
:provider "oidc"
:method (if (:discover? opts) "discover" "manual")
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts))
@@ -122,7 +129,7 @@
:roles (:roles opts))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :oidc)
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "oidc")
nil))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -144,13 +151,13 @@
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider :google
:provider "google"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :google)
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "google")
nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -196,13 +203,13 @@
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider :github
:provider "github"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :github)
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "github")
nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -225,14 +232,14 @@
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider :gitlab
:provider "gitlab"
:base-uri base
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :gitlab)
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "gitlab")
nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -275,8 +282,19 @@
"accept" "application/json"}
:uri (:token-uri provider)
:body (u/map->query-string params)}]
(l/trace :hint "request access token"
:provider (:name provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))
:grant-type (:grant_type params)
:redirect-uri (:redirect_uri params))
(->> (http/req! cfg req)
(p/map (fn [{:keys [status body] :as res}]
(l/trace :hint "access token response"
:status status
:body body)
(if (= status 200)
(let [data (json/decode body)]
{:token (get data :access_token)
@@ -289,12 +307,19 @@
(defn- retrieve-user-info
[{:keys [provider] :as cfg} tdata]
(letfn [(retrieve []
(l/trace :hint "request user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token tdata))
:token-type (:type tdata))
(http/req! cfg
{:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}))
(validate-response [response]
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
@@ -309,7 +334,7 @@
(if-let [get-email-fn (:get-email-fn provider)]
(get-email-fn tdata info)
(let [attr-kw (cf/get :oidc-email-attr :email)]
(get info attr-kw))))
(p/resolved (get info attr-kw)))))
(get-name [info]
(let [attr-kw (cf/get :oidc-name-attr :name)]
@@ -325,6 +350,7 @@
(qualify-props provider))}))
(validate-info [info]
(l/trace :hint "authentication info" :info info)
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
:info (pr-str info))
@@ -334,10 +360,10 @@
:info info))
info)]
(-> (retrieve)
(p/then validate-response)
(p/then process-response)
(p/then validate-info))))
(->> (retrieve)
(p/fmap validate-response)
(p/mcat process-response)
(p/fmap validate-info))))
(s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string)
@@ -434,12 +460,11 @@
(ex/raise :type :restriction
:code :profile-blocked))
(when-let [collector (::audit/collector cfg)]
(audit/submit! collector {:type "command"
:name "login"
:profile-id (:id profile)
:ip-addr (audit/parse-client-ip request)
:props (audit/profile->props profile)}))
(audit/submit! cfg {:type "command"
:name "login-with-password"
:profile-id (:id profile)
:ip-addr (audit/parse-client-ip request)
:props (audit/profile->props profile)})
(->> (redirect-response uri)
(sxf request)))

View File

@@ -61,11 +61,9 @@
:public-uri "http://localhost:3449"
:host "localhost"
:tenant "main"
:tenant "default"
:redis-uri "redis://redis/0"
:srepl-host "127.0.0.1"
:srepl-port 6062
:assets-storage-backend :assets-fs
:storage-assets-fs-directory "assets"
@@ -102,13 +100,10 @@
(s/def ::audit-log-archive-uri ::us/string)
(s/def ::audit-log-http-handler-concurrency ::us/integer)
(s/def ::admins ::us/set-of-strings)
(s/def ::admins ::us/set-of-valid-emails)
(s/def ::file-change-snapshot-every ::us/integer)
(s/def ::file-change-snapshot-timeout ::dt/duration)
(s/def ::setup-admin-email ::us/email)
(s/def ::setup-admin-password ::us/not-empty-string)
(s/def ::default-executor-parallelism ::us/integer)
(s/def ::scheduled-executor-parallelism ::us/integer)
@@ -130,6 +125,16 @@
(s/def ::database-min-pool-size ::us/integer)
(s/def ::database-max-pool-size ::us/integer)
(s/def ::quotes-teams-per-profile ::us/integer)
(s/def ::quotes-projects-per-team ::us/integer)
(s/def ::quotes-invitations-per-team ::us/integer)
(s/def ::quotes-profiles-per-team ::us/integer)
(s/def ::quotes-files-per-project ::us/integer)
(s/def ::quotes-files-per-team ::us/integer)
(s/def ::quotes-font-variants-per-team ::us/integer)
(s/def ::quotes-comment-threads-per-file ::us/integer)
(s/def ::quotes-comments-per-file ::us/integer)
(s/def ::default-blob-version ::us/integer)
(s/def ::error-report-webhook ::us/string)
(s/def ::user-feedback-destination ::us/string)
@@ -189,18 +194,15 @@
(s/def ::smtp-ssl ::us/boolean)
(s/def ::smtp-tls ::us/boolean)
(s/def ::smtp-username (s/nilable ::us/string))
(s/def ::srepl-host ::us/string)
(s/def ::srepl-port ::us/integer)
(s/def ::urepl-host ::us/string)
(s/def ::urepl-port ::us/integer)
(s/def ::prepl-host ::us/string)
(s/def ::prepl-port ::us/integer)
(s/def ::assets-storage-backend ::us/keyword)
(s/def ::fdata-storage-backend ::us/keyword)
(s/def ::storage-assets-fs-directory ::us/string)
(s/def ::storage-assets-s3-bucket ::us/string)
(s/def ::storage-assets-s3-region ::us/keyword)
(s/def ::storage-assets-s3-endpoint ::us/string)
(s/def ::storage-fdata-s3-bucket ::us/string)
(s/def ::storage-fdata-s3-region ::us/keyword)
(s/def ::storage-fdata-s3-prefix ::us/string)
(s/def ::storage-fdata-s3-endpoint ::us/string)
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
@@ -277,6 +279,17 @@
::profile-complaint-max-age
::profile-complaint-threshold
::public-uri
::quotes-teams-per-profile
::quotes-projects-per-team
::quotes-invitations-per-team
::quotes-profiles-per-team
::quotes-files-per-project
::quotes-files-per-team
::quotes-font-variants-per-team
::quotes-comment-threads-per-file
::quotes-comments-per-file
::redis-uri
::registration-domain-whitelist
::rpc-rlimit-config
@@ -295,22 +308,16 @@
::smtp-tls
::smtp-username
::srepl-host
::srepl-port
::setup-admin-email
::setup-admin-password
::urepl-host
::urepl-port
::prepl-host
::prepl-port
::assets-storage-backend
::storage-assets-fs-directory
::storage-assets-s3-bucket
::storage-assets-s3-region
::storage-assets-s3-endpoint
::fdata-storage-backend
::storage-fdata-s3-bucket
::storage-fdata-s3-region
::storage-fdata-s3-prefix
::storage-fdata-s3-endpoint
::telemetry-enabled
::telemetry-uri
::telemetry-referer

View File

@@ -167,6 +167,11 @@
(instance? javax.sql.DataSource v))
(s/def ::pool pool?)
(s/def ::conn some?)
;; DEPRECATED: to be removed in 1.18
(s/def ::conn-or-pool some?)
(s/def ::pool-or-conn some?)
(defn closed?
[pool]

View File

@@ -257,15 +257,17 @@
"Schedule an already defined email to be sent using asynchronously
using worker task."
[{:keys [::conn ::factory] :as context}]
(us/verify fn? factory)
(us/verify some? conn)
(let [email (factory context)]
(wrk/submit! (assoc email
::wrk/task :sendmail
::wrk/delay 0
::wrk/max-retries 4
::wrk/priority 200
::wrk/conn conn))))
(let [email (if factory
(factory context)
(dissoc context ::conn))]
(wrk/submit! (merge
{::wrk/task :sendmail
::wrk/delay 0
::wrk/max-retries 4
::wrk/priority 200
::wrk/conn conn}
email))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SENDMAIL FN / TASK HANDLER

View File

@@ -91,9 +91,7 @@
(let [params (:path-params match)
result (:result match)
handler (or (:handler result) not-found-handler)
request (-> request
(assoc :path-params params)
(update :params merge params))]
request (assoc request :path-params params)]
(handler request respond raise))
(not-found-handler request respond raise)))

View File

@@ -8,6 +8,7 @@
"Services related to the user activity (audit log)."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
@@ -19,7 +20,7 @@
[app.loggers.audit.tasks :as-alias tasks]
[app.loggers.webhooks :as-alias webhooks]
[app.main :as-alias main]
[app.metrics :as mtx]
[app.rpc :as-alias rpc]
[app.tokens :as tokens]
[app.util.retry :as rtry]
[app.util.time :as dt]
@@ -28,7 +29,6 @@
[cuerdas.core :as str]
[integrant.core :as ig]
[lambdaisland.uri :as u]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]))
@@ -75,28 +75,20 @@
(merge (:props profile))
(d/without-nils)))
(defn clean-props
[{:keys [profile-id] :as event}]
(let [invalid-keys #{:session-id
:password
:old-password
:token}
xform (comp
(remove (fn [kv]
(qualified-keyword? (first kv))))
(remove (fn [kv]
(contains? invalid-keys (first kv))))
(remove (fn [[k v]]
(and (= k :profile-id)
(= v profile-id))))
(filter (fn [[_ v]]
(or (string? v)
(keyword? v)
(uuid? v)
(boolean? v)
(number? v)))))]
(def reserved-props
#{:session-id
:password
:old-password
:token})
(update event :props #(into {} xform %))))
(defn clean-props
[props]
(into {}
(comp
(d/without-nils)
(d/without-qualified)
(remove #(contains? reserved-props (key %))))
props))
;; --- SPECS
@@ -130,7 +122,7 @@
(s/keys :req [::wrk/executor ::db/pool]))
(defmethod ig/pre-init-spec ::collector [_]
(s/keys :req [::db/pool ::wrk/executor ::mtx/metrics]))
(s/keys :req [::db/pool ::wrk/executor]))
(defmethod ig/init-key ::collector
[_ {:keys [::db/pool] :as cfg}]
@@ -141,8 +133,8 @@
:else
cfg))
(defn- persist-event!
[pool event]
(defn- handle-event!
[conn-or-pool event]
(us/verify! ::event event)
(let [params {:id (uuid/next)
:name (:name event)
@@ -159,7 +151,7 @@
::rtry/max-retries 6
::rtry/label "persist-audit-log-event"}
(let [now (dt/now)]
(db/insert! pool :audit-log
(db/insert! conn-or-pool :audit-log
(-> params
(update :props db/tjson)
(update :ip-addr db/inet)
@@ -171,32 +163,37 @@
(::webhooks/event? event))
(let [batch-key (::webhooks/batch-key event)
batch-timeout (::webhooks/batch-timeout event)
label-suffix (when (ifn? batch-key)
(str/ffmt ":%" (batch-key (:props params))))
dedupe? (boolean
(and batch-key batch-timeout))]
(wrk/submit! ::wrk/conn pool
label (dm/str "rpc:" (:name params))
label (cond
(ifn? batch-key) (dm/str label ":" (batch-key (::rpc/params event)))
(string? batch-key) (dm/str label ":" batch-key)
:else label)
dedupe? (boolean (and batch-key batch-timeout))]
(wrk/submit! ::wrk/conn conn-or-pool
::wrk/task :process-webhook-event
::wrk/queue :webhooks
::wrk/max-retries 0
::wrk/delay (or batch-timeout 0)
::wrk/dedupe dedupe?
::wrk/label
(str/ffmt "rpc:%1%2" (:name params) label-suffix)
::wrk/label label
::webhooks/event
(-> params
(dissoc :ip-addr)
(dissoc :type)))))))
(dissoc :type)))))
params))
(defn submit!
"Submit audit event to the collector."
[{:keys [::wrk/executor ::db/pool] :as collector} params]
(us/assert! ::collector collector)
(->> (px/submit! executor (partial persist-event! pool (d/without-nils params)))
(p/merr (fn [cause]
(l/error :hint "audit: unexpected error processing event" :cause cause)
(p/resolved nil)))))
[{:keys [::wrk/executor] :as cfg} params]
(let [conn (or (::db/conn cfg) (::db/pool cfg))]
(us/assert! ::wrk/executor executor)
(us/assert! ::db/pool-or-conn conn)
(try
(handle-event! conn (d/without-nils params))
(catch Throwable cause
(l/error :hint "audit: unexpected error processing event" :cause cause)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TASK: ARCHIVE
@@ -243,7 +240,7 @@
from audit_log
where archived_at is null
order by created_at asc
limit 256
limit 128
for update skip locked;")
(defn archive-events
@@ -319,7 +316,7 @@
where archived_at is not null")
(defn- clean-archived
[{:keys [pool]}]
[{:keys [::db/pool]}]
(let [result (db/exec-one! pool [sql:clean-archived])
result (:next.jdbc/update-count result)]
(l/debug :hint "delete archived audit log entries" :deleted result)

View File

@@ -11,12 +11,12 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.util.async :as aa]
[app.worker :as wrk]
[app.loggers.zmq :as lzmq]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]))
[integrant.core :as ig]
[promesa.exec :as px]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Error Listener
@@ -27,7 +27,7 @@
(defonce enabled (atom true))
(defn- persist-on-database!
[{:keys [pool] :as cfg} {:keys [id] :as event}]
[{:keys [::db/pool] :as cfg} {:keys [id] :as event}]
(when-not (db/read-only? pool)
(db/insert! pool :server-error-report {:id id :content (db/tjson event)})))
@@ -53,41 +53,49 @@
(assoc :version (:full cf/version))
(update :id #(or % (uuid/next)))))
(defn handle-event
[{:keys [executor] :as cfg} event]
(aa/with-thread executor
(try
(let [event (parse-event event)
uri (cf/get :public-uri)]
(defn- handle-event
[cfg event]
(try
(let [event (parse-event event)
uri (cf/get :public-uri)]
(l/debug :hint "registering error on database" :id (:id event)
:uri (str uri "/dbg/error/" (:id event)))
(l/debug :hint "registering error on database" :id (:id event)
:uri (str uri "/dbg/error/" (:id event)))
(persist-on-database! cfg event))
(catch Exception cause
(l/warn :hint "unexpected exception on database error logger" :cause cause)))))
(persist-on-database! cfg event))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]))
(defn error-event?
(defn- error-event?
[event]
(= "error" (:logger/level event)))
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req [::db/pool ::lzmq/receiver]))
(defmethod ig/init-key ::reporter
[_ {:keys [receiver] :as cfg}]
(l/info :msg "initializing database error persistence")
(let [output (a/chan (a/sliding-buffer 5) (filter error-event?))]
(receiver :sub output)
(a/go-loop []
(let [msg (a/<! output)]
(if (nil? msg)
(l/info :msg "stopping error reporting loop")
(do
(a/<! (handle-event cfg msg))
(recur)))))
output))
[_ {:keys [::lzmq/receiver] :as cfg}]
(px/thread
{:name "penpot/database-reporter"}
(l/info :hint "initializing database error persistence")
(let [input (a/chan (a/sliding-buffer 5)
(filter error-event?))]
(try
(lzmq/sub! receiver input)
(loop []
(when-let [msg (a/<!! input)]
(handle-event cfg msg))
(recur))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(a/close! input)
(l/info :hint "reporter terminated"))))))
(defmethod ig/halt-key! ::reporter
[_ output]
(a/close! output))
[_ thread]
(some-> thread px/interrupt!))

View File

@@ -38,13 +38,13 @@
(defn handle-event
[cfg event]
(try
(let [event (ldb/parse-event event)]
(when @enabled
(send-mattermost-notification! cfg event)))
(catch Throwable cause
(l/warn :hint "unhandled error"
:cause cause))))
(when @enabled
(try
(let [event (ldb/parse-event event)]
(send-mattermost-notification! cfg event))
(catch Throwable cause
(l/warn :hint "unhandled error"
:cause cause)))))
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req [::http/client

View File

@@ -8,6 +8,7 @@
"A mattermost integration for error reporting."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as l]
[app.common.transit :as t]
[app.common.uri :as uri]
@@ -21,6 +22,15 @@
[cuerdas.core :as str]
[integrant.core :as ig]))
;; --- HELPERS
(defn key-fn
[k & keys]
(fn [params]
(reduce #(dm/str %1 ":" (get params %2))
(dm/str (get params k))
keys)))
;; --- PROC
(defn- lookup-webhooks-by-team

View File

@@ -6,6 +6,7 @@
(ns app.main
(:require
[app.auth.ldap :as-alias ldap]
[app.auth.oidc :as-alias oidc]
[app.auth.oidc.providers :as-alias oidc.providers]
[app.common.logging :as l]
@@ -13,13 +14,13 @@
[app.db :as-alias db]
[app.http.client :as-alias http.client]
[app.http.session :as-alias http.session]
[app.loggers.audit :as-alias audit]
[app.loggers.audit.tasks :as-alias audit.tasks]
[app.loggers.webhooks :as-alias webhooks]
[app.loggers.zmq :as-alias lzmq]
[app.metrics :as-alias mtx]
[app.metrics.definition :as-alias mdef]
[app.redis :as-alias rds]
[app.srepl :as-alias srepl]
[app.storage :as-alias sto]
[app.util.time :as dt]
[app.worker :as-alias wrk]
@@ -231,7 +232,7 @@
:max-body-size (cf/get :http-server-max-body-size)
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
:app.auth.ldap/provider
::ldap/provider
{:host (cf/get :ldap-host)
:port (cf/get :ldap-port)
:ssl (cf/get :ldap-ssl)
@@ -266,10 +267,8 @@
:github (ig/ref ::oidc.providers/github)
:gitlab (ig/ref ::oidc.providers/gitlab)
:oidc (ig/ref ::oidc.providers/generic)}
::audit/collector (ig/ref ::audit/collector)
::http.session/session (ig/ref :app.http.session/manager)}
;; TODO: revisit the dependencies of this service, looks they are too much unused of them
:app.http/router
{:assets (ig/ref :app.http.assets/handlers)
@@ -322,11 +321,11 @@
:scheduled-executor (ig/ref ::wrk/scheduled-executor)}
:app.rpc/methods
{::audit/collector (ig/ref ::audit/collector)
::http.client/client (ig/ref ::http.client/client)
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref :app.setup/props)
::ldap/provider (ig/ref ::ldap/provider)
:pool (ig/ref ::db/pool)
:session (ig/ref :app.http.session/manager)
:sprops (ig/ref :app.setup/props)
@@ -335,7 +334,6 @@
:msgbus (ig/ref :app.msgbus/msgbus)
:public-uri (cf/get :public-uri)
:redis (ig/ref ::rds/redis)
:ldap (ig/ref :app.auth.ldap/provider)
:http-client (ig/ref ::http.client/client)
:climit (ig/ref :app.rpc/climit)
:rlimit (ig/ref :app.rpc/rlimit)
@@ -403,12 +401,13 @@
::http.client/client (ig/ref ::http.client/client)
::props (ig/ref :app.setup/props)}
:app.srepl/server
{:port (cf/get :srepl-port)
:host (cf/get :srepl-host)}
[::srepl/urepl ::srepl/server]
{:port (cf/get :urepl-port 6062)
:host (cf/get :urepl-host "localhost")}
:app.setup/initial-profile
{::db/pool (ig/ref ::db/pool)}
[::srepl/prepl ::srepl/server]
{:port (cf/get :prepl-port 6063)
:host (cf/get :prepl-host "localhost")}
:app.setup/builtin-templates
{::http.client/client (ig/ref ::http.client/client)}
@@ -420,11 +419,6 @@
::lzmq/receiver
{}
::audit/collector
{::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::mtx/metrics (ig/ref ::mtx/metrics)}
::audit.tasks/archive
{::props (ig/ref :app.setup/props)
::db/pool (ig/ref ::db/pool)
@@ -450,9 +444,8 @@
::http.client/client (ig/ref ::http.client/client)}
:app.loggers.database/reporter
{:receiver (ig/ref :app.loggers.zmq/receiver)
:pool (ig/ref ::db/pool)
:executor (ig/ref ::wrk/executor)}
{::lzmq/receiver (ig/ref :app.loggers.zmq/receiver)
::db/pool (ig/ref ::db/pool)}
::sto/storage
{:pool (ig/ref ::db/pool)

View File

@@ -299,10 +299,10 @@
{:name "0096-del-storage-pending-table"
:fn (mg/resource "app/migrations/sql/0096-del-storage-pending-table.sql")}
{:name "0097-mod-profile-table"
:fn (mg/resource "app/migrations/sql/0097-mod-profile-table.sql")}
{:name "0098-add-quotes-table"
:fn (mg/resource "app/migrations/sql/0098-add-quotes-table.sql")}
])
])
(defmethod ig/init-key ::migrations [_ _] migrations)

View File

@@ -1,2 +0,0 @@
ALTER TABLE profile
ADD COLUMN is_admin boolean DEFAULT false;

View File

@@ -0,0 +1,82 @@
CREATE TABLE usage_quote (
id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY,
target text NOT NULL,
quote bigint NOT NULL,
profile_id uuid NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE,
project_id uuid NULL REFERENCES project(id) ON DELETE CASCADE DEFERRABLE,
team_id uuid NULL REFERENCES team(id) ON DELETE CASCADE DEFERRABLE,
file_id uuid NULL REFERENCES file(id) ON DELETE CASCADE DEFERRABLE
);
ALTER TABLE usage_quote
ALTER COLUMN target SET STORAGE external;
CREATE INDEX usage_quote__profile_id__idx ON usage_quote(profile_id, target);
CREATE INDEX usage_quote__project_id__idx ON usage_quote(project_id, target);
CREATE INDEX usage_quote__team_id__idx ON usage_quote(team_id, target);
-- DROP TABLE IF EXISTS usage_quote_test;
-- CREATE TABLE usage_quote_test (
-- id bigserial NOT NULL PRIMARY KEY,
-- target text NOT NULL,
-- quote bigint NOT NULL,
-- profile_id bigint NULL,
-- team_id bigint NULL,
-- project_id bigint NULL,
-- file_id bigint NULL
-- );
-- ALTER TABLE usage_quote_test
-- ALTER COLUMN target SET STORAGE external;
-- CREATE INDEX usage_quote_test__profile_id__idx ON usage_quote_test(profile_id, target);
-- CREATE INDEX usage_quote_test__project_id__idx ON usage_quote_test(project_id, target);
-- CREATE INDEX usage_quote_test__team_id__idx ON usage_quote_test(team_id, target);
-- -- CREATE INDEX usage_quote_test__target__idx ON usage_quote_test(target);
-- DELETE FROM usage_quote_test;
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
-- SELECT 'files-per-project', 50*RANDOM(), 2000*RANDOM(), null, null
-- FROM generate_series(1, 5000);
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
-- SELECT 'files-per-project', 200*RANDOM(), 300*RANDOM(), 300*RANDOM(), null
-- FROM generate_series(1, 1000);
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
-- SELECT 'files-per-project', 100*RANDOM(), 300*RANDOM(), null, 300*RANDOM()
-- FROM generate_series(1, 1000);
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
-- SELECT 'files-per-project', 100*RANDOM(), 300*RANDOM(), 300*RANDOM(), 300*RANDOM()
-- FROM generate_series(1, 1000);
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
-- SELECT 'files-per-project', 30*RANDOM(), null, 2000*RANDOM(), null
-- FROM generate_series(1, 5000);
-- INSERT INTO usage_quote_test (target, quote, profile_id, team_id, project_id)
-- SELECT 'files-per-project', 10*RANDOM(), null, null, 2000*RANDOM()
-- FROM generate_series(1, 5000);
-- VACUUM ANALYZE usage_quote_test;
-- select * from usage_quote_test
-- where target = 'files-per-project'
-- and profile_id = 1
-- and team_id is null
-- and project_id is null;
-- select * from usage_quote_test
-- where target = 'files-per-project'
-- and ((team_id = 1 and (profile_id = 1 or profile_id is null)) or
-- (profile_id = 1 and team_id is null and project_id is null));
-- select * from usage_quote_test
-- where target = 'files-per-project'
-- and ((project_id = 1 and (profile_id = 1 or profile_id is null)) or
-- (team_id = 1 and (profile_id = 1 or profile_id is null)) or
-- (profile_id = 1 and team_id is null and project_id is null));

View File

@@ -6,11 +6,12 @@
(ns app.rpc
(:require
[app.common.data :as d]
[app.auth.ldap :as-alias ldap]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http :as-alias http]
[app.http.client :as-alias http.client]
@@ -26,7 +27,7 @@
[app.rpc.rlimit :as rlimit]
[app.storage :as-alias sto]
[app.util.services :as sv]
[app.util.time :as ts]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
@@ -70,14 +71,16 @@
(defn- rpc-query-handler
"Ring handler that dispatches query requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id params] :as request} respond raise]
(let [type (keyword (:type params))
data (into {::http/request request} params)
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
(let [type (keyword (:type path-params))
data (-> params
(assoc ::request-at (dt/now))
(assoc ::http/request request))
data (if profile-id
(assoc data
:profile-id profile-id
::profile-id profile-id
::session-id session-id)
(-> data
(assoc :profile-id profile-id)
(assoc ::profile-id profile-id)
(assoc ::session-id session-id))
(dissoc data :profile-id ::profile-id))
method (get methods type default-handler)]
@@ -91,16 +94,17 @@
(defn- rpc-mutation-handler
"Ring handler that dispatches mutation requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id params] :as request} respond raise]
(let [type (keyword (:type params))
data (into {::http/request request} params)
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
(let [type (keyword (:type path-params))
data (-> params
(assoc ::request-at (dt/now))
(assoc ::http/request request))
data (if profile-id
(assoc data
:profile-id profile-id
::profile-id profile-id
::session-id session-id)
(-> data
(assoc :profile-id profile-id)
(assoc ::profile-id profile-id)
(assoc ::session-id session-id))
(dissoc data :profile-id ::profile-id))
method (get methods type default-handler)]
(-> (method data)
(p/then (partial handle-response request))
@@ -112,13 +116,18 @@
(defn- rpc-command-handler
"Ring handler that dispatches cmd requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id params] :as request} respond raise]
(let [cmd (keyword (:type params))
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
(let [cmd (keyword (:type path-params))
etag (yrq/get-header request "if-none-match")
data (into {::http/request request ::cond/key etag} params)
data (if profile-id
(assoc data ::profile-id profile-id ::session-id session-id)
(dissoc data ::profile-id))
data (-> params
(assoc ::request-at (dt/now))
(assoc ::http/request request)
(assoc ::cond/key etag)
(cond-> (uuid? profile-id)
(-> (assoc ::profile-id profile-id)
(assoc ::session-id session-id))))
method (get methods cmd default-handler)]
(binding [cond/*enabled* true]
(-> (method data)
@@ -133,7 +142,7 @@
[{:keys [metrics ::metrics-id]} f mdata]
(let [labels (into-array String [(::sv/name mdata)])]
(fn [cfg params]
(let [tp (ts/tpoint)]
(let [tp (dt/tpoint)]
(p/finally
(f cfg params)
(fn [_ _]
@@ -155,7 +164,8 @@
(defn- wrap-audit
[cfg f mdata]
(if-let [collector (::audit/collector cfg)]
(if (or (contains? cf/flags :webhooks)
(contains? cf/flags :audit-log))
(letfn [(handle-audit [params result]
(let [resultm (meta result)
request (::http/request params)
@@ -172,8 +182,7 @@
(merge (::audit/props resultm))
(dissoc :profile-id)
(dissoc :type)))
(d/without-qualified)
(d/without-nils))
(audit/clean-props))
event {:type (or (::audit/type resultm)
(::type cfg))
@@ -182,6 +191,12 @@
:profile-id profile-id
:ip-addr (some-> request audit/parse-client-ip)
:props props
;; NOTE: for batch-key lookup we need the params as-is
;; because the rpc api does not need to know the
;; audit/webhook specific object layout.
::params (dissoc params ::http/request)
::webhooks/batch-key
(or (::webhooks/batch-key mdata)
(::webhooks/batch-key resultm))
@@ -195,13 +210,14 @@
(::webhooks/event? resultm)
false)}]
(audit/submit! collector event)))
(audit/submit! cfg event)))
(handle-request [cfg params]
(->> (f cfg params)
(p/mcat (fn [result]
(->> (handle-audit params result)
(p/map (constantly result)))))))]
(p/fnly (fn [result cause]
(when-not cause
(handle-audit params result))))))]
(if-not (::audit/skip mdata)
(with-meta handle-request mdata)
f))
@@ -275,10 +291,10 @@
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
(->> (sv/scan-ns 'app.rpc.commands.binfile
'app.rpc.commands.comments
'app.rpc.commands.profile
'app.rpc.commands.management
'app.rpc.commands.verify-token
'app.rpc.commands.search
'app.rpc.commands.media
'app.rpc.commands.teams
'app.rpc.commands.auth
'app.rpc.commands.ldap
@@ -301,9 +317,9 @@
(s/def ::sprops map?)
(defmethod ig/pre-init-spec ::methods [_]
(s/keys :req [::audit/collector
::http.client/client
(s/keys :req [::http.client/client
::db/pool
::ldap/provider
::wrk/executor]
:req-un [::sto/storage
::http.session/session
@@ -314,8 +330,7 @@
::climit
::wrk/executor
::mtx/metrics
::db/pool
::ldap]))
::db/pool]))
(defmethod ig/init-key ::methods
[_ cfg]

View File

@@ -72,7 +72,7 @@
(sv/defmethod ::push-audit-events
{::climit/queue :push-audit-events
::climit/key-fn :profile-id
::climit/key-fn ::rpc/profile-id
::audit/skip true
::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} params]

View File

@@ -69,7 +69,7 @@
;; ---- COMMAND: login with password
(defn login-with-password
[{:keys [::db/pool session] :as cfg} {:keys [email password scope] :as params}]
[{:keys [::db/pool session] :as cfg} {:keys [email password] :as params}]
(when-not (or (contains? cf/flags :login)
(contains? cf/flags :login-with-password))
@@ -119,17 +119,8 @@
;; accept invitation with other email
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
{:invitation-token (:invitation-token params)}
(update profile :is-admin (fn [admin?]
(or admin?
(let [admins (cf/get :admins)]
(contains? admins (:email profile)))))))]
(when (and (nil? (:default-team-id profile))
(not= scope "admin"))
(ex/raise :type :restriction
:code :admin-only-profile
:hint "can't login with admin-only profile"))
(assoc profile :is-admin (let [admins (cf/get :admins)]
(contains? admins (:email profile)))))]
(-> response
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-meta {::audit/props (audit/profile->props profile)
@@ -322,6 +313,7 @@
(throw e)
(ex/raise :type :validation
:code :email-already-exists
:hint "email already exists"
:cause e)))))))
(defn create-profile-relations
@@ -356,7 +348,7 @@
:extra-data ptoken})))
(defn register-profile
[{:keys [conn session] :as cfg} {:keys [token] :as params}]
[{:keys [::db/conn session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
params (merge params claims)
@@ -380,11 +372,10 @@
;; accordingly.
(when-let [id (:profile-id claims)]
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
(when-let [collector (::audit/collector cfg)]
(audit/submit! collector
{:type "fact"
:name "register-profile-retry"
:profile-id id})))
(audit/submit! cfg
{:type "fact"
:name "register-profile-retry"
:profile-id id}))
(cond
;; If invitation token comes in params, this is because the
@@ -436,7 +427,7 @@
::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(-> (assoc cfg ::db/conn conn)
(register-profile params))))
;; ---- COMMAND: Request Profile Recovery

View File

@@ -9,6 +9,7 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.files.features :as ffeat]
[app.common.logging :as l]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
@@ -28,6 +29,7 @@
[app.tasks.file-gc]
[app.util.blob :as blob]
[app.util.fressian :as fres]
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -609,12 +611,23 @@
(vswap! *state* update :index update-index files)
(vswap! *state* assoc :version version :files files)))
(defn- postprocess-file
[data]
(let [omap-wrap ffeat/*wrap-with-objects-map-fn*
pmap-wrap ffeat/*wrap-with-pointer-map-fn*]
(-> data
(update :pages-index update-vals #(update % :objects omap-wrap))
(update :pages-index update-vals pmap-wrap)
(update :components update-vals #(update % :objects omap-wrap))
(update :components pmap-wrap))))
(defmethod read-section :v1/files
[{:keys [conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}]
(doseq [expected-file-id (-> *state* deref :files)]
(let [file (read-obj! input)
media' (read-obj! input)
file-id (:id file)]
(let [file (read-obj! input)
media' (read-obj! input)
file-id (:id file)
features files/default-features]
(when (not= file-id expected-file-id)
(ex/raise :type :validation
@@ -629,33 +642,42 @@
(l/debug :hint "update media references" ::l/async false)
(vswap! *state* update :media into (map #(update % :id lookup-index)) media')
(l/debug :hint "processing file" :file-id file-id ::l/async false)
(l/debug :hint "processing file" :file-id file-id ::features features ::l/async false)
(let [file-id' (lookup-index file-id)
data (-> (:data file)
(assoc :id file-id')
(cond-> migrate? (pmg/migrate-data))
(update :pages-index relink-shapes)
(update :components relink-shapes)
(update :media relink-media))
(binding [ffeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity)
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)
pmap/*tracked* (atom {})]
params {:id file-id'
:project-id project-id
:name (:name file)
:revn (:revn file)
:is-shared (:is-shared file)
:data (blob/encode data)
:created-at timestamp
:modified-at timestamp}]
(let [file-id' (lookup-index file-id)
data (-> (:data file)
(assoc :id file-id')
(cond-> migrate? (pmg/migrate-data))
(update :pages-index relink-shapes)
(update :components relink-shapes)
(update :media relink-media)
(postprocess-file))
(l/debug :hint "create file" :id file-id' ::l/async false)
params {:id file-id'
:project-id project-id
:features (db/create-array conn "text" features)
:name (:name file)
:revn (:revn file)
:is-shared (:is-shared file)
:data (blob/encode data)
:created-at timestamp
:modified-at timestamp}]
(if overwrite?
(create-or-update-file conn params)
(db/insert! conn :file params))
(l/debug :hint "create file" :id file-id' ::l/async false)
(when overwrite?
(db/delete! conn :file-thumbnail {:file-id file-id'}))))))
(if overwrite?
(create-or-update-file conn params)
(db/insert! conn :file params))
(files/persist-pointers! conn file-id')
(when overwrite?
(db/delete! conn :file-thumbnail {:file-id file-id'})))))))
(defmethod read-section :v1/rels
[{:keys [conn ::input ::timestamp]}]

View File

@@ -6,9 +6,11 @@
(ns app.rpc.commands.comments
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
@@ -16,16 +18,14 @@
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.blob :as blob]
[app.rpc.quotes :as quotes]
[app.util.pointer-map :as pmap]
[app.util.retry :as rtry]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUERY COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- GENERAL PURPOSE INTERNAL HELPERS
(defn decode-row
[{:keys [participants position] :as row}]
@@ -33,9 +33,61 @@
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
(def sql:get-file
"select f.id, f.modified_at, f.revn, f.features,
f.project_id, p.team_id, f.data
from file as f
join project as p on (p.id = f.project_id)
where f.id = ?
and f.deleted_at is null")
(defn- get-file
"A specialized version of get-file for comments module."
[conn file-id page-id]
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
(if-let [{:keys [data] :as file} (some-> (db/exec-one! conn [sql:get-file file-id]) (files/decode-row))]
(-> file
(assoc :page-name (dm/get-in data [:pages-index page-id :name]))
(assoc :page-id page-id))
(ex/raise :type :not-found
:code :object-not-found
:hint "file not found"))))
(defn- get-comment-thread
[conn thread-id & {:keys [for-update?]}]
(-> (db/get-by-id conn :comment-thread thread-id {:for-update for-update?})
(decode-row)))
(defn- get-comment
[conn comment-id & {:keys [for-update?]}]
(db/get-by-id conn :comment comment-id {:for-update for-update?}))
(defn- get-next-seqn
[conn file-id]
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
res (db/exec-one! conn [sql file-id])]
(:next-seqn res)))
(def sql:upsert-comment-thread-status
"insert into comment_thread_status (thread_id, profile_id, modified_at)
values (?, ?, ?)
on conflict (thread_id, profile_id)
do update set modified_at = ?
returning modified_at;")
(defn upsert-comment-thread-status!
([conn profile-id thread-id]
(upsert-comment-thread-status! conn profile-id thread-id (dt/now)))
([conn profile-id thread-id mod-at]
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id mod-at mod-at])))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUERY COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- COMMAND: Get Comment Threads
(declare retrieve-comment-threads)
(declare ^:private get-comment-threads)
(s/def ::team-id ::us/uuid)
(s/def ::file-id ::us/uuid)
@@ -48,9 +100,10 @@
(sv/defmethod ::get-comment-threads
{::doc/added "1.15"}
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
(with-open [conn (db/open pool)]
(retrieve-comment-threads conn params)))
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-comment-threads conn profile-id file-id)))
(def sql:comment-threads
"select distinct on (ct.id)
@@ -74,15 +127,14 @@
where ct.file_id = ?
window w as (partition by c.thread_id order by c.created_at asc)")
(defn retrieve-comment-threads
[conn {:keys [::rpc/profile-id file-id share-id]}]
(files/check-comment-permissions! conn profile-id file-id share-id)
(defn- get-comment-threads
[conn profile-id file-id]
(->> (db/exec! conn [sql:comment-threads profile-id file-id])
(into [] (map decode-row))))
;; --- COMMAND: Get Unread Comment Threads
(declare retrieve-unread-comment-threads)
(declare ^:private get-unread-comment-threads)
(s/def ::team-id ::us/uuid)
(s/def ::get-unread-comment-threads
@@ -94,7 +146,7 @@
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(retrieve-unread-comment-threads conn params)))
(get-unread-comment-threads conn profile-id team-id)))
(def sql:comment-threads-by-team
"select distinct on (ct.id)
@@ -123,19 +175,17 @@
(str "with threads as (" sql:comment-threads-by-team ")"
"select * from threads where count_unread_comments > 0"))
(defn retrieve-unread-comment-threads
[conn {:keys [::rpc/profile-id team-id]}]
(defn- get-unread-comment-threads
[conn profile-id team-id]
(->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
(into [] (map decode-row))))
;; --- COMMAND: Get Single Comment Thread
(s/def ::id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-comment-thread
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::id]
:req-un [::file-id ::us/id]
:opt-un [::share-id]))
(sv/defmethod ::get-comment-thread
@@ -148,19 +198,10 @@
(-> (db/exec-one! conn [sql profile-id file-id id])
(decode-row)))))
(defn get-comment-thread
[conn {:keys [::rpc/profile-id file-id id] :as params}]
(let [sql (str "with threads as (" sql:comment-threads ")"
"select * from threads where id = ?")]
(-> (db/exec-one! conn [sql profile-id file-id id])
(decode-row))))
;; --- COMMAND: Retrieve Comments
(declare get-comments)
(declare ^:private get-comments)
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::thread-id ::us/uuid)
(s/def ::get-comments
(s/keys :req [::rpc/profile-id]
@@ -171,16 +212,16 @@
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
(with-open [conn (db/open pool)]
(let [thread (db/get-by-id conn :comment-thread thread-id)]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id))
(get-comments conn thread-id)))
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-comments conn thread-id))))
(def sql:comments
"select c.* from comment as c
where c.thread_id = ?
order by c.created_at asc")
(defn get-comments
(defn- get-comments
[conn thread-id]
(->> (db/query conn :comment
{:thread-id thread-id}
@@ -189,26 +230,6 @@
;; --- COMMAND: Get file comments users
(declare get-file-comments-users)
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-profiles-for-file-comments
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
:opt-un [::share-id]))
(sv/defmethod ::get-profiles-for-file-comments
"Retrieves a list of profiles with limited set of properties of all
participants on comment threads of the file."
{::doc/added "1.15"
::doc/changes ["1.15" "Imported from queries and renamed."]}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
(with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-file-comments-users conn file-id profile-id)))
;; All the profiles that had comment the file, plus the current
;; profile.
@@ -231,20 +252,30 @@
[conn file-id profile-id]
(db/exec! conn [sql:file-comment-users file-id profile-id]))
(s/def ::get-profiles-for-file-comments
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
:opt-un [::share-id]))
(sv/defmethod ::get-profiles-for-file-comments
"Retrieves a list of profiles with limited set of properties of all
participants on comment threads of the file."
{::doc/added "1.15"
::doc/changes ["1.15" "Imported from queries and renamed."]}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
(with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-file-comments-users conn file-id profile-id)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private create-comment-thread)
;; --- COMMAND: Create Comment Thread
(declare upsert-comment-thread-status!)
(declare create-comment-thread)
(declare retrieve-page-name)
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::position ::gpt/point)
(s/def ::content ::us/string)
(s/def ::frame-id ::us/uuid)
@@ -257,63 +288,77 @@
(sv/defmethod ::create-comment-thread
{::doc/added "1.15"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
[{:keys [::db/pool] :as cfg}
{:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
(db/with-atomic [conn pool]
(files/check-comment-permissions! conn profile-id file-id share-id)
(let [{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(rtry/with-retry {::rtry/when rtry/conflict-exception?
::rtry/max-retries 3
::rtry/label "create-comment-thread"}
(create-comment-thread conn params))))
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/comment-threads-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id}
{::quotes/id ::quotes/comments-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id}))
(defn- retrieve-next-seqn
[conn file-id]
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
res (db/exec-one! conn [sql file-id])]
(:next-seqn res)))
(defn create-comment-thread
[conn {:keys [::rpc/profile-id file-id page-id position content frame-id] :as params}]
(let [seqn (retrieve-next-seqn conn file-id)
now (dt/now)
pname (retrieve-page-name conn params)
thread (db/insert! conn :comment-thread
{:file-id file-id
:owner-id profile-id
:participants (db/tjson #{profile-id})
:page-name pname
:page-id page-id
:created-at now
:modified-at now
:seqn seqn
:position (db/pgpoint position)
:frame-id frame-id})]
(rtry/with-retry {::rtry/when rtry/conflict-exception?
::rtry/max-retries 3
::rtry/label "create-comment-thread"}
(create-comment-thread conn
{:created-at request-at
:profile-id profile-id
:file-id file-id
:page-id page-id
:page-name page-name
:position position
:content content
:frame-id frame-id})))))
;; Create a comment entry
(db/insert! conn :comment
{:thread-id (:id thread)
:owner-id profile-id
:created-at now
:modified-at now
:content content})
(defn- create-comment-thread
[conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
(let [;; NOTE: we take the next seq number from a separate query because the whole
;; operation can be retried on conflict, and in this case the new seq shold be
;; retrieved from the database.
seqn (get-next-seqn conn file-id)
thread-id (uuid/next)
thread (db/insert! conn :comment-thread
{:id thread-id
:file-id file-id
:owner-id profile-id
:participants (db/tjson #{profile-id})
:page-name page-name
:page-id page-id
:created-at created-at
:modified-at created-at
:seqn seqn
:position (db/pgpoint position)
:frame-id frame-id})
comment (db/insert! conn :comment
{:id (uuid/next)
:thread-id thread-id
:owner-id profile-id
:created-at created-at
:modified-at created-at
:content content})]
;; Make the current thread as read.
(upsert-comment-thread-status! conn profile-id (:id thread))
(upsert-comment-thread-status! conn profile-id thread-id created-at)
;; Optimistic update of current seq number on file.
(db/update! conn :file
{:comment-thread-seqn seqn}
{:id file-id})
(select-keys thread [:id :file-id :page-id])))
(defn- retrieve-page-name
[conn {:keys [file-id page-id]}]
(let [{:keys [data]} (db/get-by-id conn :file file-id)
data (blob/decode data)]
(get-in data [:pages-index page-id :name])))
(-> thread
(select-keys [:id :file-id :page-id])
(assoc :comment-id (:id comment)))))
;; --- COMMAND: Update Comment Thread Status
@@ -329,23 +374,9 @@
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not cthr
(ex/raise :type :not-found))
(files/check-comment-permissions! conn profile-id (:file-id cthr) share-id)
(upsert-comment-thread-status! conn profile-id (:id cthr)))))
(def sql:upsert-comment-thread-status
"insert into comment_thread_status (thread_id, profile_id)
values (?, ?)
on conflict (thread_id, profile_id)
do update set modified_at = clock_timestamp()
returning modified_at;")
(defn upsert-comment-thread-status!
[conn profile-id thread-id]
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id]))
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(upsert-comment-thread-status! conn profile-id id))))
;; --- COMMAND: Update Comment Thread
@@ -360,12 +391,8 @@
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not thread
(ex/raise :type :not-found))
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(db/update! conn :comment-thread
{:is-resolved is-resolved}
{:id id})
@@ -374,6 +401,7 @@
;; --- COMMAND: Add Comment
(declare get-comment-thread)
(declare create-comment)
(s/def ::create-comment
@@ -384,66 +412,52 @@
(sv/defmethod ::create-comment
{::doc/added "1.15"
::webhooks/event? true}
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}]
(db/with-atomic [conn pool]
(create-comment conn params)))
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id :for-update? true)
{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)]
(defn create-comment
[conn {:keys [::rpc/profile-id thread-id content share-id] :as params}]
(let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
(decode-row))
pname (retrieve-page-name conn thread)]
(files/check-comment-permissions! conn profile-id (:id file) share-id)
(quotes/check-quote! conn
{::quotes/id ::quotes/comments-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id (:id file)})
;; Standard Checks
(when-not thread (ex/raise :type :not-found))
;; Update the page-name cached attribute on comment thread table.
(when (not= page-name (:page-name thread))
(db/update! conn :comment-thread
{:page-name page-name}
{:id thread-id}))
;; Permission Checks
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
(let [comment (db/insert! conn :comment
{:id (uuid/next)
:created-at request-at
:modified-at request-at
:thread-id thread-id
:owner-id profile-id
:content content})
props {:file-id file-id
:share-id nil}]
;; Update the page-name cachedattribute on comment thread table.
(when (not= pname (:page-name thread))
(db/update! conn :comment-thread
{:page-name pname}
{:id thread-id}))
;; Update thread modified-at attribute and assoc the current
;; profile to the participant set.
(db/update! conn :comment-thread
{:modified-at request-at
:participants (-> (:participants thread #{})
(conj profile-id)
(db/tjson))}
{:id thread-id})
;; NOTE: is important that all timestamptz related fields are
;; created or updated on the database level for avoid clock
;; inconsistencies (some user sees something read that is not
;; read, etc...)
(let [ppants (:participants thread #{})
comment (db/insert! conn :comment
{:thread-id thread-id
:owner-id profile-id
:content content})]
;; Update the current profile status in relation to the
;; current thread.
(upsert-comment-thread-status! conn profile-id thread-id request-at)
;; NOTE: this is done in SQL instead of using db/update!
;; helper because currently the helper does not allow pass raw
;; function call parameters to the underlying prepared
;; statement; in a future when we fix/improve it, this can be
;; changed to use the helper.
;; Update thread modified-at attribute and assoc the current
;; profile to the participant set.
(let [ppants (conj ppants profile-id)
sql "update comment_thread
set modified_at = clock_timestamp(),
participants = ?
where id = ?"]
(db/exec-one! conn [sql (db/tjson ppants) thread-id]))
;; Update the current profile status in relation to the
;; current thread.
(upsert-comment-thread-status! conn profile-id thread-id)
;; Return the created comment object.
(rph/with-meta comment
{::audit/props {:file-id (:file-id thread)
:share-id nil}}))))
(vary-meta comment assoc ::audit/props props)))))
;; --- COMMAND: Update Comment
(declare update-comment)
(s/def ::update-comment
(s/keys :req [::rpc/profile-id]
:req-un [::id ::content]
@@ -451,72 +465,70 @@
(sv/defmethod ::update-comment
{::doc/added "1.15"}
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}]
(db/with-atomic [conn pool]
(update-comment conn params)))
(let [{:keys [thread-id] :as comment} (get-comment conn id :for-update? true)
{:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id :for-update? true)]
(defn update-comment
[conn {:keys [::rpc/profile-id id content share-id] :as params}]
(let [comment (db/get-by-id conn :comment id {:for-update true})
_ (when-not comment (ex/raise :type :not-found))
thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
_ (when-not thread (ex/raise :type :not-found))
pname (retrieve-page-name conn thread)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
;; Don't allow edit comments to not owners
(when-not (= owner-id profile-id)
(ex/raise :type :validation
:code :not-allowed))
;; Don't allow edit comments to not owners
(when-not (= (:owner-id thread) profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/update! conn :comment
{:content content
:modified-at (dt/now)}
{:id (:id comment)})
(db/update! conn :comment-thread
{:modified-at (dt/now)
:page-name pname}
{:id (:id thread)})
nil))
(let [{:keys [page-name] :as file} (get-file conn file-id page-id)]
(db/update! conn :comment
{:content content
:modified-at request-at}
{:id id})
(db/update! conn :comment-thread
{:modified-at request-at
:page-name page-name}
{:id thread-id})
nil))))
;; --- COMMAND: Delete Comment Thread
(s/def ::delete-comment-thread
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
:req-un [::id]
:opt-un [::share-id]))
(sv/defmethod ::delete-comment-thread
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not (= (:owner-id thread) profile-id)
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(when-not (= owner-id profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/delete! conn :comment-thread {:id id})
nil)))
;; --- COMMAND: Delete comment
(s/def ::delete-comment
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
:req-un [::id]
:opt-un [::share-id]))
(sv/defmethod ::delete-comment
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})]
(when-not (= (:owner-id comment) profile-id)
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id :for-update? true)
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(when-not (= owner-id profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/delete! conn :comment {:id id}))))
;; --- COMMAND: Update comment thread position
(s/def ::update-comment-thread-position
@@ -528,10 +540,10 @@
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(db/update! conn :comment-thread
{:modified-at (dt/now)
{:modified-at (::rpc/request-at params)
:position (db/pgpoint position)
:frame-id frame-id}
{:id (:id thread)})
@@ -548,10 +560,10 @@
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(db/update! conn :comment-thread
{:modified-at (dt/now)
{:modified-at (::rpc/request-at params)
:frame-id frame-id}
{:id (:id thread)})
{:id id})
nil)))

View File

@@ -15,6 +15,7 @@
[app.common.spec :as us]
[app.common.types.file :as ctf]
[app.common.types.shape-tree :as ctt]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.loggers.audit :as-alias audit]
@@ -43,7 +44,13 @@
"storage/pointer-map"
"components/v2"})
(def default-features #{})
(def default-features
(cond-> #{}
(contains? cf/flags :fdata-storage-pointer-map)
(conj "storage/pointer-map")
(contains? cf/flags :fdata-storage-objects-map)
(conj "storage/objects-map")))
;; --- SPECS
@@ -151,11 +158,14 @@
(def check-read-permissions!
(perms/make-check-fn has-read-permissions?))
;; A user has comment permissions if she has read permissions, or comment permissions
;; A user has comment permissions if she has read permissions, or
;; explicit comment permissions through the share-id
(defn check-comment-permissions!
[conn profile-id file-id share-id]
(let [can-read (has-read-permissions? conn profile-id file-id)
can-comment (has-comment-permissions? conn profile-id file-id share-id)]
(let [perms (get-permissions conn profile-id file-id share-id)
can-read (has-read-permissions? perms)
can-comment (has-comment-permissions? perms)]
(when-not (or can-read can-comment)
(ex/raise :type :not-found
:code :object-not-found
@@ -241,7 +251,6 @@
[conn id client-features]
;; here we check if client requested features are supported
(check-features-compatibility! client-features)
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(-> (db/get-by-id conn :file id)
(decode-row)
@@ -266,7 +275,7 @@
{::doc/added "1.17"
::cond/get-object #(get-minimal-file %1 (:id %2))
::cond/key-fn get-file-etag}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features] :as params}]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features]}]
(with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id id)]
(check-read-permissions! perms)
@@ -294,7 +303,7 @@
"Retrieve a file by its ID. Only authenticated users."
{::doc/added "1.17"
::rpc/:auth false}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] :as params}]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
(with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id file-id share-id)]
(check-read-permissions! perms)
@@ -361,7 +370,7 @@
(sv/defmethod ::get-project-files
"Get all files for the specified project."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
(with-open [conn (db/open pool)]
(projects/check-read-permissions! conn profile-id project-id)
(get-project-files conn project-id)))
@@ -374,15 +383,16 @@
(s/def ::file-id ::us/uuid)
(s/def ::has-file-libraries
(s/keys :req [::rpc/profile-id] :req-un [::file-id]))
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]))
(sv/defmethod ::has-file-libraries
"Checks if the file has libraries. Returns a boolean"
{::doc/added "1.15.1"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(with-open [conn (db/open pool)]
(check-read-permissions! pool profile-id file-id)
(get-has-file-libraries conn params)))
(get-has-file-libraries conn file-id)))
(def ^:private sql:has-file-libraries
"SELECT COUNT(*) > 0 AS has_libraries
@@ -393,7 +403,7 @@
fl.deleted_at > now())")
(defn- get-has-file-libraries
[conn {:keys [file-id]}]
[conn file-id]
(let [row (db/exec-one! conn [sql:has-file-libraries file-id])]
(:has-libraries row)))
@@ -472,37 +482,37 @@
order by f.modified_at desc")
(defn get-team-shared-files
[conn {:keys [team-id] :as params}]
(let [assets-sample
(fn [assets limit]
(let [sorted-assets (->> (vals assets)
(sort-by #(str/lower (:name %))))]
[conn team-id]
(letfn [(assets-sample [assets limit]
(let [sorted-assets (->> (vals assets)
(sort-by #(str/lower (:name %))))]
{:count (count sorted-assets)
:sample (into [] (take limit sorted-assets))}))
{:count (count sorted-assets)
:sample (into [] (take limit sorted-assets))}))
(library-summary [{:keys [id data] :as file}]
(binding [pmap/*load-fn* (partial load-pointer conn id)]
{:components (assets-sample (:components data) 4)
:media (assets-sample (:media data) 3)
:colors (assets-sample (:colors data) 3)
:typographies (assets-sample (:typographies data) 3)}))]
library-summary
(fn [data]
{:components (assets-sample (:components data) 4)
:colors (assets-sample (:colors data) 3)
:typographies (assets-sample (:typographies data) 3)})
xform (comp
(map decode-row)
(map #(assoc % :library-summary (library-summary (:data %))))
(map #(dissoc % :data)))]
(into #{} xform (db/exec! conn [sql:team-shared-files team-id]))))
(->> (db/exec! conn [sql:team-shared-files team-id])
(into #{} (comp
(map decode-row)
(map #(assoc % :library-summary (library-summary %)))
(map #(dissoc % :data)))))))
(s/def ::get-team-shared-files
(s/keys :req [::rpc/profile-id] :req-un [::team-id]))
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(sv/defmethod ::get-team-shared-files
"Get all file (libraries) for the specified team."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(get-team-shared-files conn params)))
(teams/check-read-permissions! conn profile-id team-id)
(get-team-shared-files conn team-id)))
;; --- COMMAND QUERY: get-file-libraries
@@ -536,12 +546,14 @@
[conn file-id client-features]
(check-features-compatibility! client-features)
(->> (db/exec! conn [sql:file-libraries file-id])
(mapv (fn [{:keys [id] :as row}]
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(-> (decode-row row)
(assoc :is-indirect false)
(update :data dissoc :pages-index)
(handle-file-features client-features)))))))
(map decode-row)
(map #(assoc % :is-indirect false))
(map (fn [{:keys [id] :as row}]
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(-> row
(update :data dissoc :pages-index)
(handle-file-features client-features)))))
(vec)))
(s/def ::get-file-libraries
(s/keys :req [::rpc/profile-id]
@@ -551,7 +563,7 @@
(sv/defmethod ::get-file-libraries
"Get libraries used by the specified file."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as params}]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
(get-file-libraries conn file-id features)))
@@ -582,7 +594,6 @@
(check-read-permissions! conn profile-id file-id)
(get-library-file-references conn file-id)))
;; --- COMMAND QUERY: get-team-recent-files
(def sql:team-recent-files
@@ -711,28 +722,30 @@
objects)))]
(let [frame (get-thumbnail-frame data)
frame-id (:id frame)
page-id (or (:page-id frame)
(-> data :pages first))
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(let [frame (get-thumbnail-frame data)
frame-id (:id frame)
page-id (or (:page-id frame)
(-> data :pages first))
page (dm/get-in data [:pages-index page-id])
frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page))))
page (dm/get-in data [:pages-index page-id])
page (cond-> page (pmap/pointer-map? page) deref)
frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page))))
obj-ids (map #(str page-id %) frame-ids)
thumbs (get-object-thumbnails conn id obj-ids)]
obj-ids (map #(str page-id %) frame-ids)
thumbs (get-object-thumbnails conn id obj-ids)]
(cond-> page
;; If we have frame, we need to specify it on the page level
;; and remove the all other unrelated objects.
(some? frame-id)
(-> (assoc :thumbnail-frame-id frame-id)
(update :objects filter-objects frame-id))
(cond-> page
;; If we have frame, we need to specify it on the page level
;; and remove the all other unrelated objects.
(some? frame-id)
(-> (assoc :thumbnail-frame-id frame-id)
(update :objects filter-objects frame-id))
;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecessary data.
:always
(update :objects assoc-thumbnails page-id thumbs)))))
;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecessary data.
:always
(update :objects assoc-thumbnails page-id thumbs))))))
(s/def ::get-file-data-for-thumbnail
(s/keys :req [::rpc/profile-id]
@@ -746,12 +759,15 @@
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
(let [file (get-file conn file-id features)]
;; NOTE: we force here the "storage/pointer-map" feature, because
;; it used internally only and is independent if user supports it
;; or not.
(let [feat (into #{"storage/pointer-map"} features)
file (get-file conn file-id feat)]
{:file-id file-id
:revn (:revn file)
:page (get-file-data-for-thumbnail conn file)})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -759,7 +775,7 @@
;; --- MUTATION COMMAND: rename-file
(defn rename-file
[conn {:keys [id name] :as params}]
[conn {:keys [id name]}]
(db/update! conn :file
{:name name
:modified-at (dt/now)}
@@ -893,7 +909,7 @@
;; --- MUTATION COMMAND: unlink-file-from-library
(defn unlink-file-from-library
[conn {:keys [file-id library-id] :as params}]
[conn {:keys [file-id library-id]}]
(db/delete! conn :file-library-rel
{:file-id file-id
:library-file-id library-id}))
@@ -954,7 +970,8 @@
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
(ignore-sync conn params)))
(-> (ignore-sync conn params)
(update :features db/decode-pgarray #{}))))
;; --- MUTATION COMMAND: upsert-file-object-thumbnail
@@ -979,7 +996,8 @@
:opt-un [::data]))
(sv/defmethod ::upsert-file-object-thumbnail
{::doc/added "1.17"}
{::doc/added "1.17"
::audit/skip true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
@@ -1009,7 +1027,8 @@
(sv/defmethod ::upsert-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.17"}
{::doc/added "1.17"
::audit/skip true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)

View File

@@ -18,6 +18,7 @@
[app.rpc.doc :as-alias doc]
[app.rpc.permissions :as perms]
[app.rpc.queries.projects :as proj]
[app.rpc.quotes :as quotes]
[app.util.blob :as blob]
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
@@ -84,6 +85,12 @@
(proj/check-edition-permissions! conn profile-id project-id)
(let [team-id (files/get-team-id conn project-id)
params (assoc params :profile-id profile-id)]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
::quotes/profile-id profile-id
::quotes/project-id project-id}))
(-> (create-file conn params)
(vary-meta assoc ::audit/props {:team-id team-id})))))

View File

@@ -40,7 +40,7 @@
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(db/with-atomic [conn pool]
(proj/check-edition-permissions! conn profile-id project-id)
(files.create/create-file conn (assoc params :deleted-at (dt/in-future {:days 1})))))
(files.create/create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1})))))
;; --- MUTATION COMMAND: update-temp-file

View File

@@ -17,7 +17,7 @@
[app.config :as cf]
[app.db :as db]
[app.loggers.audit :as audit]
[app.loggers.webhooks :as-alias webhooks]
[app.loggers.webhooks :as webhooks]
[app.metrics :as mtx]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
@@ -130,7 +130,7 @@
::climit/key-fn :id
::webhooks/event? true
::webhooks/batch-timeout (dt/duration "2m")
::webhooks/batch-key :id
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
@@ -168,7 +168,18 @@
(->> changes-with-metadata (mapcat :changes) vec)
(vec changes))
params (assoc params :file file :changes changes)]
params (-> params
(assoc :file file)
(assoc :changes changes)
(assoc ::created-at (dt/now)))]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
@@ -180,24 +191,15 @@
(-> (update-fn cfg params)
(vary-meta assoc ::audit/replace-props
{:id (:id file)
:name (:name file)
:features (:features file)
{:id (:id file)
:name (:name file)
:features (:features file)
:project-id (:project-id file)
:team-id (:team-id file)}))))))
(defn- update-file*
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id] :as params}]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
(let [ts (dt/now)
file (-> file
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
(let [file (-> file
(update :revn inc)
(update :data (fn [data]
(cond-> data
@@ -217,7 +219,7 @@
{:id (uuid/next)
:session-id session-id
:profile-id profile-id
:created-at ts
:created-at created-at
:file-id (:id file)
:revn (:revn file)
:features (db/create-array conn "text" (:features file))
@@ -229,12 +231,12 @@
{:revn (:revn file)
:data (:data file)
:data-backend nil
:modified-at ts
:modified-at created-at
:has-media-trimmed false}
{:id (:id file)})
(db/update! conn :project
{:modified-at ts}
{:modified-at created-at}
{:id (:project-id file)})
(let [params (assoc params :file file)]
@@ -265,13 +267,10 @@
order by s.created_at asc")
(defn- get-lagged-changes
[conn params]
(->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)])
(into [] (comp (map files/decode-row)
(map (fn [row]
(cond-> row
(= (:revn row) (:revn (:file params)))
(assoc :changes []))))))))
[conn {:keys [id revn] :as params}]
(->> (db/exec! conn [sql:lagged-changes id revn])
(map files/decode-row)
(vec)))
(defn- send-notifications!
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}]

View File

@@ -12,10 +12,13 @@
[app.db :as db]
[app.http.session :as session]
[app.loggers.audit :as-alias audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.profile :as profile]
[app.tokens :as tokens]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
@@ -34,15 +37,15 @@
(sv/defmethod ::login-with-ldap
"Performs the authentication using LDAP backend. Only works if LDAP
is properly configured and enabled with `login-with-ldap` flag."
{:auth false
{::rpc/auth false
::doc/added "1.15"}
[{:keys [session tokens ldap] :as cfg} params]
(when-not ldap
[{:keys [::main/props ::ldap/provider session] :as cfg} params]
(when-not provider
(ex/raise :type :restriction
:code :ldap-not-initialized
:hide "ldap auth provider is not initialized"))
(let [info (ldap/authenticate ldap params)]
(let [info (ldap/authenticate provider params)]
(when-not info
(ex/raise :type :validation
:code :wrong-credentials))
@@ -58,12 +61,11 @@
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged).
(let [claims (tokens :verify {:token token :iss :team-invitation})
(let [claims (tokens/verify props {:token token :iss :team-invitation})
claims (assoc claims
:member-id (:id profile)
:member-email (:email profile))
token (tokens :generate claims)]
token (tokens/generate props claims)]
(-> {:invitation-token token}
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-meta {::audit/props (:props profile)

View File

@@ -46,9 +46,9 @@
"Duplicate a single file in the same team."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(duplicate-file conn (assoc params :profile-id (::rpc/profile-id params)))))
(duplicate-file conn (assoc params :profile-id profile-id))))
(defn- remap-id
[item index key]
@@ -136,7 +136,7 @@
and so.deleted_at is null")
(defn duplicate-file*
[conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag] :as opts}]
[conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag]}]
(let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)]))
fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
@@ -329,10 +329,9 @@
"Move a set of files from one project to other."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(move-files conn (assoc params :profile-id (::rpc/profile-id params)))))
(move-files conn (assoc params :profile-id profile-id))))
;; --- COMMAND: Move project
@@ -370,9 +369,9 @@
"Move projects between teams."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(move-project conn (assoc params :profile-id (::rpc/profile-id params)))))
(move-project conn (assoc params :profile-id profile-id))))
;; --- COMMAND: Clone Template
@@ -387,10 +386,10 @@
"Clone into the specified project the template by its id."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(clone-template (assoc params :profile-id (::rpc/profile-id params))))))
(clone-template (assoc params :profile-id profile-id)))))
(defn- clone-template
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]

View File

@@ -0,0 +1,274 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.commands.media
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.media :as cm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http.client :as http]
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc]
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.io :as io]
[promesa.core :as p]
[promesa.exec :as px]))
(def default-max-file-size
(* 1024 1024 10)) ; 10 MiB
(def thumbnail-options
{:width 100
:height 100
:quality 85
:format :jpeg})
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(defn validate-content-size!
[content]
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
(ex/raise :type :restriction
:code :media-max-file-size-reached
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
(:size content)
default-max-file-size))))
;; --- Create File Media object (upload)
(declare create-file-media-object)
(s/def ::content ::media/upload)
(s/def ::is-local ::us/boolean)
(s/def ::upload-file-media-object
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::is-local ::name ::content]
:opt-un [::id]))
(sv/defmethod ::upload-file-media-object
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
(let [cfg (update cfg :storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(media/validate-media-type! content)
(validate-content-size! content)
(create-file-media-object cfg params)))
(defn- big-enough-for-thumbnail?
"Checks if the provided image info is big enough for
create a separate thumbnail storage object."
[info]
(or (> (:width info) (:width thumbnail-options))
(> (:height info) (:height thumbnail-options))))
(defn- svg-image?
[info]
(= (:mtype info) "image/svg+xml"))
;; NOTE: we use the `on conflict do update` instead of `do nothing`
;; because postgresql does not returns anything if no update is
;; performed, the `do update` does the trick.
(def sql:create-file-media-object
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
on conflict (id) do update set created_at=file_media_object.created_at
returning *")
;; NOTE: the following function executes without a transaction, this
;; means that if something fails in the middle of this function, it
;; will probably leave leaked/unreferenced objects in the database and
;; probably in the storage layer. For handle possible object leakage,
;; we create all media objects marked as touched, this ensures that if
;; something fails, all leaked (already created storage objects) will
;; be eventually marked as deleted by the touched-gc task.
;;
;; The touched-gc task, performs periodic analysis of all touched
;; storage objects and check references of it. This is the reason why
;; `reference` metadata exists: it indicates the name of the table
;; witch holds the reference to storage object (it some kind of
;; inverse, soft referential integrity).
(defn create-file-media-object
[{:keys [storage pool climit executor]}
{:keys [id file-id is-local name content]}]
(letfn [;; Function responsible to retrieve the file information, as
;; it is synchronous operation it should be wrapped into
;; with-dispatch macro.
(get-info [content]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :info :input content})))
;; Function responsible of calculating cryptographyc hash of
;; the provided data.
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))
;; Function responsible of generating thumnail. As it is synchronous
;; opetation, it should be wrapped into with-dispatch macro
(generate-thumbnail [info]
(climit/with-dispatch (:process-image climit)
(media/run (assoc thumbnail-options
:cmd :generic-thumbnail
:input info))))
(create-thumbnail [info]
(when (and (not (svg-image? info))
(big-enough-for-thumbnail? info))
(p/let [thumb (generate-thumbnail info)
hash (calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
(sto/put-object! storage
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype thumb)
:bucket "file-media-object"}))))
(create-image [info]
(p/let [data (:path info)
hash (calculate-hash data)
content (-> (sto/content data)
(sto/wrap-with-hash hash))]
(sto/put-object! storage
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype info)
:bucket "file-media-object"})))
(insert-into-database [info image thumb]
(px/with-dispatch executor
(db/exec-one! pool [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
(:id image)
(:id thumb)
(:width info)
(:height info)
(:mtype info)])))]
(p/let [info (get-info content)
thumb (create-thumbnail info)
image (create-image info)]
(insert-into-database info image thumb))))
;; --- Create File Media Object (from URL)
(declare ^:private create-file-media-object-from-url)
(s/def ::create-file-media-object-from-url
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::is-local ::url]
:opt-un [::id ::name]))
(sv/defmethod ::create-file-media-object-from-url
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(let [cfg (update cfg :storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(create-file-media-object-from-url cfg params)))
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(letfn [(parse-and-validate-size [headers]
(let [size (some-> (get headers "content-length") d/parse-integer)
mtype (get headers "content-type")
format (cm/mtype->format mtype)
max-size (cf/get :media-max-file-size default-max-file-size)]
(when-not size
(ex/raise :type :validation
:code :unknown-size
:hint "seems like the url points to resource with unknown size"))
(when (> size max-size)
(ex/raise :type :validation
:code :file-too-large
:hint (str/ffmt "the file size % is greater than the maximum %"
size
default-max-file-size)))
(when (nil? format)
(ex/raise :type :validation
:code :media-type-not-allowed
:hint "seems like the url points to an invalid media object"))
{:size size
:mtype mtype
:format format}))
(download-media [uri]
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
(p/then process-response)))
(process-response [{:keys [body headers] :as response}]
(let [{:keys [size mtype]} (parse-and-validate-size headers)
path (tmp/tempfile :prefix "penpot.media.download.")
written (io/write-to-file! body path :size size)]
(when (not= written size)
(ex/raise :type :internal
:code :mismatch-write-size
:hint "unexpected state: unable to write to file"))
{:filename "tempfile"
:size size
:path path
:mtype mtype}))]
(p/let [content (download-media url)]
(->> (merge params {:content content :name (or name (:filename content))})
(create-file-media-object cfg)))))
;; --- Clone File Media object (Upload and create from url)
(declare clone-file-media-object)
(s/def ::clone-file-media-object
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::is-local ::id]))
(sv/defmethod ::clone-file-media-object
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(-> (assoc cfg :conn conn)
(clone-file-media-object params))))
(defn clone-file-media-object
[{:keys [conn]} {:keys [id file-id is-local]}]
(let [mobj (db/get-by-id conn :file-media-object id)]
(db/insert! conn :file-media-object
{:id (uuid/next)
:file-id file-id
:is-local is-local
:name (:name mobj)
:media-id (:media-id mobj)
:thumbnail-id (:thumbnail-id mobj)
:width (:width mobj)
:height (:height mobj)
:mtype (:mtype mobj)})))

View File

@@ -1,75 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.commands.profile
(:require
[app.auth :as auth]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- MUTATION: Set profile password
(declare update-profile-password!)
(s/def ::profile-id ::us/uuid)
(s/def ::password ::us/not-empty-string)
(s/def ::get-derived-password
(s/keys :req [::rpc/profile-id]
:req-un [::password]))
(sv/defmethod ::get-derived-password
"Get derived password, only ADMINS allowed to call this RPC
methods. Designed for administration pannel integration."
{::climit/queue :auth
::climit/key-fn ::rpc/profile-id
::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [password] :as params}]
(db/with-atomic [conn pool]
(let [admins (cf/get :admins)
profile (db/get-by-id conn :profile (::rpc/profile-id params))]
(if (or (:is-admin profile)
(contains? admins (:email profile)))
{:password (auth/derive-password password)}
(ex/raise :type :authentication
:code :only-admins-allowed
:hint "only admins allowed to call this RPC method")))))
;; --- MUTATION: Check profile password
(s/def ::attempt ::us/not-empty-string)
(s/def ::check-profile-password
(s/keys :req [::rpc/profile-id]
:req-un [::profile-id ::password]))
(sv/defmethod ::check-profile-password
"Check profile password, only ADMINS allowed to call this RPC
methods. Designed for administration pannel integration."
{::climit/queue :auth
::climit/key-fn ::rpc/profile-id
::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [profile-id password] :as params}]
(db/with-atomic [conn pool]
(let [admins (cf/get :admins)
profile (db/get-by-id pool :profile (::rpc/profile-id params))]
(if (or (:is-admin profile)
(contains? admins (:email profile)))
(let [profile (if (not= (::rpc/profile-id params) profile-id)
(db/get-by-id conn :profile profile-id)
profile)]
(auth/verify-password password (:password profile)))
(ex/raise :type :authentication
:code :only-admins-allowed
:hint "only admins allowed to call this RPC method")))))

View File

@@ -48,7 +48,7 @@
order by f.created_at asc")
(defn search-files
[conn {:keys [::rpc/profile-id team-id search-term] :as params}]
[conn profile-id team-id search-term]
(db/exec! conn [sql:search-files
profile-id team-id
profile-id team-id
@@ -64,6 +64,5 @@
(sv/defmethod ::search-files
{::doc/added "1.17"}
[{:keys [pool]} {:keys [search-term] :as params}]
(when search-term
(search-files pool params)))
[{:keys [pool]} {:keys [::rpc/profile-id team-id search-term]}]
(some->> search-term (search-files pool profile-id team-id)))

View File

@@ -23,6 +23,7 @@
[app.rpc.helpers :as rph]
[app.rpc.permissions :as perms]
[app.rpc.queries.profile :as profile]
[app.rpc.quotes :as quotes]
[app.storage :as sto]
[app.tokens :as tokens]
[app.util.services :as sv]
@@ -297,6 +298,9 @@
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id})
(create-team conn (assoc params :profile-id profile-id))))
(defn create-team
@@ -381,14 +385,8 @@
(declare role->params)
(s/def ::reassign-to ::us/uuid)
(s/def ::leave-team
(s/keys :req [::rpc/profile-id]
:req-un [::id]
:opt-un [::reassign-to]))
(defn leave-team
[conn {:keys [::rpc/profile-id id reassign-to]}]
[conn {:keys [profile-id id reassign-to]}]
(let [perms (get-permissions conn profile-id id)
members (retrieve-team-members conn id)]
@@ -433,12 +431,17 @@
nil))
(s/def ::reassign-to ::us/uuid)
(s/def ::leave-team
(s/keys :req [::rpc/profile-id]
:req-un [::id]
:opt-un [::reassign-to]))
(sv/defmethod ::leave-team
{::doc/added "1.17"}
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(leave-team conn params)))
(leave-team conn (assoc params :profile-id profile-id))))
;; --- Mutation: Delete Team
@@ -470,7 +473,7 @@
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/uxboxproject/issue/1083
;; https://tree.taiga.io/project/penpot/issue/1083
;; (s/def ::role #{:owner :admin :editor :viewer})
(s/def ::role #{:owner :admin :editor})
@@ -535,9 +538,9 @@
(sv/defmethod ::update-team-member-role
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} params]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(update-team-member-role conn (assoc params :profile-id (::rpc/profile-id params)))))
(update-team-member-role conn (assoc params :profile-id profile-id))))
;; --- Mutation: Delete Team Member
@@ -635,10 +638,11 @@
;; --- Mutation: Create Team Invitation
(def sql:upsert-team-invitation
"insert into team_invitation(team_id, email_to, role, valid_until)
values (?, ?, ?, ?)
"insert into team_invitation(id, team_id, email_to, role, valid_until)
values (?, ?, ?, ?, ?)
on conflict(team_id, email_to) do
update set role = ?, updated_at = now();")
update set role = ?, valid_until = ?, updated_at = now()
returning *")
(defn- create-invitation-token
[cfg {:keys [profile-id valid-until team-id member-id member-email role]}]
@@ -659,16 +663,8 @@
:exp (dt/in-future {:days 30})}))
(defn- create-invitation
[{:keys [::conn] :as cfg} {:keys [team profile role email] :as params}]
(let [member (profile/retrieve-profile-data-by-email conn email)
expire (dt/in-future "168h") ;; 7 days
itoken (create-invitation-token cfg {:profile-id (:id profile)
:valid-until expire
:team-id (:id team)
:member-email (or (:email member) email)
:member-id (:id member)
:role role})
ptoken (create-profile-identity-token cfg profile)]
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
(let [member (profile/retrieve-profile-data-by-email conn email)]
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
@@ -683,9 +679,6 @@
:email email
:hint "the email you invite has been repeatedly reported as spam or bounce"))
(when (contains? cf/flags :log-invitation-tokens)
(l/trace :hint "invitation token" :token itoken))
;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the
;; team as-is, without email roundtrip.
@@ -706,10 +699,38 @@
(when-not (:is-active member)
(db/update! conn :profile
{:is-active true}
{:id (:id member)})))
(do
(db/exec-one! conn [sql:upsert-team-invitation
(:id team) (str/lower email) (name role) expire (name role)])
{:id (:id member)}))
nil)
(let [id (uuid/next)
expire (dt/in-future "168h") ;; 7 days
invitation (db/exec-one! conn [sql:upsert-team-invitation id
(:id team) (str/lower email)
(name role) expire
(name role) expire])
updated? (not= id (:id invitation))
tprops {:profile-id (:id profile)
:invitation-id (:id invitation)
:valid-until expire
:team-id (:id team)
:member-email (:email-to invitation)
:member-id (:id member)
:role role}
itoken (create-invitation-token cfg tprops)
ptoken (create-profile-identity-token cfg profile)]
(when (contains? cf/flags :log-invitation-tokens)
(l/info :hint "invitation token" :token itoken))
(audit/submit! cfg
{:type "action"
:name (if updated?
"update-team-invitation"
"create-team-invitation")
:profile-id (:id profile)
:props (-> (dissoc tprops :profile-id)
(d/without-nils))})
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
:public-uri (cf/get :public-uri)
@@ -717,9 +738,9 @@
:invited-by (:fullname profile)
:team (:name team)
:token itoken
:extra-data ptoken})))
:extra-data ptoken})
itoken))
itoken))))
(s/def ::email ::us/email)
(s/def ::emails ::us/set-of-valid-emails)
@@ -739,6 +760,17 @@
team (db/get-by-id conn :team team-id)
emails (cond-> (or emails #{}) (string? email) (conj email))]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
@@ -749,14 +781,14 @@
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(let [cfg (assoc cfg ::conn conn)
(let [cfg (assoc cfg ::db/conn conn)
invitations (->> emails
(map (fn [email]
{:email (str/lower email)
:team team
:profile profile
:role role}))
(map (partial create-invitation cfg)))]
(keep (partial create-invitation cfg)))]
(with-meta (vec invitations)
{::audit/props {:invitations (count invitations)}})))))
@@ -772,9 +804,10 @@
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [team (create-team conn params)
(let [params (assoc params :profile-id profile-id)
team (create-team conn params)
profile (db/get-by-id conn :profile profile-id)
cfg (assoc cfg ::conn conn)]
cfg (assoc cfg ::db/conn conn)]
;; Create invitations for all provided emails.
(->> emails
@@ -785,18 +818,28 @@
:role role}))
(run! (partial create-invitation cfg)))
(-> team
(vary-meta assoc ::audit/props {:invitations (count emails)})
(rph/with-defer
#(when-let [collector (::audit/collector cfg)]
(audit/submit! collector
{:type "command"
:name "create-team-invitations"
:profile-id profile-id
:props {:emails emails
:role role
:profile-id profile-id
:invitations (count emails)}})))))))
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}
{::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(audit/submit! cfg
{:type "command"
:name "create-team-invitations"
:profile-id profile-id
:props {:emails emails
:role role
:profile-id profile-id
:invitations (count emails)}})
(vary-meta team assoc ::audit/props {:invitations (count emails)}))))
;; --- Query: get-team-invitation-token
@@ -812,7 +855,7 @@
{:team-id team-id
:email-to (str/lower email)})
(update :role keyword))
member (profile/retrieve-profile-data-by-email pool (:email invit))
member (profile/retrieve-profile-data-by-email pool (:email-to invit))
token (create-invitation-token cfg {:team-id (:team-id invit)
:profile-id profile-id
:valid-until (:valid-until invit)
@@ -858,6 +901,7 @@
(ex/raise :type :validation
:code :insufficient-permissions))
(db/delete! conn :team-invitation
{:team-id team-id :email-to (str/lower email)})
nil)))
(let [invitation (db/delete! conn :team-invitation
{:team-id team-id
:email-to (str/lower email)})]
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}})))))

View File

@@ -16,6 +16,7 @@
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.profile :as profile]
[app.rpc.quotes :as quotes]
[app.tokens :as tokens]
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
[app.util.services :as sv]
@@ -96,6 +97,11 @@
(ex/raise :type :restriction
:code :profile-blocked))
(quotes/check-quote! conn
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
@@ -127,7 +133,7 @@
:opt-un [::spec.team-invitation/member-id]))
(defmethod process-token :team-invitation
[{:keys [conn session] :as cfg}
[{:keys [conn] :as cfg}
{:keys [::rpc/profile-id token]}
{:keys [member-id team-id member-email] :as claims}]
@@ -146,45 +152,30 @@
(if (some? profile)
(if (or (= member-id profile-id)
(= member-email (:email profile)))
;; if we have logged-in user and it matches the invitation we
;; proceed with accepting the invitation and joining the
;; current profile to the invited team.
;; if we have logged-in user and it matches the invitation we proceed
;; with accepting the invitation and joining the current profile to the
;; invited team.
(let [profile (accept-invitation cfg claims invitation profile)]
(-> (assoc claims :state :created)
(rph/with-meta {::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id profile-id})))
::audit/profile-id (:id profile)
::audit/props {:team-id (:team-id claims)
:role (:role claims)
:invitation-id (:id invitation)}})))
(ex/raise :type :validation
:code :invalid-token
:hint "logged-in user does not matches the invitation"))
;; If we have not logged-in user, we try find the invited
;; profile by member-id or member-email props of the invitation
;; token; If profile is found, we accept the invitation and
;; leave the user logged-in.
(if-let [member (db/get* conn :profile
(if member-id
{:id member-id}
{:email member-email})
{:columns [:id :email]})]
(let [profile (accept-invitation cfg claims invitation member)]
(-> (assoc claims :state :created)
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-meta {::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id member-id})))
;; If we have not logged-in user, and invitation comes with member-id we
;; redirect user to login, if no memeber-id is present in the invitation
;; token, we redirect user the the register page.
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-register
:state :pending}))))
{:invitation-token token
:iss :team-invitation
:redirect-to (if member-id :auth-login :auth-register)
:state :pending})))
;; --- Default

View File

@@ -84,6 +84,6 @@
::cond/key-fn files/get-file-etag
::cond/reuse-key? true
::doc/added "1.17"}
[{:keys [pool]} params]
[{:keys [pool]} {:keys [::rpc/profile-id] :as params}]
(with-open [conn (db/open pool)]
(get-view-only-bundle conn (assoc params :profile-id (::rpc/profile-id params)))))
(get-view-only-bundle conn (assoc params :profile-id profile-id))))

View File

@@ -8,6 +8,7 @@
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.db :as db]
[app.http.client :as http]
@@ -22,14 +23,18 @@
[cuerdas.core :as str]
[promesa.core :as p]))
(defn decode-row
[{:keys [uri] :as row}]
(cond-> row
(string? uri) (assoc :uri (u/uri uri))))
;; --- Mutation: Create Webhook
(s/def ::team-id ::us/uuid)
(s/def ::uri ::us/not-empty-string)
(s/def ::uri ::us/uri)
(s/def ::is-active ::us/boolean)
(s/def ::mtype
#{"application/json"
"application/x-www-form-urlencoded"
"application/transit+json"})
(s/def ::create-webhook
@@ -60,7 +65,7 @@
(if (not= (:uri whook) (:uri params))
(->> (http/req! cfg {:method :head
:uri (:uri params)
:uri (str (:uri params))
:timeout (dt/duration "3s")})
(p/hmap (fn [response exception]
(if exception
@@ -80,22 +85,24 @@
(defn- insert-webhook!
[{:keys [::db/pool]} {:keys [team-id uri mtype is-active] :as params}]
(db/insert! pool :webhook
{:id (uuid/next)
:team-id team-id
:uri uri
:is-active is-active
:mtype mtype}))
(-> (db/insert! pool :webhook
{:id (uuid/next)
:team-id team-id
:uri (str uri)
:is-active is-active
:mtype mtype})
(decode-row)))
(defn- update-webhook!
[{:keys [::db/pool] :as cfg} {:keys [id] :as wook} {:keys [uri mtype is-active] :as params}]
(db/update! pool :webhook
{:uri uri
:is-active is-active
:mtype mtype
:error-code nil
:error-count 0}
{:id id}))
(-> (db/update! pool :webhook
{:uri (str uri)
:is-active is-active
:mtype mtype
:error-code nil
:error-count 0}
{:id id})
(decode-row)))
(sv/defmethod ::create-webhook
{::doc/added "1.17"}
@@ -111,7 +118,7 @@
(sv/defmethod ::update-webhook
{::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(let [whook (db/get pool :webhook {:id id})]
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
(check-edition-permissions! pool profile-id (:team-id whook))
(->> (validate-webhook! cfg whook params)
(p/fmap executor (fn [_] (update-webhook! cfg whook params))))))
@@ -124,7 +131,7 @@
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
(db/with-atomic [conn pool]
(let [whook (db/get conn :webhook {:id id})]
(let [whook (-> (db/get conn :webhook {:id id}) decode-row)]
(check-edition-permissions! conn profile-id (:team-id whook))
(db/delete! conn :webhook {:id id})
nil)))
@@ -144,4 +151,5 @@
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(db/exec! conn [sql:get-webhooks team-id])))
(->> (db/exec! conn [sql:get-webhooks team-id])
(mapv decode-row))))

View File

@@ -10,7 +10,7 @@
[app.common.logging :as l]
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as audit]
[app.loggers.audit :as-alias audit]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.files :as cmd.files]
[app.rpc.commands.files.create :as cmd.files.create]
@@ -176,7 +176,8 @@
(sv/defmethod ::upsert-file-object-thumbnail
{::doc/added "1.13"
::doc/deprecated "1.17"}
::doc/deprecated "1.17"
::audit/skip true}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(cmd.files/check-edition-permissions! conn profile-id file-id)
@@ -192,7 +193,8 @@
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.13"
::doc/deprecated "1.17"}
::doc/deprecated "1.17"
::audit/skip true}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(cmd.files/check-edition-permissions! conn profile-id file-id)

View File

@@ -18,6 +18,7 @@
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.quotes :as quotes]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -49,6 +50,9 @@
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
(let [cfg (update cfg :storage media/configure-assets-storage)]
(teams/check-edition-permissions! pool profile-id team-id)
(quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(create-font-variant cfg params)))
(defn create-font-variant

View File

@@ -6,280 +6,49 @@
(ns app.rpc.mutations.media
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.media :as cm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http.client :as http]
[app.media :as media]
[app.rpc.climit :as climit]
[app.rpc.commands.teams :as teams]
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.rpc.commands.files :as files]
[app.rpc.commands.media :as cmd.media]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.io :as io]
[promesa.core :as p]
[promesa.exec :as px]))
(def default-max-file-size (* 1024 1024 10)) ; 10 MiB
(def thumbnail-options
{:width 100
:height 100
:quality 85
:format :jpeg})
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
[clojure.spec.alpha :as s]))
;; --- Create File Media object (upload)
(declare create-file-media-object)
(declare select-file)
(s/def ::content ::media/upload)
(s/def ::is-local ::us/boolean)
(s/def ::upload-file-media-object
(s/keys :req-un [::profile-id ::file-id ::is-local ::name ::content]
:opt-un [::id]))
(s/def ::upload-file-media-object ::cmd.media/upload-file-media-object)
(sv/defmethod ::upload-file-media-object
{::doc/added "1.2"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
(let [file (select-file pool file-id)
cfg (update cfg :storage media/configure-assets-storage)]
(teams/check-edition-permissions! pool profile-id (:team-id file))
(let [cfg (update cfg :storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(media/validate-media-type! content)
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
(ex/raise :type :restriction
:code :media-max-file-size-reached
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
(:size content)
default-max-file-size)))
(create-file-media-object cfg params)))
(defn- big-enough-for-thumbnail?
"Checks if the provided image info is big enough for
create a separate thumbnail storage object."
[info]
(or (> (:width info) (:width thumbnail-options))
(> (:height info) (:height thumbnail-options))))
(defn- svg-image?
[info]
(= (:mtype info) "image/svg+xml"))
;; NOTE: we use the `on conflict do update` instead of `do nothing`
;; because postgresql does not returns anything if no update is
;; performed, the `do update` does the trick.
(def sql:create-file-media-object
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
on conflict (id) do update set created_at=file_media_object.created_at
returning *")
;; NOTE: the following function executes without a transaction, this
;; means that if something fails in the middle of this function, it
;; will probably leave leaked/unreferenced objects in the database and
;; probably in the storage layer. For handle possible object leakage,
;; we create all media objects marked as touched, this ensures that if
;; something fails, all leaked (already created storage objects) will
;; be eventually marked as deleted by the touched-gc task.
;;
;; The touched-gc task, performs periodic analysis of all touched
;; storage objects and check references of it. This is the reason why
;; `reference` metadata exists: it indicates the name of the table
;; witch holds the reference to storage object (it some kind of
;; inverse, soft referential integrity).
(defn create-file-media-object
[{:keys [storage pool climit executor] :as cfg}
{:keys [id file-id is-local name content] :as params}]
(letfn [;; Function responsible to retrieve the file information, as
;; it is synchronous operation it should be wrapped into
;; with-dispatch macro.
(get-info [content]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :info :input content})))
;; Function responsible of calculating cryptographyc hash of
;; the provided data.
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))
;; Function responsible of generating thumnail. As it is synchronous
;; opetation, it should be wrapped into with-dispatch macro
(generate-thumbnail [info]
(climit/with-dispatch (:process-image climit)
(media/run (assoc thumbnail-options
:cmd :generic-thumbnail
:input info))))
(create-thumbnail [info]
(when (and (not (svg-image? info))
(big-enough-for-thumbnail? info))
(p/let [thumb (generate-thumbnail info)
hash (calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
(sto/put-object! storage
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype thumb)
:bucket "file-media-object"}))))
(create-image [info]
(p/let [data (:path info)
hash (calculate-hash data)
content (-> (sto/content data)
(sto/wrap-with-hash hash))]
(sto/put-object! storage
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype info)
:bucket "file-media-object"})))
(insert-into-database [info image thumb]
(px/with-dispatch executor
(db/exec-one! pool [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
(:id image)
(:id thumb)
(:width info)
(:height info)
(:mtype info)])))]
(p/let [info (get-info content)
thumb (create-thumbnail info)
image (create-image info)]
(insert-into-database info image thumb))))
(cmd.media/validate-content-size! content)
(cmd.media/create-file-media-object cfg params)))
;; --- Create File Media Object (from URL)
(declare ^:private create-file-media-object-from-url)
(s/def ::create-file-media-object-from-url
(s/keys :req-un [::profile-id ::file-id ::is-local ::url]
:opt-un [::id ::name]))
(s/def ::create-file-media-object-from-url ::cmd.media/create-file-media-object-from-url)
(sv/defmethod ::create-file-media-object-from-url
{::doc/added "1.3"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(let [file (select-file pool file-id)
cfg (update cfg :storage media/configure-assets-storage)]
(teams/check-edition-permissions! pool profile-id (:team-id file))
(create-file-media-object-from-url cfg params)))
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(letfn [(parse-and-validate-size [headers]
(let [size (some-> (get headers "content-length") d/parse-integer)
mtype (get headers "content-type")
format (cm/mtype->format mtype)
max-size (cf/get :media-max-file-size default-max-file-size)]
(when-not size
(ex/raise :type :validation
:code :unknown-size
:hint "seems like the url points to resource with unknown size"))
(when (> size max-size)
(ex/raise :type :validation
:code :file-too-large
:hint (str/ffmt "the file size % is greater than the maximum %"
size
default-max-file-size)))
(when (nil? format)
(ex/raise :type :validation
:code :media-type-not-allowed
:hint "seems like the url points to an invalid media object"))
{:size size
:mtype mtype
:format format}))
(download-media [uri]
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
(p/then process-response)))
(process-response [{:keys [body headers] :as response}]
(let [{:keys [size mtype]} (parse-and-validate-size headers)
path (tmp/tempfile :prefix "penpot.media.download.")
written (io/write-to-file! body path :size size)]
(when (not= written size)
(ex/raise :type :internal
:code :mismatch-write-size
:hint "unexpected state: unable to write to file"))
{:filename "tempfile"
:size size
:path path
:mtype mtype}))]
(p/let [content (download-media url)]
(->> (merge params {:content content :name (or name (:filename content))})
(create-file-media-object cfg)))))
(let [cfg (update cfg :storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(#'cmd.media/create-file-media-object-from-url cfg params)))
;; --- Clone File Media object (Upload and create from url)
(declare clone-file-media-object)
(s/def ::clone-file-media-object
(s/keys :req-un [::profile-id ::file-id ::is-local ::id]))
(s/def ::clone-file-media-object ::cmd.media/clone-file-media-object)
(sv/defmethod ::clone-file-media-object
{::doc/added "1.2"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(let [file (select-file conn file-id)]
(teams/check-edition-permissions! conn profile-id (:team-id file))
(-> (assoc cfg :conn conn)
(clone-file-media-object params)))))
(defn clone-file-media-object
[{:keys [conn] :as cfg} {:keys [id file-id is-local]}]
(let [mobj (db/get-by-id conn :file-media-object id)]
(db/insert! conn :file-media-object
{:id (uuid/next)
:file-id file-id
:is-local is-local
:name (:name mobj)
:media-id (:media-id mobj)
:thumbnail-id (:thumbnail-id mobj)
:width (:width mobj)
:height (:height mobj)
:mtype (:mtype mobj)})))
;; --- HELPERS
(def ^:private
sql:select-file
"select file.*,
project.team_id as team_id
from file
inner join project on (project.id = file.project_id)
where file.id = ?")
(defn- select-file
[conn id]
(let [row (db/exec-one! conn [sql:select-file id])]
(when-not row
(ex/raise :type :not-found))
row))
(files/check-edition-permissions! conn profile-id file-id)
(-> (assoc cfg :conn conn)
(cmd.media/clone-file-media-object params))))

View File

@@ -14,6 +14,7 @@
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.projects :as proj]
[app.rpc.quotes :as quotes]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -37,6 +38,10 @@
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(let [project (teams/create-project conn params)]
(teams/create-project-role conn profile-id (:id project) :owner)

View File

@@ -179,6 +179,5 @@
(sv/defmethod ::search-files
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [pool]} {:keys [search-term] :as params}]
(when search-term
(search/search-files pool params)))
[{:keys [pool]} {:keys [profile-id team-id search-term]}]
(some->> search-term (search/search-files pool profile-id team-id)))

View File

@@ -17,6 +17,7 @@
;; --- Query: Font Variants
(s/def ::team-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::font-variants

View File

@@ -0,0 +1,339 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.quotes
"Penpot resource usage quotes."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(defmulti check-quote ::id)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::conn ::db/conn-or-pool)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::incr (s/and int? pos?))
(s/def ::target ::us/string)
(s/def ::quote
(s/keys :req [::id ::profile-id]
:opt [::conn
::team-id
::project-id
::file-id
::incr]))
(def ^:private enabled (volatile! true))
(defn enable!
"Enable quotes checking at runtime (from server REPL)."
[]
(vswap! enabled (constantly true)))
(defn disable!
"Disable quotes checking at runtime (from server REPL)."
[]
(vswap! enabled (constantly false)))
(defn check-quote!
[conn quote]
(us/assert! ::db/conn-or-pool conn)
(us/assert! ::quote quote)
(when (contains? cf/flags :quotes)
(when @enabled
(check-quote (assoc quote ::conn conn ::target (name (::id quote)))))))
(defn- send-notification!
[{:keys [::conn] :as params}]
(l/warn :hint "max quote reached"
:target (::target params)
:profile-id (some-> params ::profile-id str)
:team-id (some-> params ::team-id str)
:project-id (some-> params ::project-id str)
:file-id (some-> params ::file-id str)
:quote (::quote params)
:total (::total params)
:incr (::inc params 1))
(when-let [admins (seq (cf/get :admins))]
(let [subject (str/istr "[quotes:notification]: max quote reached ~(::target params)")
content (str/istr "- Param: profile-id '~(::profile-id params)}'\n"
"- Param: team-id '~(::team-id params)'\n"
"- Param: project-id '~(::project-id params)'\n"
"- Param: file-id '~(::file-id params)'\n"
"- Quote ID: '~(::target params)'\n"
"- Max: ~(::quote params)\n"
"- Total: ~(::total params) (INCR ~(::incr params 1))\n")]
(wrk/submit! {::wrk/task :sendmail
::wrk/delay (dt/duration "30s")
::wrk/max-retries 4
::wrk/priority 200
::wrk/conn conn
::wrk/dedupe true
::wrk/label "quotes-notification"
:to (vec admins)
:subject subject
:body [{:type "text/plain"
:content content}]}))))
(defn- generic-check!
[{:keys [::conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}]
(let [quote (->> (db/exec! conn quote-sql)
(map :quote)
(reduce max (- Integer/MAX_VALUE)))
quote (if (pos? quote) quote default)
total (->> (db/exec! conn count-sql) first :total)]
(when (> (+ total incr) quote)
(if (contains? cf/flags :soft-quotes)
(send-notification! (assoc params ::quote quote ::total total))
(ex/raise :type :restriction
:code :max-quote-reached
:target target
:quote quote
:count total)))))
(def ^:private sql:get-quotes-1
"select id, quote from usage_quote
where target = ?
and profile_id = ?
and team_id is null
and project_id is null
and file_id is null;")
(def ^:private sql:get-quotes-2
"select id, quote from usage_quote
where target = ?
and ((team_id = ? and (profile_id = ? or profile_id is null)) or
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
(def ^:private sql:get-quotes-3
"select id, quote from usage_quote
where target = ?
and ((project_id = ? and (profile_id = ? or profile_id is null)) or
(team_id = ? and (profile_id = ? or profile_id is null)) or
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
(def ^:private sql:get-quotes-4
"select id, quote from usage_quote
where target = ?
and ((file_id = ? and (profile_id = ? or profile_id is null)) or
(project_id = ? and (profile_id = ? or profile_id is null)) or
(team_id = ? and (profile_id = ? or profile_id is null)) or
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: TEAMS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-teams-per-profile
"select count(*) as total
from team_profile_rel
where profile_id = ?")
(s/def ::profile-id ::us/uuid)
(s/def ::teams-per-profile
(s/keys :req [::profile-id ::target]))
(defmethod check-quote ::teams-per-profile
[{:keys [::profile-id ::target] :as quote}]
(us/assert! ::teams-per-profile quote)
(-> quote
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: PROJECTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-projects-per-team
"select count(*) as total
from project as p
where p.team_id = ?
and p.deleted_at is null")
(s/def ::team-id ::us/uuid)
(s/def ::projects-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(defmethod check-quote ::projects-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(-> quote
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-projects-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FONT-VARIANTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-font-variants-per-team
"select count(*) as total
from team_font_variant as v
where v.team_id = ?")
(s/def ::font-variants-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(defmethod check-quote ::font-variants-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::font-variants-per-team quote)
(-> quote
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-font-variants-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: INVITATIONS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-invitations-per-team
"select count(*) as total
from team_invitation
where team_id = ?")
(s/def ::invitations-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(defmethod check-quote ::invitations-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::invitations-per-team quote)
(-> quote
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-invitations-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: PROFILES-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-profiles-per-team
"select (select count(*)
from team_profile_rel
where team_id = ?) +
(select count(*)
from team_invitation
where team_id = ?
and valid_until > now()) as total;")
;; NOTE: the total number of profiles is determined by the number of
;; effective members plus ongoing valid invitations.
(s/def ::profiles-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(defmethod check-quote ::profiles-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::profiles-per-team quote)
(-> quote
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FILES-PER-PROJECT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-files-per-project
"select count(*) as total
from file as f
where f.project_id = ?
and f.deleted_at is null")
(s/def ::project-id ::us/uuid)
(s/def ::files-per-project
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(defmethod check-quote ::files-per-project
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
(us/assert! ::files-per-project quote)
(-> quote
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
(assoc ::count-sql [sql:get-files-per-project project-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: COMMENT-THREADS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comment-threads-per-file
"select count(*) as total
from comment_thread as ct
where ct.file_id = ?")
(s/def ::comment-threads-per-file
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(defmethod check-quote ::comment-threads-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(us/assert! ::files-per-project quote)
(-> quote
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
profile-id team-id profile-id profile-id])
(assoc ::count-sql [sql:get-comment-threads-per-file file-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: COMMENTS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comments-per-file
"select count(*) as total
from comment as c
join comment_thread as ct on (ct.id = c.thread_id)
where ct.file_id = ?")
(s/def ::comments-per-file
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(defmethod check-quote ::comments-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(us/assert! ::files-per-project quote)
(-> quote
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
profile-id team-id profile-id profile-id])
(assoc ::count-sql [sql:get-comments-per-file file-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: DEFAULT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod check-quote :default
[{:keys [::id]}]
(ex/raise :type :internal
:code :quote-not-defined
:quote id
:hint "backend using a quote identifier not defined"))

View File

@@ -182,78 +182,94 @@
(assoc ::lresult/remaining remaining)
(assoc ::lresult/reset (dt/plus ts {unit 1})))))))))
(defn- process-limits
(defn- process-limits!
[redis user-id limits now]
(-> (p/all (map (partial process-limit redis user-id now) limits))
(p/then (fn [results]
(let [remaining (->> results
(d/index-by ::name ::lresult/remaining)
(uri/map->query-string))
reset (->> results
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
(uri/map->query-string))
rejected (->> results
(filter (complement ::lresult/allowed?))
(first))]
(->> (p/all (map (partial process-limit redis user-id now) limits))
(p/fmap (fn [results]
(let [remaining (->> results
(d/index-by ::name ::lresult/remaining)
(uri/map->query-string))
reset (->> results
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
(uri/map->query-string))
rejected (->> results
(filter (complement ::lresult/allowed?))
(first))]
(when rejected
(l/warn :hint "rejected rate limit"
:user-id (str user-id)
:limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name)))
(when rejected
(l/warn :hint "rejected rate limit"
:user-id (str user-id)
:limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name)))
{:enabled? true
:allowed? (not (some? rejected))
:headers {"x-rate-limit-remaining" remaining
"x-rate-limit-reset" reset}})))))
{:enabled? true
:allowed? (not (some? rejected))
:headers {"x-rate-limit-remaining" remaining
"x-rate-limit-reset" reset}})))))
(defn- handle-response
[f cfg params result]
(if (:enabled? result)
(let [headers (:headers result)]
(when-not (:allowed? result)
(ex/raise :type :rate-limit
:code :request-blocked
:hint "rate limit reached"
::http/headers headers))
(-> (f cfg params)
(p/then (fn [response]
(vary-meta response update ::http/headers merge headers)))))
(if (:allowed? result)
(->> (f cfg params)
(p/fmap (fn [response]
(vary-meta response update ::http/headers merge headers))))
(p/rejected
(ex/error :type :rate-limit
:code :request-blocked
:hint "rate limit reached"
::http/headers headers))))
(f cfg params)))
(defn- get-limits
[state skey sname]
(some->> (or (get-in @state [::limits skey])
(get-in @state [::limits :default]))
(map #(assoc % ::service sname))
(seq)))
(defn- get-uid
[{:keys [::http/request] :as params}]
(or (::rpc/profile-id params)
(some-> request parse-client-ip)
uuid/zero))
(defn wrap
[{:keys [rlimit redis] :as cfg} f mdata]
(if rlimit
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))]
(fn [cfg {:keys [::http/request] :as params}]
(let [uid (or (:profile-id params)
(some-> request parse-client-ip)
uuid/zero)
rsp (when (and uid @enabled?)
(when-let [limits (or (get-in @rlimit [::limits skey])
(get-in @rlimit [::limits :default]))]
(let [redis (redis/get-or-connect redis ::rlimit default-options)
limits (map #(assoc % ::service sname) limits)
resp (-> (process-limits redis uid limits (dt/now))
(p/catch (fn [cause]
;; If we have an error on processing the rate-limit we just skip
;; it for do not cause service interruption because of redis
;; downtime or similar situation.
(l/error :hint "error on processing rate-limit" :cause cause)
{:enabled? false})))]
(fn [cfg params]
(if @enabled?
(try
(let [uid (get-uid params)
rsp (when-let [limits (get-limits rlimit skey sname)]
(let [redis (redis/get-or-connect redis ::rpc/rlimit default-options)
rsp (->> (process-limits! redis uid limits (dt/now))
(p/merr (fn [cause]
;; If we have an error on processing the rate-limit we just skip
;; it for do not cause service interruption because of redis
;; downtime or similar situation.
(l/error :hint "error on processing rate-limit" :cause cause)
(p/resolved {:enabled? false}))))]
;; If soft rate are enabled, we process the rate-limit but return unprotected
;; response.
(if (contains? cf/flags :soft-rpc-rlimit)
(p/resolved {:enabled? false})
resp))))
;; If soft rate are enabled, we process the rate-limit but return unprotected
;; response.
(if (contains? cf/flags :soft-rpc-rlimit)
{:enabled? false}
rsp)))]
rsp (or rsp (p/resolved {:enabled? false}))]
(->> (p/promise rsp)
(p/fmap #(or % {:enabled? false}))
(p/mcat #(handle-response f cfg params %))))
(p/then rsp (partial handle-response f cfg params)))))
(catch Throwable cause
(p/rejected cause)))
(f cfg params))))
f))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -13,7 +13,6 @@
[app.db :as db]
[app.main :as-alias main]
[app.setup.builtin-templates]
[app.setup.initial-user]
[app.setup.keys :as keys]
[buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]
@@ -69,5 +68,5 @@
(let [secret (or key (generate-random-key))]
(-> (retrieve-all conn)
(assoc :secret-key secret)
(assoc :tokens-key (keys/derive secret :salt "tokens" :size 32))
(assoc :tokens-key (keys/derive secret :salt "tokens"))
(update :instance-id handle-instance-id conn (db/read-only? pool))))))

View File

@@ -1,41 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.setup.initial-user
"Initial data setup of instance."
(:require
[app.auth :as auth]
[app.common.logging :as l]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.setup :as-alias setup]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(def ^:private sql:insert-profile
"insert into profile (id, fullname, email, password, is_active, is_admin, created_at, modified_at)
values ('00000000-0000-0000-0000-000000000000', 'Admin', ?, ?, true, true, now(), now())
on conflict (id)
do update set email = ?, password = ?")
(defmethod ig/pre-init-spec ::setup/initial-profile [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::setup/initial-profile
[_ {:keys [::db/pool]}]
(let [email (cf/get :setup-admin-email)
password (cf/get :setup-admin-password)]
(when (and email password)
(db/with-atomic [conn pool]
(let [pwd (auth/derive-password password)]
(db/exec-one! conn [sql:insert-profile email pwd email pwd])
(l/info :hint "setting initial user (admin)"
:email email
:password "********"))))
nil))

View File

@@ -13,7 +13,7 @@
(defn derive
"Derive a key from secret-key"
[secret-key & {:keys [salt size]}]
[secret-key & {:keys [salt size] :or {size 32}}]
(us/assert! ::us/not-empty-string secret-key)
(let [engine (bk/engine {:key secret-key
:salt salt

View File

@@ -9,7 +9,11 @@
(:require
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf]
[app.srepl.ext]
[app.srepl.main]
[app.util.json :as json]
[app.util.locks :as locks]
[clojure.core.server :as ccs]
[clojure.main :as cm]
[clojure.spec.alpha :as s]
@@ -20,39 +24,59 @@
(ccs/repl-init)
(in-ns 'app.srepl.main))
(defn repl
(defn user-repl
[]
(cm/repl
:init repl-init
:read ccs/repl-read))
(defn json-repl
[]
(let [out *out*
lock (locks/create)]
(ccs/prepl *in*
(fn [m]
(binding [*out* out, *flush-on-newline* true, *print-readably* true]
(locks/locking lock
(println (json/encode-str m))))))))
;; --- State initialization
(s/def ::name ::us/not-empty-string)
(s/def ::port int?)
(s/def ::port ::us/integer)
(s/def ::host ::us/not-empty-string)
(s/def ::flag #{:urepl-server :prepl-server})
(s/def ::type #{::prepl ::urepl})
(s/def ::key (s/tuple ::type ::us/keyword))
(defmethod ig/pre-init-spec ::server
[_]
(s/keys :opt-un [::port ::host ::name]))
(s/keys :req [::flag]
:req-un [::port ::host]))
(defmethod ig/prep-key ::server
[_ cfg]
(merge {:name "main"} cfg))
[[type _] cfg]
(assoc cfg ::flag (keyword (str (name type) "-server"))))
(defmethod ig/init-key ::server
[_ {:keys [port host name] :as cfg}]
(when (and port host name)
(l/info :msg "initializing server repl" :port port :host host :name name)
(ccs/start-server {:address host
:port port
:name name
:accept 'app.srepl/repl})
cfg))
[[type _] {:keys [::flag port host] :as cfg}]
(when (contains? cf/flags flag)
(let [accept (case type
::prepl 'app.srepl/json-repl
::urepl 'app.srepl/user-repl)
params {:address host
:port port
:name (name type)
:accept accept}]
(l/info :msg "initializing repl server"
:name (name type)
:port port
:host host)
(ccs/start-server params)
params)))
(defmethod ig/halt-key! ::server
[_ cfg]
(when cfg
(ccs/stop-server (:name cfg))))
[_ params]
(some-> params :name ccs/stop-server))

View File

@@ -0,0 +1,77 @@
;; 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.srepl.ext
"PREPL API for external usage (CLI or ADMIN)"
(:require
[app.auth :as auth]
[app.common.exceptions :as ex]
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc.commands.auth :as cmd.auth]
[app.util.json :as json]
[cuerdas.core :as str]))
(defn- get-current-system
[]
(or (deref (requiring-resolve 'app.main/system))
(deref (requiring-resolve 'user/system))))
(defmulti ^:private run-json-cmd* ::cmd)
(defn run-json-cmd
"Entry point with external tools integrations that uses PREPL
interface for interacting with running penpot backend."
[data]
(let [data (json/decode data)
params (merge {::cmd (keyword (:cmd data "default"))}
(:params data))]
(run-json-cmd* params)))
(defmethod run-json-cmd* :create-profile
[{:keys [fullname email password is-active]
:or {is-active true}}]
(when-let [system (get-current-system)]
(db/with-atomic [conn (:app.db/pool system)]
(let [params {:id (uuid/next)
:email email
:fullname fullname
:is-active is-active
:password password
:props {}}]
(->> (cmd.auth/create-profile conn params)
(cmd.auth/create-profile-relations conn))))))
(defmethod run-json-cmd* :update-profile
[{:keys [fullname email password is-active]}]
(when-let [system (get-current-system)]
(db/with-atomic [conn (:app.db/pool system)]
(let [params (cond-> {}
(some? fullname)
(assoc :fullname fullname)
(some? password)
(assoc :password (auth/derive-password password))
(some? is-active)
(assoc :is-active is-active))]
(when (seq params)
(let [res (db/update! conn :profile
params
{:email email
:deleted-at nil}
{:return-keys false})]
(pos? (:next.jdbc/update-count res))))))))
(defmethod run-json-cmd* :derive-password
[{:keys [password]}]
(auth/derive-password password))
(defmethod run-json-cmd* :default
[{:keys [::cmd]}]
(ex/raise :type :internal
:code :not-implemented
:hint (str/ffmt "command '%' not implemented" (name cmd))))

View File

@@ -41,3 +41,35 @@
([file state]
(repair-orphaned-shapes (:data file))
(update state :total (fnil inc 0))))
(defn rename-layout-attrs
([file]
(let [found? (volatile! false)]
(letfn [(update-shape
[shape]
(when (or (= (:layout-flex-dir shape) :reverse-row)
(= (:layout-flex-dir shape) :reverse-column)
(= (:layout-wrap-type shape) :no-wrap))
(vreset! found? true))
(cond-> shape
(= (:layout-flex-dir shape) :reverse-row)
(assoc :layout-flex-dir :row-reverse)
(= (:layout-flex-dir shape) :reverse-column)
(assoc :layout-flex-dir :column-reverse)
(= (:layout-wrap-type shape) :no-wrap)
(assoc :layout-wrap-type :nowrap)))
(update-page
[page]
(h/update-shapes page update-shape))]
(let [new-file (update file :data h/update-pages update-page)]
(when @found?
(l/info :hint "Found attrs to rename in file"
:id (:id file)
:name (:name file)))
new-file))))
([file state]
(rename-layout-attrs file)
(update state :total (fnil inc 0))))

View File

@@ -11,6 +11,7 @@
[app.auth :refer [derive-password]]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.files.features :as ffeat]
[app.common.logging :as l]
[app.common.pages :as cp]
[app.common.pages.migrations :as pmg]
@@ -21,8 +22,11 @@
[app.db :as db]
[app.db.sql :as sql]
[app.main :refer [system]]
[app.rpc.commands.files :as files]
[app.rpc.queries.profile :as prof]
[app.util.blob :as blob]
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.stacktrace :as strace]
@@ -66,23 +70,33 @@
[system & {:keys [update-fn id save? migrate? inc-revn?]
:or {save? false migrate? true inc-revn? true}}]
(db/with-atomic [conn (:app.db/pool system)]
(let [file (db/get-by-id conn :file id {:for-update true})
file (-> file
(update :features db/decode-pgarray #{})
(update :data blob/decode)
(cond-> migrate? (update :data pmg/migrate-data)))
file (binding [*conn* conn]
(-> (update-fn file)
(cond-> inc-revn? (update :revn inc))))]
(when save?
(let [features (db/create-array conn "text" (:features file))
data (blob/encode (:data file))]
(db/update! conn :file
{:data data
:revn (:revn file)
:features features}
{:id id})))
file)))
(let [file (-> (db/get-by-id conn :file id {:for-update true})
(update :features db/decode-pgarray #{}))]
(binding [*conn* conn
pmap/*tracked* (atom {})
pmap/*load-fn* (partial files/load-pointer conn id)
ffeat/*wrap-with-pointer-map-fn*
(if (contains? (:features file) "storage/pointer-map") pmap/wrap identity)
ffeat/*wrap-with-objects-map-fn*
(if (contains? (:features file) "storage/objectd-map") omap/wrap identity)]
(let [file (-> file
(update :data blob/decode)
(cond-> migrate? (update :data pmg/migrate-data))
(update-fn)
(cond-> inc-revn? (update :revn inc)))]
(when save?
(let [features (db/create-array conn "text" (:features file))
data (blob/encode (:data file))]
(db/update! conn :file
{:data data
:revn (:revn file)
:features features}
{:id id})
(when (contains? (:features file) "storage/pointer-map")
(files/persist-pointers! conn id))))
(dissoc file :data))))))
(def ^:private sql:retrieve-files-chunk
"SELECT id, name, created_at, data FROM file

View File

@@ -110,10 +110,10 @@
(if (contains? features "storage/objects-map")
file
(-> file
(update :data migrate-to-omap)
(update :data migrate)
(update :features conj "storage/objects-map"))))
(migrate-to-omap [data]
(migrate [data]
(-> data
(update :pages-index update-vals #(update % :objects omap/wrap))
(update :components update-vals #(update % :objects omap/wrap))))]
@@ -125,24 +125,17 @@
(defn enable-pointer-map-feature-on-file!
[system & {:keys [save? id]}]
(letfn [(update-file [{:keys [features id] :as file}]
(letfn [(update-file [{:keys [features] :as file}]
(if (contains? features "storage/pointer-map")
file
(-> file
(update :data migrate-to-omap id)
(update :data migrate)
(update :features conj "storage/pointer-map"))))
(migrate-to-omap [data file-id]
(binding [pmap/*tracked* (atom {})]
(let [data (-> data
(update :pages-index update-vals pmap/wrap)
(update :components pmap/wrap))]
(doseq [[id item] @pmap/*tracked*]
(db/insert! h/*conn* :file-data-fragment
{:id id
:file-id file-id
:content (-> item deref blob/encode)}))
data)))]
(migrate [data]
(-> data
(update :pages-index update-vals pmap/wrap)
(update :components pmap/wrap)))]
(h/update-file! system
:id id

View File

@@ -24,7 +24,9 @@
(:import
java.io.FilterInputStream
java.io.InputStream
java.net.URI
java.nio.ByteBuffer
java.nio.file.Path
java.time.Duration
java.util.Collection
java.util.Optional
@@ -40,6 +42,7 @@
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
software.amazon.awssdk.regions.Region
software.amazon.awssdk.services.s3.S3AsyncClient
software.amazon.awssdk.services.s3.S3Configuration
software.amazon.awssdk.services.s3.model.Delete
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
@@ -151,46 +154,51 @@
(defn build-s3-client
[{:keys [region endpoint executor]}]
(let [hclient (.. (NettyNioAsyncHttpClient/builder)
(eventLoopGroupBuilder (.. (SdkEventLoopGroup/builder)
(numberOfThreads (int default-eventloop-threads))))
(connectionAcquisitionTimeout default-timeout)
(connectionTimeout default-timeout)
(readTimeout default-timeout)
(writeTimeout default-timeout)
(build))
client (.. (S3AsyncClient/builder)
(asyncConfiguration (.. (ClientAsyncConfiguration/builder)
(advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR
executor)
(build)))
(httpClient hclient)
(region (lookup-region region)))]
(let [aconfig (-> (ClientAsyncConfiguration/builder)
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
(.build))
(when-let [uri (some-> endpoint (java.net.URI.))]
(.endpointOverride client uri))
sconfig (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))
(let [client (.build client)]
(reify
clojure.lang.IDeref
(deref [_] client)
hclient (-> (NettyNioAsyncHttpClient/builder)
(.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder)
(.numberOfThreads (int default-eventloop-threads))))
(.connectionAcquisitionTimeout default-timeout)
(.connectionTimeout default-timeout)
(.readTimeout default-timeout)
(.writeTimeout default-timeout)
(.build))
java.lang.AutoCloseable
(close [_]
(.close hclient)
(.close client))))))
client (-> (S3AsyncClient/builder)
(.serviceConfiguration ^S3Configuration sconfig)
(.asyncConfiguration ^ClientAsyncConfiguration aconfig)
(.httpClient ^NettyNioAsyncHttpClient hclient)
(.region (lookup-region region))
(cond-> (some? endpoint) (.endpointOverride (URI. endpoint)))
(.build))]
(reify
clojure.lang.IDeref
(deref [_] client)
java.lang.AutoCloseable
(close [_]
(.close ^NettyNioAsyncHttpClient hclient)
(.close ^S3AsyncClient client)))))
(defn build-s3-presigner
[{:keys [region endpoint]}]
(if (string? endpoint)
(let [uri (java.net.URI. endpoint)]
(.. (S3Presigner/builder)
(endpointOverride uri)
(region (lookup-region region))
(build)))
(.. (S3Presigner/builder)
(region (lookup-region region))
(build))))
(let [config (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))]
(-> (S3Presigner/builder)
(cond-> (some? endpoint) (.endpointOverride (URI. endpoint)))
(.region (lookup-region region))
(.serviceConfiguration ^S3Configuration config)
(.build))))
(defn- make-request-body
[content]
@@ -198,7 +206,7 @@
buff-size (* 1024 64)
sem (Semaphore. 0)
writer-fn (fn [s]
writer-fn (fn [^Subscriber s]
(try
(loop []
(.acquire sem 1)
@@ -261,7 +269,7 @@
;; not, read the contento into memory using bytearrays.
(if (> size (* 1024 1024 2))
(p/let [path (tmp/tempfile :prefix "penpot.storage.s3.")
rxf (AsyncResponseTransformer/toFile path)
rxf (AsyncResponseTransformer/toFile ^Path path)
_ (.getObject ^S3AsyncClient client
^GetObjectRequest gor
^AsyncResponseTransformer rxf)]
@@ -283,9 +291,9 @@
(key (str prefix (impl/id->path id)))
(build))
rxf (AsyncResponseTransformer/toBytes)
obj (.getObjectAsBytes ^S3AsyncClient client
^GetObjectRequest gor
^AsyncResponseTransformer rxf)]
obj (.getObject ^S3AsyncClient client
^GetObjectRequest gor
^AsyncResponseTransformer rxf)]
(.asByteArray ^ResponseBytes obj)))
(def default-max-age

View File

@@ -59,7 +59,8 @@
(def default-flags
[:enable-secure-session-cookies
:enable-email-verification
:enable-smtp])
:enable-smtp
:enable-quotes])
(defn state-init
[next]
@@ -322,7 +323,9 @@
[{:keys [::type] :as data}]
(let [method-fn (get-in *system* [:app.rpc/methods :commands type])]
;; (app.common.pprint/pprint (:app.rpc/methods *system*))
(try-on! (method-fn (dissoc data ::type)))))
(try-on! (method-fn (-> data
(dissoc ::type)
(assoc :app.rpc/request-at (dt/now)))))))
(defn mutation!
[{:keys [::type profile-id] :as data}]

View File

@@ -0,0 +1,290 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns backend-tests.rpc-comment-test
(:require
[app.common.geom.point :as gpt]
[app.common.uuid :as uuid]
[app.db :as db]
[app.http :as http]
[app.rpc :as-alias rpc]
[app.rpc.commands.comments :as comments]
[app.rpc.cond :as cond]
[app.rpc.quotes :as-alias quotes]
[app.util.time :as dt]
[backend-tests.helpers :as th]
[clojure.test :as t]
[datoteka.core :as fs]
[mockery.core :refer [with-mocks]]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest comment-and-threads-crud
(with-mocks [mock {:target 'app.config/get
:return (th/config-get-mock
{:quotes-teams-per-profile 200})}]
(let [profile-1 (th/create-profile* 1 {:is-active true})
profile-2 (th/create-profile* 2 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile-1)})
;; role (th/create-team-role* {:team-id (:id team)
;; :profile-id (:id profile-2)
;; :role :admin})
project (th/create-project* 1 {:team-id (:id team)
:profile-id (:id profile-1)})
file-1 (th/create-file* 1 {:profile-id (:id profile-1)
:project-id (:id project)})
file-2 (th/create-file* 2 {:profile-id (:id profile-1)
:project-id (:id project)})
page-id (get-in file-1 [:data :pages 0])]
(t/testing "comment thread creation"
(let [data {::th/type :create-comment-thread
::rpc/profile-id (:id profile-1)
:file-id (:id file-1)
:page-id page-id
:position (gpt/point 0)
:content "hello world"
:frame-id uuid/zero}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (uuid? (:id result)))
(t/is (uuid? (:file-id result)))
(t/is (uuid? (:page-id result)))
(t/is (uuid? (:comment-id result)))
(t/is (= (:file-id result) (:id file-1)))
(t/is (= (:page-id result) page-id)))))
(t/testing "comment thread status update"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
;; comment (-> (th/db-query :comment {:thread-id (:id thread)}) first)
data {::th/type :update-comment-thread-status
::rpc/profile-id (:id profile-1)
:id (:id thread)}
status (th/db-get :comment-thread-status
{:thread-id (:id thread)
:profile-id (:id profile-1)})]
(t/is (= (:modified-at status) (:modified-at thread)))
(let [{:keys [result] :as out} (th/command! data)]
(t/is (th/success? out))
(t/is (dt/instant? (:modified-at result))))
(let [status' (th/db-get :comment-thread-status
{:thread-id (:id thread)
:profile-id (:id profile-1)})]
(t/is (not= (:modified-at status') (:modified-at thread))))))
(t/testing "comment thread status update 2"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
data {::th/type :update-comment-thread-status
::rpc/profile-id (:id profile-2)
:id (:id thread)}]
(let [{:keys [error] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(t/is (= :not-found (th/ex-type error))))))
(t/testing "update comment thread"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
data {::th/type :update-comment-thread
::rpc/profile-id (:id profile-1)
:is-resolved true
:id (:id thread)}]
(t/is (false? (:is-resolved thread)))
(let [{:keys [result] :as out} (th/command! data)]
(t/is (th/success? out))
(t/is (nil? result)))
(let [thread (th/db-get :comment-thread {:id (:id thread)})]
(t/is (true? (:is-resolved thread))))))
(t/testing "create comment"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
data {::th/type :create-comment
::rpc/profile-id (:id profile-1)
:thread-id (:id thread)
:content "comment 2"}]
(let [{:keys [result] :as out} (th/command! data)
{:keys [modified-at]} (th/db-get :comment-thread-status
{:thread-id (:id thread)
:profile-id (:id profile-1)})]
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (uuid? (:id result)))
(t/is (= (:owner-id result) (:id profile-1)))
(t/is (:modified-at result) modified-at))))
(t/testing "update comment"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
comment (-> (th/db-query :comment {:thread-id (:id thread) :content "comment 2"}) first)
data {::th/type :update-comment
::rpc/profile-id (:id profile-1)
:id (:id comment)
:content "comment 2 mod"}]
(let [{:keys [result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (nil? result)))
(let [comment' (th/db-get :comment {:id (:id comment)})]
(t/is (not= (:modified-at comment) (:modified-at comment')))
(t/is (= (:content data) (:content comment'))))))
(t/testing "retrieve threads"
(let [data {::th/type :get-comment-threads
::rpc/profile-id (:id profile-1)
:file-id (:id file-1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [[thread :as result] (:result out)]
(t/is (= 1 (count result)))
(t/is (= "Page 1" (:page-name thread)))
(t/is (= "hello world" (:content thread)))
(t/is (= 2 (:count-comments thread)))
(t/is (true? (:is-resolved thread))))))
(t/testing "unread comment threads"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
data {::th/type :get-unread-comment-threads
::rpc/profile-id (:id profile-1)}]
(let [{:keys [result] :as out} (th/command! (assoc data :team-id (:default-team-id profile-1)))]
(t/is (th/success? out))
(t/is (= [] result)))
(let [{:keys [error] :as out} (th/command! (assoc data :team-id (:default-team-id profile-2)))]
(t/is (not (th/success? out)))
(t/is (= :not-found (th/ex-type error))))
(let [{:keys [result] :as out} (th/command! (assoc data :team-id (:id team)))]
;; (th/print-result! out)
(t/is (th/success? out))
(let [[thread :as result] (:result out)]
(t/is (= 1 (count result)))))
(let [data {::th/type :update-comment-thread-status
::rpc/profile-id (:id profile-1)
:id (:id thread)}
out (th/command! data)]
(t/is (th/success? out)))
(let [{:keys [result] :as out} (th/command! (assoc data :team-id (:id team)))]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= 0 (count result)))))))
(t/testing "get comment thread"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
data {::th/type :get-comment-thread
::rpc/profile-id (:id profile-1)
:file-id (:id file-1)
:id (:id thread)}]
(let [{:keys [result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= (:id thread) (:id result))))))
(t/testing "get comments"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
data {::th/type :get-comments
::rpc/profile-id (:id profile-1)
:thread-id (:id thread)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [comments (:result out)]
(t/is (= 2 (count comments))))))
(t/testing "get profiles"
(let [data {::th/type :get-profiles-for-file-comments
::rpc/profile-id (:id profile-1)
:file-id (:id file-1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [[profile :as profiles] (:result out)]
(t/is (= 1 (count profiles)))
(t/is (= (:id profile-1) (:id profile))))))
(t/testing "get profiles 2"
(let [data {::th/type :get-profiles-for-file-comments
::rpc/profile-id (:id profile-2)
:file-id (:id file-1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(t/is (= :not-found (th/ex-type (:error out))))))
(t/testing "delete comment"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
comment (-> (th/db-query :comment {:thread-id (:id thread) :content "comment 2 mod"}) first)
data {::th/type :delete-comment
::rpc/profile-id (:id profile-2)
:id (:id comment)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(t/is (= :not-found (th/ex-type (:error out))))
(let [comments (th/db-query :comment {:thread-id (:id thread)})]
(t/is (= 2 (count comments))))))
(t/testing "delete comment 2"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
comment (-> (th/db-query :comment {:thread-id (:id thread) :content "comment 2 mod"}) first)
data {::th/type :delete-comment
::rpc/profile-id (:id profile-1)
:id (:id comment)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [comments (th/db-query :comment {:thread-id (:id thread)})]
(t/is (= 1 (count comments))))))
(t/testing "delete comment thread"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
data {::th/type :delete-comment-thread
::rpc/profile-id (:id profile-2)
:id (:id thread)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(t/is (= :not-found (th/ex-type (:error out))))
(let [threads (th/db-query :comment-thread {:file-id (:id file-1)})]
(t/is (= 1 (count threads))))))
(t/testing "delete comment thread 2"
(let [thread (-> (th/db-query :comment-thread {:file-id (:id file-1)}) first)
data {::th/type :delete-comment-thread
::rpc/profile-id (:id profile-1)
:id (:id thread)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [threads (th/db-query :comment-thread {:file-id (:id file-1)})]
(t/is (= 0 (count threads))))))
)))

View File

@@ -6,13 +6,14 @@
(ns backend-tests.rpc-file-test
(:require
[backend-tests.helpers :as th]
[app.common.uuid :as uuid]
[app.db :as db]
[app.db.sql :as sql]
[app.http :as http]
[app.rpc :as-alias rpc]
[app.storage :as sto]
[app.util.time :as dt]
[backend-tests.helpers :as th]
[clojure.test :as t]
[datoteka.core :as fs]))
@@ -28,13 +29,13 @@
(t/testing "create file"
(let [data {::th/type :create-file
:profile-id (:id prof)
::rpc/profile-id (:id prof)
:project-id proj-id
:id file-id
:name "foobar"
:is-shared false
:components-v2 true}
out (th/mutation! data)]
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
@@ -47,8 +48,8 @@
(let [data {::th/type :rename-file
:id file-id
:name "new name"
:profile-id (:id prof)}
out (th/mutation! data)]
::rpc/profile-id (:id prof)}
out (th/command! data)]
;; (th/print-result! out)
(let [result (:result out)]
@@ -56,10 +57,10 @@
(t/is (= (:name data) (:name result))))))
(t/testing "query files"
(let [data {::th/type :project-files
:project-id proj-id
:profile-id (:id prof)}
out (th/query! data)]
(let [data {::th/type :get-project-files
::rpc/profile-id (:id prof)
:project-id proj-id}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
@@ -70,11 +71,11 @@
(t/is (= "new name" (get-in result [0 :name]))))))
(t/testing "query single file without users"
(let [data {::th/type :file
:profile-id (:id prof)
(let [data {::th/type :get-file
::rpc/profile-id (:id prof)
:id file-id
:components-v2 true}
out (th/query! data)]
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
@@ -88,18 +89,18 @@
(t/testing "delete file"
(let [data {::th/type :delete-file
:id file-id
:profile-id (:id prof)}
out (th/mutation! data)]
::rpc/profile-id (:id prof)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))))
(t/testing "query single file after delete"
(let [data {::th/type :file
:profile-id (:id prof)
(let [data {::th/type :get-file
::rpc/profile-id (:id prof)
:id file-id
:components-v2 true}
out (th/query! data)]
out (th/command! data)]
;; (th/print-result! out)
@@ -109,10 +110,10 @@
(t/is (= (:type error-data) :not-found)))))
(t/testing "query list files after delete"
(let [data {::th/type :project-files
:project-id proj-id
:profile-id (:id prof)}
out (th/query! data)]
(let [data {::th/type :get-project-files
::rpc/profile-id (:id prof)
:project-id proj-id}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
@@ -136,19 +137,18 @@
out (th/mutation! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))
(update-file [{:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::th/type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:profile-id profile-id
:revn revn
:components-v2 true
:changes changes}
out (th/mutation! params)]
out (th/command! params)]
(t/is (nil? (:error out)))
(:result out)))]
@@ -257,12 +257,12 @@
profile2 (th/create-profile* 2)
data {::th/type :create-file
:profile-id (:id profile2)
::rpc/profile-id (:id profile2)
:project-id (:default-project-id profile1)
:name "foobar"
:is-shared false
:components-v2 true}
out (th/mutation! data)
out (th/command! data)
error (:error out)]
;; (th/print-result! out)
@@ -277,9 +277,9 @@
:profile-id (:id profile1)})
data {::th/type :rename-file
:id (:id file)
:profile-id (:id profile2)
::rpc/profile-id (:id profile2)
:name "foobar"}
out (th/mutation! data)
out (th/command! data)
error (:error out)]
;; (th/print-result! out)
@@ -293,9 +293,9 @@
file (th/create-file* 1 {:project-id (:default-project-id profile1)
:profile-id (:id profile1)})
data {::th/type :delete-file
:profile-id (:id profile2)
::rpc/profile-id (:id profile2)
:id (:id file)}
out (th/mutation! data)
out (th/command! data)
error (:error out)]
;; (th/print-result! out)
@@ -308,10 +308,10 @@
file (th/create-file* 1 {:project-id (:default-project-id profile1)
:profile-id (:id profile1)})
data {::th/type :set-file-shared
:profile-id (:id profile2)
::rpc/profile-id (:id profile2)
:id (:id file)
:is-shared true}
out (th/mutation! data)
out (th/command! data)
error (:error out)]
;; (th/print-result! out)
@@ -328,11 +328,11 @@
:profile-id (:id profile1)})
data {::th/type :link-file-to-library
:profile-id (:id profile2)
::rpc/profile-id (:id profile2)
:file-id (:id file2)
:library-id (:id file1)}
out (th/mutation! data)
out (th/command! data)
error (:error out)]
;; (th/print-result! out)
@@ -350,11 +350,11 @@
:profile-id (:id profile2)})
data {::th/type :link-file-to-library
:profile-id (:id profile2)
::rpc/profile-id (:id profile2)
:file-id (:id file2)
:library-id (:id file1)}
out (th/mutation! data)
out (th/command! data)
error (:error out)]
;; (th/print-result! out)
@@ -372,10 +372,10 @@
(t/is (= 0 (:processed result))))
;; query the list of files
(let [data {::th/type :project-files
:project-id (:default-project-id profile1)
:profile-id (:id profile1)}
out (th/query! data)]
(let [data {::th/type :get-project-files
::rpc/profile-id (:id profile1)
:project-id (:default-project-id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
@@ -384,15 +384,15 @@
;; Request file to be deleted
(let [params {::th/type :delete-file
:id (:id file)
:profile-id (:id profile1)}
out (th/mutation! params)]
::rpc/profile-id (:id profile1)}
out (th/command! params)]
(t/is (nil? (:error out))))
;; query the list of files after soft deletion
(let [data {::th/type :project-files
:project-id (:default-project-id profile1)
:profile-id (:id profile1)}
out (th/query! data)]
(let [data {::th/type :get-project-files
::rpc/profile-id (:id profile1)
:project-id (:default-project-id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
@@ -403,10 +403,10 @@
(t/is (= 0 (:processed result))))
;; query the list of file libraries of a after hard deletion
(let [data {::th/type :file-libraries
:file-id (:id file)
:profile-id (:id profile1)}
out (th/query! data)]
(let [data {::th/type :get-file-libraries
::rpc/profile-id (:id profile1)
:file-id (:id file)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
@@ -417,10 +417,10 @@
(t/is (= 1 (:processed result))))
;; query the list of file libraries of a after hard deletion
(let [data {::th/type :file-libraries
:file-id (:id file)
:profile-id (:id profile1)}
out (th/query! data)]
(let [data {::th/type :get-file-libraries
::rpc/profile-id (:id profile1)
:file-id (:id file)}
out (th/command! data)]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
@@ -483,11 +483,11 @@
(t/testing "RPC page query (rendering purposes)"
;; Query :page RPC method without passing page-id
(let [data {::th/type :page
:profile-id (:id prof)
(let [data {::th/type :get-page
::rpc/profile-id (:id prof)
:file-id (:id file)
:components-v2 true}
{:keys [error result] :as out} (th/query! data)]
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (map? result))
@@ -500,12 +500,12 @@
)
;; Query :page RPC method with page-id
(let [data {::th/type :page
:profile-id (:id prof)
(let [data {::th/type :get-page
::rpc/profile-id (:id prof)
:file-id (:id file)
:page-id page-id
:components-v2 true}
{:keys [error result] :as out} (th/query! data)]
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (map? result))
(t/is (contains? result :objects))
@@ -516,13 +516,13 @@
(t/is (contains? (:objects result) uuid/zero)))
;; Query :page RPC method with page-id and object-id
(let [data {::th/type :page
:profile-id (:id prof)
(let [data {::th/type :get-page
::rpc/profile-id (:id prof)
:file-id (:id file)
:page-id page-id
:object-id frame1-id
:components-v2 true}
{:keys [error result] :as out} (th/query! data)]
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (map? result))
@@ -534,12 +534,12 @@
(t/is (not (contains? (:objects result) shape2-id))))
;; Query :page RPC method with wrong params
(let [data {::th/type :page
:profile-id (:id prof)
(let [data {::th/type :get-page
::rpc/profile-id (:id prof)
:file-id (:id file)
:object-id frame1-id
:components-v2 true}
out (th/query! data)]
out (th/command! data)]
(t/is (not (th/success? out)))
(let [{:keys [type code]} (-> out :error ex-data)]
@@ -551,21 +551,21 @@
(t/testing "RPC :file-data-for-thumbnail"
;; Insert a thumbnail data for the frame-id
(let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof)
::rpc/profile-id (:id prof)
:file-id (:id file)
:object-id (str page-id frame1-id)
:data "random-data-1"}
{:keys [error result] :as out} (th/mutation! data)]
{:keys [error result] :as out} (th/command! data)]
(t/is (nil? error))
(t/is (nil? result)))
;; Check the result
(let [data {::th/type :file-data-for-thumbnail
:profile-id (:id prof)
(let [data {::th/type :get-file-data-for-thumbnail
::rpc/profile-id (:id prof)
:file-id (:id file)
:components-v2 true}
{:keys [error result] :as out} (th/query! data)]
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (map? result))
(t/is (contains? result :page))
@@ -578,21 +578,21 @@
;; Delete thumbnail data
(let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof)
::rpc/profile-id (:id prof)
:file-id (:id file)
:object-id (str page-id frame1-id)
:data nil}
{:keys [error result] :as out} (th/mutation! data)]
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (nil? result)))
;; Check the result
(let [data {::th/type :file-data-for-thumbnail
:profile-id (:id prof)
(let [data {::th/type :get-file-data-for-thumbnail
::rpc/profile-id (:id prof)
:file-id (:id file)
:components-v2 true}
{:keys [error result] :as out} (th/query! data)]
{:keys [error result] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (map? result))
(t/is (contains? result :page))
@@ -606,11 +606,11 @@
;; insert object snapshot for known frame
(let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof)
::rpc/profile-id (:id prof)
:file-id (:id file)
:object-id (str page-id frame1-id)
:data "new-data"}
{:keys [error result] :as out} (th/mutation! data)]
{:keys [error result] :as out} (th/command! data)]
(t/is (nil? error))
(t/is (nil? result)))
@@ -629,11 +629,11 @@
;; insert object snapshot for for unknown frame
(let [data {::th/type :upsert-file-object-thumbnail
:profile-id (:id prof)
::rpc/profile-id (:id prof)
:file-id (:id file)
:object-id (str page-id (uuid/next))
:data "new-data-2"}
{:keys [error result] :as out} (th/mutation! data)]
{:keys [error result] :as out} (th/command! data)]
(t/is (nil? error))
(t/is (nil? result)))
@@ -661,8 +661,8 @@
:project-id (:default-project-id prof)
:revn 2
:is-shared false})
data {::th/type :file-thumbnail
:profile-id (:id prof)
data {::th/type :get-file-thumbnail
::rpc/profile-id (:id prof)
:file-id (:id file)}]
(t/testing "query a thumbnail with single revn"
@@ -673,7 +673,7 @@
:revn 1
:data "testvalue1"})
(let [{:keys [result error] :as out} (th/query! data)]
(let [{:keys [result error] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (= 4 (count result)))
@@ -687,7 +687,7 @@
:revn 2
:data "testvalue2"})
(let [{:keys [result error] :as out} (th/query! data)]
(let [{:keys [result error] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (= 4 (count result)))
@@ -695,7 +695,7 @@
(t/is (= 2 (:revn result))))
;; Then query the specific revn
(let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))]
(let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (= 4 (count result)))
@@ -704,18 +704,18 @@
(t/testing "upsert file-thumbnail"
(let [data {::th/type :upsert-file-thumbnail
:profile-id (:id prof)
::rpc/profile-id (:id prof)
:file-id (:id file)
:data "foobar"
:props {:baz 1}
:revn 2}
{:keys [result error] :as out} (th/mutation! data)]
{:keys [result error] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (nil? result))))
(t/testing "query last result"
(let [{:keys [result error] :as out} (th/query! data)]
(let [{:keys [result error] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (= 4 (count result)))
@@ -734,7 +734,7 @@
(t/is (= 1 (:processed res))))
;; Then query the specific revn
(let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))]
(let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))]
(t/is (th/ex-of-type? error :not-found))
(t/is (th/ex-of-code? error :file-thumbnail-not-found))))
))

View File

@@ -6,52 +6,56 @@
(ns backend-tests.rpc-font-test
(:require
[backend-tests.helpers :as th]
[app.common.uuid :as uuid]
[app.db :as db]
[app.http :as http]
[app.storage :as sto]
[backend-tests.helpers :as th]
[clojure.test :as t]
[datoteka.fs :as fs]
[datoteka.io :as io]))
[datoteka.io :as io]
[mockery.core :refer [with-mocks]]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest ttf-font-upload-1
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
font-id (uuid/custom 10 1)
(with-mocks [mock {:target 'app.rpc.quotes/check-quote! :return nil}]
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
font-id (uuid/custom 10 1)
ttfdata (-> (io/resource "backend_tests/test_files/font-1.ttf")
io/input-stream
io/read-as-bytes)
ttfdata (-> (io/resource "backend_tests/test_files/font-1.ttf")
io/input-stream
io/read-as-bytes)
params {::th/type :create-font-variant
:profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/ttf" ttfdata}}
out (th/mutation! params)]
params {::th/type :create-font-variant
:profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/ttf" ttfdata}}
out (th/mutation! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (uuid? (:id result)))
(t/is (uuid? (:ttf-file-id result)))
(t/is (uuid? (:otf-file-id result)))
(t/is (uuid? (:woff1-file-id result)))
(t/are [k] (= (get params k)
(get result k))
:team-id
:font-id
:font-family
:font-weight
:font-style))))
(t/is (= 1 (:call-count @mock)))
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (uuid? (:id result)))
(t/is (uuid? (:ttf-file-id result)))
(t/is (uuid? (:otf-file-id result)))
(t/is (uuid? (:woff1-file-id result)))
(t/are [k] (= (get params k)
(get result k))
:team-id
:font-id
:font-family
:font-weight
:font-style)))))
(t/deftest ttf-font-upload-2
(let [prof (th/create-profile* 1 {:is-active true})

View File

@@ -6,10 +6,11 @@
(ns backend-tests.rpc-media-test
(:require
[backend-tests.helpers :as th]
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.storage :as sto]
[backend-tests.helpers :as th]
[clojure.test :as t]
[datoteka.core :as fs]))
@@ -134,3 +135,123 @@
(t/is (= "image/jpeg" (:mtype result)))
(t/is (uuid? (:media-id result)))
(t/is (uuid? (:thumbnail-id result))))))
(t/deftest media-object-from-url-command
(let [prof (th/create-profile* 1)
proj (th/create-project* 1 {:profile-id (:id prof)
:team-id (:default-team-id prof)})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
url "https://raw.githubusercontent.com/uxbox/uxbox/develop/sample_media/images/unsplash/anna-pelzer.jpg"
params {::th/type :create-file-media-object-from-url
::rpc/profile-id (:id prof)
:file-id (:id file)
:is-local true
:url url}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
(t/is (= (:id file) (:file-id result)))
(t/is (= 1024 (:width result)))
(t/is (= 683 (:height result)))
(t/is (= "image/jpeg" (:mtype result)))
(t/is (uuid? media-id))
(t/is (uuid? thumbnail-id))
(let [storage (:app.storage/storage th/*system*)
mobj1 @(sto/get-object storage media-id)
mobj2 @(sto/get-object storage thumbnail-id)]
(t/is (sto/storage-object? mobj1))
(t/is (sto/storage-object? mobj2))
(t/is (= 122785 (:size mobj1)))
;; This is because in ubuntu 21.04 generates different
;; thumbnail that in ubuntu 22.04. This hack should be removed
;; when we all use the ubuntu 22.04 devenv image.
(t/is (or (= 3302 (:size mobj2))
(= 3303 (:size mobj2))))))))
(t/deftest media-object-upload-command
(let [prof (th/create-profile* 1)
proj (th/create-project* 1 {:profile-id (:id prof)
:team-id (:default-team-id prof)})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
mfile {:filename "sample.jpg"
:path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"
:size 312043}
params {::th/type :upload-file-media-object
::rpc/profile-id (:id prof)
:file-id (:id file)
:is-local true
:name "testfile"
:content mfile}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
(t/is (= (:id file) (:file-id result)))
(t/is (= 800 (:width result)))
(t/is (= 800 (:height result)))
(t/is (= "image/jpeg" (:mtype result)))
(t/is (uuid? media-id))
(t/is (uuid? thumbnail-id))
(let [storage (:app.storage/storage th/*system*)
mobj1 @(sto/get-object storage media-id)
mobj2 @(sto/get-object storage thumbnail-id)]
(t/is (sto/storage-object? mobj1))
(t/is (sto/storage-object? mobj2))
(t/is (= 312043 (:size mobj1)))
(t/is (= 3887 (:size mobj2)))))
))
(t/deftest media-object-upload-idempotency-command
(let [prof (th/create-profile* 1)
proj (th/create-project* 1 {:profile-id (:id prof)
:team-id (:default-team-id prof)})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
mfile {:filename "sample.jpg"
:path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"
:size 312043}
params {::th/type :upload-file-media-object
::rpc/profile-id (:id prof)
:file-id (:id file)
:is-local true
:name "testfile"
:content mfile
:id (uuid/next)}]
;; First try
(let [{:keys [result error] :as out} (th/command! params)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (= (:id params) (:id result)))
(t/is (= (:file-id params) (:file-id result)))
(t/is (= 800 (:width result)))
(t/is (= 800 (:height result)))
(t/is (= "image/jpeg" (:mtype result)))
(t/is (uuid? (:media-id result)))
(t/is (uuid? (:thumbnail-id result))))
;; Second try
(let [{:keys [result error] :as out} (th/command! params)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (= (:id params) (:id result)))
(t/is (= (:file-id params) (:file-id result)))
(t/is (= 800 (:width result)))
(t/is (= 800 (:height result)))
(t/is (= "image/jpeg" (:mtype result)))
(t/is (uuid? (:media-id result)))
(t/is (uuid? (:thumbnail-id result))))))

View File

@@ -0,0 +1,344 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns backend-tests.rpc-quotes-test
(:require
[app.common.uuid :as uuid]
[app.db :as db]
[app.http :as http]
[app.rpc :as-alias rpc]
[app.rpc.cond :as cond]
[app.rpc.quotes :as-alias quotes]
[backend-tests.helpers :as th]
[clojure.test :as t]
[datoteka.core :as fs]
[mockery.core :refer [with-mocks]]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest teams-per-profile-quote
(with-mocks [mock {:target 'app.config/get
:return (th/config-get-mock
{:quotes-teams-per-profile 2})}]
(let [profile-1 (th/create-profile* 1)
profile-2 (th/create-profile* 2)
data {::th/type :create-team
::rpc/profile-id (:id profile-1)}
check-ok! (fn [n]
(let [data (assoc data :name (str "team" n))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (some? (:result out)))))
check-ko! (fn [n]
(let [data (assoc data :name (str "team" n))
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [error (:error out)]
(t/is (= :restriction (th/ex-type error)))
(t/is (= :max-quote-reached (th/ex-code error)))
(t/is (= "teams-per-profile" (:target (ex-data error)))))))]
(th/db-insert! :usage-quote
{:profile-id (:id profile-2)
:target "teams-per-profile"
:quote 100})
(check-ok! 1)
(check-ko! 2)
(th/db-insert! :usage-quote
{:profile-id (:id profile-1)
:target "teams-per-profile"
:quote 3})
(check-ok! 2)
(check-ko! 3))))
(t/deftest projects-per-team-quote
(with-mocks [mock {:target 'app.config/get
:return (th/config-get-mock
{:quotes-projects-per-team 2})}]
(let [profile-1 (th/create-profile* 1)
profile-2 (th/create-profile* 2)
team-id (:default-team-id profile-1)
data {::th/type :create-project
:profile-id (:id profile-1)
:team-id team-id}
check-ok! (fn [name]
(let [data (assoc data :name (str "project" name))
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (some? (:result out)))))
check-ko! (fn [name]
;; create second project
(let [data (assoc data :name (str "project" name))
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [error (:error out)]
(t/is (= :restriction (th/ex-type error)))
(t/is (= :max-quote-reached (th/ex-code error)))
(t/is (= "projects-per-team" (:target (ex-data error)))))))]
(check-ok! 1)
(check-ko! 2)
(th/db-insert! :usage-quote
{:team-id team-id
:target "projects-per-team"
:quote 3})
(th/db-insert! :usage-quote
{:team-id team-id
:profile-id (:id profile-2)
:target "projects-per-team"
:quote 10})
(check-ok! 2)
(check-ko! 3)
(th/db-insert! :usage-quote
{:team-id team-id
:profile-id (:id profile-1)
:target "projects-per-team"
:quote 4})
(check-ok! 3)
(check-ko! 4)
(th/db-insert! :usage-quote
{:profile-id (:id profile-1)
:target "projects-per-team"
:quote 5})
(check-ok! 4)
(check-ko! 5)
)))
(t/deftest invitations-per-team-quote
(with-mocks [mock {:target 'app.config/get
:return (th/config-get-mock
{:quotes-invitations-per-team 2})}]
(let [profile-1 (th/create-profile* 1)
profile-2 (th/create-profile* 2)
data {::th/type :create-team-invitations
::rpc/profile-id (:id profile-1)
:team-id (:default-team-id profile-1)
:role :editor}
check-ok! (fn [n]
(let [data (assoc data :emails [(str "foo" n "@example.net")])
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (some? (:result out)))))
check-ko! (fn [n]
(let [data (assoc data :emails [(str "foo" n "@example.net")])
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [error (:error out)]
(t/is (= :restriction (th/ex-type error)))
(t/is (= :max-quote-reached (th/ex-code error)))
(t/is (= "invitations-per-team" (:target (ex-data error)))))))]
(th/db-insert! :usage-quote
{:profile-id (:id profile-2)
:target "invitations-per-team"
:quote 100})
(th/db-insert! :usage-quote
{:team-id (:default-team-id profile-2)
:target "invitations-per-team"
:quote 100})
(check-ok! 1)
(check-ok! 2)
(check-ko! 3)
(th/db-insert! :usage-quote
{:team-id (:default-team-id profile-1)
:target "invitations-per-team"
:quote 3})
(th/db-insert! :usage-quote
{:team-id (:default-team-id profile-1)
:profile-id (:id profile-2)
:target "invitations-per-team"
:quote 100})
(check-ok! 3)
(check-ko! 4)
(th/db-insert! :usage-quote
{:team-id (:default-team-id profile-1)
:profile-id (:id profile-1)
:target "invitations-per-team"
:quote 4})
(check-ok! 4)
(check-ko! 5)
(th/db-insert! :usage-quote
{:profile-id (:id profile-1)
:target "invitations-per-team"
:quote 5})
(check-ok! 5)
(check-ko! 6))))
(t/deftest profiles-per-team-quote
(with-mocks [mock {:target 'app.config/get
:return (th/config-get-mock
{:quotes-profiles-per-team 3})}]
(let [profile-1 (th/create-profile* 1)
profile-2 (th/create-profile* 2)
data {::th/type :create-team-invitations
::rpc/profile-id (:id profile-1)
:team-id (:default-team-id profile-1)
:role :editor}
check-ok! (fn [n]
(let [data (assoc data :emails [(str "foo" n "@example.net")])
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (some? (:result out)))))
check-ko! (fn [n]
(let [data (assoc data :emails [(str "foo" n "@example.net")])
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [error (:error out)]
(t/is (= :restriction (th/ex-type error)))
(t/is (= :max-quote-reached (th/ex-code error)))
(t/is (= "profiles-per-team" (:target (ex-data error)))))))]
(th/create-team-role* {:team-id (:default-team-id profile-1)
:profile-id (:id profile-2)
:role :admin})
(th/db-insert! :usage-quote
{:profile-id (:id profile-2)
:target "profiles-per-team"
:quote 100})
(th/db-insert! :usage-quote
{:team-id (:default-team-id profile-2)
:target "profiles-per-team"
:quote 100})
(check-ok! 1)
(check-ko! 2)
(th/db-insert! :usage-quote
{:team-id (:default-team-id profile-1)
:target "profiles-per-team"
:quote 4})
(check-ok! 2)
(check-ko! 3))))
(t/deftest files-per-project-quote
(with-mocks [mock {:target 'app.config/get
:return (th/config-get-mock
{:quotes-files-per-project 1})}]
(let [profile-1 (th/create-profile* 1)
profile-2 (th/create-profile* 2)
project-1 (th/create-project* 1 {:profile-id (:id profile-1)
:team-id (:default-team-id profile-1)})
project-2 (th/create-project* 2 {:profile-id (:id profile-2)
:team-id (:default-team-id profile-2)})
data {::th/type :create-file
::rpc/profile-id (:id profile-1)
:project-id (:id project-1)}
check-ok! (fn [n]
(let [data (assoc data :name (str "file" n))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (some? (:result out)))))
check-ko! (fn [n]
(let [data (assoc data :name (str "file" n))
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [error (:error out)]
(t/is (= :restriction (th/ex-type error)))
(t/is (= :max-quote-reached (th/ex-code error)))
(t/is (= "files-per-project" (:target (ex-data error)))))))]
(th/db-insert! :usage-quote
{:project-id (:id project-2)
:target "files-per-project"
:quote 100})
(th/db-insert! :usage-quote
{:team-id (:team-id project-2)
:target "files-per-project"
:quote 100})
(th/db-insert! :usage-quote
{:profile-id (:id profile-2)
:target "files-per-project"
:quote 100})
(check-ok! 1)
(check-ko! 2)
(th/db-insert! :usage-quote
{:project-id (:id project-1)
:target "files-per-project"
:quote 2})
(th/db-insert! :usage-quote
{:project-id (:id project-1)
:profile-id (:id profile-2)
:target "files-per-project"
:quote 100})
(check-ok! 2)
(check-ko! 3)
(th/db-insert! :usage-quote
{:team-id (:team-id project-1)
:target "files-per-project"
:quote 3})
(th/db-insert! :usage-quote
{:team-id (:team-id project-1)
:profile-id (:id profile-2)
:target "files-per-project"
:quote 100})
(check-ok! 3)
(check-ko! 4)
(th/db-insert! :usage-quote
{:profile-id (:id profile-1)
:target "files-per-project"
:quote 4})
(check-ok! 4)
(check-ko! 5)
)))

View File

@@ -21,7 +21,7 @@
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest invite-team-member
(t/deftest create-team-invitations
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})
profile2 (th/create-profile* 2 {:is-active true})
@@ -30,14 +30,14 @@
team (th/create-team* 1 {:profile-id (:id profile1)})
pool (:app.db/pool th/*system*)
data {::th/type :invite-team-member
data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:team-id (:id team)
:role :editor
:profile-id (:id profile1)}]
:role :editor}]
;; invite external user without complaints
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)
out (th/command! data)
;; retrieve the value from the database and check its content
invitation (db/exec-one!
th/*pool*
@@ -52,7 +52,7 @@
;; invite internal user without complaints
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile2))
out (th/mutation! data)]
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
@@ -60,7 +60,7 @@
(th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"})
(th/reset-mock! mock)
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)]
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
@@ -79,7 +79,7 @@
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)]
out (th/command! data)]
(t/is (not (th/success? out)))
(t/is (= 0 (:call-count @mock)))
@@ -92,7 +92,7 @@
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile3))
out (th/mutation! data)]
out (th/command! data)]
(t/is (not (th/success? out)))
(t/is (= 0 (:call-count @mock)))
@@ -115,12 +115,12 @@
pool (:app.db/pool th/*system*)]
;; Try to invite a not existing user
(let [data {::th/type :invite-team-member
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:email "notexisting@example.com"
:team-id (:id team)
:role :editor
:profile-id (:id profile1)}
out (th/mutation! data)]
:role :editor}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
@@ -139,12 +139,12 @@
(th/reset-mock! mock)
;; Try to invite existing user
(let [data {::th/type :invite-team-member
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:email (:email profile2)
:team-id (:id team)
:role :editor
:profile-id (:id profile1)}
out (th/mutation! data)]
:role :editor}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
@@ -166,6 +166,7 @@
(t/deftest accept-invitation-tokens
(let [profile1 (th/create-profile* 1 {:is-active true})
profile2 (th/create-profile* 2 {:is-active true})
profile3 (th/create-profile* 3 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)})
@@ -181,25 +182,29 @@
:member-email (:email profile2)
:member-id (:id profile2)})]
;; --- Verify token as anonymous user
(t/testing "Verify token as anonymous user"
(db/insert! pool :team-invitation
{:team-id (:id team)
:email-to (:email profile2)
:role "editor"
:valid-until (dt/in-future "48h")})
(db/insert! pool :team-invitation
{:team-id (:id team)
:email-to (:email profile2)
:role "editor"
:valid-until (dt/in-future "48h")})
(let [data {::th/type :verify-token :token token}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [data {::th/type :verify-token :token token}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= :created (:state result)))
(t/is (= (:email profile2) (:member-email result)))
(t/is (= (:id profile2) (:member-id result))))
(let [result (:result out)]
(t/is (contains? result :invitation-token))
(t/is (contains? result :iss))
(t/is (contains? result :redirect-to))
(t/is (contains? result :state))
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
(t/is (= 2 (count rows)))))
(t/is (= :pending (:state result)))
(t/is (= :auth-login (:redirect-to result))))
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
(t/is (= 1 (count rows))))))
;; Clean members
(db/delete! pool :team-profile-rel
@@ -207,46 +212,41 @@
:profile-id (:id profile2)})
;; --- Verify token as logged-in user
(t/testing "Verify token as logged-in user"
(let [data {::th/type :verify-token
::rpc/profile-id (:id profile2)
:token token}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= :created (:state result)))
(t/is (= (:email profile2) (:member-email result)))
(t/is (= (:id profile2) (:member-id result))))
(db/insert! pool :team-invitation
{:team-id (:id team)
:email-to (:email profile2)
:role "editor"
:valid-until (dt/in-future "48h")})
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
(t/is (= 2 (count rows))))))
(let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile2)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (= :created (:state result)))
(t/is (= (:email profile2) (:member-email result)))
(t/is (= (:id profile2) (:member-id result))))
(t/testing "Verify token as logged-in wrong user"
(db/insert! pool :team-invitation
{:team-id (:id team)
:email-to (:email profile3)
:role "editor"
:valid-until (dt/in-future "48h")})
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
(t/is (= 2 (count rows)))))
;; --- Verify token as logged-in wrong user
(db/insert! pool :team-invitation
{:team-id (:id team)
:email-to (:email profile2)
:role "editor"
:valid-until (dt/in-future "48h")})
(let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :invalid-token (:code edata)))))
(let [data {::th/type :verify-token
::rpc/profile-id (:id profile1)
:token token}
out (th/command! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :invalid-token (:code edata))))))
)))
(t/deftest invite-team-member-with-email-verification-disabled
(t/deftest create-team-invitations-with-email-verification-disabled
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})
profile2 (th/create-profile* 2 {:is-active true})
@@ -255,16 +255,16 @@
team (th/create-team* 1 {:profile-id (:id profile1)})
pool (:app.db/pool th/*system*)
data {::th/type :invite-team-member
data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:team-id (:id team)
:role :editor
:profile-id (:id profile1)}]
:role :editor}]
;; invite internal user without complaints
(with-redefs [app.config/flags #{}]
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile2))
out (th/mutation! data)]
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 0 (:call-count (deref mock)))))
@@ -279,8 +279,8 @@
team (th/create-team* 1 {:profile-id (:id profile1)})
pool (:app.db/pool th/*system*)
data {::th/type :delete-team
:team-id (:id team)
:profile-id (:id profile1)}]
::rpc/profile-id (:id profile1)
:team-id (:id team)}]
;; team is not deleted because it does not meet all
;; conditions to be deleted.
@@ -288,9 +288,9 @@
(t/is (= 0 (:processed result))))
;; query the list of teams
(let [data {::th/type :teams
:profile-id (:id profile1)}
out (th/query! data)]
(let [data {::th/type :get-teams
::rpc/profile-id (:id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
@@ -300,15 +300,15 @@
;; Request team to be deleted
(let [params {::th/type :delete-team
:id (:id team)
:profile-id (:id profile1)}
out (th/mutation! params)]
::rpc/profile-id (:id profile1)
:id (:id team)}
out (th/command! params)]
(t/is (th/success? out)))
;; query the list of teams after soft deletion
(let [data {::th/type :teams
:profile-id (:id profile1)}
out (th/query! data)]
(let [data {::th/type :get-teams
::rpc/profile-id (:id profile1)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
@@ -321,8 +321,8 @@
;; query the list of projects after hard deletion
(let [data {::th/type :projects
:team-id (:id team)
:profile-id (:id profile1)}
:profile-id (:id profile1)
:team-id (:id team)}
out (th/query! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
@@ -335,8 +335,8 @@
;; query the list of projects of a after hard deletion
(let [data {::th/type :projects
:team-id (:id team)
:profile-id (:id profile1)}
:profile-id (:id profile1)
:team-id (:id team)}
out (th/query! data)]
;; (th/print-result! out)
@@ -348,8 +348,8 @@
(t/deftest query-team-invitations
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
data {::th/type :team-invitations
:profile-id (:id prof)
data {::th/type :get-team-invitations
::rpc/profile-id (:id prof)
:team-id (:id team)}]
;; insert an entry on the database with an enabled invitation
@@ -366,7 +366,7 @@
:role "editor"
:valid-until (dt/in-past "48h")})
(let [out (th/query! data)]
(let [out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
one (first result)
@@ -381,7 +381,7 @@
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
data {::th/type :update-team-invitation-role
:profile-id (:id prof)
::rpc/profile-id (:id prof)
:team-id (:id team)
:email "TEST1@mail.com"
:role :admin}]
@@ -393,7 +393,7 @@
:role "editor"
:valid-until (dt/in-future "48h")})
(let [out (th/mutation! data)
(let [out (th/command! data)
;; retrieve the value from the database and check its content
res (db/get* th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"})]
@@ -405,7 +405,7 @@
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
data {::th/type :delete-team-invitation
:profile-id (:id prof)
::rpc/profile-id (:id prof)
:team-id (:id team)
:email "TEST1@mail.com"}]
@@ -416,7 +416,7 @@
:role "editor"
:valid-until (dt/in-future "48h")})
(let [out (th/mutation! data)
(let [out (th/command! data)
;; retrieve the value from the database and check its content
res (db/get* th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"})]

View File

@@ -6,11 +6,12 @@
(ns backend-tests.rpc-webhooks-test
(:require
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.db :as db]
[app.http :as http]
[app.storage :as sto]
[app.rpc :as-alias rpc]
[app.storage :as sto]
[backend-tests.helpers :as th]
[clojure.test :as t]
[mockery.core :refer [with-mocks]]))
@@ -31,7 +32,7 @@
(let [params {::th/type :create-webhook
::rpc/profile-id (:id prof)
:team-id team-id
:uri "http://example.com"
:uri (u/uri "http://example.com")
:mtype "application/json"}
out (th/command! params)]

View File

@@ -1,8 +1,8 @@
{:deps
{org.clojure/clojure {:mvn/version "1.11.1"}
org.clojure/data.json {:mvn/version "2.4.0"}
org.clojure/tools.cli {:mvn/version "1.0.206"}
metosin/jsonista {:mvn/version "0.3.6"}
org.clojure/tools.cli {:mvn/version "1.0.214"}
metosin/jsonista {:mvn/version "0.3.7"}
org.clojure/clojurescript {:mvn/version "1.11.60"}
org.clojure/test.check {:mvn/version "1.1.1"}
@@ -23,7 +23,7 @@
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/promesa {:mvn/version "10.0.571"}
funcool/promesa {:mvn/version "10.0.594"}
funcool/cuerdas {:mvn/version "2022.06.16-403"}
lambdaisland/uri {:mvn/version "1.13.95"
@@ -37,21 +37,22 @@
;; exception printing
fipp/fipp {:mvn/version "0.6.26"}
io.aviso/pretty {:mvn/version "1.1.1"}
io.aviso/pretty {:mvn/version "1.3"}
environ/environ {:mvn/version "1.2.0"}}
:paths ["src" "target/classes"]
:aliases
{:dev
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "2.20.2"}
thheller/shadow-cljs {:mvn/version "2.20.16"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
mockery/mockery {:mvn/version "RELEASE"}}
:extra-paths ["test" "dev"]}
:build
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.9.0" :git/sha "8c93e0c"}}
:ns-default build}
:test

View File

@@ -4,7 +4,7 @@
"main": "index.js",
"license": "MPL-2.0",
"dependencies": {
"luxon": "^1.27.0"
"luxon": "^3.1.1"
},
"scripts": {
"compile-and-watch-test": "clojure -M:dev:shadow-cljs watch test",
@@ -13,8 +13,8 @@
"test": "yarn run compile-test && yarn run run-test"
},
"devDependencies": {
"shadow-cljs": "2.19.8",
"source-map-support": "^0.5.19",
"ws": "^7.4.6"
"shadow-cljs": "2.20.16",
"source-map-support": "^0.5.21",
"ws": "^8.11.0"
}
}

View File

@@ -203,19 +203,22 @@
([coll value]
(sequence (replace-by-id value) coll)))
(defn without-nils
"Given a map, return a map removing key-value
pairs when value is `nil`."
[data]
(into {} (remove (comp nil? second)) data))
(defn vec-without-nils
[coll]
(into [] (remove nil?) coll))
(defn without-nils
"Given a map, return a map removing key-value
pairs when value is `nil`."
([] (remove (comp nil? val)))
([data]
(into {} (without-nils) data)))
(defn without-qualified
[data]
(into {} (remove (comp qualified-keyword? first)) data))
([]
(remove (comp qualified-keyword? key)))
([data]
(into {} (without-qualified) data)))
(defn without-keys
"Return a map without the keys provided

View File

@@ -16,7 +16,7 @@
(defmacro error
[& {:keys [type hint] :as params}]
`(ex-info ~(or hint (pr-str type))
`(ex-info ~(or hint (name type))
(merge
~(dissoc params :cause ::data)
~(::data params))
@@ -89,9 +89,9 @@
(contains? data :explain))
(explain (:explain data) opts)
(and (::s/problems data)
(::s/value data)
(::s/spec data))
(and (contains? data ::s/problems)
(contains? data ::s/value)
(contains? data ::s/spec))
(binding [s/*explain-out* expound/printer]
(with-out-str
(s/explain-out (update data ::s/problems #(take max-problems %))))))))

View File

@@ -44,7 +44,12 @@
(let [component-id (:current-component-id file)
change (cond-> change
(and add-container? (some? component-id))
(assoc :component-id component-id)
(cond->
:always
(assoc :component-id component-id)
(some? (:current-frame-id file))
(assoc :frame-id (:current-frame-id file)))
(and add-container? (nil? component-id))
(assoc :page-id (:current-page-id file)
@@ -195,7 +200,7 @@
(assert (nil? (:current-component-id file)))
(let [page-id (or (:id data) (uuid/next))
page (-> (ctp/make-empty-page page-id "Page-1")
page (-> (ctp/make-empty-page page-id "Page 1")
(d/deep-merge data))]
(-> file
(commit-change
@@ -223,7 +228,6 @@
(clear-names)))
(defn add-artboard [file data]
(assert (nil? (:current-component-id file)))
(let [obj (-> (cts/make-minimal-shape :frame)
(merge data)
(check-name file :frame)
@@ -237,11 +241,11 @@
(update :parent-stack conjv (:id obj)))))
(defn close-artboard [file]
(assert (nil? (:current-component-id file)))
(let [parent-id (-> file :parent-id peek)
parent (lookup-shape file parent-id)
current-frame-id (or (:frame-id parent) root-frame)]
current-frame-id (or (:frame-id parent)
(when (nil? (:current-component-id file))
root-frame))]
(-> file
(assoc :current-frame-id current-frame-id)
(update :parent-stack pop))))
@@ -561,35 +565,37 @@
:id id}))))
(defn start-component
[file data]
([file data] (start-component file data :group))
([file data root-type]
(let [selrect (or (gsh/make-selrect (:x data) (:y data) (:width data) (:height data))
cts/empty-selrect)
name (:name data)
path (:path data)
main-instance-id (:main-instance-id data)
main-instance-page (:main-instance-page data)
obj (-> (cts/make-shape root-type selrect data)
(dissoc :path
:main-instance-id
:main-instance-page
:main-instance-x
:main-instance-y)
(check-name file root-type)
(d/without-nils))]
(-> file
(commit-change
{:type :add-component
:id (:id obj)
:name name
:path path
:main-instance-id main-instance-id
:main-instance-page main-instance-page
:shapes [obj]})
(let [selrect cts/empty-selrect
name (:name data)
path (:path data)
main-instance-id (:main-instance-id data)
main-instance-page (:main-instance-page data)
obj (-> (cts/make-minimal-group nil selrect name)
(merge data)
(dissoc :path
:main-instance-id
:main-instance-page
:main-instance-x
:main-instance-y)
(check-name file :group)
(d/without-nils))]
(-> file
(commit-change
{:type :add-component
:id (:id obj)
:name name
:path path
:main-instance-id main-instance-id
:main-instance-page main-instance-page
:shapes [obj]})
(assoc :last-id (:id obj))
(update :parent-stack conjv (:id obj))
(assoc :current-component-id (:id obj)))))
(assoc :last-id (:id obj))
(update :parent-stack conjv (:id obj))
(assoc :current-component-id (:id obj))
(assoc :current-frame-id (when (= (:type obj) :frame)
(:id obj)))))))
(defn finish-component
[file]
@@ -624,7 +630,7 @@
{:add-container? true}))
:else
(= (:type component) :group)
(let [component' (gsh/update-group-selrect component children)]
(commit-change
file
@@ -637,11 +643,13 @@
{:type :set :attr :y :val (-> component' :selrect :y) :ignore-touched true}
{:type :set :attr :width :val (-> component' :selrect :width) :ignore-touched true}
{:type :set :attr :height :val (-> component' :selrect :height) :ignore-touched true}]}
{:add-container? true}))
{:add-container? true})))]
:else file)]
(-> file
(dissoc :current-component-id)
(dissoc :current-frame-id)
(update :parent-stack pop))))
(defn finish-deleted-component
@@ -700,7 +708,7 @@
(gpt/point x
y)
#_{:main-instance? true
:force-id main-instance-id})]
:force-id main-instance-id})]
(as-> file $
(reduce #(commit-change %1

View File

@@ -19,7 +19,6 @@
(def precision 6)
;; --- Matrix Impl
(defrecord Matrix [^double a
^double b
^double c
@@ -28,13 +27,13 @@
^double f]
Object
(toString [_]
(str "matrix("
(mth/precision a precision) ","
(mth/precision b precision) ","
(mth/precision c precision) ","
(mth/precision d precision) ","
(mth/precision e precision) ","
(mth/precision f precision) ")")))
(dm/fmt "matrix(%, %, %, %, %, %)"
(mth/to-fixed a precision)
(mth/to-fixed b precision)
(mth/to-fixed c precision)
(mth/to-fixed d precision)
(mth/to-fixed e precision)
(mth/to-fixed f precision))))
(defn matrix?
"Return true if `v` is Matrix instance."

View File

@@ -13,6 +13,7 @@
:clj [clojure.core :as c])
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.math :as mth]
[app.common.spec :as us]
[clojure.spec.alpha :as s]
@@ -59,10 +60,10 @@
(point v v)
(point-like? v)
(map->Point v)
(Point. (:x v) (:y v))
:else
(throw (ex-info "Invalid arguments" {:v v}))))
(ex/raise :hint "invalid arguments (on pointer constructor)" :value v)))
([x y]
(Point. x y)))
@@ -169,8 +170,7 @@
(dm/get-prop p2 :x))
dy (- (dm/get-prop p1 :y)
(dm/get-prop p2 :y))]
(mth/sqrt (+ (mth/pow dx 2)
(mth/pow dy 2)))))
(mth/hypot dx dy)))
(defn distance-vector
"Calculate the distance, separated x and y."
@@ -190,8 +190,7 @@
(assert (point? pt) "point instance expected")
(let [x (dm/get-prop pt :x)
y (dm/get-prop pt :y)]
(mth/sqrt (+ (mth/pow x 2)
(mth/pow y 2)))))
(mth/hypot x y)))
(defn angle
"Returns the smaller angle between two vectors.
@@ -275,12 +274,12 @@
(Point. (mth/precision (dm/get-prop pt :x) decimals)
(mth/precision (dm/get-prop pt :y) decimals))))
(defn half-round
(defn round-step
"Round the coordinates to the closest half-point"
[pt]
[pt step]
(assert (point? pt) "expected point instance")
(Point. (mth/half-round (dm/get-prop pt :x))
(mth/half-round (dm/get-prop pt :y))))
(Point. (mth/round (dm/get-prop pt :x) step)
(mth/round (dm/get-prop pt :y) step)))
(defn transform
"Transform a point applying a matrix transformation."

View File

@@ -43,13 +43,13 @@
(defn bounding-box
"Returns a rect that wraps the shape after all transformations applied."
[shape]
; TODO: perhaps we need to store this calculation in a shape attribute
;; TODO: perhaps we need to store this calculation in a shape attribute
(gpr/points->rect (:points shape)))
(defn left-bound
"Returns the lowest x coord of the shape BEFORE applying transformations."
; TODO: perhaps some day we want after transformations, but for the
; moment it's enough as is now.
;; TODO: perhaps some day we want after transformations, but for the
;; moment it's enough as is now.
[shape]
(or (:x shape) (:x (:selrect shape)))) ; Paths don't have :x attribute
@@ -82,24 +82,12 @@
(update :height (comp inc inc))))))
(defn selrect->areas [bounds selrect]
(let [make-selrect
(fn [x1 y1 x2 y2]
(let [x1 (min x1 x2)
x2 (max x1 x2)
y1 (min y1 y2)
y2 (max y1 y2)]
{:x1 x1 :y1 y1
:x2 x2 :y2 y2
:x x1 :y y1
:width (- x2 x1)
:height (- y2 y1)
:type :rect}))
{bound-x1 :x1 bound-x2 :x2 bound-y1 :y1 bound-y2 :y2} bounds
(let [{bound-x1 :x1 bound-x2 :x2 bound-y1 :y1 bound-y2 :y2} bounds
{sr-x1 :x1 sr-x2 :x2 sr-y1 :y1 sr-y2 :y2} selrect]
{:left (make-selrect bound-x1 sr-y1 sr-x1 sr-y2)
:top (make-selrect sr-x1 bound-y1 sr-x2 sr-y1)
:right (make-selrect sr-x2 sr-y1 bound-x2 sr-y2)
:bottom (make-selrect sr-x1 sr-y2 sr-x2 bound-y2)}))
{:left (gpr/corners->selrect bound-x1 sr-y1 sr-x1 sr-y2)
:top (gpr/corners->selrect sr-x1 bound-y1 sr-x2 sr-y1)
:right (gpr/corners->selrect sr-x2 sr-y1 bound-x2 sr-y2)
:bottom (gpr/corners->selrect sr-x1 sr-y2 sr-x2 bound-y2)}))
(defn distance-selrect [selrect other]
(let [{:keys [x1 y1]} other
@@ -118,8 +106,8 @@
([attr val1 val2 precision]
(let [close-val? (fn [num1 num2]
(when (and (number? num1) (number? num2))
(< (mth/abs (- num1 num2)) precision)))]
(when (and (number? num1) (number? num2))
(< (mth/abs (- num1 num2)) precision)))]
(cond
(and (number? val1) (number? val2))
(close-val? val1 val2)
@@ -161,6 +149,7 @@
(dm/export gpr/contains-selrect?)
(dm/export gpr/contains-point?)
(dm/export gpr/close-selrect?)
(dm/export gpr/clip-selrect)
(dm/export gtr/move)
(dm/export gtr/absolute-move)
@@ -194,7 +183,6 @@
(dm/export gsi/rect-contains-shape?)
;; Bool
(dm/export gsb/calc-bool-content)
;; Constraints
@@ -207,4 +195,3 @@
;; Modifiers
(dm/export gsm/set-objects-modifiers)

View File

@@ -146,10 +146,13 @@
((if (= :x axis) center-horizontal-vector center-vertical-vector) child-points parent-points))
(defn displacement
[before-v after-v]
(let [angl (gpt/angle-with-other before-v after-v)
sign (if (mth/close? angl 180) -1 1)
[before-v after-v before-parent-side-v after-parent-side-v]
(let [before-angl (gpt/angle-with-other before-v before-parent-side-v)
after-angl (gpt/angle-with-other after-v after-parent-side-v)
sign (if (mth/close? before-angl after-angl) 1 -1)
length (* sign (gpt/length before-v))]
(if (mth/almost-zero? length)
after-v
(gpt/subtract after-v (gpt/scale (gpt/unit after-v) length)))))
@@ -173,14 +176,18 @@
(defmethod constraint-modifier :start
[_ axis child-points-before parent-points-before child-points-after parent-points-after]
(let [start-before (start-vector axis child-points-before parent-points-before)
start-after (start-vector axis child-points-after parent-points-after)]
(ctm/move-modifiers (displacement start-before start-after))))
start-after (start-vector axis child-points-after parent-points-after)
before-side-vector (side-vector axis parent-points-before)
after-side-vector (side-vector axis parent-points-after)]
(ctm/move-modifiers (displacement start-before start-after before-side-vector after-side-vector))))
(defmethod constraint-modifier :end
[_ axis child-points-before parent-points-before child-points-after parent-points-after]
(let [end-before (end-vector axis child-points-before parent-points-before)
end-after (end-vector axis child-points-after parent-points-after)]
(ctm/move-modifiers (displacement end-before end-after))))
end-after (end-vector axis child-points-after parent-points-after)
before-side-vector (side-vector axis parent-points-before)
after-side-vector (side-vector axis parent-points-after)]
(ctm/move-modifiers (displacement end-before end-after before-side-vector after-side-vector))))
(defmethod constraint-modifier :fixed
[_ axis child-points-before parent-points-before child-points-after parent-points-after]
@@ -190,8 +197,11 @@
start-before (start-vector axis child-points-before parent-points-before)
start-after (start-vector axis child-points-after parent-points-after)
disp-end (displacement end-before end-after)
disp-start (displacement start-before start-after)
before-side-vector (side-vector axis parent-points-before)
after-side-vector (side-vector axis parent-points-after)
disp-end (displacement end-before end-after before-side-vector after-side-vector)
disp-start (displacement start-before start-after before-side-vector after-side-vector)
;; We get the current axis side and grow it on both side by the end+start displacements
before-vec (side-vector axis child-points-after)
@@ -200,7 +210,7 @@
;; after-vec will contain the side length of the grown side
;; we scale the shape by the diference and translate it by the start
;; displacement (so its left+top position is constant)
scale (/ (gpt/length after-vec) (gpt/length before-vec))
scale (/ (gpt/length after-vec) (max 0.01 (gpt/length before-vec)))
resize-origin (gpo/origin child-points-after)
@@ -214,8 +224,10 @@
(defmethod constraint-modifier :center
[_ axis child-points-before parent-points-before child-points-after parent-points-after]
(let [center-before (center-vector axis child-points-before parent-points-before)
center-after (center-vector axis child-points-after parent-points-after)]
(ctm/move-modifiers (displacement center-before center-after))))
center-after (center-vector axis child-points-after parent-points-after)
before-side-vector (side-vector axis parent-points-before)
after-side-vector (side-vector axis parent-points-after)]
(ctm/move-modifiers (displacement center-before center-after before-side-vector after-side-vector))))
(defmethod constraint-modifier :default [_ _ _ _ _]
[])
@@ -256,11 +268,11 @@
scale-x (if (= :scale constraints-h)
1
(/ (gpo/width-points child-bb-before) (gpo/width-points child-bb-after)))
(/ (gpo/width-points child-bb-before) (max 0.01 (gpo/width-points child-bb-after))))
scale-y (if (= :scale constraints-v)
1
(/ (gpo/height-points child-bb-before) (gpo/height-points child-bb-after)))
(/ (gpo/height-points child-bb-before) (max 0.01 (gpo/height-points child-bb-after))))
resize-vector (gpt/point scale-x scale-y)
resize-origin (gpo/origin transformed-child-bounds)

View File

@@ -13,9 +13,10 @@
[app.common.geom.shapes.flex-layout.modifiers :as fmo]))
(dm/export fbo/layout-content-bounds)
(dm/export fbo/layout-content-points)
(dm/export fbo/child-layout-bound-points)
(dm/export fdr/get-drop-index)
(dm/export fdr/layout-drop-areas)
(dm/export fdr/get-drop-areas)
(dm/export fli/calc-layout-data)
(dm/export fmo/layout-child-modifiers)

View File

@@ -6,6 +6,7 @@
(ns app.common.geom.shapes.flex-layout.bounds
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.points :as gpo]
[app.common.types.shape.layout :as ctl]))
@@ -27,16 +28,19 @@
h-center? (ctl/h-center? parent)
h-end? (ctl/h-end? parent)
fill-w? (ctl/fill-width? child)
fill-h? (ctl/fill-height? child)
base-p (gpo/origin child-bounds)
width (gpo/width-points child-bounds)
height (gpo/height-points child-bounds)
min-width (if (ctl/fill-width? child)
min-width (if fill-w?
(ctl/child-min-width child)
width)
min-height (if (ctl/fill-height? child)
min-height (if fill-h?
(ctl/child-min-height child)
height)
@@ -60,26 +64,60 @@
min-width (max min-width 0.01)
min-height (max min-height 0.01)]
(-> [base-p]
(conj (cond-> base-p
(or row? h-start?)
(gpt/add (hv min-width))
(cond-> [base-p
(gpt/add base-p (hv 0.01))
(gpt/add base-p (vv 0.01))]
(and col? h-center?)
(gpt/add (hv (/ min-width 2)))
col?
(conj (gpt/add base-p (vv min-height)))
(and col? h-center?)
(gpt/subtract (hv min-width))))
row?
(conj (gpt/add base-p (hv min-width)))
(conj (cond-> base-p
(or col? v-start?)
(gpt/add (vv min-height))
(and col? h-start?)
(conj (gpt/add base-p (hv min-width)))
(and row? v-center?)
(gpt/add (vv (/ min-height 2)))
(and col? h-center?)
(conj (gpt/add base-p (hv (/ min-width 2)))
(gpt/subtract base-p (hv (/ min-width 2))))
(and row? v-end?)
(gpt/subtract (vv min-height)))))))
(and col? h-end?)
(conj (gpt/subtract base-p (hv min-width)))
(and row? v-start?)
(conj (gpt/add base-p (vv min-height)))
(and row? v-center?)
(conj (gpt/add base-p (vv (/ min-height 2)))
(gpt/subtract base-p (vv (/ min-height 2))))
(and row? v-end?)
(conj (gpt/subtract base-p (vv min-height))))))
(defn layout-content-points
[bounds parent children]
(let [parent-id (:id parent)
parent-bounds @(get bounds parent-id)
get-child-bounds
(fn [child]
(let [child-id (:id child)
child-bounds @(get bounds child-id)
[margin-top margin-right margin-bottom margin-left] (ctl/child-margins child)
child-bounds
(if (or (ctl/fill-width? child) (ctl/fill-height? child))
(child-layout-bound-points parent child parent-bounds child-bounds)
child-bounds)
child-bounds
(when (d/not-empty? child-bounds)
(-> (gpo/parent-coords-bounds child-bounds parent-bounds)
(gpo/pad-points (- margin-top) (- margin-right) (- margin-bottom) (- margin-left))))]
child-bounds))]
(->> children (map get-child-bounds))))
(defn layout-content-bounds
[bounds {:keys [layout-padding] :as parent} children]
@@ -87,27 +125,37 @@
(let [parent-id (:id parent)
parent-bounds @(get bounds parent-id)
row? (ctl/row? parent)
col? (ctl/col? parent)
space-around? (ctl/space-around? parent)
space-evenly? (ctl/space-evenly? parent)
content-evenly? (ctl/content-evenly? parent)
[layout-gap-row layout-gap-col] (ctl/gaps parent)
row-pad (if (or (and col? space-evenly?)
(and col? space-around?)
(and row? content-evenly?))
layout-gap-row
0)
col-pad (if (or(and row? space-evenly?)
(and row? space-around?)
(and col? content-evenly?))
layout-gap-col
0)
{pad-top :p1 pad-right :p2 pad-bottom :p3 pad-left :p4} layout-padding
pad-top (or pad-top 0)
pad-right (or pad-right 0)
pad-bottom (or pad-bottom 0)
pad-left (or pad-left 0)
pad-top (+ (or pad-top 0) row-pad)
pad-right (+ (or pad-right 0) col-pad)
pad-bottom (+ (or pad-bottom 0) row-pad)
pad-left (+ (or pad-left 0) col-pad)
child-bounds
(fn [child]
(let [child-id (:id child)
child-bounds @(get bounds child-id)
child-bounds
(if (or (ctl/fill-height? child) (ctl/fill-height? child))
(child-layout-bound-points parent child parent-bounds child-bounds)
child-bounds)
[margin-top margin-right margin-bottom margin-left] (ctl/child-margins child)]
(-> (gpo/parent-coords-bounds child-bounds parent-bounds)
(gpo/pad-points (- margin-top) (- margin-right) (- margin-bottom) (- margin-left)))))]
(as-> children $
(map child-bounds $)
(gpo/merge-parent-coords-bounds $ parent-bounds)
(gpo/pad-points $ (- pad-top) (- pad-right) (- pad-bottom) (- pad-left)))))
layout-points
(layout-content-points bounds parent children)]
(if (d/not-empty? layout-points)
(-> layout-points
(gpo/merge-parent-coords-bounds parent-bounds)
(gpo/pad-points (- pad-top) (- pad-right) (- pad-bottom) (- pad-left)))
;; Cannot create some bounds from the children so we return the parent's
parent-bounds)))

View File

@@ -8,11 +8,14 @@
(:require
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.flex-layout.lines :as fli]
[app.common.geom.shapes.points :as gpo]
[app.common.geom.shapes.rect :as gsr]
[app.common.geom.shapes.transforms :as gtr]
[app.common.pages.helpers :as cph]
[app.common.types.modifiers :as ctm]
[app.common.types.shape.layout :as ctl]))
(defn drop-child-areas
@@ -41,7 +44,7 @@
(:width parent-rect)
:else
(+ box-width (- box-x prev-x) (/ layout-gap-row 2)))
(+ box-width (- box-x prev-x) (/ layout-gap-col 2)))
height
(cond
@@ -52,7 +55,7 @@
(:height parent-rect)
:else
(+ box-height (- box-y prev-y) (/ layout-gap-col 2)))]
(+ box-height (- box-y prev-y) (/ layout-gap-row 2)))]
(if row?
(let [half-point-width (+ (- box-x x) (/ box-width 2))]
@@ -87,14 +90,14 @@
(if row?
(:width frame)
(+ line-width margin-x
(if row? (* layout-gap-row (dec num-children)) 0)))
(if row? (* layout-gap-col (dec num-children)) 0)))
line-height
(if col?
(:height frame)
(+ line-height margin-y
(if col?
(* layout-gap-col (dec num-children))
(* layout-gap-row (dec num-children))
0)))
box-x
@@ -122,7 +125,7 @@
(:width frame)
:else
(+ line-width (- box-x prev-x) (/ layout-gap-row 2)))
(+ line-width (- box-x prev-x) (/ layout-gap-col 2)))
height (cond
(and row? last?)
@@ -132,7 +135,7 @@
(:height frame)
:else
(+ line-height (- box-y prev-y) (/ layout-gap-col 2)))]
(+ line-height (- box-y prev-y) (/ layout-gap-row 2)))]
(gsr/make-rect x y width height)))
(defn layout-drop-areas
@@ -179,13 +182,36 @@
(+ (:y line-area) (:height line-area))
(rest lines)))))))
(defn get-flip-modifiers
[{:keys [flip-x flip-y transform transform-inverse] :as shape}]
(if (or flip-x flip-y)
(let [modifiers
(-> (ctm/empty)
(ctm/resize (gpt/point (if flip-x -1.0 1.0)
(if flip-y -1.0 1.0))
(gco/center-shape shape)
transform
transform-inverse))]
[(gtr/transform-shape shape modifiers) modifiers])
[shape nil]))
(defn get-drop-areas
[frame objects]
(let [[frame modifiers] (get-flip-modifiers frame)
children (->> (cph/get-immediate-children objects (:id frame))
(remove :hidden)
(map #(cond-> % (some? modifiers)
(gtr/transform-shape modifiers)))
(map #(vector (gpo/parent-coords-bounds (:points %) (:points frame)) %)))
layout-data (fli/calc-layout-data frame children (:points frame))
drop-areas (layout-drop-areas frame layout-data children)]
drop-areas))
(defn get-drop-index
[frame-id objects position]
(let [frame (get objects frame-id)
drop-areas (get-drop-areas frame objects)
position (gmt/transform-point-center position (gco/center-shape frame) (:transform-inverse frame))
children (->> (cph/get-immediate-children objects frame-id)
(map #(vector (gpo/parent-coords-bounds (:points %) (:points frame)) %)))
layout-data (fli/calc-layout-data frame children (:points frame))
drop-areas (layout-drop-areas frame layout-data children)
area (d/seek #(gsr/contains-point? % position) drop-areas)]
(:index area)))

View File

@@ -24,8 +24,10 @@
"Calculates the lines basic data and accumulated values. The positions will be calculated in a different operation"
[shape children layout-bounds]
(let [col? (ctl/col? shape)
row? (ctl/row? shape)
(let [col? (ctl/col? shape)
row? (ctl/row? shape)
space-around? (ctl/space-around? shape)
space-evenly? (ctl/space-evenly? shape)
wrap? (and (ctl/wrap? shape)
(or col? (not (ctl/auto-width? shape)))
@@ -77,8 +79,28 @@
next-max-width (+ child-margin-width (if fill-width? child-max-width child-width))
next-max-height (+ child-margin-height (if fill-height? child-max-height child-height))
next-line-min-width (+ line-min-width next-min-width (* layout-gap-row num-children))
next-line-min-height (+ line-min-height next-min-height (* layout-gap-col num-children))]
total-gap-col (cond
space-evenly?
(* layout-gap-col (+ num-children 2))
space-around?
(* layout-gap-col (+ num-children 1))
:else
(* layout-gap-col num-children))
total-gap-row (cond
space-evenly?
(* layout-gap-row (+ num-children 2))
space-around?
(* layout-gap-row (+ num-children 1))
:else
(* layout-gap-row num-children))
next-line-min-width (+ line-min-width next-min-width total-gap-col)
next-line-min-height (+ line-min-height next-min-height total-gap-row)]
(if (and (some? line-data)
(or (not wrap?)
@@ -139,8 +161,12 @@
(defn add-lines-positions
[parent layout-bounds layout-lines]
(let [row? (ctl/row? parent)
col? (ctl/col? parent)
(let [row? (ctl/row? parent)
col? (ctl/col? parent)
auto-width? (ctl/auto-width? parent)
auto-height? (ctl/auto-height? parent)
space-evenly? (ctl/space-evenly? parent)
space-around? (ctl/space-around? parent)
[layout-gap-row layout-gap-col] (ctl/gaps parent)
@@ -168,48 +194,101 @@
(let [[total-min-width total-min-height total-max-width total-max-height]
(->> layout-lines (reduce add-ranges [0 0 0 0]))
get-layout-width (fn [{:keys [num-children]}] (- layout-width (* layout-gap-row (dec num-children))))
get-layout-height (fn [{:keys [num-children]}] (- layout-height (* layout-gap-col (dec num-children))))
get-layout-width (fn [{:keys [num-children]}]
(let [num-gap (cond
space-evenly?
(inc num-children)
space-around?
num-children
:else
(dec num-children))]
(- layout-width (* layout-gap-col num-gap))))
get-layout-height (fn [{:keys [num-children]}]
(let [num-gap (cond
space-evenly?
(inc num-children)
space-around?
num-children
:else
(dec num-children))]
(- layout-height (* layout-gap-row num-gap))))
num-lines (count layout-lines)
;; When align-items is stretch we need to adjust the main axis size to grow for the full content
stretch-width-fix
(if (and col? (ctl/content-stretch? parent))
(/ (- layout-width (* layout-gap-row (dec num-lines)) total-max-width) num-lines)
(if (and col? (ctl/content-stretch? parent) (not auto-width?))
(/ (- layout-width (* layout-gap-col (dec num-lines)) total-max-width) num-lines)
0)
stretch-height-fix
(if (and row? (ctl/content-stretch? parent))
(/ (- layout-height (* layout-gap-col (dec num-lines)) total-max-height) num-lines)
(if (and row? (ctl/content-stretch? parent) (not auto-height?))
(/ (- layout-height (* layout-gap-row (dec num-lines)) total-max-height) num-lines)
0)
rest-layout-height (- layout-height (* (dec num-lines) layout-gap-row))
rest-layout-width (- layout-width (* (dec num-lines) layout-gap-col))
;; Distributes the space between the layout lines based on its max/min constraints
layout-lines
(cond->> layout-lines
row?
(map #(assoc % :line-width (max (:line-min-width %) (min (get-layout-width %) (:line-max-width %)))))
(map #(assoc % :line-width
(if (ctl/auto-width? parent)
(:line-min-width %)
(max (:line-min-width %) (min (get-layout-width %) (:line-max-width %))))))
col?
(map #(assoc % :line-height (max (:line-min-height %) (min (get-layout-height %) (:line-max-height %)))))
(map #(assoc % :line-height
(if (ctl/auto-height? parent)
(:line-min-height %)
(max (:line-min-height %) (min (get-layout-height %) (:line-max-height %))))))
(and row? (>= total-min-height layout-height))
(and row? (or (>= total-min-height rest-layout-height) (ctl/auto-height? parent)))
(map #(assoc % :line-height (:line-min-height %)))
(and row? (<= total-max-height layout-height))
(and row? (<= total-max-height rest-layout-height) (not (ctl/auto-height? parent)))
(map #(assoc % :line-height (+ (:line-max-height %) stretch-height-fix)))
(and row? (< total-min-height layout-height total-max-height))
(distribute-space :line-height :line-min-height :line-max-height total-min-height (- layout-height (* (dec num-lines) layout-gap-col)))
(and row? (< total-min-height rest-layout-height total-max-height) (not (ctl/auto-height? parent)))
(distribute-space :line-height :line-min-height :line-max-height total-min-height rest-layout-height)
(and col? (>= total-min-width layout-width))
(and col? (or (>= total-min-width rest-layout-width) (ctl/auto-width? parent)))
(map #(assoc % :line-width (:line-min-width %)))
(and col? (<= total-max-width layout-width))
(and col? (<= total-max-width rest-layout-width) (not (ctl/auto-width? parent)))
(map #(assoc % :line-width (+ (:line-max-width %) stretch-width-fix)))
(and col? (< total-min-width layout-width total-max-width))
(distribute-space :line-width :line-min-width :line-max-width total-min-width (- layout-width (* (dec num-lines) layout-gap-row))))
(and col? (< total-min-width rest-layout-width total-max-width) (not (ctl/auto-width? parent)))
(distribute-space :line-width :line-min-width :line-max-width total-min-width rest-layout-width))
;; Add information to limit the growth of width: 100% shapes to the bounds of the layout
layout-lines
(cond
row?
(->> layout-lines
(reduce
(fn [[result rest-layout-height] {:keys [line-height] :as line}]
[(conj result (assoc line :to-bound-height rest-layout-height))
(- rest-layout-height line-height layout-gap-row)])
[[] layout-height])
(first))
col?
(->> layout-lines
(reduce
(fn [[result rest-layout-width] {:keys [line-width] :as line}]
[(conj result (assoc line :to-bound-width rest-layout-width))
(- rest-layout-width line-width layout-gap-col)])
[[] layout-width])
(first))
:else
layout-lines)
[total-width total-height] (->> layout-lines (reduce add-lines [0 0]))
@@ -226,40 +305,71 @@
row? (ctl/row? shape)
col? (ctl/col? shape)
auto-height? (ctl/auto-height? shape)
auto-width? (ctl/auto-width? shape)
space-between? (ctl/space-between? shape)
space-evenly? (ctl/space-evenly? shape)
space-around? (ctl/space-around? shape)
[layout-gap-row layout-gap-col] (ctl/gaps shape)
layout-gap-row
(cond (and row? space-around?)
0
margin-x
(cond (and row? space-evenly? (not auto-width?))
(max layout-gap-col (/ (- width line-width) (inc num-children)))
(and row? space-between?)
(/ (- width line-width) (dec num-children))
(and row? space-around? (not auto-width?))
(/ (max layout-gap-col (/ (- width line-width) num-children)) 2)
(and row? (or space-evenly? space-around?) auto-width?)
layout-gap-col
:else
layout-gap-row)
0)
margin-y
(cond (and col? space-evenly? (not auto-height?))
(max layout-gap-row (/ (- height line-height) (inc num-children)))
(and col? space-around? (not auto-height?))
(/ (max layout-gap-row (/ (- height line-height) num-children)) 2)
(and col? (or space-evenly? space-around?) auto-height?)
layout-gap-row
:else
0)
layout-gap-col
(cond (and col? space-around?)
(cond (and row? space-evenly?)
0
(and col? space-between?)
(/ (- height line-height) (dec num-children))
(and row? space-around? auto-width?)
0
(and row? space-around?)
(/ (max layout-gap-col (/ (- width line-width) num-children)) 2)
(and row? space-between? (not auto-width?))
(max layout-gap-col (/ (- width line-width) (dec num-children)))
:else
layout-gap-col)
margin-x
(if (and row? space-around?)
(/ (- width line-width) (inc num-children))
0)
layout-gap-row
(cond (and col? space-evenly?)
0
margin-y
(if (and col? space-around?)
(/ (- height line-height) (inc num-children))
0)]
(and col? space-evenly? auto-height?)
0
(and col? space-around?)
(/ (max layout-gap-row (/ (- height line-height) num-children)) 2)
(and col? space-between? (not auto-height?))
(max layout-gap-row (/ (- height line-height) (dec num-children)))
:else
layout-gap-row)]
(assoc line-data
:layout-bounds layout-bounds
:layout-gap-row layout-gap-row
@@ -308,4 +418,3 @@
{:layout-lines layout-lines
:layout-bounds layout-bounds
:reverse? reverse?}))

View File

@@ -20,7 +20,7 @@
transform-inverse
child
child-origin child-width
{:keys [children-data line-width] :as layout-data}]
{:keys [children-data line-width to-bound-width] :as layout-data}]
(cond
(ctl/row? parent)
@@ -30,8 +30,9 @@
:modifiers (ctm/resize-modifiers (gpt/point fill-scale 1) child-origin transform transform-inverse)})
(ctl/col? parent)
(let [target-width (max (- line-width (ctl/child-width-margin child)) 0.01)
max-width (ctl/child-max-width child)
(let [line-width (min line-width (or to-bound-width line-width))
target-width (max (- line-width (ctl/child-width-margin child)) 0.01)
max-width (max (ctl/child-max-width child) 0.01)
target-width (min max-width target-width)
fill-scale (/ target-width child-width)]
{:width target-width
@@ -43,7 +44,7 @@
transform transform-inverse
child
child-origin child-height
{:keys [children-data line-height] :as layout-data}]
{:keys [children-data line-height to-bound-height] :as layout-data}]
(cond
(ctl/col? parent)
@@ -53,8 +54,9 @@
:modifiers (ctm/resize-modifiers (gpt/point 1 fill-scale) child-origin transform transform-inverse)})
(ctl/row? parent)
(let [target-height (max (- line-height (ctl/child-height-margin child)) 0.01)
max-height (ctl/child-max-height child)
(let [line-height (min line-height (or to-bound-height line-height))
target-height (max (- line-height (ctl/child-height-margin child)) 0.01)
max-height (max (ctl/child-max-height child) 0.01)
target-height (min max-height target-height)
fill-scale (/ target-height child-height)]
{:height target-height
@@ -68,11 +70,16 @@
child-height (gpo/height-points child-bounds)
[_ transform transform-inverse]
(when (or (ctl/fill-width? child) (ctl/fill-width? child))
(when (or (ctl/fill-width? child) (ctl/fill-height? child))
(gtr/calculate-geometry @parent-bounds))
fill-width (when (ctl/fill-width? child) (calc-fill-width-data parent transform transform-inverse child child-origin child-width layout-line))
fill-height (when (ctl/fill-height? child) (calc-fill-height-data parent transform transform-inverse child child-origin child-height layout-line))
fill-width
(when (ctl/fill-width? child)
(calc-fill-width-data parent transform transform-inverse child child-origin child-width layout-line))
fill-height
(when (ctl/fill-height? child)
(calc-fill-height-data parent transform transform-inverse child child-origin child-height layout-line))
child-width (or (:width fill-width) child-width)
child-height (or (:height fill-height) child-height)

View File

@@ -20,17 +20,22 @@
hv (partial gpo/start-hv layout-bounds)
vv (partial gpo/start-vv layout-bounds)
end? (ctl/content-end? parent)
center? (ctl/content-center? parent)
around? (ctl/content-around? parent)
wrap? (ctl/wrap? parent)
end? (or (and wrap? (ctl/content-end? parent))
(and (not wrap?) (ctl/align-items-end? parent)))
center? (or (and wrap? (ctl/content-center? parent))
(and (not wrap?) (ctl/align-items-center? parent)))
around? (and wrap? (ctl/content-around? parent))
evenly? (and wrap? (ctl/content-evenly? parent))
;; Adjust the totals so it takes into account the gaps
[layout-gap-row layout-gap-col] (ctl/gaps parent)
lines-gap-row (* (dec num-lines) layout-gap-row)
lines-gap-col (* (dec num-lines) layout-gap-col)
free-width-gap (- layout-width total-width lines-gap-row)
free-height-gap (- layout-height total-height lines-gap-col)
free-width-gap (- layout-width total-width lines-gap-col)
free-height-gap (- layout-height total-height lines-gap-row)
free-width (- layout-width total-width)
free-height (- layout-height total-height)]
@@ -43,7 +48,10 @@
(gpt/add (vv free-height-gap))
around?
(gpt/add (vv (/ free-height (inc num-lines)))))
(gpt/add (vv (max lines-gap-row (/ free-height num-lines 2))))
evenly?
(gpt/add (vv (max lines-gap-row (/ free-height (inc num-lines))))))
col?
(cond-> center?
@@ -53,7 +61,10 @@
(gpt/add (hv free-width-gap))
around?
(gpt/add (hv (/ free-width (inc num-lines))))))))
(gpt/add (hv (max lines-gap-col (/ free-width num-lines) 2)))
evenly?
(gpt/add (hv (max lines-gap-col (/ free-width (inc num-lines)))))))))
(defn get-next-line
[parent layout-bounds {:keys [line-width line-height]} base-p total-width total-height num-lines]
@@ -63,6 +74,9 @@
row? (ctl/row? parent)
col? (ctl/col? parent)
auto-width? (ctl/auto-width? parent)
auto-height? (ctl/auto-height? parent)
[layout-gap-row layout-gap-col] (ctl/gaps parent)
hv #(gpo/start-hv layout-bounds %)
@@ -71,12 +85,16 @@
stretch? (ctl/content-stretch? parent)
between? (ctl/content-between? parent)
around? (ctl/content-around? parent)
evenly? (ctl/content-evenly? parent)
free-width (- layout-width total-width)
free-height (- layout-height total-height)
line-gap-row
line-gap-col
(cond
auto-width?
layout-gap-col
stretch?
(/ free-width num-lines)
@@ -84,13 +102,19 @@
(/ free-width (dec num-lines))
around?
(/ free-width num-lines)
evenly?
(/ free-width (inc num-lines))
:else
layout-gap-row)
layout-gap-col)
line-gap-col
line-gap-row
(cond
auto-height?
layout-gap-row
stretch?
(/ free-height num-lines)
@@ -98,17 +122,20 @@
(/ free-height (dec num-lines))
around?
(/ free-height num-lines)
evenly?
(/ free-height (inc num-lines))
:else
layout-gap-col)]
layout-gap-row)]
(cond-> base-p
row?
(gpt/add (vv (+ line-height (max layout-gap-col line-gap-col))))
(gpt/add (vv (+ line-height (max layout-gap-row line-gap-row))))
col?
(gpt/add (hv (+ line-width (max layout-gap-row line-gap-row)))))))
(gpt/add (hv (+ line-width (max layout-gap-col line-gap-col)))))))
(defn get-start-line
"Cross axis line. It's position is fixed along the different lines"
@@ -121,43 +148,46 @@
col? (ctl/col? parent)
space-between? (ctl/space-between? parent)
space-around? (ctl/space-around? parent)
space-evenly? (ctl/space-evenly? parent)
h-center? (ctl/h-center? parent)
h-end? (ctl/h-end? parent)
v-center? (ctl/v-center? parent)
v-end? (ctl/v-end? parent)
content-stretch? (ctl/content-stretch? parent)
auto-width? (ctl/auto-width? parent)
auto-height? (ctl/auto-height? parent)
hv (partial gpo/start-hv layout-bounds)
vv (partial gpo/start-vv layout-bounds)
children-gap-width (* layout-gap-row (dec num-children))
children-gap-height (* layout-gap-col (dec num-children))
children-gap-width (* layout-gap-col (dec num-children))
children-gap-height (* layout-gap-row (dec num-children))
line-height
(if (and row? content-stretch?)
(if (and row? content-stretch? (not auto-height?))
(+ line-height (/ (- layout-height total-height) num-lines))
line-height)
line-width
(if (and col? content-stretch?)
(if (and col? content-stretch? (not auto-width?))
(+ line-width (/ (- layout-width total-width) num-lines))
line-width)
start-p
(cond-> base-p
;; X AXIS
(and row? h-center? (not space-around?) (not space-between?))
(and row? h-center? (not space-around?) (not space-evenly?) (not space-between?))
(-> (gpt/add (hv (/ layout-width 2)))
(gpt/subtract (hv (/ (+ line-width children-gap-width) 2))))
(and row? h-end? (not space-around?) (not space-between?))
(and row? h-end? (not space-around?) (not space-evenly?) (not space-between?))
(-> (gpt/add (hv layout-width))
(gpt/subtract (hv (+ line-width children-gap-width))))
;; Y AXIS
(and col? v-center? (not space-around?) (not space-between?))
(and col? v-center? (not space-around?) (not space-evenly?) (not space-between?))
(-> (gpt/add (vv (/ layout-height 2)))
(gpt/subtract (vv (/ (+ line-height children-gap-height) 2))))
(and col? v-end? (not space-around?) (not space-between?))
(and col? v-end? (not space-around?) (not space-evenly?) (not space-between?))
(-> (gpt/add (vv layout-height))
(gpt/subtract (vv (+ line-height children-gap-height)))))]
@@ -257,13 +287,13 @@
next-p
(cond-> start-p
row?
(-> (gpt/add (hv (+ child-width layout-gap-row)))
(-> (gpt/add (hv (+ child-width layout-gap-col)))
(gpt/add (hv (+ margin-left margin-right))))
col?
(-> (gpt/add (vv (+ margin-top margin-bottom)))
(gpt/add (vv (+ child-height layout-gap-col))))
(gpt/add (vv (+ child-height layout-gap-row))))
(some? margin-x)
(gpt/add (hv margin-x))

View File

@@ -363,10 +363,10 @@
c2 (+ (* a2 (:x c)) (* b2 (:y c)))
;; Cramer's rule
det (- (* a1 b2) (* a2 b1))]
det (- (* a1 b2) (* a2 b1))
det (if (mth/almost-zero? det) 0.001 det)
;; If almost zero the lines are parallel
(when (not (mth/almost-zero? det))
(let [x (/ (- (* b2 c1) (* b1 c2)) det)
y (/ (- (* c2 a1) (* c1 a2)) det)]
(gpt/point x y)))))
x (/ (- (* b2 c1) (* b1 c2)) det)
y (/ (- (* c2 a1) (* c1 a2)) det)]
(gpt/point x y)))

View File

@@ -28,6 +28,16 @@
;; [(get-in objects [k :name]) v]))
;; modif-tree))))
(defn children-sequence
"Given an id returns a sequence of its children"
[id objects]
(->> (tree-seq
#(d/not-empty? (dm/get-in objects [% :shapes]))
#(dm/get-in objects [% :shapes])
id)
(map #(get objects %))))
(defn resolve-tree-sequence
"Given the ids that have changed search for layout roots to recalculate"
[ids objects]
@@ -75,20 +85,12 @@
(cond-> result
(not contains-parent?)
(conj root)))))
(generate-tree ;; Generate a tree sequence from a given root id
[id]
(->> (tree-seq
#(d/not-empty? (dm/get-in objects [% :shapes]))
#(dm/get-in objects [% :shapes])
id)
(map #(get objects %))))]
(conj root)))))]
(let [roots (->> ids (reduce calculate-common-roots #{}))]
(concat
(when (contains? ids uuid/zero) [(get objects uuid/zero)])
(mapcat generate-tree roots)))))
(mapcat #(children-sequence % objects) roots)))))
(defn- set-children-modifiers
"Propagates the modifiers from a parent too its children applying constraints if necesary"
@@ -113,13 +115,15 @@
(if (empty? children)
modif-tree
(let [child-id (first children)
child (get objects child-id)
child-bounds @(get bounds child-id)
child-modifiers (gct/calc-child-modifiers parent child modifiers ignore-constraints child-bounds parent-bounds transformed-parent-bounds)]
(recur (cond-> modif-tree
(not (ctm/empty? child-modifiers))
(update-in [child-id :modifiers] ctm/add-modifiers child-modifiers))
(rest children)))))))))
child (get objects child-id)]
(if (some? child)
(let [child-bounds @(get bounds child-id)
child-modifiers (gct/calc-child-modifiers parent child modifiers ignore-constraints child-bounds parent-bounds transformed-parent-bounds)]
(recur (cond-> modif-tree
(not (ctm/empty? child-modifiers))
(update-in [child-id :modifiers] ctm/add-modifiers child-modifiers))
(rest children)))
(recur modif-tree (rest children))))))))))
(defn get-group-bounds
[objects bounds modif-tree shape]
@@ -130,7 +134,7 @@
children (cph/get-immediate-children objects shape-id)]
(cond
(cph/mask-shape? shape)
(and (cph/mask-shape? shape) (seq children))
(get-group-bounds objects bounds modif-tree (-> children first))
(cph/group-shape? shape)
@@ -169,8 +173,9 @@
[layout-line modif-tree]))]
(let [children (->> (:shapes parent)
(map (comp apply-modifiers (d/getf objects))))
(let [children (->> (cph/get-immediate-children objects (:id parent))
(remove :hidden)
(map apply-modifiers))
layout-data (gcl/calc-layout-data parent children @transformed-parent-bounds)
children (into [] (cond-> children (not (:reverse? layout-data)) reverse))
max-idx (dec (count children))
@@ -209,7 +214,9 @@
(-> modifiers
(ctm/resize-parent (gpt/point 1 scale-height) origin (:transform parent) (:transform-inverse parent)))))
children (->> parent :shapes (map (d/getf objects)))
children (->> (cph/get-immediate-children objects parent-id)
(remove :hidden))
content-bounds
(when (and (d/not-empty? children) (or (ctl/auto-height? parent) (ctl/auto-width? parent)))
(gcl/layout-content-bounds bounds parent children))
@@ -293,57 +300,136 @@
result (assoc result (:id shape) new-bounds)]
(recur result (rest shapes)))))))
(defn reflow-layout
[objects old-modif-tree bounds ignore-constraints id]
(let [tree-seq (children-sequence id objects)
[modif-tree _]
(reduce
#(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}]
tree-seq)
bounds (transform-bounds bounds objects modif-tree tree-seq)
modif-tree (merge-modif-tree old-modif-tree modif-tree)]
[modif-tree bounds]))
(defn sizing-auto-modifiers
"Recalculates the layouts to adjust the sizing: auto new sizes"
[modif-tree sizing-auto-layouts objects bounds ignore-constraints]
(loop [modif-tree modif-tree
bounds bounds
sizing-auto-layouts (reverse sizing-auto-layouts)]
(if-let [current (first sizing-auto-layouts)]
(let [parent-base (get objects current)
(let [;; Step-1 resize the auto-width/height. Reflow the parents if they are also auto-width/height
[modif-tree bounds to-reflow]
(loop [modif-tree modif-tree
bounds bounds
sizing-auto-layouts (reverse sizing-auto-layouts)
to-reflow #{}]
(if-let [current (first sizing-auto-layouts)]
(let [parent-base (get objects current)
resize-modif-tree
{current {:modifiers (calc-auto-modifiers objects bounds parent-base)}}
[modif-tree bounds]
(if (contains? to-reflow current)
(reflow-layout objects modif-tree bounds ignore-constraints current)
[modif-tree bounds])
tree-seq (resolve-tree-sequence #{current} objects)
auto-resize-modifiers
(calc-auto-modifiers objects bounds parent-base)
[resize-modif-tree _]
(reduce #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [resize-modif-tree #{}] tree-seq)
to-reflow
(cond-> to-reflow
(contains? to-reflow current)
(disj current))]
bounds (transform-bounds bounds objects resize-modif-tree tree-seq)
(if (ctm/empty? auto-resize-modifiers)
(recur modif-tree
bounds
(rest sizing-auto-layouts)
to-reflow)
modif-tree (merge-modif-tree modif-tree resize-modif-tree)]
(recur modif-tree bounds (rest sizing-auto-layouts)))
modif-tree)))
(let [resize-modif-tree {current {:modifiers auto-resize-modifiers}}
tree-seq (children-sequence current objects)
[resize-modif-tree _]
(reduce
#(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [resize-modif-tree #{}]
tree-seq)
bounds (transform-bounds bounds objects resize-modif-tree tree-seq)
modif-tree (merge-modif-tree modif-tree resize-modif-tree)
to-reflow
(cond-> to-reflow
(and (ctl/layout-descent? objects parent-base)
(not= uuid/zero (:frame-id parent-base)))
(conj (:frame-id parent-base)))]
(recur modif-tree
bounds
(rest sizing-auto-layouts)
to-reflow))))
[modif-tree bounds to-reflow]))
;; Step-2: After resizing we still need to reflow the layout parents that are not auto-width/height
tree-seq (resolve-tree-sequence to-reflow objects)
[reflow-modif-tree _]
(reduce
#(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}]
tree-seq)
result (merge-modif-tree modif-tree reflow-modif-tree)]
result))
(defn set-objects-modifiers
[modif-tree objects ignore-constraints snap-pixel?]
([modif-tree objects]
(set-objects-modifiers modif-tree objects nil))
(let [objects (apply-structure-modifiers objects modif-tree)
([modif-tree objects params]
(set-objects-modifiers nil modif-tree objects params))
bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points]))
shapes-tree (resolve-tree-sequence (-> modif-tree keys set) objects)
([old-modif-tree modif-tree objects
{:keys [ignore-constraints snap-pixel? snap-precision snap-ignore-axis]
:or {ignore-constraints false snap-pixel? false snap-precision 1 snap-ignore-axis nil}}]
(let [objects (-> objects
(cond-> (some? old-modif-tree)
(apply-structure-modifiers old-modif-tree))
(apply-structure-modifiers modif-tree))
;; Calculate the input transformation and constraints
modif-tree (reduce #(propagate-modifiers-constraints objects bounds ignore-constraints %1 %2) modif-tree shapes-tree)
bounds (transform-bounds bounds objects modif-tree shapes-tree)
modif-tree
(cond-> modif-tree
snap-pixel? (gpp/adjust-pixel-precision objects snap-precision snap-ignore-axis))
[modif-tree-layout sizing-auto-layouts]
(reduce #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}] shapes-tree)
bounds (d/lazy-map (keys objects) #(dm/get-in objects [% :points]))
bounds (cond-> bounds
(some? old-modif-tree)
(transform-bounds objects old-modif-tree))
modif-tree (merge-modif-tree modif-tree modif-tree-layout)
shapes-tree (resolve-tree-sequence (-> modif-tree keys set) objects)
;; Calculate hug layouts positions
bounds (transform-bounds bounds objects modif-tree-layout shapes-tree)
;; Calculate the input transformation and constraints
modif-tree (reduce #(propagate-modifiers-constraints objects bounds ignore-constraints %1 %2) modif-tree shapes-tree)
bounds (transform-bounds bounds objects modif-tree shapes-tree)
modif-tree
(-> modif-tree
(sizing-auto-modifiers sizing-auto-layouts objects bounds ignore-constraints))
[modif-tree-layout sizing-auto-layouts]
(reduce #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}] shapes-tree)
modif-tree
(cond-> modif-tree
snap-pixel? (gpp/adjust-pixel-precision objects))]
modif-tree (merge-modif-tree modif-tree modif-tree-layout)
;;#?(:cljs
;; (.log js/console ">result" (modif->js modif-tree objects)))
modif-tree))
;; Calculate hug layouts positions
bounds (transform-bounds bounds objects modif-tree-layout shapes-tree)
modif-tree
(-> modif-tree
(sizing-auto-modifiers sizing-auto-layouts objects bounds ignore-constraints))
modif-tree
(if old-modif-tree
(merge-modif-tree old-modif-tree modif-tree)
modif-tree)]
;;#?(:cljs
;; (.log js/console ">result" (modif->js modif-tree objects)))
modif-tree)))

View File

@@ -117,7 +117,7 @@
[x y] (->> coords (mapv solve-derivative))
;; normalize value
d (mth/sqrt (+ (* x x) (* y y)))]
d (mth/hypot x y)]
(if (mth/almost-zero? d)
(gpt/point 0 0)

View File

@@ -18,7 +18,7 @@
[app.common.types.modifiers :as ctm]))
(defn size-pixel-precision
[modifiers shape points]
[modifiers shape points precision]
(let [origin (gpo/origin points)
curr-width (gpo/width-points points)
curr-height (gpo/height-points points)
@@ -29,34 +29,42 @@
vertical-line? (and path? (<= curr-width 0.01))
horizontal-line? (and path? (<= curr-height 0.01))
target-width (if vertical-line? curr-width (max 1 (mth/round curr-width)))
target-height (if horizontal-line? curr-height (max 1 (mth/round curr-height)))
target-width (if vertical-line? curr-width (max 1 (mth/round curr-width precision)))
target-height (if horizontal-line? curr-height (max 1 (mth/round curr-height precision)))
ratio-width (/ target-width curr-width)
ratio-height (/ target-height curr-height)
scalev (gpt/point ratio-width ratio-height)]
(-> modifiers
(ctm/resize scalev origin transform transform-inverse))))
(ctm/resize scalev origin transform transform-inverse {:precise? true}))))
(defn position-pixel-precision
[modifiers _ points]
[modifiers _ points precision ignore-axis]
(let [bounds (gpr/bounds->rect points)
corner (gpt/point bounds)
target-corner (gpt/round corner)
target-corner
(cond-> corner
(= ignore-axis :x)
(update :y mth/round precision)
(= ignore-axis :y)
(update :x mth/round precision)
(nil? ignore-axis)
(gpt/round-step precision))
deltav (gpt/to-vec corner target-corner)]
(ctm/move modifiers deltav)))
(defn set-pixel-precision
"Adjust modifiers so they adjust to the pixel grid"
[modifiers shape]
[modifiers shape precision ignore-axis]
(let [points (-> shape :points (gco/transform-points (ctm/modifiers->transform modifiers)))
has-resize? (not (ctm/only-move? modifiers))
[modifiers points]
(let [modifiers
(cond-> modifiers
has-resize? (size-pixel-precision shape points))
has-resize? (size-pixel-precision shape points precision))
points
(if has-resize?
@@ -64,16 +72,16 @@
(gco/transform-points (ctm/modifiers->transform modifiers)) )
points)]
[modifiers points])]
(position-pixel-precision modifiers shape points)))
(position-pixel-precision modifiers shape points precision ignore-axis)))
(defn adjust-pixel-precision
[modif-tree objects]
[modif-tree objects precision ignore-axis]
(let [update-modifiers
(fn [modif-tree shape]
(let [modifiers (dm/get-in modif-tree [(:id shape) :modifiers])]
(cond-> modif-tree
(ctm/has-geometry? modifiers)
(update-in [(:id shape) :modifiers] set-pixel-precision shape))))]
(update-in [(:id shape) :modifiers] set-pixel-precision shape precision ignore-axis))))]
(->> (keys modif-tree)
(map (d/getf objects))

View File

@@ -6,6 +6,7 @@
(ns app.common.geom.shapes.points
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.intersect :as gsi]
@@ -54,11 +55,11 @@
(defn width-points
[[p0 p1 _ _]]
(gpt/length (gpt/to-vec p0 p1)))
(max 0.01 (gpt/length (gpt/to-vec p0 p1))))
(defn height-points
[[p0 _ _ p3]]
(gpt/length (gpt/to-vec p0 p3)))
(max 0.01 (gpt/length (gpt/to-vec p0 p3))))
(defn pad-points
[[p0 p1 p2 p3 :as points] pad-top pad-right pad-bottom pad-left]
@@ -115,7 +116,9 @@
(max tv-max ctv)]))
[th-min th-max tv-min tv-max]
(->> child-bounds (reduce find-boundary-ts [##Inf ##-Inf ##Inf ##-Inf]))
(->> child-bounds
(filter #(and (d/num? (:x %)) (d/num? (:y %))))
(reduce find-boundary-ts [##Inf ##-Inf ##Inf ##-Inf]))
minv-start (pv tv-min)
minv-end (gpt/add minv-start hv)
@@ -131,6 +134,7 @@
i2 (gsi/line-line-intersect minv-start minv-end maxh-start maxh-end)
i3 (gsi/line-line-intersect maxv-start maxv-end maxh-start maxh-end)
i4 (gsi/line-line-intersect maxv-start maxv-end minh-start minh-end)]
[i1 i2 i3 i4])))
(defn merge-parent-coords-bounds
@@ -143,3 +147,8 @@
height (height-points points)
center (gco/center-points points)]
(gre/center->selrect center width height)))
(defn move
[bounds vector]
(->> bounds
(map #(gpt/add % vector))))

View File

@@ -212,9 +212,13 @@
(<= (:y2 sr2) (:y2 sr1))))
(defn corners->selrect
[p1 p2]
(let [xp1 (:x p1)
xp2 (:x p2)
yp1 (:y p1)
yp2 (:y p2)]
(make-selrect (min xp1 xp2) (min yp1 yp2) (abs (- xp1 xp2)) (abs (- yp1 yp2)))))
([p1 p2]
(corners->selrect (:x p1) (:y p1) (:x p2) (:y p2)))
([xp1 yp1 xp2 yp2]
(make-selrect (min xp1 xp2) (min yp1 yp2) (abs (- xp1 xp2)) (abs (- yp1 yp2)))))
(defn clip-selrect
[{:keys [x1 y1 x2 y2] :as sr} bounds]
(when (some? sr)
(let [{bx1 :x1 by1 :y1 bx2 :x2 by2 :y2} (rect->selrect bounds)]
(corners->selrect (max bx1 x1) (max by1 y1) (min bx2 x2) (min by2 y2)))))

View File

@@ -17,6 +17,7 @@
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.path :as gpa]
[app.common.geom.shapes.rect :as gpr]
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.types.modifiers :as ctm]
[app.common.uuid :as uuid]))
@@ -43,14 +44,16 @@
(mapv #(gpt/add % move-vec))))
(defn move-position-data
[position-data dx dy]
([position-data {:keys [x y]}]
(move-position-data position-data x y))
(when (some? position-data)
(cond->> position-data
(d/num? dx dy)
(mapv #(-> %
(update :x + dx)
(update :y + dy))))))
([position-data dx dy]
(when (some? position-data)
(cond->> position-data
(d/num? dx dy)
(mapv #(-> %
(update :x + dx)
(update :y + dy)))))))
(defn move
"Move the shape relatively to its current
@@ -111,10 +114,10 @@
(cond-> (some? transform)
(gmt/multiply transform))
(cond-> (and flip-x (not no-flip))
(cond-> (and flip-x no-flip)
(gmt/scale (gpt/point -1 1)))
(cond-> (and flip-y (not no-flip))
(cond-> (and flip-y no-flip)
(gmt/scale (gpt/point 1 -1)))
(gmt/translate (gpt/negate shape-center)))))
@@ -126,8 +129,8 @@
([{:keys [transform flip-x flip-y] :as shape} {:keys [no-flip] :as params}]
(if (and (some? shape)
(or (some? transform)
(and (not no-flip) flip-x)
(and (not no-flip) flip-y)))
(and no-flip flip-x)
(and no-flip flip-y)))
(dm/str (transform-matrix shape params))
"")))
@@ -157,67 +160,79 @@
"Calculate the transform matrix to convert from the selrect to the points bounds
TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)"
[{:keys [x1 y1 x2 y2]} [d1 d2 _ d4]]
#?(:clj
;; NOTE: the source matrix may not be invertible we can't
;; calculate the transform, so on exception we return `nil`
(ex/ignoring
(let [target-points-matrix
(->> (list (:x d1) (:x d2) (:x d4)
(:y d1) (:y d2) (:y d4)
1 1 1 )
(into-array Double/TYPE)
(Matrix/from1DArray 3 3))
;; If the coordinates are very close to zero (but not zero) the rounding can mess with the
;; transforms. So we round to zero the values
(let [x1 (mth/round-to-zero x1)
y1 (mth/round-to-zero y1)
x2 (mth/round-to-zero x2)
y2 (mth/round-to-zero y2)
d1x (mth/round-to-zero (:x d1))
d1y (mth/round-to-zero (:y d1))
d2x (mth/round-to-zero (:x d2))
d2y (mth/round-to-zero (:y d2))
d4x (mth/round-to-zero (:x d4))
d4y (mth/round-to-zero (:y d4))]
#?(:clj
;; NOTE: the source matrix may not be invertible we can't
;; calculate the transform, so on exception we return `nil`
(ex/ignoring
(let [target-points-matrix
(->> (list d1x d2x d4x
d1y d2y d4y
1 1 1)
(into-array Double/TYPE)
(Matrix/from1DArray 3 3))
source-points-matrix
(->> (list x1 x2 x1
y1 y1 y2
1 1 1)
(into-array Double/TYPE)
(Matrix/from1DArray 3 3))
source-points-matrix
(->> (list x1 x2 x1
y1 y1 y2
1 1 1)
(into-array Double/TYPE)
(Matrix/from1DArray 3 3))
;; May throw an exception if the matrix is not invertible
source-points-matrix-inv
(.. source-points-matrix
(withInverter LinearAlgebra/GAUSS_JORDAN)
(inverse))
;; May throw an exception if the matrix is not invertible
source-points-matrix-inv
(.. source-points-matrix
(withInverter LinearAlgebra/GAUSS_JORDAN)
(inverse))
transform-jvm
(.. target-points-matrix
(multiply source-points-matrix-inv))]
transform-jvm
(.. target-points-matrix
(multiply source-points-matrix-inv))]
(gmt/matrix (.get transform-jvm 0 0)
(.get transform-jvm 1 0)
(.get transform-jvm 0 1)
(.get transform-jvm 1 1)
(.get transform-jvm 0 2)
(.get transform-jvm 1 2))))
(gmt/matrix (.get transform-jvm 0 0)
(.get transform-jvm 1 0)
(.get transform-jvm 0 1)
(.get transform-jvm 1 1)
(.get transform-jvm 0 2)
(.get transform-jvm 1 2))))
:cljs
(let [target-points-matrix
(Matrix. #js [#js [(:x d1) (:x d2) (:x d4)]
#js [(:y d1) (:y d2) (:y d4)]
#js [ 1 1 1]])
:cljs
(let [target-points-matrix
(Matrix. #js [#js [d1x d2x d4x]
#js [d1y d2y d4y]
#js [ 1 1 1]])
source-points-matrix
(Matrix. #js [#js [x1 x2 x1]
#js [y1 y1 y2]
#js [ 1 1 1]])
source-points-matrix
(Matrix. #js [#js [x1 x2 x1]
#js [y1 y1 y2]
#js [ 1 1 1]])
;; returns nil if not invertible
source-points-matrix-inv (.getInverse source-points-matrix)
;; returns nil if not invertible
source-points-matrix-inv (.getInverse source-points-matrix)
;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)
transform-js
(when source-points-matrix-inv
(.multiply target-points-matrix source-points-matrix-inv))]
;; TargetM = SourceM * Transform ==> Transform = TargetM * inv(SourceM)
transform-js
(when source-points-matrix-inv
(.multiply target-points-matrix source-points-matrix-inv))]
(when transform-js
(gmt/matrix (.getValueAt transform-js 0 0)
(.getValueAt transform-js 1 0)
(.getValueAt transform-js 0 1)
(.getValueAt transform-js 1 1)
(.getValueAt transform-js 0 2)
(.getValueAt transform-js 1 2))))))
(when transform-js
(gmt/matrix (.getValueAt transform-js 0 0)
(.getValueAt transform-js 1 0)
(.getValueAt transform-js 0 1)
(.getValueAt transform-js 1 1)
(.getValueAt transform-js 0 2)
(.getValueAt transform-js 1 2)))))))
(defn calculate-geometry
[points]
@@ -236,7 +251,15 @@
points-transform-mtx
(gmt/translate-matrix center)))
transform-inverse (when transform (gmt/inverse transform))]
transform-inverse (when transform (gmt/inverse transform))
;; There is a rounding error when the matrix returned have float point values
;; when the matrix is unit we return a "pure" matrix so we don't accumulate
;; rounding problems
[transform transform-inverse]
(if (gmt/unit? transform)
[(gmt/matrix) (gmt/matrix)]
[transform transform-inverse])]
[sr transform transform-inverse]))

View File

@@ -175,14 +175,15 @@
#?(:clj
(defn get-error-context
[error]
(when-let [data (ex-data error)]
(merge
{:hint (ex-message error)
:spec-problems (some->> data ::s/problems (take 10) seq vec)
:spec-value (some->> data ::s/value)
:data (some-> data (dissoc ::s/problems ::s/value ::s/spec))}
(when-let [explain (ex/explain data)]
{:spec-explain explain})))))
(merge
{:hint (ex-message error)}
(when-let [data (ex-data error)]
(merge
{:spec-problems (some->> data ::s/problems (take 10) seq vec)
:spec-value (some->> data ::s/value)
:data (some-> data (dissoc ::s/problems ::s/value ::s/spec))}
(when-let [explain (ex/explain data)]
{:spec-explain explain}))))))
(defmacro log
[& props]

View File

@@ -34,7 +34,7 @@
(defn abs
[v]
#?(:cljs (js/Math.abs v)
:clj (Math/abs v)))
:clj (Math/abs (double v))))
(defn sin
"Returns the sine of a number"
@@ -104,15 +104,16 @@
(defn round
"Returns the value of a number rounded to
the nearest integer."
[v]
#?(:cljs (js/Math.round v)
:clj (Math/round (float v))))
the nearest integer.
If given step rounds to the next closest step, for example:
(round 13.4 0.5) => 13.5
(round 13.4 0.3) => 13.3"
([v step]
(* (round (/ v step)) step))
(defn half-round
"Returns a value rounded to the next point or half point"
[v]
(/ (round (* v 2)) 2))
([v]
#?(:cljs (js/Math.round v)
:clj (Math/round (float v)))))
(defn ceil
"Returns the smallest integer greater than
@@ -127,6 +128,12 @@
(let [d (pow 10 n)]
(/ (round (* v d)) d))))
(defn to-fixed
"Returns a string representing the given number, using fixed precision."
[v n]
#?(:cljs (.toFixed ^js v n)
:clj (str (precision v n))))
(defn radians
"Converts degrees to radians."
[degrees]
@@ -139,12 +146,18 @@
#?(:cljs (math/toDegrees radians)
:clj (Math/toDegrees radians)))
(defn hypot
"Square root of the squares addition"
[a b]
#?(:cljs (js/Math.hypot a b)
:clj (Math/hypot a b)))
(defn distance
"Calculate the distance between two points."
[[x1 y1] [x2 y2]]
(let [dx (- x1 x2)
dy (- y1 y2)]
(-> (sqrt (+ (pow dx 2) (pow dy 2)))
(-> (hypot dx dy)
(precision 2))))
(defn log10
@@ -161,6 +174,13 @@
(defn almost-zero? [num]
(< (abs (double num)) 1e-4))
(defn round-to-zero
"Given a number if it's close enough to zero round to the zero to avoid precision problems"
[num]
(if (almost-zero? num)
0
num))
(defonce float-equal-precision 0.001)
(defn close?
@@ -182,3 +202,4 @@
"Get the sign (+1 / -1) for the number"
[n]
(if (neg? n) -1 1))

View File

@@ -9,7 +9,8 @@
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(def valid-font-types #{"font/ttf" "font/woff", "application/font-woff", "font/otf"})
;; We have added ".ttf" as string to solve a problem with chrome input selector
(def valid-font-types #{"font/ttf", ".ttf", "font/woff", "application/font-woff", "font/otf"})
(def valid-image-types #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
(def str-image-types (str/join "," valid-image-types))
(def str-font-types (str/join "," valid-font-types))

View File

@@ -19,6 +19,8 @@
(dm/export common/file-version)
(dm/export common/default-color)
(dm/export common/component-sync-attrs)
(dm/export common/retrieve-used-names)
(dm/export common/generate-unique-name)
;; Focus
(dm/export focus/focus-objects)

View File

@@ -182,7 +182,7 @@
(d/update-in-when data [:components component-id :objects] reg-objects))))
(defmethod process-change :mov-objects
[data {:keys [parent-id shapes index page-id component-id ignore-touched]}]
[data {:keys [parent-id shapes index page-id component-id ignore-touched after-shape]}]
(letfn [(calculate-invalid-targets [objects shape-id]
(let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))]
(->> (get-in objects [shape-id :shapes])
@@ -200,21 +200,9 @@
(cph/insert-at-index prev-shapes index shapes)
(cph/append-at-the-end prev-shapes shapes))))
(check-insert-items [prev-shapes parent index shapes]
(if-not (:masked-group? parent)
(insert-items prev-shapes index shapes)
;; For masked groups, the first shape is the mask
;; and it cannot be moved.
(let [mask-id (first prev-shapes)
other-ids (rest prev-shapes)
not-mask-shapes (without-obj shapes mask-id)
new-index (if (nil? index) nil (max (dec index) 0))
new-shapes (insert-items other-ids new-index not-mask-shapes)]
(into [mask-id] new-shapes))))
(add-to-parent [parent index shapes]
(let [parent (-> parent
(update :shapes check-insert-items parent index shapes)
(update :shapes insert-items index shapes)
;; We need to ensure that no `nil` in the
;; shapes list after adding all the
;; incoming shapes to the parent.
@@ -260,9 +248,13 @@
(not= :frame (:type obj))
(as-> $$ (reduce (partial assign-frame-id frame-id) $$ (:shapes obj))))))
(move-objects [objects]
(let [valid? (every? (partial is-valid-move? objects) shapes)
parent (get objects parent-id)
after-shape-index (d/index-of (:shapes parent) after-shape)
index (if (nil? after-shape-index) index (inc after-shape-index))
frame-id (if (= :frame (:type parent))
(:id parent)
(:frame-id parent))]
@@ -283,7 +275,7 @@
;; Ensure that all shapes of the new parent has a
;; correct link to the topside frame.
(reduce (partial assign-frame-id frame-id) $ shapes))
objects)))]
objects)))]
(if page-id
(d/update-in-when data [:pages-index page-id :objects] move-objects)
@@ -395,6 +387,10 @@
is-geometry? (and (or (= group :geometry-group)
(and (= group :content-group) (= (:type shape) :path)))
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
;; TODO: the check of :width and :height probably may be removed
;; after the check added in data/workspace/modifiers/check-delta
;; function. Better check it and test toroughly when activating
;; components-v2 mode.
shape-ref (:shape-ref shape)
root-name? (and (= group :name-group)
(:component-root? shape))

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