Compare commits

..

138 Commits

Author SHA1 Message Date
Andrey Antukh
136d00797c Merge branch 'release-1.2.0' into main 2021-02-15 13:29:36 +01:00
Andrey Antukh
964dad0d5b Merge pull request #641 from penpot/select-all
🐛 Fix behavior of select all command when there are objects outsi…
2021-02-15 10:57:39 +01:00
Andrés Moya
30819a08f4 🐛 Fix behavior of select all command when there are objects outside frames 2021-02-15 10:51:45 +01:00
Andrey Antukh
22b8eb856e Merge pull request #639 from penpot/fix/bugfixing
Bugfixing
2021-02-15 10:51:41 +01:00
Andrey Antukh
f8ccd0b120 📎 Add bigger window for quantiles on metrics. 2021-02-14 18:01:04 +01:00
alonso.torres
9d49d781cc 🐛 Fixes problem with text immediately after creation 2021-02-12 15:42:24 +01:00
alonso.torres
a81d20a2d1 🐛 Fixes console error for kebab-case properties 2021-02-12 12:14:31 +01:00
Andrey Antukh
d5ff5ea91e 📎 Update changelog. 2021-02-12 09:43:10 +01:00
alonso.torres
cf465d93f9 🐛 Fixes problem when shrinking text 2021-02-11 17:26:02 +01:00
Andrés Moya
521ccc25cf Merge pull request #633 from penpot/bugfixing
Bugfixing
2021-02-11 16:21:22 +01:00
alonso.torres
dc0765f6b0 Improved calculations for auto-resize 2021-02-11 16:01:21 +01:00
alonso.torres
8cfc2ec21a 🐛 Fixes problem with red handler indicator on resize 2021-02-11 15:49:18 +01:00
alonso.torres
10cad69fac 🐛 Fixes problem with multiple selection and groups 2021-02-11 14:43:59 +01:00
alonso.torres
b7d3158514 📚 Updates changelog with Taiga references 2021-02-11 13:45:30 +01:00
Andrés Moya
4b8334fe1c 🐛 Fix ordering when restoring deleted shapes in sync 2021-02-11 13:30:56 +01:00
Andrey Antukh
608b5cc9f9 Merge pull request #631 from penpot/bugfixing
Bugfixing
2021-02-11 13:21:25 +01:00
alonso.torres
42a55015fa 🐛 Fixes problem when pasting URL's from the browser address bar 2021-02-11 13:03:41 +01:00
alonso.torres
0a6e0d0f2c 🐛 Fixes dashboard preview text alignment 2021-02-11 11:58:45 +01:00
alonso.torres
7846682223 🐛 Fixes logo icon navigation in viewer 2021-02-11 11:34:24 +01:00
alonso.torres
5336bbbe65 🐛 Fixes problem change color to texts from the palette 2021-02-11 11:23:48 +01:00
Andrey Antukh
8e5fd5892e Merge pull request #624 from penpot/feature/flip
Adds flip vertical/horizontal commands
2021-02-11 10:52:24 +01:00
alonso.torres
eaff888486 Translations for flip commands 2021-02-11 10:47:43 +01:00
alonso.torres
f1383f4dca Updates changelog 2021-02-11 10:46:13 +01:00
alonso.torres
d9c10cea5d Flip horizontal/vertical operations 2021-02-11 10:46:13 +01:00
alonso.torres
d48a1ca0b0 Relative gradient rendering 2021-02-11 10:46:13 +01:00
alonso.torres
bfcfe2fd31 🐛 Fixes problems with path transforms 2021-02-11 10:46:13 +01:00
alonso.torres
648c088d02 🐛 Fixes problem with remote changes 2021-02-11 09:36:55 +01:00
alonso.torres
70258e0eee 🐛 Fixes problem with locking proportions in paths 2021-02-11 09:35:56 +01:00
alonso.torres
5b1e9ec7da 📚 Updates changelog 2021-02-10 17:32:23 +01:00
Andrey Antukh
7a250a170e 📎 Update changelog. 2021-02-10 17:06:09 +01:00
Andrey Antukh
2e438385f3 Increase default deletion delay. 2021-02-10 17:06:09 +01:00
Andrés Moya
d6f3efb358 🎉 Add more tests for components 2021-02-10 14:46:10 +01:00
Andrés Moya
884410c0d8 🎉 Add more tests for components 2021-02-10 14:46:10 +01:00
Andrés Moya
cdab9ff69c 🎉 Add more tests of components 2021-02-10 14:46:10 +01:00
Andrey Antukh
1da43bb5b5 Merge branch 'hotfixes' into main 2021-02-10 12:30:04 +01:00
Andrey Antukh
6f3a08be0c 🐛 Remove file lock contention on media upload. 2021-02-10 12:25:32 +01:00
Andrey Antukh
e5cb6ebec7 More improvements on background task scheduling. 2021-02-10 12:25:22 +01:00
Andrey Antukh
f60ad9e559 🐛 Fix unexpected 404 error on access shared link. 2021-02-10 12:24:58 +01:00
Andrey Antukh
69b23e4000 Change background tasks schedule. 2021-02-10 12:24:06 +01:00
Andrey Antukh
bedfb9a1ee Increment default statement timeout. 2021-02-10 12:23:51 +01:00
Andrey Antukh
e4fb802d7a Minor improvement on telemetry server error reporting. 2021-02-10 12:23:29 +01:00
Andrés Moya
068a099f37 Merge pull request #616 from penpot/niwinz/bugfixes-1
Bugfixes
2021-02-10 12:13:47 +01:00
Andrey Antukh
fa573f8a24 🐛 Remove file lock contention on media upload. 2021-02-10 12:07:35 +01:00
Andrey Antukh
ebb745cc11 More improvements on background task scheduling. 2021-02-10 12:07:35 +01:00
Andrey Antukh
2b33300d79 🐛 Fix unexpected exception on uploading invalid svg file. 2021-02-10 12:07:35 +01:00
Andrey Antukh
946d40e6cd Improve error handling on google auth. 2021-02-10 12:07:35 +01:00
Andrey Antukh
36285a65d2 🐛 Show correct error when google auth is disabled on backend. 2021-02-10 12:07:35 +01:00
Andrey Antukh
fc49674997 🐛 Add better error handling on upload image by url. 2021-02-10 12:07:35 +01:00
Andrey Antukh
d0a8647186 🐛 Fix unexpected 404 error on access shared link. 2021-02-10 12:07:35 +01:00
Andrey Antukh
9b875aba21 🐛 Fix unexpected exception on upload invalid image. 2021-02-10 12:07:35 +01:00
Andrey Antukh
76e43f339a 🎉 Add missing index to file_change table. 2021-02-10 12:07:35 +01:00
Andrey Antukh
32e832eb39 🎉 Add srepl helper for migrate page storage to new blob format. 2021-02-10 12:07:35 +01:00
Andrey Antukh
60704bca17 Change background tasks schedule. 2021-02-10 12:07:35 +01:00
Andrey Antukh
43e4712b86 📚 Fix CLA mention on CONTRIBUTING.md file.
Closing #590
2021-02-10 12:07:35 +01:00
Andrey Antukh
5359c3a7ed Increment default statement timeout. 2021-02-10 12:07:35 +01:00
Andrey Antukh
81bf68c67c Minor improvement on telemetry server error reporting. 2021-02-10 12:07:35 +01:00
alonso.torres
4d5231598f 🐛 Fixes issues with moving shapes outside groups 2021-02-09 15:42:16 +01:00
Andrey Antukh
c1a139fc51 🎉 Add user feedback module. 2021-02-09 14:12:31 +01:00
Andrey Antukh
1cb18ad7cb Merge branch 'main' into develop 2021-02-09 12:53:52 +01:00
Andrey Antukh
6f0258c8d4 Improve build scripts. 2021-02-09 12:53:09 +01:00
Andrey Antukh
124efc0d88 Improve build scripts. 2021-02-09 12:18:14 +01:00
mathieu.brunot
924ecd998f 🐛 Fix ldap function called on login click
Signed-off-by: mathieu.brunot <mathieu.brunot@monogramm.io>
2021-02-09 09:40:12 +01:00
Andrés Moya
07a94de607 Merge branch 'main' into develop 2021-02-08 16:49:15 +01:00
Andrés Moya
7bd05d63ac 🐛 Fix error 500 when requesting a password reset 2021-02-08 16:30:35 +01:00
mathieu.brunot
bb15924c95 🐳 Frontend configuration on env var
Signed-off-by: mathieu.brunot <mathieu.brunot@monogramm.io>
2021-02-08 14:26:23 +01:00
Nishant Srivastava
1ebce37e17 Update getting started guide 2021-02-08 14:15:45 +01:00
Danny Lin
b93dc752fe 💄 Update UXBOX name in emails
When registering for a new account, I noticed that the HTML emails had
the new Penpot name but the plain-text versions were still using the old
UXBOX name. This commit fixes the discrepancy.

Signed-off-by: Danny Lin <danny@kdrag0n.dev>
2021-02-08 13:59:26 +01:00
Andrey Antukh
dbbe1f7df2 📎 Minor improvement on main ns on srepl module. 2021-02-08 13:52:51 +01:00
Andrey Antukh
a709c47f6f 🎉 Add zstd+nippy based blob storage format. 2021-02-08 13:52:51 +01:00
Andrey Antukh
68ed30ff35 📚 Update CONTRIBUTING.md file. 2021-02-05 15:01:50 +01:00
Andrés Moya
a65a31810c Merge branch 'patch-1' of https://github.com/tomer/penpot into tomer-patch-1 2021-02-05 14:46:18 +01:00
Tomer Cohen
8c50dc0c72 Fix broken link to Taiga.io in README.md
Signed-off-by: Tomer Cohen <tomer@users.noreply.github.com>
2021-02-05 13:47:15 +02:00
alonso.torres
a8a036206b Pixel grid 2021-02-05 12:19:05 +01:00
Andrés Moya
8313f1d96e Merge branch 'Monogramm-i18n/fr' into develop 2021-02-05 11:48:37 +01:00
Andrés Moya
1898ed215e Merge branch 'i18n/fr' of https://github.com/Monogramm/penpot into Monogramm-i18n/fr 2021-02-05 11:44:13 +01:00
alonso.torres
83aceba913 Makes images proportion lock by default 2021-02-05 11:29:39 +01:00
mathieu.brunot
c56fb0ea47 🌐 Update French locale
Signed-off-by: mathieu.brunot <mathieu.brunot@monogramm.io>
2021-02-04 20:10:46 +01:00
Andrey Antukh
83a2df3ef3 🎉 Add changelog. 2021-02-04 16:27:54 +01:00
alonso.torres
4703f6d5c7 🐛 Fixes problem with multiple selection 2021-02-04 15:29:19 +01:00
alonso.torres
8d2797f8a1 🐛 Fixes problem with multiple selection 2021-02-04 15:08:47 +01:00
alonso.torres
6cdde84445 🐛 Color palette text-wrap and showing when open color palette 2021-02-04 14:54:40 +01:00
Andrey Antukh
afa35379b2 🐛 Fix onboarding after logging with token. 2021-02-04 14:47:14 +01:00
alonso.torres
1099e08b7d 🐛 Fixed small visual problem for images in handoff 2021-02-04 14:31:49 +01:00
alonso.torres
89cb20ada7 🐛 Fixes Ctrl+a for viewer 2021-02-04 14:31:49 +01:00
alonso.torres
32b0fd7b36 🐛 Fixes issue with multiple selection and shadows 2021-02-04 14:31:49 +01:00
Andrey Antukh
04670bb5f2 Reset some message timeout defaults. 2021-02-04 14:29:39 +01:00
Andrey Antukh
8566fe4ac1 Show close icon on messages by default. 2021-02-04 14:29:39 +01:00
Andrey Antukh
e607e8315c Auto login after email verify. 2021-02-04 14:29:39 +01:00
Andrés Moya
a9b7cf61a5 🐛 Fix display of custom shape strokes 2021-02-04 14:22:39 +01:00
elhombretecla
7c7bda669c Add better layout for register success page. 2021-02-04 13:36:47 +01:00
Andrey Antukh
0c82c6f2f5 🐛 Fix recursion error on not-found. 2021-02-04 13:34:38 +01:00
alonso.torres
b7cbe49cb2 🐛 Fixes image upload position when uploading from left sidebar 2021-02-04 12:43:50 +01:00
alonso.torres
7378089f4a 🐛 Fixes problems with multiple values in fill and stroke 2021-02-04 12:39:41 +01:00
Andrey Antukh
62b6b12066 Merge branch 'violoncelloCH-fix/js-var-prefix' into develop 2021-02-04 12:16:24 +01:00
Jonas Sulzer
39fdff9052 🐛 Fix js variable prefix app->penpot on config doc.
Signed-off-by: Jonas Sulzer <jonas@violoncello.ch>
2021-02-04 12:15:40 +01:00
alonso.torres
32c0913f00 🐛 Fixes problem with pixel-level movement 2021-02-04 11:54:45 +01:00
Andrey Antukh
7eb90d62b0 🐛 Fix typos on translation strings. 2021-02-04 11:48:47 +01:00
Andrey Antukh
ec2683417f 🐛 Fix image upload internal error. 2021-02-04 11:48:47 +01:00
Andrey Antukh
cb23c8b093 Increase default flash message timeout. 2021-02-04 11:48:47 +01:00
Andrey Antukh
687f7ddf64 Don't send emails on recovery password on not verified profile.
And show proper message to the user saying that the profile
need to be verfied before proceed.
2021-02-04 11:48:47 +01:00
Andrey Antukh
992a8e9aef Improve posible race condition handling on user registration. 2021-02-04 11:48:47 +01:00
Andrey Antukh
6e08c6bc35 📎 Fix linter issues. 2021-02-04 11:48:47 +01:00
Andrey Antukh
b71d05935a Expose user-agent and frontend-version on error report. 2021-02-04 11:48:47 +01:00
Andrey Antukh
c14dbc19f8 🎉 Add register confirmation page. 2021-02-04 11:48:47 +01:00
Andrey Antukh
1eff1c94c4 🔥 Remove goodbye page (useless). 2021-02-04 11:48:47 +01:00
Andrey Antukh
53be7feee1 🎉 Add 3rd party auth buttons to register page. 2021-02-04 11:48:47 +01:00
Andrey Antukh
e182cc4028 Add default headers to frontend http client. 2021-02-04 11:48:47 +01:00
Andrey Antukh
80309cbff3 Improve error reporting of tasks. 2021-02-04 11:48:47 +01:00
alonso.torres
816db29f9c Refactor of shortcuts and adaptations for macosx 2021-02-04 11:34:00 +01:00
Andrés Moya
526e0afc70 💄 Fix args and docstrings 2021-02-04 11:24:19 +01:00
Andrés Moya
77973af49f Remember assets libraries open in local session 2021-02-04 11:24:19 +01:00
Andrés Moya
dc5cff645a Remember color picker library in local session 2021-02-04 11:24:19 +01:00
alonso.torres
0ea8e9e750 🐛 Fixes issue with lock proportions 2021-02-04 11:18:59 +01:00
alonso.torres
69b4968578 Change to add when selected shape 2021-02-04 11:17:40 +01:00
Andrey Antukh
b7e266e350 Revert "🐛 Fixes problems with multiple values in fill and stroke"
This reverts commit 8fd8bc4537.
2021-02-03 17:27:08 +01:00
alonso.torres
b056cc35e4 🐛 Fixes problem when moving parent to children group 2021-02-03 15:36:28 +01:00
alonso.torres
d66452423f 🐛 Fixes recursion problems when creating component 2021-02-03 15:36:28 +01:00
Andrey Antukh
d85537fa7b Merge branch 'main' into develop 2021-02-03 15:18:35 +01:00
Andrey Antukh
fc11fb6e3d Reduce system resources for frontend build. 2021-02-03 15:18:16 +01:00
alonso.torres
cbdfb4349b 🐛 Fixed problem when editing paths 2021-02-03 13:30:59 +01:00
Abtin
19ed0b70c2 Update 00-Getting-Started.md
fix link to configuration guide
2021-02-03 13:29:55 +01:00
Andrey Antukh
3092747b5f Merge branch 'main' into develop 2021-02-03 13:27:03 +01:00
Andrey Antukh
0adfc2ddab Update manage.sh
Make the bundle use LZ4 compression by default.
2021-02-03 13:25:58 +01:00
alonso.torres
8fd8bc4537 🐛 Fixes problems with multiple values in fill and stroke 2021-02-03 12:30:58 +01:00
Andrey Antukh
e7d6a54907 🐛 Fix static file handling on docker images. 2021-02-03 11:30:10 +01:00
Hirunatan
e3c273c84b Merge pull request #532 from penpot/hotfix/texts
🐛 Fixes problems with paste empty text
2021-02-02 15:43:19 +01:00
alonso.torres
8aedbd1418 🐛 Fixes problems with paste empty text 2021-02-02 15:36:49 +01:00
Andrés Moya
e713c30785 🐛 Prevent browser dragging of images in some cases 2021-02-02 15:01:36 +01:00
Andrey Antukh
74a168d87e 🐛 Use proper config value. 2021-02-02 14:39:44 +01:00
Andrey Antukh
ca63ff621a 🐛 Fix email from handling. 2021-02-02 14:39:44 +01:00
Andrés Moya
d120af2c81 🐛 Fix workspace breadcrumb 2021-02-02 13:03:36 +01:00
alonso.torres
95ab5b57b7 🐛 Removes problems with texts 2021-02-02 13:03:21 +01:00
alonso.torres
2e7f90f3cc Adds commands to load data into user 2021-02-02 13:03:21 +01:00
Andrés Moya
8403352af8 🐛 Fix error in fixtures loading 2021-02-02 10:40:13 +01:00
Andrey Antukh
526b6e1f03 🐛 Unexpected exception on handling of invitation user registration. 2021-02-02 09:30:43 +01:00
Andrey Antukh
f2fd976934 📎 Replace develop with latest in default compose file. 2021-02-01 22:37:28 +01:00
Andrey Antukh
8b9371d7e1 🎉 Add the ability to disable mattermost webhook on runtime. 2021-02-01 22:37:28 +01:00
Andrés Moya
948a4038c6 Update social cards meta tags 2021-02-01 18:19:07 +01:00
156 changed files with 4162 additions and 1909 deletions

View File

@@ -1,5 +1,6 @@
{:lint-as {potok.core/reify clojure.core/reify
promesa.core/let clojure.core/let
rumext.alpha/defc clojure.core/defn
app.db/with-atomic clojure.core/with-open}
:output
{:exclude-files ["data_readers.clj"]}

63
CHANGES.md Normal file
View File

@@ -0,0 +1,63 @@
# CHANGELOG #
## Next
### New features
### Bugs fixed
## 1.2.0-alpha
### New features
- Add horizontal/vertical flip
- Add images lock proportions by default [#541](https://github.com/penpot/penpot/discussions/541), [#609](https://github.com/penpot/penpot/issues/609)
- Add new blob storage format (Zstd+nippy)
- Add user feedback form
- Improve French translations
- Improve component testing
- Increase default deletion delay to 7 days
- Show a pixel grid when zoom greater than 800% [#519](https://github.com/penpot/penpot/discussions/519)
- Fix behavior of select all command when there are objects outside frames [Taiga #1209](https://tree.taiga.io/project/penpot/issue/1209)
### Bugs fixed
- Fix 404 when access shared link [#615](https://github.com/penpot/penpot/issues/615)
- Fix 500 when requestion password reset
- Fix Problems when transforming path shapes [Taiga #1170](https://tree.taiga.io/project/penpot/issue/1170)
- Fix apply a color to a text selection from color palette was not working [Taiga #1189](https://tree.taiga.io/project/penpot/issue/1189)
- Fix issues when moving shapes outside groups [Taiga #1138](https://tree.taiga.io/project/penpot/issue/1138)
- Fix ldap function called on login click
- Fix logo icon in viewer should go to dashboard [Taiga #1149](https://tree.taiga.io/project/penpot/issue/1149)
- Fix ordering when restoring deleted shapes in sync [Taiga #1163](https://tree.taiga.io/project/penpot/issue/1163)
- Fix problem when editing text immediately after creating [Taiga #1207](https://tree.taiga.io/project/penpot/issue/1207)
- Fix problem when pasting URL's copied from the browser url bar [Taiga #1187](https://tree.taiga.io/project/penpot/issue/1187)
- Fix problem with multiple selection and groups [Taiga #1128](https://tree.taiga.io/project/penpot/issue/1128)
- Fix problem with red handler indicator on resize [Taiga #1188](https://tree.taiga.io/project/penpot/issue/1188)
- Fix show correct error when google auth is disabled [Taiga #1119](https://tree.taiga.io/project/penpot/issue/1119)
- Fix text alignment in preview [#594](https://github.com/penpot/penpot/issues/594)
- Fix unexpected exception when uploading image [Taiga #1120](https://tree.taiga.io/project/penpot/issue/1120)
- Fix updates on collaborative editing not updating selection rectangles [Taiga #1127](https://tree.taiga.io/project/penpot/issue/1127)
- Make the team deletion deferred (in the same way other objects)
### Community contributions by (Thank you! :heart:)
- abtinmo [#538](https://github.com/penpot/penpot/pull/538)
- kdrag0n [#585](https://github.com/penpot/penpot/pull/585)
- nisrulz [#586](https://github.com/penpot/penpot/pull/586)
- tomer [#575](https://github.com/penpot/penpot/pull/575)
- violoncelloCH [#554](https://github.com/penpot/penpot/pull/554)
## 1.1.0-alpha
- Bugfixing and stabilization post-launch
- Some changes to the register flow
- Improved MacOS shortcuts and helpers
- Small changes to shape creation
## 1.0.0-alpha
Initial release

View File

@@ -1,8 +1,8 @@
# Contributing Guide #
Thank you for your interest in contributing to Penpot. This is a
generic guide that details how to contribute to Penpot in a way that is
efficient for everyone. If you want a specific documentation for
generic guide that details how to contribute to Penpot in a way that
is efficient for everyone. If you want a specific documentation for
different parts of the platform, please refer to `docs/` directory.
@@ -19,12 +19,20 @@ If you found a bug, please report it, as far as possible with:
- a browser and the browser version used
- a dev tools console exception stack trace (if it is available)
If you found a bug that you consider better discuse in private (for
example: security bugs), consider first send an email to
`info@penpot.app`.
**We don't have formal bug bounty program for security reports; this
is an open source application and your contribution will be recognized
in the changelog.**
## Pull requests ##
If you want propose a change or bug fix with the Pull-Request system
firstly you should carefully read the **Contributor License Aggreement**
section and format your commits accordingly.
firstly you should carefully read the **DCO** section and format your
commits accordingly.
If you intend to fix a bug it's fine to submit a pull request right
away but we still recommend to file an issue detailing what you're
@@ -127,7 +135,7 @@ This Code of Conduct is adapted from the Contributor Covenant, version
1.1.0, available from http://contributor-covenant.org/version/1/1/0/
## Contributor License Agreement ##
## Developer's Certificate of Origin (DCO) ##
By submitting code you are agree and can certify the below:
@@ -157,9 +165,9 @@ By submitting code you are agree and can certify the below:
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
Then, all your patches should contain a sign-off at the end of the
patch/commit description body. It can be automatically added on adding
`-s` parameter to `git commit`.
Then, all your code patches (**documentation are excluded**) should
contain a sign-off at the end of the patch/commit description body. It
can be automatically added on adding `-s` parameter to `git commit`.
This is an example of the aspect of the line:

View File

@@ -4,7 +4,7 @@
[![License: MPL-2.0][uri_license_image]][uri_license]
[![Gitter](https://badges.gitter.im/sereno-xyz/community.svg)](https://gitter.im/penpot/community)
[![Managed with Taiga.io](https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg)](https://tree.taiga.io/project/uxboxproject/ "Managed with Taiga.io")
[![Managed with Taiga.io](https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg)](https://tree.taiga.io/project/penpot/ "Managed with Taiga.io")
# PENPOT #

View File

@@ -18,6 +18,8 @@
org.slf4j/slf4j-api {:mvn/version "1.7.30"}
org.graalvm.js/js {:mvn/version "20.3.0"}
com.taoensso/nippy {:mvn/version "3.1.1"}
com.github.luben/zstd-jni {:mvn/version "1.4.8-3"}
io.prometheus/simpleclient {:mvn/version "0.9.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.9.0"}

View File

@@ -14,6 +14,7 @@
[app.util.time :as dt]
[app.util.transit :as t]
[app.common.exceptions :as ex]
[taoensso.nippy :as nippy]
[clojure.data.json :as json]
[clojure.java.io :as io]
[clojure.test :as test]

View File

@@ -30,14 +30,14 @@
for security reasons.
</mj-text>
<mj-text>Enjoy!</mj-text>
<mj-text>The UXBOX team.</mj-text>
<mj-text>The Penpot team.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 0 0 0">
<mj-column width="425px">
<mj-text align="center" font-size="14px" color="#64666A">
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
</mj-text>
</mj-column>
</mj-section>
@@ -57,7 +57,7 @@
<mj-section padding="0 0 24px 0">
<mj-column>
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
UXBOX © 2020 | Made with &lt;3 and Open Source
Penpot © 2020 | Made with &lt;3 and Open Source
</mj-text>
</mj-column>
</mj-section>

View File

@@ -23,14 +23,14 @@
Accept invite
</mj-button>
<mj-text>Enjoy!</mj-text>
<mj-text>The UXBOX team.</mj-text>
<mj-text>The Penpot team.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 0 0 0">
<mj-column width="425px">
<mj-text align="center" font-size="14px" color="#64666A">
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
</mj-text>
</mj-column>
</mj-section>
@@ -50,7 +50,7 @@
<mj-section padding="0 0 24px 0">
<mj-column>
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
UXBOX © 2020 | Made with &lt;3 and Open Source
Penpot © 2020 | Made with &lt;3 and Open Source
</mj-text>
</mj-column>
</mj-section>

View File

@@ -32,14 +32,14 @@
it. Your password won't be changed.
</mj-text>
<mj-text>Enjoy!</mj-text>
<mj-text>The UXBOX team.</mj-text>
<mj-text>The Penpot team.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 0 0 0">
<mj-column width="425px">
<mj-text align="center" font-size="14px" color="#64666A">
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
</mj-text>
</mj-column>
</mj-section>
@@ -59,7 +59,7 @@
<mj-section padding="0 0 24px 0">
<mj-column>
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
UXBOX © 2020 | Made with &lt;3 and Open Source
Penpot © 2020 | Made with &lt;3 and Open Source
</mj-text>
</mj-column>
</mj-section>

View File

@@ -21,7 +21,7 @@
<mj-column>
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
<mj-text>
Thanks for signing up for your UXBOX account! Please verify your
Thanks for signing up for your Penpot account! Please verify your
email using the link below adn get started building mockups and
prototypes today!
</mj-text>
@@ -29,14 +29,14 @@
Verify email
</mj-button>
<mj-text>Enjoy!</mj-text>
<mj-text>The UXBOX team.</mj-text>
<mj-text>The Penpot team.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 0 0 0">
<mj-column width="425px">
<mj-text align="center" font-size="14px" color="#64666A">
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
</mj-text>
</mj-column>
</mj-section>
@@ -56,7 +56,7 @@
<mj-section padding="0 0 24px 0">
<mj-column>
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
UXBOX © 2020 | Made with &lt;3 and Open Source
Penpot © 2020 | Made with &lt;3 and Open Source
</mj-text>
</mj-column>
</mj-section>

View File

@@ -10,4 +10,4 @@ If you received this email by mistake, please consider changing your password
for security reasons.
Enjoy!
The UXBOX team.
The Penpot team.

View File

@@ -0,0 +1 @@
[FEEDBACK]: From {{ profile.email }}

View File

@@ -0,0 +1,7 @@
Feedback from: {{profile.fullname}} <{{profile.email}}>
Profile ID: {{profile.id}}
Subject: {{subject}}
{{content}}

View File

@@ -7,4 +7,4 @@ Accept invitation using this link:
{{ public-uri }}/#/auth/verify-token?token={{token}}
Enjoy!
The UXBOX team.
The Penpot team.

View File

@@ -9,4 +9,4 @@ If you received this email by mistake, you can safely ignore it. Your password
won't be changed.
Enjoy!
The UXBOX team.
The Penpot team.

View File

@@ -1,9 +1,9 @@
Hello {{name}}!
Thanks for signing up for your UXBOX account! Please verify your email using the
Thanks for signing up for your Penpot account! Please verify your email using the
link below adn get started building mockups and prototypes today!
{{ public-uri }}/#/auth/verify-token?token={{token}}
Enjoy!
The UXBOX team.
The Penpot team.

View File

@@ -67,39 +67,61 @@
<div class="table-val">{{profile-id}}</div>
</div>
{% endif %}
{% if user-agent %}
<div class="table-row">
<div class="table-key">VERS: </div>
<div class="table-key">UAGENT: </div>
<div class="table-val">{{user-agent}}</div>
</div>
{% endif %}
{% if frontend-version %}
<div class="table-row">
<div class="table-key">FVERS: </div>
<div class="table-val">{{frontend-version}}</div>
</div>
{% endif %}
<div class="table-row">
<div class="table-key">BVERS: </div>
<div class="table-val">{{version}}</div>
</div>
<div class="table-row">
<div class="table-key">HOST: </div>
<div class="table-val">{{host}}</div>
</div>
{% if type %}
<div class="table-row">
<div class="table-key">TYPE: </div>
<div class="table-val">{{type}}</div>
</div>
{% endif %}
{% if code %}
<div class="table-row">
<div class="table-key">CODE: </div>
<div class="table-val">{{code}}</div>
</div>
{% endif %}
<div class="table-row">
<div class="table-key">CLASS: </div>
<div class="table-val">{{class}}</div>
</div>
<div class="table-row">
<div class="table-key">HINT: </div>
<div class="table-val">{{hint}}</div>
</div>
{% if method %}
<div class="table-row">
<div class="table-key">PATH: </div>
<div class="table-val">{{method|upper}} {{path}}</div>
</div>
{% endif %}
{% if params %}
<div class="table-row multiline">
@@ -128,7 +150,6 @@
</div>
{% endif %}
<div class="table-row multiline">
<div class="table-key">TRACE:</div>
<div class="table-val">

View File

@@ -75,7 +75,7 @@
(let [rng (java.util.Random. 1)]
(letfn [(create-profile [conn index]
(let [id (mk-uuid "profile" index)
_ (log/info "create profile" id)
_ (log/info "create profile" index id)
prof (register-profile conn
{:id id
@@ -98,10 +98,9 @@
(create-team [conn index]
(let [id (mk-uuid "team" index)
name (str "Team" index)]
(log/info "create team" id)
(log/info "create team" index id)
(db/insert! conn :team {:id id
:name name
:photo ""})
:name name})
id))
(create-teams [conn]
@@ -113,7 +112,7 @@
(let [id (mk-uuid "file" project-id index)
name (str "file" index)
data (cp/make-file-data id)]
(log/info "create file" id)
(log/info "create file" index id)
(db/insert! conn :file
{:id id
:data (blob/encode data)
@@ -135,7 +134,7 @@
(create-project [conn team-id owner-id index]
(let [id (mk-uuid "project" team-id index)
name (str "project " index)]
(log/info "create project" id)
(log/info "create project" index id)
(db/insert! conn :project
{:id id
:team-id team-id
@@ -188,7 +187,7 @@
project-id (:default-project-id owner)
data (cp/make-file-data id)]
(log/info "create draft file" id)
(log/info "create draft file" index id)
(db/insert! conn :file
{:id id
:data (blob/encode data)

View File

@@ -24,6 +24,8 @@
:database-username "penpot"
:database-password "penpot"
:default-blob-version 1
:asserts-enabled false
:public-uri "http://localhost:3449"
@@ -38,21 +40,23 @@
:storage-s3-region :eu-central-1
:storage-s3-bucket "penpot-devenv-assets-pre"
:feedback-destination "info@example.com"
:feedback-enabled false
:assets-path "/internal/assets/"
:rlimits-password 10
:rlimits-image 2
:smtp-enabled false
:smtp-default-reply-to "no-reply@example.com"
:smtp-default-from "no-reply@example.com"
:smtp-default-reply-to "Penpot <no-reply@example.com>"
:smtp-default-from "Penpot <no-reply@example.com>"
:allow-demo-users true
:registration-enabled true
:registration-domain-whitelist ""
:telemetry-enabled false
:telemetry-with-taiga true
:telemetry-uri "https://telemetry.penpot.app/"
;; LDAP auth disabled by default. Set ldap-auth-host to enable
@@ -80,6 +84,7 @@
(s/def ::database-uri ::us/string)
(s/def ::redis-uri ::us/string)
(s/def ::storage-backend ::us/keyword)
(s/def ::storage-fs-directory ::us/string)
(s/def ::assets-path ::us/string)
@@ -90,10 +95,14 @@
(s/def ::media-directory ::us/string)
(s/def ::asserts-enabled ::us/boolean)
(s/def ::feedback-enabled ::us/boolean)
(s/def ::feedback-destination ::us/string)
(s/def ::error-report-webhook ::us/string)
(s/def ::smtp-enabled ::us/boolean)
(s/def ::smtp-default-reply-to ::us/email)
(s/def ::smtp-default-from ::us/email)
(s/def ::smtp-default-reply-to ::us/string)
(s/def ::smtp-default-from ::us/string)
(s/def ::smtp-host ::us/string)
(s/def ::smtp-port ::us/integer)
(s/def ::smtp-username (s/nilable ::us/string))
@@ -143,13 +152,18 @@
(s/def ::initial-data-file ::us/string)
(s/def ::initial-data-project-name ::us/string)
(s/def ::default-blob-version ::us/integer)
(s/def ::config
(s/keys :opt-un [::allow-demo-users
::asserts-enabled
::database-password
::database-uri
::database-username
::default-blob-version
::error-report-webhook
::feedback-enabled
::feedback-destination
::github-client-id
::github-client-secret
::gitlab-base-uri
@@ -231,5 +245,5 @@
(def config (read-config env))
(def test-config (read-test-config env))
(def default-deletion-delay
(dt/duration {:hours 48}))
(def deletion-delay
(dt/duration {:days 7}))

View File

@@ -72,7 +72,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def initsql
(str "SET statement_timeout = 10000;\n"
(str "SET statement_timeout = 60000;\n"
"SET idle_in_transaction_session_timeout = 120000;"))
(defn- create-datasource-config

View File

@@ -76,38 +76,42 @@
(tr/decode-stream input))))
(defn create-profile-initial-data
[conn profile]
(when-let [initial-data-path (:initial-data-file cfg/config)]
(when-let [{:keys [file file-library-rel file-media-object]} (read-initial-data initial-data-path)]
(let [sample-project-name (:initial-data-project-name cfg/config "Penpot Onboarding")
proj (projects/create-project conn {:profile-id (:id profile)
:team-id (:default-team-id profile)
:name sample-project-name})
([conn profile]
(when-let [initial-data-path (:initial-data-file cfg/config)]
(create-profile-initial-data conn initial-data-path profile)))
map-ids {}
([conn file profile]
(when-let [{:keys [file file-library-rel file-media-object]} (read-initial-data file)]
(let [sample-project-name (:initial-data-project-name cfg/config "Penpot Onboarding")
;; Create new ID's and change the references
[map-ids file] (change-ids map-ids file #{:id})
[map-ids file-library-rel] (change-ids map-ids file-library-rel #{:file-id :library-file-id})
[_ file-media-object] (change-ids map-ids file-media-object #{:id :file-id :media-id :thumbnail-id})
proj (projects/create-project conn {:profile-id (:id profile)
:team-id (:default-team-id profile)
:name sample-project-name})
file (map #(assoc % :project-id (:id proj)) file)
file-profile-rel (map #(array-map :file-id (:id %)
:profile-id (:id profile)
:is-owner true
:is-admin true
:can-edit true)
file)]
map-ids {}
(projects/create-project-profile conn {:project-id (:id proj)
:profile-id (:id profile)})
;; Create new ID's and change the references
[map-ids file] (change-ids map-ids file #{:id})
[map-ids file-library-rel] (change-ids map-ids file-library-rel #{:file-id :library-file-id})
[_ file-media-object] (change-ids map-ids file-media-object #{:id :file-id :media-id :thumbnail-id})
(projects/create-team-project-profile conn {:team-id (:default-team-id profile)
:project-id (:id proj)
:profile-id (:id profile)})
file (map #(assoc % :project-id (:id proj)) file)
file-profile-rel (map #(array-map :file-id (:id %)
:profile-id (:id profile)
:is-owner true
:is-admin true
:can-edit true)
file)]
;; Re-insert into the database
(db/insert-multi! conn :file file)
(db/insert-multi! conn :file-profile-rel file-profile-rel)
(db/insert-multi! conn :file-library-rel file-library-rel)
(db/insert-multi! conn :file-media-object file-media-object)))))
(projects/create-project-profile conn {:project-id (:id proj)
:profile-id (:id profile)})
(projects/create-team-project-profile conn {:team-id (:default-team-id profile)
:project-id (:id proj)
:profile-id (:id profile)})
;; Re-insert into the database
(db/insert-multi! conn :file file)
(db/insert-multi! conn :file-profile-rel file-profile-rel)
(db/insert-multi! conn :file-library-rel file-library-rel)
(db/insert-multi! conn :file-media-object file-media-object)))))

View File

@@ -43,6 +43,16 @@
;; --- Emails
(s/def ::subject ::us/string)
(s/def ::content ::us/string)
(s/def ::feedback
(s/keys :req-un [::subject ::content]))
(def feedback
"A profile feedback email."
(emails/template-factory ::feedback default-context))
(s/def ::name ::us/string)
(s/def ::register
(s/keys :req-un [::name]))

View File

@@ -36,6 +36,7 @@
(declare handle-event)
(defonce enabled-mattermost (atom true))
(defonce queue (a/chan (a/sliding-buffer 64)))
(defonce queue-fn (fn [event] (a/>!! queue event)))
@@ -117,7 +118,7 @@
[cfg event]
(try
(let [cdata (get-context-data event)]
(when (:uri cfg)
(when (and (:uri cfg) @enabled-mattermost)
(send-mattermost-notification! cfg cdata))
(persist-on-database! cfg cdata))
(catch Exception e

View File

@@ -16,6 +16,7 @@
[app.http.errors :as errors]
[app.http.middleware :as middleware]
[app.metrics :as mtx]
[app.util.log4j :refer [update-thread-context!]]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]
@@ -103,7 +104,7 @@
(catch Throwable e
(try
(let [cdata (errors/get-error-context request e)]
(errors/update-thread-context! cdata)
(update-thread-context! cdata)
(log/errorf e "Unhandled exception: %s (id: %s)" (ex-message e) (str (:id cdata)))
{:status 500
:body "internal server error"})

View File

@@ -35,51 +35,40 @@
(defn- get-access-token
[cfg code]
(let [params {:code code
:client_id (:client-id cfg)
:client_secret (:client-secret cfg)
:redirect_uri (build-redirect-url cfg)
:grant_type "authorization_code"}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"}
:uri "https://oauth2.googleapis.com/token"
:body (uri/map->query-string params)}
res (http/send! req)]
(try
(let [params {:code code
:client_id (:client-id cfg)
:client_secret (:client-secret cfg)
:redirect_uri (build-redirect-url cfg)
:grant_type "authorization_code"}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"}
:uri "https://oauth2.googleapis.com/token"
:body (uri/map->query-string params)}
res (http/send! req)]
(when (not= 200 (:status res))
(ex/raise :type :internal
:code :invalid-response-from-google
:context {:status (:status res)
:body (:body res)}))
(when (= 200 (:status res))
(-> (json/read-str (:body res))
(get "access_token"))))
(try
(let [data (json/read-str (:body res))]
(get data "access_token"))
(catch Throwable e
(log/error "unexpected error on parsing response body from google access token request" e)
nil))))
(catch Exception e
(log/error e "unexpected error on get-access-token")
nil)))
(defn- get-user-info
[token]
(let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo"
:headers {"Authorization" (str "Bearer " token)}
:method :get}
res (http/send! req)]
(when (not= 200 (:status res))
(ex/raise :type :internal
:code :invalid-response-from-google
:context {:status (:status res)
:body (:body res)}))
(try
(let [data (json/read-str (:body res))]
;; (clojure.pprint/pprint data)
{:email (get data "email")
:fullname (get data "name")})
(catch Throwable e
(log/error "unexpected error on parsing response body from google access token request" e)
nil))))
(try
(let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo"
:headers {"Authorization" (str "Bearer " token)}
:method :get}
res (http/send! req)]
(when (= 200 (:status res))
(let [data (json/read-str (:body res))]
{:email (get data "email")
:fullname (get data "name")})))
(catch Exception e
(log/error e "unexpected exception on get-user-info")
nil)))
(defn- auth
[{:keys [tokens] :as cfg} _req]
@@ -99,33 +88,39 @@
(defn- callback
[{:keys [tokens rpc session] :as cfg} request]
(let [token (get-in request [:params :state])
_ (tokens :verify {:token token :iss :google-oauth})
info (some->> (get-in request [:params :code])
(get-access-token cfg)
(get-user-info))]
(when-not info
(ex/raise :type :authentication
:code :unable-to-authenticate-with-google))
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
(try
(let [token (get-in request [:params :state])
_ (tokens :verify {:token token :iss :google-oauth})
info (some->> (get-in request [:params :code])
(get-access-token cfg)
(get-user-info))
_ (when-not info
(ex/raise :type :internal
:code :unable-to-auth))
method-fn (get-in rpc [:methods :mutation :login-or-register])
profile (method-fn {:email (:email info)
:fullname (:fullname info)})
uagent (get-in request [:headers "user-agent"])
token (tokens :generate {:iss :auth
:exp (dt/in-future "15m")
:profile-id (:id profile)})
uri (-> (uri/uri (:public-uri cfg))
(assoc :path "/#/auth/verify-token")
(assoc :query (uri/map->query-string {:token token})))
sid (session/create! session {:profile-id (:id profile)
:user-agent uagent})]
{:status 302
:headers {"location" (str uri)}
:cookies (session/cookies session {:value sid})
:body ""})))
:body ""})
(catch Exception _e
(let [uri (-> (uri/uri (:public-uri cfg))
(assoc :path "/#/auth/login")
(assoc :query (uri/map->query-string {:error "unable-to-auth"})))]
{:status 302
:headers {"location" (str uri)}
:body ""}))))
(s/def ::client-id ::us/not-empty-string)
(s/def ::client-secret ::us/not-empty-string)

View File

@@ -12,25 +12,10 @@
(:require
[app.common.uuid :as uuid]
[app.config :as cfg]
[clojure.pprint :refer [pprint]]
[app.util.log4j :refer [update-thread-context!]]
[clojure.tools.logging :as log]
[cuerdas.core :as str]
[expound.alpha :as expound])
(:import
org.apache.logging.log4j.ThreadContext))
(defn update-thread-context!
[data]
(run! (fn [[key val]]
(ThreadContext/put
(name key)
(cond
(coll? val)
(binding [clojure.pprint/*print-right-margin* 120]
(with-out-str (pprint val)))
(instance? clojure.lang.Named val) (name val)
:else (str val))))
data))
[expound.alpha :as expound]))
(defn- explain-error
[error]
@@ -48,10 +33,12 @@
:version (:full cfg/version)
:host (:public-uri cfg/config)
:class (.getCanonicalName ^java.lang.Class (class error))
:hint (ex-message error)}
:hint (ex-message error)
:data edata}
(when (map? edata)
edata)
(let [headers (:headers request)]
{:user-agent (get headers "user-agent")
:frontend-version (get headers "x-frontend-version" "unknown")})
(when (and (map? edata) (:data edata))
{:explain (explain-error edata)}))))

View File

@@ -10,7 +10,7 @@
(ns app.http.session
(:require
[app.db :as db]
[app.http.errors :refer [update-thread-context!]]
[app.util.log4j :refer [update-thread-context!]]
[buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]
[clojure.spec.alpha :as s]

View File

@@ -94,7 +94,7 @@
:app.http.assets/handlers
{:metrics (ig/ref :app.metrics/metrics)
:assets-path (:assets-path cfg/config)
:assets-path (:assets-path config)
:storage (ig/ref :app.storage/storage)
:cache-max-age (dt/duration {:hours 24})
:signature-max-age (dt/duration {:hours 24 :minutes 5})}
@@ -148,11 +148,11 @@
;; RLimit definition for password hashing
:app.rlimits/password
(:rlimits-password cfg/config)
(:rlimits-password config)
;; RLimit definition for image processing
:app.rlimits/image
(:rlimits-image cfg/config)
(:rlimits-image config)
;; A collection of rlimits as hash-map.
:app.rlimits/all
@@ -192,29 +192,29 @@
:fn (ig/ref :app.tasks.file-media-gc/handler)}
{:id "file-xlog-gc"
:cron #app/cron "0 0 */6 * * ?" ;; every 2 hours
:cron #app/cron "0 0 */1 * * ?" ;; hourly
:fn (ig/ref :app.tasks.file-xlog-gc/handler)}
{:id "storage-deleted-gc"
:cron #app/cron "0 0 */6 * * ?" ;; every 6 hours
:cron #app/cron "0 0 1 */1 * ?" ;; daily (1 hour shift)
:fn (ig/ref :app.storage/gc-deleted-task)}
{:id "storage-touched-gc"
:cron #app/cron "0 30 */6 * * ?" ;; every 6 hours
:cron #app/cron "0 0 2 */1 * ?" ;; daily (2 hour shift)
:fn (ig/ref :app.storage/gc-touched-task)}
{:id "storage-recheck"
:cron #app/cron "0 0 */6 * * ?" ;; every 6 hours
:cron #app/cron "0 0 */1 * * ?" ;; hourly
:fn (ig/ref :app.storage/recheck-task)}
{:id "tasks-gc"
:cron #app/cron "0 0 0 */1 * ?" ;; daily
:fn (ig/ref :app.tasks.tasks-gc/handler)}
(when (:telemetry-enabled cfg/config)
(when (:telemetry-enabled config)
{:id "telemetry"
:cron #app/cron "0 0 */6 * * ?" ;; every 6h
:uri (:telemetry-uri cfg/config)
:uri (:telemetry-uri config)
:fn (ig/ref :app.tasks.telemetry/handler)})]}
:app.tasks/all
@@ -260,23 +260,23 @@
:app.tasks.file-xlog-gc/handler
{:pool (ig/ref :app.db/pool)
:metrics (ig/ref :app.metrics/metrics)
:max-age (dt/duration {:hours 24})}
:max-age (dt/duration {:hours 48})}
:app.tasks.telemetry/handler
{:pool (ig/ref :app.db/pool)
:version (:full cfg/version)
:uri (:telemetry-uri cfg/config)
:uri (:telemetry-uri config)
:sprops (ig/ref :app.sprops/props)}
:app.srepl/server
{:port (:srepl-port cfg/config)
:host (:srepl-host cfg/config)}
{:port (:srepl-port config)
:host (:srepl-host config)}
:app.sprops/props
{:pool (ig/ref :app.db/pool)}
:app.error-reporter/reporter
{:uri (:error-report-webhook cfg/config)
{:uri (:error-report-webhook config)
:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}
@@ -286,18 +286,18 @@
:app.storage/storage
{:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)
:backend (:storage-backend cfg/config :fs)
:backend (:storage-backend config :fs)
:backends {:s3 (ig/ref [::main :app.storage.s3/backend])
:db (ig/ref [::main :app.storage.db/backend])
:fs (ig/ref [::main :app.storage.fs/backend])
:tmp (ig/ref [::tmp :app.storage.fs/backend])}}
[::main :app.storage.s3/backend]
{:region (:storage-s3-region cfg/config)
:bucket (:storage-s3-bucket cfg/config)}
{:region (:storage-s3-region config)
:bucket (:storage-s3-bucket config)}
[::main :app.storage.fs/backend]
{:directory (:storage-fs-directory cfg/config)}
{:directory (:storage-fs-directory config)}
[::tmp :app.storage.fs/backend]
{:directory "/tmp/penpot"}
@@ -305,7 +305,7 @@
[::main :app.storage.db/backend]
{:pool (ig/ref :app.db/pool)}}
(when (:telemetry-server-enabled cfg/config)
(when (:telemetry-server-enabled config)
{:app.telemetry/handler
{:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}

View File

@@ -175,7 +175,12 @@
(ex/raise :type :internal
:code :rlimit-not-configured
:hint ":image rlimit not configured"))
(rlm/execute rlimit (process params))))
(try
(rlm/execute rlimit (process params))
(catch org.im4java.core.InfoException e
(ex/raise :type :validation
:code :invalid-image
:cause e)))))
;; --- Utility functions

View File

@@ -155,11 +155,12 @@
:dec (.. ^Gauge instance (labels labels) (dec)))))))
(defn make-summary
[{:keys [name help registry reg labels] :as props}]
[{:keys [name help registry reg labels max-age] :or {max-age 3600} :as props}]
(let [registry (or registry reg)
instance (doto (Summary/build)
(.name name)
(.help help)
(.maxAgeSeconds max-age)
(.quantile 0.75 0.02)
(.quantile 0.99 0.001))
_ (when (seq labels)

View File

@@ -145,6 +145,9 @@
{:name "0044-add-storage-refcount"
:fn (mg/resource "app/migrations/sql/0044-add-storage-refcount.sql")}
{:name "0045-add-index-to-file-change-table"
:fn (mg/resource "app/migrations/sql/0045-add-index-to-file-change-table.sql")}
])

View File

@@ -0,0 +1,2 @@
CREATE INDEX file_change__created_at_idx
ON file_change (created_at);

View File

@@ -126,6 +126,7 @@
'app.rpc.mutations.projects
'app.rpc.mutations.viewer
'app.rpc.mutations.teams
'app.rpc.mutations.feedback
'app.rpc.mutations.verify-token)
(map (partial process-method cfg))
(into {}))))

View File

@@ -52,7 +52,7 @@
;; Schedule deletion of the demo profile
(tasks/submit! conn {:name "delete-profile"
:delay cfg/default-deletion-delay
:delay cfg/deletion-delay
:props {:profile-id id}})
{:email email

View File

@@ -0,0 +1,41 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2021 UXBOX Labs SL
(ns app.rpc.mutations.feedback
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cfg]
[app.db :as db]
[app.emails :as emails]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
(s/def ::subject ::us/string)
(s/def ::content ::us/string)
(s/def ::send-profile-feedback
(s/keys :req-un [::profile-id ::subject ::content]))
(sv/defmethod ::send-profile-feedback
[{:keys [pool] :as cfg} {:keys [profile-id subject content] :as params}]
(when-not (:feedback-enabled cfg/config)
(ex/raise :type :validation
:code :feedback-disabled
:hint "feedback module is disabled"))
(db/with-atomic [conn pool]
(let [profile (profile/retrieve-profile-data conn profile-id)]
(emails/send! conn emails/feedback
{:to (:feedback-destination cfg/config)
:profile profile
:subject subject
:content content})
nil)))

View File

@@ -129,7 +129,7 @@
;; Schedule object deletion
(tasks/submit! conn {:name "delete-object"
:delay cfg/default-deletion-delay
:delay cfg/deletion-delay
:props {:id id :type :file}})
(mark-file-deleted conn params)))

View File

@@ -38,7 +38,7 @@
;; --- Create File Media object (upload)
(declare create-file-media-object)
(declare select-file-for-update)
(declare select-file)
(s/def ::content ::media/upload)
(s/def ::is-local ::us/boolean)
@@ -50,7 +50,7 @@
(sv/defmethod ::upload-file-media-object
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(let [file (select-file-for-update conn file-id)]
(let [file (select-file conn file-id)]
(teams/check-edition-permissions! conn profile-id (:team-id file))
(-> (assoc cfg :conn conn)
(create-file-media-object params)))))
@@ -66,9 +66,18 @@
[info]
(= (:mtype info) "image/svg+xml"))
(defn- fetch-url
[url]
(try
(http/get! url {:as :byte-array})
(catch Exception e
(ex/raise :type :validation
:code :unable-to-access-to-url
:cause e))))
(defn- download-media
[{:keys [storage] :as cfg} url]
(let [result (http/get! url {:as :byte-array})
(let [result (fetch-url url)
data (:body result)
mtype (get (:headers result) "content-type")
format (cm/mtype->format mtype)]
@@ -129,7 +138,7 @@
(sv/defmethod ::create-file-media-object-from-url
[{:keys [pool storage] :as cfg} {:keys [profile-id file-id url name] :as params}]
(db/with-atomic [conn pool]
(let [file (select-file-for-update conn file-id)]
(let [file (select-file conn file-id)]
(teams/check-edition-permissions! conn profile-id (:team-id file))
(let [mobj (download-media cfg url)
content {:filename "tempfile"
@@ -152,7 +161,7 @@
(sv/defmethod ::clone-file-media-object
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(let [file (select-file-for-update conn file-id)]
(let [file (select-file conn file-id)]
(teams/check-edition-permissions! conn profile-id (:team-id file))
(-> (assoc cfg :conn conn)
@@ -175,17 +184,17 @@
;; --- HELPERS
(def ^:private sql:select-file-for-update
(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 = ?
for update of file")
where file.id = ?")
(defn- select-file-for-update
(defn- select-file
[conn id]
(let [row (db/exec-one! conn [sql:select-file-for-update id])]
(let [row (db/exec-one! conn [sql:select-file id])]
(when-not row
(ex/raise :type :not-found))
row))

View File

@@ -78,8 +78,10 @@
;; profile data.
(let [claims (tokens :verify {:token token :iss :team-invitation})
claims (assoc claims :member-id (:id profile))
params (assoc params :profile-id (:id profile))]
(process-token conn params claims)
params (assoc params :profile-id (:id profile))
cfg (assoc cfg :conn conn)]
(process-token cfg params claims)
;; Automatically mark the created profile as active because
;; we already have the verification of email with the
@@ -168,15 +170,24 @@
active? (if demo? true is-active)
props (db/tjson (or props {}))
password (derive-password password)]
(-> (db/insert! conn :profile
{:id id
:fullname fullname
:email (str/lower email)
:password password
:props props
:is-active active?
:is-demo demo?})
(update :props db/decode-transit-pgobject))))
(try
(-> (db/insert! conn :profile
{:id id
:fullname fullname
:email (str/lower email)
:password password
:props props
:is-active active?
:is-demo demo?})
(update :props db/decode-transit-pgobject))
(catch org.postgresql.util.PSQLException e
(let [state (.getSQLState e)]
(if (not= state "23505")
(throw e)
(ex/raise :type :validation
:code :email-already-exists
:cause e)))))))
(defn- create-profile-relations
[conn profile]
@@ -389,14 +400,18 @@
(emails/send! conn emails/password-recovery
{:to (:email profile)
:token (:token profile)
:name (:fullname profile)}))]
:name (:fullname profile)})
nil)]
(db/with-atomic [conn pool]
(some->> email
(profile/retrieve-profile-data-by-email conn)
(create-recovery-token)
(send-email-notification conn))
nil)))
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
(when-not (:is-active profile)
(ex/raise :type :validation
:code :profile-not-verified
:hint "the user need to validate profile before recover password"))
(->> profile
(create-recovery-token)
(send-email-notification conn))))))
;; --- Mutation: Recover Profile
@@ -457,7 +472,7 @@
;; Schedule a complete deletion of profile
(tasks/submit! conn {:name "delete-profile"
:delay (dt/duration {:hours 48})
:delay cfg/deletion-delay
:props {:profile-id profile-id}})
(db/update! conn :profile

View File

@@ -16,6 +16,7 @@
[app.rpc.queries.projects :as proj]
[app.tasks :as tasks]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
;; --- Helpers & Specs
@@ -113,8 +114,6 @@
;; --- Mutation: Delete Project
(declare mark-project-deleted)
(s/def ::delete-project
(s/keys :req-un [::id ::profile-id]))
@@ -125,18 +124,10 @@
;; Schedule object deletion
(tasks/submit! conn {:name "delete-object"
:delay cfg/default-deletion-delay
:delay cfg/deletion-delay
:props {:id id :type :project}})
(mark-project-deleted conn params)))
(def ^:private sql:mark-project-deleted
"update project
set deleted_at = clock_timestamp()
where id = ?
returning id")
(defn mark-project-deleted
[conn {:keys [id] :as params}]
(db/exec! conn [sql:mark-project-deleted id])
nil)
(db/update! conn :project
{:deleted-at (dt/now)}
{:id id})
nil))

View File

@@ -13,6 +13,7 @@
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.emails :as emails]
[app.media :as media]
@@ -20,6 +21,7 @@
[app.rpc.queries.profile :as profile]
[app.rpc.queries.teams :as teams]
[app.storage :as sto]
[app.tasks :as tasks]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
@@ -133,7 +135,14 @@
(ex/raise :type :validation
:code :only-owner-can-delete-team))
(db/delete! conn :team {:id id})
;; Schedule object deletion
(tasks/submit! conn {:name "delete-object"
:delay cfg/deletion-delay
:props {:id id :type :team}})
(db/update! conn :team
{:deleted-at (dt/now)}
{:id id})
nil)))

View File

@@ -7,8 +7,6 @@
;;
;; Copyright (c) 2020 UXBOX Labs SL
;; TODO: session
(ns app.rpc.mutations.verify-token
(:require
[app.common.exceptions :as ex]
@@ -44,20 +42,29 @@
claims)
(defmethod process-token :verify-email
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (db/get-by-id conn :profile profile-id {:for-update true})]
(when (:is-active profile)
(ex/raise :type :validation
:code :email-already-validated))
(when (not= (:email profile)
(:email claims))
(ex/raise :type :validation
:code :invalid-token))
[{:keys [conn session] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)
claims (assoc claims :profile profile)]
(when-not (:is-active profile)
(when (not= (:email profile)
(:email claims))
(ex/raise :type :validation
:code :invalid-token))
(db/update! conn :profile
{:is-active true}
{:id (:id profile)}))
(with-meta claims
{:transform-response
(fn [request response]
(let [uagent (get-in request [:headers "user-agent"])
id (session/create! session {:profile-id profile-id
:user-agent uagent})]
(assoc response
:cookies (session/cookies session {:value id}))))})))
(db/update! conn :profile
{:is-active true}
{:id (:id profile)})
claims))
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]

View File

@@ -4,9 +4,11 @@
(:require
[app.common.pages :as cp]
[app.common.pages.migrations :as pmg]
[app.config :as cfg]
[app.db :as db]
[app.db.profile-initial-data :as pid]
[app.main :refer [system]]
[app.rpc.queries.profile :as prof]
[app.srepl.dev :as dev]
[app.util.blob :as blob]
[clojure.pprint :refer [pprint]]))
@@ -36,7 +38,7 @@
{:id id})))
(defn get-file
[id]
[system id]
(with-open [conn (db/open (:app.db/pool system))]
(let [file (db/get-by-id conn :file id)]
(-> file
@@ -58,3 +60,29 @@
([system project-id path]
(db/with-atomic [conn (:app.db/pool system)]
(pid/create-initial-data-dump conn project-id path))))
(defn load-data-into-user
([system user-email]
(if-let [file (:initial-data-file cfg/config)]
(load-data-into-user system file user-email)
(prn "Data file not found in configuration")))
([system file user-email]
(db/with-atomic [conn (:app.db/pool system)]
(let [profile (prof/retrieve-profile-data-by-email conn user-email)
profile (merge profile (prof/retrieve-additional-data conn (:id profile)))]
(pid/create-profile-initial-data conn file profile)))))
;; Migrate
(defn update-file-data-blob-format
[system]
(db/with-atomic [conn (:app.db/pool system)]
(doseq [id (->> (db/exec! conn ["select id from file;"]) (map :id))]
(let [{:keys [data]} (db/get-by-id conn :file id {:columns [:id :data]})]
(prn "Updating file:" id)
(db/update! conn :file
{:data (-> (blob/decode data)
(blob/encode {:version 2}))}
{:id id})))))

View File

@@ -121,11 +121,16 @@
(defn parse
[data]
(with-open [istream (IOUtils/toInputStream data "UTF-8")]
(xml/parse istream)))
(try
(with-open [istream (IOUtils/toInputStream data "UTF-8")]
(xml/parse istream))
(catch org.xml.sax.SAXParseException _e
(ex/raise :type :validation
:code :invalid-svg-file))))
(defn process-request
[{:keys [svgc] :as cfg} body]
(let [data (slurp body)
data (svgc data)]
(parse data)))

View File

@@ -42,11 +42,12 @@
(db/with-atomic [conn pool]
(handle-deletion conn props)))
(defmulti handle-deletion (fn [_ props] (:type props)))
(defmulti handle-deletion
(fn [_ props] (:type props)))
(defmethod handle-deletion :default
[_conn {:keys [type]}]
(log/warn "no handler found for" type))
(log/warnf "no handler found for %s" type))
(defmethod handle-deletion :file
[conn {:keys [id] :as props}]
@@ -57,3 +58,8 @@
[conn {:keys [id] :as props}]
(let [sql "delete from project where id=? and deleted_at is not null"]
(db/exec-one! conn [sql id])))
(defmethod handle-deletion :team
[conn {:keys [id] :as props}]
(let [sql "delete from team where id=? and deleted_at is not null"]
(db/exec-one! conn [sql id])))

View File

@@ -63,6 +63,7 @@
:uri (:uri cfg)
:headers {"content-type" "application/json"}
:body (json/encode-str data)})]
(when (not= 200 (:status response))
(ex/raise :type :internal
:code :invalid-response-from-google
@@ -129,7 +130,7 @@
[{:keys [conn version]}]
(merge
{:version version
:with-taiga (:telemetry-with-taiga cfg/config)
:with-taiga (:telemetry-with-taiga cfg/config false)
:total-teams (retrieve-num-teams conn)
:total-projects (retrieve-num-projects conn)
:total-files (retrieve-num-files conn)}

View File

@@ -12,6 +12,7 @@
[app.common.spec :as us]
[app.db :as db]
[app.http.middleware :refer [wrap-parse-request-body]]
[clojure.pprint :refer [pprint]]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]
@@ -87,7 +88,12 @@
(catch Exception e
;; We don't want notify user of a error, just log it for posible
;; future investigation.
(log/warnf e "Unexpected error on telemetry.")))
(log/warn e (str "Unexpected error on telemetry:\n"
(when-let [edata (ex-data e)]
(str "ex-data: \n"
(with-out-str (pprint edata))))
(str "params: \n"
(with-out-str (pprint params)))))))
{:status 200
:body "OK\n"})

View File

@@ -10,61 +10,93 @@
(ns app.util.blob
"A generic blob storage encoding. Mainly used for
page data, page options and txlog payload storage."
(:require [app.util.transit :as t])
(:require
[app.config :as cfg]
[app.util.transit :as t]
[taoensso.nippy :as n])
(:import
java.io.ByteArrayInputStream
java.io.ByteArrayOutputStream
java.io.DataInputStream
java.io.DataOutputStream
com.github.luben.zstd.Zstd
net.jpountz.lz4.LZ4Factory
net.jpountz.lz4.LZ4FastDecompressor
net.jpountz.lz4.LZ4Compressor))
(defprotocol IDataToBytes
(->bytes [data] "convert data to bytes"))
(extend-protocol IDataToBytes
(Class/forName "[B")
(->bytes [data] data)
String
(->bytes [data] (.getBytes ^String data "UTF-8")))
(def lz4-factory (LZ4Factory/fastestInstance))
(defn encode
[data]
(let [data (t/encode data {:type :json})
data-len (alength ^bytes data)
cp (.fastCompressor ^LZ4Factory lz4-factory)
max-len (.maxCompressedLength cp data-len)
cdata (byte-array max-len)
clen (.compress ^LZ4Compressor cp ^bytes data 0 data-len cdata 0 max-len)]
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
^DataOutputStream dos (DataOutputStream. baos)]
(.writeShort dos (short 1)) ;; version number
(.writeInt dos (int data-len))
(.write dos ^bytes cdata (int 0) clen)
(.toByteArray baos))))
(declare decode-v1)
(declare decode-v2)
(declare encode-v1)
(declare encode-v2)
(def default-version
(:default-blob-version cfg/config 1))
(defn encode
([data] (encode data nil))
([data {:keys [version] :or {version default-version}}]
(case version
1 (encode-v1 data)
2 (encode-v2 data)
(throw (ex-info "unsupported version" {:version version})))))
(defn decode
"A function used for decode persisted blobs in the database."
[^bytes data]
(with-open [bais (ByteArrayInputStream. data)
dis (DataInputStream. bais)]
(let [version (.readShort dis)
ulen (.readInt dis)]
(case version
1 (decode-v1 data ulen)
2 (decode-v2 data ulen)
(throw (ex-info "unsupported version" {:version version}))))))
;; --- IMPL
(defn- encode-v1
[data]
(let [data (->bytes data)]
(with-open [bais (ByteArrayInputStream. data)
dis (DataInputStream. bais)]
(let [version (.readShort dis)
udata-len (.readInt dis)]
(case version
1 (decode-v1 data udata-len)
(throw (ex-info "unsupported version" {:version version})))))))
(let [data (t/encode data {:type :json})
dlen (alength ^bytes data)
cp (.fastCompressor ^LZ4Factory lz4-factory)
mlen (.maxCompressedLength cp dlen)
cdata (byte-array mlen)
clen (.compress ^LZ4Compressor cp ^bytes data 0 dlen cdata 0 mlen)]
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
^DataOutputStream dos (DataOutputStream. baos)]
(.writeShort dos (short 1)) ;; version number
(.writeInt dos (int dlen))
(.write dos ^bytes cdata (int 0) clen)
(.toByteArray baos))))
(defn- decode-v1
[^bytes cdata ^long udata-len]
(let [^LZ4FastDecompressor dcp (.fastDecompressor ^LZ4Factory lz4-factory)
^bytes udata (byte-array udata-len)]
(.decompress dcp cdata 6 udata 0 udata-len)
[^bytes cdata ^long ulen]
(let [dcp (.fastDecompressor ^LZ4Factory lz4-factory)
udata (byte-array ulen)]
(.decompress ^LZ4FastDecompressor dcp cdata 6 ^bytes udata 0 ulen)
(t/decode udata {:type :json})))
(defn- encode-v2
[data]
(let [data (n/fast-freeze data)
dlen (alength data)
mlen (Zstd/compressBound dlen)
cdata (byte-array mlen)
clen (Zstd/compressByteArray ^bytes cdata 0 mlen
^bytes data 0 dlen
8)]
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
^DataOutputStream dos (DataOutputStream. baos)]
(.writeShort dos (short 2)) ;; version number
(.writeInt dos (int dlen))
(.write dos ^bytes cdata (int 0) clen)
(.toByteArray baos))))
(defn- decode-v2
[^bytes cdata ^long ulen]
(let [udata (byte-array ulen)]
(Zstd/decompressByteArray ^bytes udata 0 ulen
^bytes cdata 6 (- (alength cdata) 6))
(n/fast-thaw udata)))

View File

@@ -9,6 +9,7 @@
(ns app.util.emails
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.util.template :as tmpl]
@@ -29,24 +30,11 @@
;; Email Building
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn build-address
[v charset]
(try
(cond
(string? v)
(InternetAddress. v nil charset)
(defn- parse-address
[v]
(InternetAddress/parse ^String v))
(map? v)
(InternetAddress. (:addr v)
(:name v)
(:charset v charset))
:else
(throw (ex-info "Invalid address" {:data v})))
(catch Exception e
(throw (ex-info "Invalid address" {:data v} e)))))
(defn- resolve-recipient-type
(defn- ^Message$RecipientType resolve-recipient-type
[type]
(case type
:to Message$RecipientType/TO
@@ -54,33 +42,33 @@
:bcc Message$RecipientType/BCC))
(defn- assign-recipient
[^MimeMessage mmsg type address charset]
[^MimeMessage mmsg type address]
(if (sequential? address)
(reduce #(assign-recipient %1 type %2 charset) mmsg address)
(let [address (build-address address charset)
(reduce #(assign-recipient %1 type %2) mmsg address)
(let [address (parse-address address)
type (resolve-recipient-type type)]
(.addRecipient mmsg type address)
(.addRecipients mmsg type address)
mmsg)))
(defn- assign-recipients
[mmsg {:keys [to cc bcc charset] :or {charset "utf-8"} :as params}]
[mmsg {:keys [to cc bcc] :as params}]
(cond-> mmsg
(some? to) (assign-recipient :to to charset)
(some? cc) (assign-recipient :cc cc charset)
(some? bcc) (assign-recipient :bcc bcc charset)))
(some? to) (assign-recipient :to to)
(some? cc) (assign-recipient :cc cc)
(some? bcc) (assign-recipient :bcc bcc)))
(defn- assign-from
[mmsg {:keys [from charset] :or {charset "utf-8"}}]
(when from
(let [from (build-address from charset)]
(.setFrom ^MimeMessage mmsg ^InternetAddress from))))
[mmsg {:keys [default-from]} {:keys [from] :as props}]
(let [from (or from default-from)]
(when from
(let [from (parse-address from)]
(.addFrom ^MimeMessage mmsg from)))))
(defn- assign-reply-to
[mmsg {:keys [defaut-reply-to]} {:keys [reply-to charset] :or {charset "utf-8"}}]
(let [reply-to (or reply-to defaut-reply-to)]
[mmsg {:keys [default-reply-to] :as cfg} {:keys [reply-to] :as params}]
(let [reply-to (or reply-to default-reply-to)]
(when reply-to
(let [reply-to (build-address reply-to charset)
reply-to (into-array InternetAddress [reply-to])]
(let [reply-to (parse-address reply-to)]
(.setReplyTo ^MimeMessage mmsg reply-to)))))
(defn- assign-subject
@@ -136,7 +124,7 @@
[cfg session params]
(let [mmsg (MimeMessage. ^Session session)]
(assign-recipients mmsg params)
(assign-from mmsg params)
(assign-from mmsg cfg params)
(assign-reply-to mmsg cfg params)
(assign-subject mmsg params)
(assign-extra-headers mmsg params)
@@ -156,12 +144,12 @@
(Properties.)
{"mail.user" username
"mail.host" host
"mail.from" default-from
"mail.smtp.auth" (boolean username)
"mail.smtp.starttls.enable" tls
"mail.smtp.starttls.required" tls
"mail.smtp.host" host
"mail.smtp.port" port
"mail.smtp.from" default-from
"mail.smtp.user" username
"mail.smtp.timeout" timeout
"mail.smtp.connectiontimeout" timeout}))
@@ -183,7 +171,9 @@
(defn send!
[cfg message]
(let [^MimeMessage message (smtp-message cfg message)]
(Transport/send message (:username cfg) (:password cfg))
(Transport/send message
(:username cfg)
(:password cfg))
nil))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -207,15 +197,17 @@
text (render-email-template-part :txt id context)
html (render-email-template-part :html id context)]
(when (or (not subj)
(not text)
(not html))
(and (not text)
(not html)))
(ex/raise :type :internal
:code :missing-email-templates))
{:subject subj
:body [{:type "text/plain"
:content text}
{:type "text/html"
:content html}]}))
:body (d/concat
[{:type "text/plain"
:content text}]
(when html
[{:type "text/html"
:content html}]))}))
(s/def ::priority #{:high :low})
(s/def ::to (s/or :sigle ::us/email

View File

@@ -0,0 +1,27 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2021 UXBOX Labs SL
(ns app.util.log4j
(:require
[clojure.pprint :refer [pprint]])
(:import
org.apache.logging.log4j.ThreadContext))
(defn update-thread-context!
[data]
(run! (fn [[key val]]
(ThreadContext/put
(name key)
(cond
(coll? val)
(binding [clojure.pprint/*print-right-margin* 120]
(with-out-str (pprint val)))
(instance? clojure.lang.Named val) (name val)
:else (str val))))
data))

View File

@@ -12,11 +12,12 @@
(:require
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.util.async :as aa]
[app.util.log4j :refer [update-thread-context!]]
[app.util.time :as dt]
[clojure.core.async :as a]
[clojure.pprint :refer [pprint]]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]
@@ -205,6 +206,17 @@
(log/warn "no task handler found for" (pr-str name)))
{:status :completed :task item}))
(defn get-error-context
[error item]
(let [edata (ex-data error)]
{:id (uuid/next)
:version (:full cfg/version)
:host (:public-uri cfg/config)
:class (.getCanonicalName ^java.lang.Class (class error))
:hint (ex-message error)
:data edata
:params item}))
(defn- handle-exception
[error item]
(let [edata (ex-data error)]
@@ -218,14 +230,9 @@
(= ::noop (:strategy edata))
(assoc :inc-by 0))
(do
(log/errorf error
(str "Unhandled exception.\n"
"=> task: " (:name item) "\n"
"=> retry: " (:retry-num item) "\n"
"=> props: \n"
(with-out-str
(pprint (:props item)))))
(let [cdata (get-error-context error item)]
(update-thread-context! cdata)
(log/errorf error "Unhandled exception on task (id: %s)" (:id cdata))
(if (>= (:retry-num item) (:max-retries item))
{:status :failed :task item :error error}
{:status :retry :task item :error error})))))

View File

@@ -12,6 +12,7 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :refer [close?]]
[app.common.pages :refer [make-minimal-shape]]
[clojure.test :as t]))
@@ -32,7 +33,9 @@
:points points)))
(defn add-rect-data [shape]
(let [selrect (gsh/rect->selrect shape)
(let [shape (-> shape
(assoc :width 20 :height 20))
selrect (gsh/rect->selrect shape)
points (gsh/rect->points selrect)]
(assoc shape
:selrect selrect
@@ -64,17 +67,17 @@
shape-after (gsh/transform-shape shape-before)]
(t/is (not= shape-before shape-after))
(t/is (== (get-in shape-before [:selrect :x])
(- 10 (get-in shape-after [:selrect :x]))))
(t/is (close? (get-in shape-before [:selrect :x])
(- 10 (get-in shape-after [:selrect :x]))))
(t/is (== (get-in shape-before [:selrect :y])
(+ 10 (get-in shape-after [:selrect :y]))))
(t/is (close? (get-in shape-before [:selrect :y])
(+ 10 (get-in shape-after [:selrect :y]))))
(t/is (== (get-in shape-before [:selrect :width])
(get-in shape-after [:selrect :width])))
(t/is (close? (get-in shape-before [:selrect :width])
(get-in shape-after [:selrect :width])))
(t/is (== (get-in shape-before [:selrect :height])
(get-in shape-after [:selrect :height])))))
(t/is (close? (get-in shape-before [:selrect :height])
(get-in shape-after [:selrect :height])))))
:rect :path))
@@ -84,8 +87,8 @@
shape-before (create-test-shape type {:modifiers modifiers})
shape-after (gsh/transform-shape shape-before)]
(t/are [prop]
(t/is (== (get-in shape-before [:selrect prop])
(get-in shape-after [:selrect prop])))
(t/is (close? (get-in shape-before [:selrect prop])
(get-in shape-after [:selrect prop])))
:x :y :width :height :x1 :y1 :x2 :y2))
:rect :path))
@@ -98,17 +101,17 @@
shape-after (gsh/transform-shape shape-before)]
(t/is (not= shape-before shape-after))
(t/is (== (get-in shape-before [:selrect :x])
(get-in shape-after [:selrect :x])))
(t/is (close? (get-in shape-before [:selrect :x])
(get-in shape-after [:selrect :x])))
(t/is (== (get-in shape-before [:selrect :y])
(get-in shape-after [:selrect :y])))
(t/is (close? (get-in shape-before [:selrect :y])
(get-in shape-after [:selrect :y])))
(t/is (== (* 2 (get-in shape-before [:selrect :width]))
(get-in shape-after [:selrect :width])))
(t/is (close? (* 2 (get-in shape-before [:selrect :width]))
(get-in shape-after [:selrect :width])))
(t/is (== (* 2 (get-in shape-before [:selrect :height]))
(get-in shape-after [:selrect :height]))))
(t/is (close? (* 2 (get-in shape-before [:selrect :height]))
(get-in shape-after [:selrect :height]))))
:rect :path))
(t/testing "Transform with empty resize"
@@ -119,8 +122,8 @@
shape-before (create-test-shape type {:modifiers modifiers})
shape-after (gsh/transform-shape shape-before)]
(t/are [prop]
(t/is (== (get-in shape-before [:selrect prop])
(get-in shape-after [:selrect prop])))
(t/is (close? (get-in shape-before [:selrect prop])
(get-in shape-after [:selrect prop])))
:x :y :width :height :x1 :y1 :x2 :y2))
:rect :path))
@@ -145,13 +148,23 @@
(let [modifiers {:rotation 30}
shape-before (create-test-shape type {:modifiers modifiers})
shape-after (gsh/transform-shape shape-before)]
(t/is (not= shape-before shape-after))
(t/is (not (== (get-in shape-before [:selrect :x])
(get-in shape-after [:selrect :x]))))
;; Selrect won't change with a rotation, but points will
(t/is (close? (get-in shape-before [:selrect :x])
(get-in shape-after [:selrect :x])))
(t/is (not (== (get-in shape-before [:selrect :y])
(get-in shape-after [:selrect :y])))))
(t/is (close? (get-in shape-before [:selrect :y])
(get-in shape-after [:selrect :y])))
(t/is (= (count (:points shape-before)) (count (:points shape-after))))
(for [idx (range 0 (count (:point shape-before)))]
(do (t/is (not (close? (get-in shape-before [:points idx :x])
(get-in shape-after [:points idx :x]))))
(t/is (not (close? (get-in shape-before [:points idx :y])
(get-in shape-after [:points idx :y])))))))
:rect :path))
(t/testing "Transform shape with rotation = 0 should leave equal selrect"
@@ -160,8 +173,8 @@
shape-before (create-test-shape type {:modifiers modifiers})
shape-after (gsh/transform-shape shape-before)]
(t/are [prop]
(t/is (== (get-in shape-before [:selrect prop])
(get-in shape-after [:selrect prop])))
(t/is (close? (get-in shape-before [:selrect prop])
(get-in shape-after [:selrect prop])))
:x :y :width :height :x1 :y1 :x2 :y2))
:rect :path))

View File

@@ -360,7 +360,7 @@
(t/is (= [rect-a-id rect-e-id rect-d-id]
(get-in objects [group-b-id :shapes]))))))
(t/testing "Move elements and delete the empty group"
(t/testing "Move all elements from a group"
(let [changes [{:type :mov-objects
:page-id page-id
:parent-id group-a-id
@@ -368,9 +368,9 @@
res (cp/process-changes data changes)]
(let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= [group-a-id rect-e-id]
(t/is (= [group-a-id group-b-id rect-e-id]
(get-in objects [frame-a-id :shapes])))
(t/is (nil? (get-in objects [group-b-id]))))))
(t/is (empty? (get-in objects [group-b-id :shapes]))))))
(t/testing "Move elements to a group with different frame"
(let [changes [{:type :mov-objects
@@ -727,11 +727,11 @@
;; After
(t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id]
(t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id group-1-id]
(get-in res [:pages-index page-id :objects cp/root :shapes])))
(t/is (= nil
(get-in res [:pages-index page-id :objects group-1-id])))
(t/is (not= nil
(get-in res [:pages-index page-id :objects group-1-id])))
))

View File

@@ -60,6 +60,7 @@
(let [new-val (get curr attr ::undefined)
value (cond
(= new-val ::undefined) value
(= new-val :multiple) :multiple
(= value ::undefined) (sel new-val)
(eqfn new-val value) value
:else :multiple)]

View File

@@ -328,6 +328,11 @@
nil
(apply f args))))
(defn nilv
"Returns a default value if the given value is nil"
[v default]
(if (some? v) v default))
(defn check-num
"Function that checks if a number is nil or nan. Will return 0 when not
valid and the number otherwise."

View File

@@ -134,3 +134,9 @@
(th-eq m1f m2f))))
(defmethod pp/simple-dispatch Matrix [obj] (pr obj))
(defn transform-in [pt mtx]
(-> (matrix)
(translate pt)
(multiply mtx)
(translate (gpt/negate pt))))

View File

@@ -11,42 +11,36 @@
;; --- Proportions
(declare assign-proportions-path)
(declare assign-proportions-rect)
(defn assign-proportions
[{:keys [type] :as shape}]
(case type
:path (assign-proportions-path shape)
(assign-proportions-rect shape)))
(defn- assign-proportions-rect
[{:keys [width height] :as shape}]
(assoc shape :proportion (/ width height)))
[shape]
(let [{:keys [width height]} (:selrect shape)]
(assoc shape :proportion (/ width height))))
;; --- Setup Proportions
(declare setup-proportions-const)
(declare setup-proportions-image)
(defn setup-proportions
[shape]
(case (:type shape)
:icon (setup-proportions-image shape)
:image (setup-proportions-image shape)
:text shape
(setup-proportions-const shape)))
(defn setup-proportions-image
[{:keys [metadata] :as shape}]
(let [{:keys [width height]} metadata]
(assoc shape
:proportion (/ width height)
:proportion-lock false)))
:proportion-lock true)))
(defn setup-proportions-svg
[{:keys [width height] :as shape}]
(assoc shape
:proportion (/ width height)
:proportion-lock true))
(defn setup-proportions-const
[shape]
(assoc shape
:proportion 1
:proportion-lock false))
(defn setup-proportions
[shape]
(case (:type shape)
:svg-raw (setup-proportions-svg shape)
:image (setup-proportions-image shape)
:text shape
(setup-proportions-const shape)))

View File

@@ -43,10 +43,13 @@
(let [shape-center (or (gco/center-shape shape)
(gpt/point 0 0))]
(inverse-transform-matrix shape shape-center)))
([shape center]
([{:keys [flip-x flip-y] :as shape} center]
(let []
(-> (gmt/matrix)
(gmt/translate center)
(cond->
flip-x (gmt/scale (gpt/point -1 1))
flip-y (gmt/scale (gpt/point 1 -1)))
(gmt/multiply (:transform-inverse shape (gmt/matrix)))
(gmt/translate (gpt/negate center))))))
@@ -203,29 +206,7 @@
(gmt/rotate (- rotation-angle)))]
[stretch-matrix stretch-matrix-inverse]))
(defn apply-transform-path
[shape transform]
(let [content (gpa/transform-content (:content shape) transform)
;; Calculate the new selrect by "unrotate" the shape
rotation (modif-rotation shape)
center (gpt/transform (gco/center-shape shape) transform)
content-rotated (gpa/transform-content content (gmt/rotate-matrix (- rotation) center))
selrect (gpa/content->selrect content-rotated)
;; Transform the points
points (-> (:points shape)
(transform-points transform))]
(assoc shape
:content content
:points points
:selrect selrect
:transform (gmt/rotate-matrix rotation)
:transform-inverse (gmt/rotate-matrix (- rotation))
:rotation rotation)))
(defn apply-transform-rect
(defn apply-transform
"Given a new set of points transformed, set up the rectangle so it keeps
its properties. We adjust de x,y,width,height and create a custom transform"
[shape transform]
@@ -246,13 +227,21 @@
(:height points-temp-dim))
rect-points (gpr/rect->points rect-shape)
[matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape))]
[matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape))
shape (cond
(= :path (:type shape))
(-> shape
(update :content #(gpa/transform-content % transform)))
:else
(-> shape
(merge rect-shape)
(update :x #(mth/precision % 0))
(update :y #(mth/precision % 0))
(update :width #(mth/precision % 0))
(update :height #(mth/precision % 0))))]
(as-> shape $
(merge $ rect-shape)
(update $ :x #(mth/precision % 0))
(update $ :y #(mth/precision % 0))
(update $ :width #(mth/precision % 0))
(update $ :height #(mth/precision % 0))
(update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix))
(update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix))))
(assoc $ :points (into [] points))
@@ -260,37 +249,6 @@
(update $ :rotation #(mod (+ (or % 0)
(or (get-in $ [:modifiers :rotation]) 0)) 360)))))
(defn apply-transform [shape transform]
(let [apply-transform-fn
(case (:type shape)
:path apply-transform-path
apply-transform-rect)]
(apply-transform-fn shape transform)))
(defn transform-gradients [shape modifiers]
(let [angle (d/check-num (get modifiers :rotation))
;; Gradients are represented with unit vectors so its center is 0.5, 0.5
center (gpt/point 0.5 0.5)
transform (gmt/rotate-matrix angle center)
transform-gradient
(fn [{:keys [start-x start-y end-x end-y] :as gradient}]
(let [start-point (gpt/point start-x start-y)
end-point (gpt/point end-x end-y)
{start-x :x start-y :y} (gpt/transform start-point transform)
{end-x :x end-y :y} (gpt/transform end-point transform)]
(assoc gradient
:start-x start-x
:start-y start-y
:end-x end-x
:end-y end-y)))]
(cond-> shape
(:fill-color-gradient shape)
(update :fill-color-gradient transform-gradient)
(:stroke-color-gradient shape)
(update :stroke-color-gradient transform-gradient))))
(defn set-flip [shape modifiers]
(let [rx (get-in modifiers [:resize-vector :x])
ry (get-in modifiers [:resize-vector :y])]
@@ -305,12 +263,13 @@
(-> shape
(set-flip (:modifiers shape))
(apply-transform transform)
(transform-gradients (:modifiers shape))
(dissoc :modifiers)))
shape)))
(defn update-group-selrect [group children]
(let [shape-center (gco/center-shape group)
transform (:transform group (gmt/matrix))
transform-inverse (:transform-inverse group (gmt/matrix))
;; Points for every shape inside the group
points (->> children (mapcat :points))
@@ -330,5 +289,10 @@
(-> group
(assoc :selrect new-selrect)
(assoc :points new-points)
(apply-transform-rect (gmt/matrix)))))
;; We're regenerating the selrect from its children so we
;; need to remove the flip flags
(assoc :flip-x false)
(assoc :flip-y false)
(apply-transform (gmt/matrix)))))

View File

@@ -142,3 +142,10 @@
(defn almost-zero? [num]
(< (abs num) 1e-8))
(defonce float-equal-precision 0.001)
(defn close?
"Equality for float numbers. Check if the difference is within a range"
[num1 num2]
(<= (abs (- num1 num2)) float-equal-precision))

View File

@@ -61,6 +61,7 @@
(d/export helpers/set-touched-group)
(d/export helpers/touched-group?)
(d/export helpers/get-base-shape)
(d/export helpers/is-parent?)
;; Process changes
(d/export changes/process-changes)

View File

@@ -36,8 +36,20 @@
(when verify?
(us/verify ::spec/changes items))
(->> items
(reduce #(or (process-change %1 %2) %1) data))))
(let [pages (into #{} (map :page-id) items)
result (->> items
(reduce #(or (process-change %1 %2) %1) data))]
;; Validate result shapes (only on the backend)
#?(:clj
(doseq [page-id pages]
(let [page (get-in result [:pages-index page-id])]
(doseq [[id shape] (:objects page)]
(if-not (= shape (get-in data [:pages-index page-id :objects id]))
;; If object has change verify is correct
(us/verify ::spec/shape shape))))))
result)))
(defmethod process-change :set-option
[data {:keys [page-id option value]}]
@@ -94,7 +106,6 @@
(let [update-fn (fn [objects]
(if-let [obj (get objects id)]
(let [result (reduce process-operation obj operations)]
#?(:clj (us/verify ::spec/shape result))
(assoc objects id result))
objects))]
(if page-id
@@ -142,16 +153,25 @@
(map :id)
(distinct))
shapes)))
(set-mask-selrect [group children]
(let [mask (first children)]
(-> group
(merge (select-keys mask [:selrect :points]))
(assoc :x (-> mask :selrect :x)
:y (-> mask :selrect :y)
:width (-> mask :selrect :width)
:height (-> mask :selrect :height)))))
(update-group [group objects]
(let [children (->> group :shapes (map #(get objects %)))]
(if (:masked-group? group)
(let [mask (first children)]
(-> group
(merge (select-keys mask [:selrect :points]))
(assoc :x (-> mask :selrect :x)
:y (-> mask :selrect :y)
:width (-> mask :selrect :width)
:height (-> mask :selrect :height))))
(cond
;; If the group is empty we don't make any changes. Should be removed by a later process
(empty? children)
group
(:masked-group? group)
(set-mask-selrect group children)
:else
(gsh/update-group-selrect group children))))]
(if page-id
@@ -206,23 +226,17 @@
pid prev-parent-id
objects objects]
(let [obj (get objects pid)]
(if (and (= 1 (count (:shapes obj)))
(= sid (first (:shapes obj)))
(= :group (:type obj)))
(recur pid
(:parent-id obj)
(dissoc objects pid))
(cond-> objects
true
(update-in [pid :shapes] strip-id sid)
(cond-> objects
true
(update-in [pid :shapes] strip-id sid)
(and (:shape-ref obj)
(= (:type obj) :group)
(not ignore-touched))
(->
(update-in [pid :touched]
cph/set-touched-group :shapes-group)
(d/dissoc-in [pid :remote-synced?])))))))))
(and (:shape-ref obj)
(= (:type obj) :group)
(not ignore-touched))
(->
(update-in [pid :touched]
cph/set-touched-group :shapes-group)
(d/dissoc-in [pid :remote-synced?]))))))))
(update-parent-id [objects id]
(assoc-in objects [id :parent-id] parent-id))

View File

@@ -224,7 +224,9 @@
(defn select-toplevel-shapes
([objects] (select-toplevel-shapes objects nil))
([objects {:keys [include-frames?] :or {include-frames? false}}]
([objects {:keys [include-frames? include-frame-children?]
:or {include-frames? false
include-frame-children? true}}]
(let [lookup #(get objects %)
root (lookup uuid/zero)
root-children (:shapes root)
@@ -241,7 +243,7 @@
(or (not= :frame typ) include-frames?)
(d/concat [obj])
(= :frame typ)
(and (= :frame typ) include-frame-children?)
(d/concat (map lookup children))))))]
(reduce lookup-shapes [] root-children))))
@@ -376,3 +378,25 @@
;; The first id will be the top-most
(get objects (first sorted-ids))))
(defn is-parent?
"Check if `parent-candidate` is parent of `shape-id`"
[objects shape-id parent-candidate]
(loop [current (get objects parent-candidate)
done #{}
pending (:shapes current)]
(cond
(contains? done (:id current))
(recur (get objects (first pending))
done
(rest pending))
(empty? pending) false
(and current (contains? (set (:shapes current)) shape-id)) true
:else
(recur (get objects (first pending))
(conj done (:id current))
(concat (rest pending) (:shapes current))))))

View File

@@ -16,6 +16,6 @@ RUN set -ex; \
apt-get -qqy install adoptopenjdk-15-hotspot; \
rm -rf /var/lib/apt/lists/*;
ADD ./bundle/backend/ /opt/bundle/
ADD ./bundle-app/backend/ /opt/bundle/
WORKDIR /opt/bundle
CMD ["/bin/bash", "run.sh"]

View File

@@ -83,7 +83,7 @@ RUN set -ex; \
WORKDIR /opt/app
ADD ./bundle/exporter/ /opt/app/
ADD ./bundle-exporter/ /opt/app/
RUN set -ex; \
export PATH="$PATH:/usr/local/nodejs/bin"; \

View File

@@ -1,7 +1,8 @@
FROM nginx:latest
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ADD ./bundle/frontend /var/www/app/
ADD ./bundle-app/frontend /var/www/app/
ADD ./files/config.js /var/www/app/js/config.js
ADD ./files/nginx.conf /etc/nginx/nginx.conf
ADD ./files/nginx-entrypoint.sh /entrypoint.sh

View File

@@ -10,7 +10,7 @@ volumes:
services:
penpot-frontend:
image: "penpotapp/frontend:develop"
image: "penpotapp/frontend:latest"
ports:
- 9001:80
@@ -24,7 +24,7 @@ services:
- penpot
penpot-backend:
image: "penpotapp/backend:develop"
image: "penpotapp/backend:latest"
volumes:
- penpot_assets_data:/opt/data
@@ -79,7 +79,7 @@ services:
- penpot
penpot-exporter:
image: "penpotapp/exporter:develop"
image: "penpotapp/exporter:latest"
environment:
# Don't touch it; this uses internal docker network to
# communicate with the frontend.

View File

@@ -0,0 +1,9 @@
// Frontend configuration
//var penpotPublicURI = "https://penpot.example.com";
//var penpotDemoWarning = <true|false>;
//var penpotAllowDemoUsers = <true|false>;
//var penpotGoogleClientID = "<google-client-id-here>";
//var penpotGitlabClientID = "<gitlab-client-id-here>";
//var penpotGithubClientID = "<github-client-id-here>";
//var penpotLoginWithLDAP = <true|false>;

View File

@@ -1,3 +1,90 @@
#!/usr/bin/env bash
log() {
echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)] $*"
}
#########################################
## App Frontend config
#########################################
update_public_uri() {
if [ -n "$PENPOT_PUBLIC_URI" ]; then
log "Updating Public URI: $PENPOT_PUBLIC_URI"
sed -i \
-e "s|^//var penpotPublicURI = \".*\";|var penpotPublicURI = \"$PENPOT_PUBLIC_URI\";|g" \
"$1"
fi
}
update_demo_warning() {
if [ -n "$PENPOT_DEMO_WARNING" ]; then
log "Updating Demo Warning: $PENPOT_DEMO_WARNING"
sed -i \
-e "s|^//var penpotDemoWarning = .*;|var penpotDemoWarning = $PENPOT_DEMO_WARNING;|g" \
"$1"
fi
}
update_allow_demo_users() {
if [ -n "$PENPOT_ALLOW_DEMO_USERS" ]; then
log "Updating Allow Demo Users: $PENPOT_ALLOW_DEMO_USERS"
sed -i \
-e "s|^//var penpotAllowDemoUsers = .*;|var penpotAllowDemoUsers = $PENPOT_ALLOW_DEMO_USERS;|g" \
"$1"
fi
}
update_google_client_id() {
if [ -n "$PENPOT_GOOGLE_CLIENT_ID" ]; then
log "Updating Google Client Id: $PENPOT_GOOGLE_CLIENT_ID"
sed -i \
-e "s|^//var penpotGoogleClientID = \".*\";|var penpotGoogleClientID = \"$PENPOT_GOOGLE_CLIENT_ID\";|g" \
"$1"
fi
}
update_gitlab_client_id() {
if [ -n "$PENPOT_GITLAB_CLIENT_ID" ]; then
log "Updating GitLab Client Id: $PENPOT_GITLAB_CLIENT_ID"
sed -i \
-e "s|^//var penpotGitlabClientID = \".*\";|var penpotGitlabClientID = \"$PENPOT_GITLAB_CLIENT_ID\";|g" \
"$1"
fi
}
update_github_client_id() {
if [ -n "$PENPOT_GITHUB_CLIENT_ID" ]; then
log "Updating GitHub Client Id: $PENPOT_GITHUB_CLIENT_ID"
sed -i \
-e "s|^//var penpotGithubClientID = \".*\";|var penpotGithubClientID = \"$PENPOT_GITHUB_CLIENT_ID\";|g" \
"$1"
fi
}
update_login_with_ldap() {
if [ -n "$PENPOT_LOGIN_WITH_LDAP" ]; then
log "Updating Login with LDAP: $PENPOT_LOGIN_WITH_LDAP"
sed -i \
-e "s|^//var penpotLoginWithLDAP = .*;|var penpotLoginWithLDAP = $PENPOT_LOGIN_WITH_LDAP;|g" \
"$1"
fi
}
update_public_uri /var/www/app/js/config.js
update_demo_warning /var/www/app/js/config.js
update_allow_demo_users /var/www/app/js/config.js
update_google_client_id /var/www/app/js/config.js
update_gitlab_client_id /var/www/app/js/config.js
update_github_client_id /var/www/app/js/config.js
update_login_with_ldap /var/www/app/js/config.js
exec "$@";

View File

@@ -107,7 +107,7 @@ http {
}
location /assets {
proxy_pass http://127.0.0.1:6060/assets;
proxy_pass http://penpot-backend:6060/assets;
recursive_error_pages on;
proxy_intercept_errors on;
error_page 301 302 307 = @handle_redirect;
@@ -115,7 +115,7 @@ http {
location /internal/assets {
internal;
alias /var/www/assets;
alias /opt/data/assets;
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
}
}

View File

@@ -7,16 +7,16 @@ The simplest approach is using docker and docker-compose.
## Install Docker ##
Skip this section if you alreasdy have docker installed, up and running.
Skip this section if you already have docker installed, up and running.
You can install docker and its dependencies from your distribution
repositores with:
repository with:
```bash
sudo apt-get install docker docker-compose
```
Or follow installation instructions from docker.com; (for debian
Or follow installation instructions from docker.com; (for Debian
https://docs.docker.com/engine/install/debian/).
Ensure that the docker is started and optionally enable it to start
@@ -33,7 +33,7 @@ And finally, add your user to the docker group:
sudo usermod -aG docker $USER
```
This will make use the docker without `sudo` command all the time.
This will make use of the docker without `sudo` command all the time.
NOTE: probably you will need to re-login again to make this change
take effect.
@@ -58,5 +58,5 @@ docker-compose -p penpot -f docker-compose.yaml up
The docker compose file contains the essential configuration for
getting the application running, and many essential configurations
already explained in comments. All other configuration options are
explained in [management guide](./05-Management-Guide.md).
already explained in the comments. All other configuration options are
explained in [configuration guide](./05-Configuration-Guide.md).

View File

@@ -200,10 +200,10 @@ If any of the following variables are defined, they will enable the
corresponding auth button in the login page
```js
var appGoogleClientID = "<google-client-id-here>";
var appGitlabClientID = "<gitlab-client-id-here>";
var appGithubClientID = "<github-client-id-here>";
var appLoginWithLDAP = <true|false>;
var penpotGoogleClientID = "<google-client-id-here>";
var penpotGitlabClientID = "<gitlab-client-id-here>";
var penpotGithubClientID = "<github-client-id-here>";
var penpotLoginWithLDAP = <true|false>;
```
**NOTE:** The configuration should match the backend configuration for
@@ -216,8 +216,8 @@ It is possible to display a warning message on a demo environment and
disable/enable demo users:
```js
var appDemoWarning = <true|false>;
var appAllowDemoUsers = <true|false>;
var penpotDemoWarning = <true|false>;
var penpotAllowDemoUsers = <true|false>;
```
**NOTE:** The configuration for demo users should match the backend

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="279.383" height="369.004" viewBox="0 0 261.922 345.941">
<path d="M68.319 0L31.94 51.236v28.962L.286 95.255 0 95.12v188.953l123.419 58.308 7.542 3.56 7.542-3.56 123.419-58.308V95.12l-.233.11-31.666-15.062V51.236l-1.106-1.558L193.645 0l-36.38 51.236v.052l-26.47-37.283L104.527 51l-.94-1.322L68.318 0zm6.436 29.762l14.07 19.815H47.81l13.908-19.583 13.036-.232zm125.325 0l14.071 19.815H173.14l13.904-19.583 13.037-.232zm-62.85 14.007l14.07 19.814h-41.008L124.195 44l13.035-.23zM43.923 59.564h19.452v65.497l-19.452-9.19V59.564zm29.438 0h19.355l-.002 79.356-19.355-9.142.002-70.214zm95.887 0h19.453l-.001 70.146-19.452 9.188V59.564zm29.438 0h19.353v56.285l-19.353 9.142V59.564zM106.4 73.57h19.451v81.004l-19.45-9.19V73.57zm29.44 0h19.35l-.001 71.971-19.353 9.145.004-81.116zm94.18 21.526l17.126 7.002-17.126 8.09V95.095zm-198.08.025v15.09l-17.12-8.09 17.12-7zm-16.857 23.81l108.337 51.178v155.588L15.082 274.52V118.93zm231.756 0v155.588l-108.335 51.178V170.11l108.335-51.179zm-19.521 44.302l-45.187 82.05-26.366-21.373-7.364 12.228 37.954 30.627 51.45-94.185-10.487-9.347z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
min-width: 25px;
padding: 0 1rem;
transition: all .4s;
text-decoration: none !important;
svg {
height: 15px;
width: 15px;

View File

@@ -38,6 +38,10 @@
width: 18%;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
.grid-item-th {
text-align: initial;
}
&.placeholder {
min-width: 115px;
max-width: 115px;

View File

@@ -61,6 +61,35 @@ textarea {
margin-bottom: 20px;
}
.notification-icon {
justify-content: center;
display: flex;
margin-bottom: 3rem;
svg {
fill: $color-gray-60;
height: 40%;
width: 40%;
}
}
.notification-text {
font-size: $fs18;
color: $color-gray-60;
margin-bottom: 20px;
}
.notification-text-email {
background: $color-gray-10;
border-radius: $br-small;
color: $color-gray-60;
font-size: $fs18;
font-weight: 500;
margin: 1.5rem 0 2.5rem 0;
padding: 1rem;
text-align: center;
}
h2 {
font-size: $fs24;
color: $color-gray-60;
@@ -73,6 +102,14 @@ textarea {
text-decoration: underline;
}
p {
color: $color-gray-60;
}
hr {
border-color: $color-gray-20;
}
.links {
display: flex;
font-size: $fs14;
@@ -102,7 +139,8 @@ textarea {
flex-direction: column;
position: relative;
input {
input,
textarea {
background-color: $color-white;
border-radius: 2px;
border: 1px solid $color-gray-20;
@@ -114,6 +152,13 @@ textarea {
width: 100%;
}
textarea {
height: auto;
font-size: $fs14;
font-family: "worksans", sans-serif;
padding-top: 20px;
}
// Makes the background for autocomplete white
input:-webkit-autofill,
input:-webkit-autofill:hover,

View File

@@ -211,8 +211,10 @@
width: calc(100% - 1rem);
min-height: 5rem;
img {
max-height: 8rem;
max-width: 100%;
width: auto;
}
}

View File

@@ -99,8 +99,14 @@
position: fixed;
right: calc(#{$width-settings-bar} + 10px);
text-align: center;
width: 110px;
width: 125px;
white-space: nowrap;
padding-bottom: 2px;
transition: bottom 0.5s;
&.color-palette-open {
bottom: 5rem;
}
span {
color: $color-white;

View File

@@ -3,7 +3,18 @@
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>PENPOT - The Open-Source prototyping tool</title>
<title>Penpot - Design Freedom for Teams</title>
<meta name="description" content="The open-source solution for design and prototyping.">
<meta property="og:locale" content="en_US">
<meta property="og:title" content="Penpot | Design Freedom for Teams">
<meta property="og:description" content="The open-source solution for design and prototyping">
<meta property="og:image" content="https://penpot.app/images/workspace-ui.jpg">
<meta name="twitter:title" content="Penpot | Design Freedom for Teams">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:description" content="The open-source solution for design and prototyping">
<meta name="twitter:image" content="https://penpot.app/images/workspace-ui.jpg">
<meta name="twitter:site" content="@penpotapp">
<meta name="twitter:creator" content="@penpotapp">
<link id="theme" href="/css/main-{{& th}}.css?ts={{& ts}}"
rel="stylesheet" type="text/css" />

View File

@@ -1,7 +1,7 @@
{:deps {:aliases [:dev]}
:http {:port 3448}
:nrepl {:port 3447}
:jvm-opts ["-Xmx1g" "-Xms512m"]
:jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC"]
:builds
{:main

View File

@@ -67,6 +67,7 @@
(def default-language "en")
(def demo-warning (obj/get global "penpotDemoWarning" false))
(def feedback-enabled (obj/get global "penpotFeedbackEnabled" false))
(def allow-demo-users (obj/get global "penpotAllowDemoUsers" true))
(def google-client-id (obj/get global "penpotGoogleClientID" nil))
(def gitlab-client-id (obj/get global "penpotGitlabClientID" nil))

View File

@@ -9,12 +9,13 @@
(ns app.main
(:require
[app.config :as cfg]
[app.common.uuid :as uuid]
[app.common.spec :as us]
[app.main.repo :as rp]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.auth :refer [logout]]
[app.main.data.messages :as dm]
[app.main.data.users :as udu]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui :as ui]
[app.main.ui.confirm]
@@ -22,12 +23,12 @@
[app.main.worker]
[app.util.dom :as dom]
[app.util.i18n :as i18n]
[app.util.logging :as log]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.storage :refer [storage]]
[app.util.theme :as theme]
[app.util.timers :as ts]
[app.util.logging :as log]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
@@ -71,7 +72,7 @@
(st/emit! (rt/nav :auth-login))
(nil? match)
(st/emit! (rt/nav :not-found))
(st/emit! (dm/assign-exception {:type :not-found}))
:else
(st/emit! #(assoc % :route match)))))

View File

@@ -36,7 +36,7 @@
(let [team-id (:default-team-id profile)]
(rx/merge
(rx/of (du/profile-fetched profile)
(rt/nav :dashboard-projects {:team-id team-id}))
(rt/nav' :dashboard-projects {:team-id team-id}))
(when-not (get-in profile [:props :onboarding-viewed])
(->> (rx/of (modal/show {:type :onboarding}))
(rx/delay 1000))))))))
@@ -77,9 +77,7 @@
ptk/WatchEvent
(watch [this state s]
(let [team-id (:default-team-id profile)]
(rx/of (du/profile-fetched profile)
(rt/nav' :dashboard-projects {:team-id team-id}))))))
(rx/of (logged-in profile)))))
(defn login-with-ldap
[{:keys [email password] :as data}]
@@ -184,10 +182,7 @@
(->> (rp/mutation :request-profile-recovery data)
(rx/tap on-success)
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
(rx/catch on-error))))))
;; --- Recovery (Password)

View File

@@ -50,7 +50,8 @@
(-> state
(update-in [:workspace-file :colors] #(d/replace-by-id % color))))))
(defn change-palette-size [size]
(defn change-palette-size
[size]
(s/assert #{:big :small} size)
(ptk/reify ::change-palette-size
ptk/UpdateEvent
@@ -58,14 +59,27 @@
(-> state
(assoc-in [:workspace-local :selected-palette-size] size)))))
(defn change-palette-selected [selected]
(defn change-palette-selected
"Change the library used by the general palette tool"
[selected]
(ptk/reify ::change-palette-selected
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :selected-palette] selected)))))
(defn show-palette [selected]
(defn change-palette-selected-colorpicker
"Change the library used by the color picker"
[selected]
(ptk/reify ::change-palette-selected-colorpicker
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :selected-palette-colorpicker] selected)))))
(defn show-palette
"Show the palette tool and change the library it uses"
[selected]
(ptk/reify ::change-palette-selected
ptk/UpdateEvent
(update [_ state]
@@ -73,14 +87,16 @@
(update :workspace-layout conj :colorpalette)
(assoc-in [:workspace-local :selected-palette] selected)))))
(defn start-picker []
(defn start-picker
[]
(ptk/reify ::start-picker
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :picking-color?] true)))))
(defn stop-picker []
(defn stop-picker
[]
(ptk/reify ::stop-picker
ptk/UpdateEvent
(update [_ state]
@@ -89,14 +105,16 @@
(update :workspace-local dissoc :picked-shift?)
(assoc-in [:workspace-local :picking-color?] false)))))
(defn pick-color [rgba]
(defn pick-color
[rgba]
(ptk/reify ::pick-color
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :picked-color] rgba)))))
(defn pick-color-select [value shift?]
(defn pick-color-select
[value shift?]
(ptk/reify ::pick-color-select
ptk/UpdateEvent
(update [_ state]
@@ -116,11 +134,21 @@
text-ids (filter is-text? ids)
shape-ids (filter (comp not is-text?) ids)
attrs {:fill-color (:color color)
:fill-color-ref-id (:id color)
:fill-color-ref-file (:file-id color)
:fill-color-gradient (:gradient color)
:fill-opacity (:opacity color)}
attrs (cond-> {}
(contains? color :color)
(assoc :fill-color (:color color))
(contains? color :id)
(assoc :fill-color-ref-id (:id color))
(contains? color :file-id)
(assoc :fill-color-ref-file (:file-id color))
(contains? color :gradient)
(assoc :fill-color-gradient (:gradient color))
(contains? color :opacity)
(assoc :fill-opacity (:opacity color)))
update-fn (fn [shape] (merge shape attrs))
editors (get-in state [:workspace-local :editors])
@@ -131,29 +159,42 @@
(map #(dwt/update-text-attrs {:id % :editor (get editors %) :attrs attrs}) text-ids)
(dwc/update-shapes shape-ids update-fn))))))))
(defn change-stroke [ids color]
(defn change-stroke
[ids color]
(ptk/reify ::change-stroke
ptk/WatchEvent
(watch [_ state s]
(let [pid (:current-page-id state)
objects (get-in state [:workspace-data :pages-index pid :objects])
not-frame (fn [shape-id] (not= (get-in objects [shape-id :type]) :frame))
update-fn (fn [s]
(cond-> s
true
(assoc :stroke-color (:color color)
:stroke-opacity (:opacity color)
:stroke-color-gradient (:gradient color)
:stroke-color-ref-id (:id color)
:stroke-color-ref-file (:file-id color))
(= (:stroke-style s) :none)
(assoc :stroke-style :solid
:stroke-width 1
:stroke-opacity 1)))]
color-attrs (cond-> {}
(contains? color :color)
(assoc :stroke-color (:color color))
(contains? color :id)
(assoc :stroke-color-ref-id (:id color))
(contains? color :file-id)
(assoc :stroke-color-ref-file (:file-id color))
(contains? color :gradient)
(assoc :stroke-color-gradient (:gradient color))
(contains? color :opacity)
(assoc :stroke-opacity (:opacity color)))
update-fn (fn [shape]
(-> shape
(merge color-attrs)
(cond-> (= (:stroke-style s) :none)
(assoc :stroke-style :solid
:stroke-width 1
:stroke-opacity 1))))]
(rx/of (dwc/update-shapes ids update-fn))))))
(defn picker-for-selected-shape []
(defn picker-for-selected-shape
[]
(let [sub (rx/subject)]
(ptk/reify ::picker-for-selected-shape
ptk/WatchEvent
@@ -189,7 +230,8 @@
:props {:on-change handle-change-color}
:allow-click-outside true})))))))
(defn start-gradient [gradient]
(defn start-gradient
[gradient]
(ptk/reify ::start-gradient
ptk/UpdateEvent
(update [_ state]
@@ -198,21 +240,24 @@
(assoc-in [:workspace-local :current-gradient] gradient)
(assoc-in [:workspace-local :current-gradient :shape-id] id))))))
(defn stop-gradient []
(defn stop-gradient
[]
(ptk/reify ::stop-gradient
ptk/UpdateEvent
(update [_ state]
(-> state
(update :workspace-local dissoc :current-gradient)))))
(defn update-gradient [changes]
(defn update-gradient
[changes]
(ptk/reify ::update-gradient
ptk/UpdateEvent
(update [_ state]
(-> state
(update-in [:workspace-local :current-gradient] merge changes)))))
(defn select-gradient-stop [spot]
(defn select-gradient-stop
[spot]
(ptk/reify ::select-gradient-stop
ptk/UpdateEvent
(update [_ state]

View File

@@ -13,6 +13,7 @@
[app.common.media :as cm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.common.exceptions :as ex]
[app.main.data.messages :as dm]
[app.main.repo :as rp]
[app.main.store :as st]
@@ -50,9 +51,13 @@
;; Check that a file obtained with the file javascript API is valid.
[file]
(when (> (.-size file) cm/max-file-size)
(throw (ex-info (tr "errors.media-too-large") {})))
(ex/raise :type :validation
:code :media-too-large
:hint (str/fmt "media size is large than 5mb (size: %s)" (.-size file))))
(when-not (contains? cm/valid-media-types (.-type file))
(throw (ex-info (tr "errors.media-format-unsupported") {})))
(ex/raise :type :validation
:code :media-type-not-allowed
:hint (str/fmt "media type %s is not supported" (.-type file))))
file)
(defn notify-start-loading

View File

@@ -22,7 +22,7 @@
(declare show)
(def default-animation-timeout 600)
(def default-timeout 2000)
(def default-timeout 5000)
(s/def ::type #{:success :error :info :warning})
(s/def ::position #{:fixed :floating :inline})

View File

@@ -0,0 +1,75 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.shortcuts
(:require
[app.main.data.colors :as mdc]
[app.main.data.workspace.transforms :as dwt]
[app.main.store :as st]
[app.util.dom :as dom]
[potok.core :as ptk]
[beicon.core :as rx]
[app.config :as cfg])
(:refer-clojure :exclude [meta]))
(def mac-command "\u2318")
(def mac-option "\u2325")
(def mac-delete "\u232B")
(def mac-shift "\u21E7")
(def mac-control "\u2303")
(def mac-esc "\u238B")
(def left-arrow "\u2190")
(def up-arrow "\u2191")
(def right-arrow "\u2192")
(def down-arrow "\u2193")
(defn c-mod
"Adds the control/command modifier to a shortcuts depending on the
operating system for the user"
[shortcut]
(if (cfg/check-platform? :macos)
(str "command+" shortcut)
(str "ctrl+" shortcut)))
(defn bind-shortcuts [shortcuts bind-fn cb-fn]
(doseq [[key {:keys [command disabled fn]}] shortcuts]
(when-not disabled
(if (vector? command)
(doseq [cmd (seq command)]
(bind-fn cmd (cb-fn key fn)))
(bind-fn command (cb-fn key fn))))))
(defn meta [key]
(str
(if (cfg/check-platform? :macos)
mac-command
"Ctrl+")
key))
(defn shift [key]
(str
(if (cfg/check-platform? :macos)
mac-shift
"Shift+")
key))
(defn meta-shift [key]
(-> key meta shift))
(defn supr []
(if (cfg/check-platform? :macos)
mac-delete
"Supr"))
(defn esc []
(if (cfg/check-platform? :macos)
mac-esc
"Escape"))

View File

@@ -112,21 +112,22 @@
(defn bundle-fetched
[{:keys [project file page share-token token libraries users] :as bundle}]
(us/verify ::bundle bundle)
(ptk/reify ::file-fetched
(ptk/reify ::bundle-fetched
ptk/UpdateEvent
(update [_ state]
(let [objects (:objects page)
frames (extract-frames objects)]
(assoc state
:viewer-libraries (d/index-by :id libraries)
:viewer-data {:project project
:objects objects
:users (d/index-by :id users)
:file file
:page page
:frames frames
:token token
:share-token share-token})))))
(-> state
(assoc :viewer-libraries (d/index-by :id libraries))
(update :viewer-data assoc
:project project
:objects objects
:users (d/index-by :id users)
:file file
:page page
:frames frames
:token token
:share-token share-token))))))
(defn fetch-comment-threads
[{:keys [file-id page-id] :as params}]
@@ -136,7 +137,8 @@
(d/index-by :id)
(assoc state :comment-threads)))
(on-error [{:keys [type] :as err}]
(if (= :authentication type)
(if (or (= :authentication type)
(= :not-found type))
(rx/empty)
(rx/throw err)))]
@@ -346,7 +348,7 @@
(defn set-current-frame [frame-id]
(ptk/reify ::current-frame
(ptk/reify ::set-current-frame
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-data :current-frame-id] frame-id))))
@@ -415,15 +417,12 @@
(update [_ state]
(assoc-in state [:viewer-local :hover] (when hover? id)))))
;; --- Shortcuts
(def shortcuts
{"+" (st/emitf increase-zoom)
"-" (st/emitf decrease-zoom)
"ctrl+a" (st/emitf (select-all))
"shift+0" (st/emitf zoom-to-50)
"shift+1" (st/emitf reset-zoom)
"shift+2" (st/emitf zoom-to-200)
"left" (st/emitf select-prev-frame)
"right" (st/emitf select-next-frame)})
(defn go-to-dashboard
([] (go-to-dashboard nil))
([{:keys [team-id]}]
(ptk/reify ::go-to-dashboard
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (or team-id (get-in state [:viewer-data :project :team-id]))]
(rx/of (rt/nav :dashboard-projects {:team-id team-id})))))))

View File

@@ -0,0 +1,57 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.viewer.shortcuts
(:require
[app.config :as cfg]
[app.main.data.colors :as mdc]
[app.main.data.shortcuts :as ds]
[app.main.data.shortcuts :refer [c-mod]]
[app.main.data.viewer :as dv]
[app.main.store :as st]
[app.util.dom :as dom]
[beicon.core :as rx]
[potok.core :as ptk]))
(def shortcuts
{:increase-zoom {:tooltip "+"
:command "+"
:fn (st/emitf dv/increase-zoom)}
:decrease-zoom {:tooltip "-"
:command "-"
:fn (st/emitf dv/decrease-zoom)}
:select-all {:tooltip (ds/meta "A")
:command (ds/c-mod "a")
:fn (st/emitf (dv/select-all))}
:zoom-50 {:tooltip (ds/shift "0")
:command "shift+0"
:fn (st/emitf dv/zoom-to-50)}
:reset-zoom {:tooltip (ds/shift "1")
:command "shift+1"
:fn (st/emitf dv/reset-zoom)}
:zoom-200 {:tooltip (ds/shift "2")
:command "shift+2"
:fn (st/emitf dv/zoom-to-200)}
:next-frame {:tooltip ds/left-arrow
:command "left"
:fn (st/emitf dv/select-prev-frame)}
:prev-frame {:tooltip ds/right-arrow
:command "right"
:fn (st/emitf dv/select-next-frame)}})
(defn get-tooltip [shortcut]
(assert (contains? shortcuts shortcut) (str shortcut))
(get-in shortcuts [shortcut :tooltip]))

View File

@@ -9,14 +9,13 @@
(ns app.main.data.workspace
(:require
[goog.string.path :as path]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.align :as gal]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.geom.proportions :as gpr]
[app.common.geom.align :as gal]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph]
@@ -27,33 +26,33 @@
[app.main.data.colors :as mdc]
[app.main.data.messages :as dm]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.drawing.path :as dwdp]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.persistence :as dwp]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.drawing.path :as dwdp]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.streams :as ms]
[app.main.worker :as uw]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.i18n :refer [tr] :as i18n]
[app.util.logging :as log]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.timers :as ts]
[app.util.transit :as t]
[app.util.webapi :as wapi]
[app.util.i18n :refer [tr] :as i18n]
[app.util.object :as obj]
[app.util.dom :as dom]
[app.util.http :as http]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[clojure.set :as set]
[cuerdas.core :as str]
;; [cljs.pprint :refer [pprint]]
[goog.string.path :as path]
[potok.core :as ptk]))
;; (log/set-level! :trace)
@@ -121,8 +120,10 @@
:left-sidebar? true
:right-sidebar? true
:color-for-rename nil
:selected-palette-colorpicker :recent
:selected-palette :recent
:selected-palette-size :big
:assets-files-open {}
:picking-color? false
:picked-color nil
:picked-color-select false})
@@ -807,6 +808,168 @@
;; --- Change Shape Order (D&D Ordering)
(defn relocate-shapes-changes [objects parents parent-id page-id to-index ids groups-to-delete groups-to-unmask shapes-to-detach shapes-to-reroot shapes-to-deroot]
(let [;; Changes to the shapes that are being move
r-mov-change
[{:type :mov-objects
:parent-id parent-id
:page-id page-id
:index to-index
:shapes (vec (reverse ids))}]
u-mov-change
(map (fn [id]
(let [obj (get objects id)]
{:type :mov-objects
:parent-id (:parent-id obj)
:page-id page-id
:index (cp/position-on-parent id objects)
:shapes [id]}))
(reverse ids))
;; Changes deleting empty groups
r-del-change
(map (fn [group-id]
{:type :del-obj
:page-id page-id
:id group-id})
groups-to-delete)
u-del-change
(d/concat
[]
;; Create the groups
(map (fn [group-id]
(let [group (get objects group-id)]
{:type :add-obj
:page-id page-id
:parent-id parent-id
:frame-id (:frame-id group)
:id group-id
:obj (-> group
(assoc :shapes []))}))
groups-to-delete)
;; Creates the hierarchy
(map (fn [group-id]
(let [group (get objects group-id)]
{:type :mov-objects
:page-id page-id
:parent-id (:id group)
:shapes (:shapes group)}))
groups-to-delete))
;; Changes removing the masks from the groups without mask shape
r-mask-change
(map (fn [group-id]
{:type :mod-obj
:page-id page-id
:id group-id
:operations [{:type :set
:attr :masked-group?
:val false}]})
groups-to-unmask)
u-mask-change
(map (fn [group-id]
(let [group (get objects group-id)]
{:type :mod-obj
:page-id page-id
:id group-id
:operations [{:type :set
:attr :masked-group?
:val (:masked-group? group)}]}))
groups-to-unmask)
;; Changes to the components metadata
detach-keys [:component-id :component-file :component-root? :remote-synced? :shape-ref :touched]
r-detach-change
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations (mapv #(hash-map :type :set :attr % :val nil) detach-keys)})
shapes-to-detach)
u-detach-change
(map (fn [id]
(let [obj (get objects id)]
{:type :mod-obj
:page-id page-id
:id id
:operations (mapv #(hash-map :type :set :attr % :val (get obj %)) detach-keys)}))
shapes-to-detach)
r-deroot-change
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-root?
:val nil}]})
shapes-to-deroot)
u-deroot-change
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-root?
:val true}]})
shapes-to-deroot)
r-reroot-change
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-root?
:val true}]})
shapes-to-reroot)
u-reroot-change
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-root?
:val nil}]})
shapes-to-reroot)
r-reg-change
[{:type :reg-objects
:page-id page-id
:shapes (vec parents)}]
u-reg-change
[{:type :reg-objects
:page-id page-id
:shapes (vec parents)}]
rchanges (d/concat []
r-mov-change
r-del-change
r-mask-change
r-detach-change
r-deroot-change
r-reroot-change
r-reg-change)
uchanges (d/concat []
u-del-change
u-reroot-change
u-deroot-change
u-detach-change
u-mask-change
u-mov-change
u-reg-change)]
[rchanges uchanges]))
(defn relocate-shapes
[ids parent-id to-index]
(us/verify (s/coll-of ::us/uuid) ids)
@@ -822,13 +985,40 @@
;; Ignore any shape whose parent is also intented to be moved
ids (cp/clean-loops objects ids)
parents (loop [res #{parent-id}
ids (seq ids)]
(if (nil? ids)
(vec res)
(recur
(conj res (cp/get-parent (first ids) objects))
(next ids))))
;; If we try to move a parent into a child we remove it
ids (filter #(not (cp/is-parent? objects parent-id %)) ids)
parents (reduce (fn [result id]
(conj result (cp/get-parent id objects)))
#{parent-id} ids)
groups-to-delete
(loop [current-id (first parents)
to-check (rest parents)
removed-id? (set ids)
result #{}]
(if-not current-id
;; Base case, no next element
result
(let [group (get objects current-id)]
(if (and (not= uuid/zero current-id)
(not= current-id parent-id)
(empty? (remove removed-id? (:shapes group))))
;; Adds group to the remove and check its parent
(let [to-check (d/concat [] to-check [(cp/get-parent current-id objects)]) ]
(recur (first to-check)
(rest to-check)
(conj removed-id? current-id)
(conj result current-id)))
;; otherwise recur
(recur (first to-check)
(rest to-check)
removed-id?
result)))))
groups-to-unmask
(reduce (fn [group-ids id]
@@ -845,6 +1035,10 @@
#{}
ids)
;; Sets the correct components metadata for the moved shapes
;; `shapes-to-detach` Detach from a component instance a shape that was inside a component and is moved outside
;; `shapes-to-deroot` Removes the root flag from a component instance moved inside another component
;; `shapes-to-reroot` Adds a root flag when a nested component instance is moved outside
[shapes-to-detach shapes-to-deroot shapes-to-reroot]
(reduce (fn [[shapes-to-detach shapes-to-deroot shapes-to-reroot] id]
(let [shape (get objects id)
@@ -872,131 +1066,18 @@
[[] [] []]
ids)
rchanges (d/concat
[{:type :mov-objects
:parent-id parent-id
:page-id page-id
:index to-index
:shapes (vec (reverse ids))}
{:type :reg-objects
:page-id page-id
:shapes parents}]
(map (fn [group-id]
{:type :mod-obj
:page-id page-id
:id group-id
:operations [{:type :set
:attr :masked-group?
:val false}]})
groups-to-unmask)
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-id
:val nil}
{:type :set
:attr :component-file
:val nil}
{:type :set
:attr :component-root?
:val nil}
{:type :set
:attr :remote-synced?
:val nil}
{:type :set
:attr :shape-ref
:val nil}
{:type :set
:attr :touched
:val nil}]})
shapes-to-detach)
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-root?
:val nil}]})
shapes-to-deroot)
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-root?
:val true}]})
shapes-to-reroot))
uchanges (d/concat
(reduce (fn [res id]
(let [obj (get objects id)]
(conj res
{:type :mov-objects
:parent-id (:parent-id obj)
:page-id page-id
:index (cp/position-on-parent id objects)
:shapes [id]})))
[] (reverse ids))
[{:type :reg-objects
:page-id page-id
:shapes parents}]
(map (fn [group-id]
{:type :mod-obj
:page-id page-id
:id group-id
:operations [{:type :set
:attr :masked-group?
:val true}]})
groups-to-unmask)
(map (fn [id]
(let [obj (get objects id)]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-id
:val (:component-id obj)}
{:type :set
:attr :component-file
:val (:component-file obj)}
{:type :set
:attr :component-root?
:val (:component-root? obj)}
{:type :set
:attr :remote-synced?
:val (:remote-synced? obj)}
{:type :set
:attr :shape-ref
:val (:shape-ref obj)}
{:type :set
:attr :touched
:val (:touched obj)}]}))
shapes-to-detach)
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-root?
:val true}]})
shapes-to-deroot)
(map (fn [id]
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set
:attr :component-root?
:val nil}]})
shapes-to-reroot))]
;; (println "================ rchanges")
;; (cljs.pprint/pprint rchanges)
;; (println "================ uchanges")
;; (cljs.pprint/pprint uchanges)
(rx/of (dwc/commit-changes rchanges uchanges
{:commit-local? true})
[rchanges uchanges] (relocate-shapes-changes objects
parents
parent-id
page-id
to-index
ids
groups-to-delete
groups-to-unmask
shapes-to-detach
shapes-to-reroot
shapes-to-deroot)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dwc/expand-collapse parent-id))))))
(defn relocate-selected-shapes
@@ -1220,20 +1301,26 @@
(defn go-to-viewer
[{:keys [file-id page-id] :as params}]
(ptk/reify ::go-to-viewer
ptk/WatchEvent
(watch [_ state stream]
(rx/of ::dwp/force-persist
(rt/nav :viewer params {:index 0})))))
([] (go-to-viewer {}))
([{:keys [file-id page-id]}]
(ptk/reify ::go-to-viewer
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [current-file-id current-page-id]} state
params {:file-id (or file-id current-file-id)
:page-id (or page-id current-page-id)}]
(rx/of ::dwp/force-persist
(rt/nav :viewer params {:index 0})))))))
(defn go-to-dashboard
[{:keys [team-id] :as project}]
(ptk/reify ::go-to-viewer
ptk/WatchEvent
(watch [_ state stream]
(rx/of ::dwp/force-persist
(rt/nav :dashboard-projects {:team-id team-id})))))
([] (go-to-dashboard nil))
([{:keys [team-id]}]
(ptk/reify ::go-to-dashboard
ptk/WatchEvent
(watch [_ state stream]
(let [team-id (or team-id (get-in state [:workspace-project :team-id]))]
(rx/of ::dwp/force-persist
(rt/nav :dashboard-projects {:team-id team-id})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Context Menu
@@ -1279,7 +1366,6 @@
;; Clipboard
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn copy-selected
[]
(letfn [;; Retrieve all ids of selected shapes with corresponding
@@ -1395,15 +1481,32 @@
ptk/WatchEvent
(watch [_ state stream]
(try
(let [paste-data (wapi/read-from-paste-event event)
(let [objects (dwc/lookup-page-objects state)
paste-data (wapi/read-from-paste-event event)
image-data (wapi/extract-images paste-data)
text-data (wapi/extract-text paste-data)
decoded-data (and (t/transit? text-data) (t/decode text-data))]
decoded-data (and (t/transit? text-data)
(t/decode text-data))
edit-id (get-in state [:workspace-local :edition])
is-editing-text? (and edit-id (= :text (get-in objects [edit-id :type])))]
(cond
(seq image-data) (rx/from (map paste-image image-data))
decoded-data (rx/of (paste-shape decoded-data in-viewport?))
(string? text-data) (rx/of (paste-text text-data))
:else (rx/empty)))
(seq image-data)
(rx/from (map paste-image image-data))
(coll? decoded-data)
(->> (rx/of decoded-data)
(rx/filter #(= :copied-shapes (:type %)))
(rx/map #(paste-shape % in-viewport?)))
;; Some paste events can be fired while we're editing a text
;; we forbid that scenario so the default behaviour is executed
(and (string? text-data) (not is-editing-text?))
(rx/of (paste-text text-data))
:else
(rx/empty)))
(catch :default err
(js/console.error "Clipboard error:" err))))))
@@ -1564,7 +1667,7 @@
(watch [_ state stream]
(let [id (uuid/next)
{:keys [x y]} @ms/mouse-position
width (min (* 7 (count text)) 700)
width (max 8 (min (* 7 (count text)) 700))
height 16
page-id (:current-page-id state)
frame-id (-> (dwc/lookup-page-objects state page-id)
@@ -1703,6 +1806,8 @@
(d/export dwt/set-modifiers)
(d/export dwt/apply-modifiers)
(d/export dwt/update-dimensions)
(d/export dwt/flip-horizontal-selected)
(d/export dwt/flip-vertical-selected)
;; Persistence
@@ -1738,80 +1843,3 @@
(d/export dwg/unmask-group)
(d/export dwg/group-selected)
(d/export dwg/ungroup-selected)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shortcuts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shortcuts impl https://github.com/ccampbell/mousetrap
(defn esc-pressed []
(ptk/reify :esc-pressed
ptk/WatchEvent
(watch [_ state stream]
;; Not interrupt when we're editing a path
(let [edition-id (or (get-in state [:workspace-drawing :object :id])
(get-in state [:workspace-local :edition]))
path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])]
(if-not (= :draw path-edit-mode)
(rx/of :interrupt (deselect-all true))
(rx/empty))))))
(defn c-mod
"Adds the control/command modifier to a shortcuts depending on the
operating system for the user"
[shortcut]
(if (cfg/check-platform? :macos)
(str "command+" shortcut)
(str "ctrl+" shortcut)))
(def shortcuts
{(c-mod "i") #(st/emit! (toggle-layout-flags :assets))
(c-mod "l") #(st/emit! (toggle-layout-flags :sitemap :layers))
(c-mod "shift+r") #(st/emit! (toggle-layout-flags :rules))
(c-mod "a") #(st/emit! (select-all))
(c-mod "p") #(st/emit! (toggle-layout-flags :colorpalette))
(c-mod "'") #(st/emit! (toggle-layout-flags :display-grid))
(c-mod "shift+'") #(st/emit! (toggle-layout-flags :snap-grid))
"+" #(st/emit! (increase-zoom nil))
"-" #(st/emit! (decrease-zoom nil))
(c-mod "g") #(st/emit! group-selected)
"shift+g" #(st/emit! ungroup-selected)
(c-mod "m") #(st/emit! mask-group)
"shift+m" #(st/emit! unmask-group)
(c-mod "k") #(st/emit! dwl/add-component)
"shift+0" #(st/emit! reset-zoom)
"shift+1" #(st/emit! zoom-to-fit-all)
"shift+2" #(st/emit! zoom-to-selected-shape)
(c-mod "d") #(st/emit! duplicate-selected)
(c-mod "z") #(st/emit! dwc/undo)
(c-mod "shift+z") #(st/emit! dwc/redo)
(c-mod "y") #(st/emit! dwc/redo)
(c-mod "q") #(st/emit! dwc/reinitialize-undo)
"a" #(st/emit! (dwd/select-for-drawing :frame))
"r" #(st/emit! (dwd/select-for-drawing :rect))
"e" #(st/emit! (dwd/select-for-drawing :circle))
"t" #(st/emit! dwtxt/start-edit-if-selected
(dwd/select-for-drawing :text))
"p" #(st/emit! (dwd/select-for-drawing :path))
"k" (fn [event]
(let [image-upload (dom/get-element "image-upload")]
(dom/click image-upload)))
(c-mod "c") #(st/emit! (copy-selected))
(c-mod "x") #(st/emit! (copy-selected) delete-selected)
"escape" #(st/emit! (esc-pressed))
"del" #(st/emit! delete-selected)
"backspace" #(st/emit! delete-selected)
(c-mod "up") #(st/emit! (vertical-order-selected :up))
(c-mod "down") #(st/emit! (vertical-order-selected :down))
(c-mod "shift+up") #(st/emit! (vertical-order-selected :top))
(c-mod "shift+down") #(st/emit! (vertical-order-selected :bottom))
"shift+up" #(st/emit! (dwt/move-selected :up true))
"shift+down" #(st/emit! (dwt/move-selected :down true))
"shift+right" #(st/emit! (dwt/move-selected :right true))
"shift+left" #(st/emit! (dwt/move-selected :left true))
"up" #(st/emit! (dwt/move-selected :up false))
"down" #(st/emit! (dwt/move-selected :down false))
"right" #(st/emit! (dwt/move-selected :right false))
"left" #(st/emit! (dwt/move-selected :left false))
"i" #(st/emit! (mdc/picker-for-selected-shape ))})

View File

@@ -198,7 +198,7 @@
(defn retrieve-used-names
[objects]
(into #{} (map :name) (vals objects)))
(into #{} (comp (map :name) (remove nil?)) (vals objects)))
(defn generate-unique-name
@@ -536,20 +536,26 @@
(defn get-shape-layer-position
[objects selected attrs]
(cond
(= :frame (:type attrs))
(if (= :frame (:type attrs))
;; Frames are alwasy positioned on the root frame
[uuid/zero uuid/zero nil]
(empty? selected)
;; Calculate the frame over which we're drawing
(let [position @ms/mouse-position
frame-id (:frame-id attrs (cp/frame-id-by-position objects position))]
[frame-id frame-id nil])
frame-id (:frame-id attrs (cp/frame-id-by-position objects position))
shape (when-not (empty? selected)
(cp/get-base-shape objects selected))]
:else
(let [shape (cp/get-base-shape objects selected)
index (cp/position-on-parent (:id shape) objects)
{:keys [frame-id parent-id]} shape]
[frame-id parent-id (inc index)])))
;; When no shapes has been selected or we're over a different frame
;; we add it as the latest shape of that frame
(if (or (not shape) (not= (:frame-id shape) frame-id))
[frame-id frame-id nil]
;; Otherwise, we add it to next to the selected shape
(let [index (cp/position-on-parent (:id shape) objects)
{:keys [frame-id parent-id]} shape]
[frame-id parent-id (inc index)])))))
(defn add-shape-changes
[page-id objects selected attrs]
@@ -562,6 +568,9 @@
shape (merge default-attrs shape)
not-frame? #(not (= :frame (get-in objects [% :type])))
selected (into #{} (filter not-frame?) selected)
[frame-id parent-id index] (get-shape-layer-position objects selected attrs)
redo-changes [{:type :add-obj

View File

@@ -21,12 +21,17 @@
[app.main.data.workspace.drawing.common :as common]
[app.common.math :as mth]))
(defn truncate-zero [num default]
(if (mth/almost-zero? num) default num))
(defn resize-shape [{:keys [x y width height transform transform-inverse] :as shape} point lock?]
(let [;; The new shape behaves like a resize on the bottom-right corner
initial (gpt/point (+ x width) (+ y height))
shapev (gpt/point width height)
deltav (gpt/to-vec initial point)
scalev (gpt/divide (gpt/add shapev deltav) shapev)
scalev (-> (gpt/divide (gpt/add shapev deltav) shapev)
(update :x truncate-zero 1)
(update :y truncate-zero 1))
scalev (if lock?
(let [v (max (:x scalev) (:y scalev))]
(gpt/point v v))

View File

@@ -90,12 +90,15 @@
path)))
(defn- points->components [shape content]
(let [rotation (:rotation shape 0)
(let [transform (:transform shape)
transform-inverse (:transform-inverse shape)
center (gsh/center-shape shape)
content-rotated (gsh/transform-content content (gmt/rotate-matrix (- rotation) center))
base-content (gsh/transform-content
content
(gmt/transform-in center transform-inverse))
;; Calculates the new selrect with points given the old center
points (-> (gsh/content->selrect content-rotated)
points (-> (gsh/content->selrect base-content)
(gsh/rect->points)
(gsh/transform-points center (:transform shape (gmt/matrix))))
@@ -692,8 +695,8 @@
point (-> content (get (if (= prefix :c1) (dec index) index)) (ugp/command->point))
handler (-> content (get index) (ugp/get-handler prefix))
current-distance (gpt/distance (ugp/opposite-handler point handler) opposite-handler)
match-opposite? (mth/almost-zero? current-distance)]
current-distance (when opposite-handler (gpt/distance (ugp/opposite-handler point handler) opposite-handler))
match-opposite? (and opposite-handler (mth/almost-zero? current-distance))]
(drag-stream
(rx/concat

View File

@@ -22,7 +22,7 @@
(defonce ^:private default-square-params
{:size 16
:color {:color "#59B9E2"
:opacity 0.2}})
:opacity 0.4}})
(defonce ^:private default-layout-params
{:size 12

View File

@@ -65,6 +65,13 @@
(declare sync-file)
(defn set-assets-box-open
[file-id box open?]
(ptk/reify ::set-assets-box-open
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :assets-files-open file-id box] open?))))
(defn default-color-name [color]
(or (:color color)
(case (get-in color [:gradient :type])
@@ -230,7 +237,8 @@
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])]
selected (get-in state [:workspace-local :selected])
selected (cp/clean-loops objects selected)]
(let [[group rchanges uchanges]
(dwlh/generate-add-component selected objects page-id file-id)]
(when-not (empty? rchanges)
@@ -311,7 +319,7 @@
(defn instantiate-component
"Create a new shape in the current page, from the component with the given id
in the given file library / current file library."
in the given file library. Then selects the newly created instance."
[file-id component-id position]
(us/assert ::us/uuid file-id)
(us/assert ::us/uuid component-id)

View File

@@ -626,6 +626,8 @@
(contains? (:touched shape-inst)
:shapes-group))
(add-shape-to-instance child-master
(d/index-of children-master
child-master)
component
container
root-inst
@@ -649,11 +651,11 @@
reset?
initial-root?)))
moved (fn [shape-inst shape-master]
moved (fn [child-inst child-master]
(move-shape
shape-inst
(d/index-of children-inst shape-inst)
(d/index-of children-master shape-master)
child-inst
(d/index-of children-inst child-inst)
(d/index-of children-master child-master)
container
omit-touched?))
@@ -742,6 +744,8 @@
only-inst (fn [child-inst]
(add-shape-to-master child-inst
(d/index-of children-inst
child-inst)
component
container
root-inst
@@ -768,11 +772,11 @@
root-master)
initial-root?)))
moved (fn [shape-inst shape-master]
moved (fn [child-inst child-master]
(move-shape
shape-master
(d/index-of children-master shape-master)
(d/index-of children-inst shape-inst)
child-master
(d/index-of children-master child-master)
(d/index-of children-inst child-inst)
component-container
false))
@@ -863,7 +867,7 @@
(concat-changes (moved-cb child-inst' child-master))))))))))))
(defn- add-shape-to-instance
[component-shape component container root-instance root-master omit-touched? set-remote-synced?]
[component-shape index component container root-instance root-master omit-touched? set-remote-synced?]
(log/info :msg (str "ADD [P] " (:name component-shape)))
(let [component-parent-shape (cp/get-shape component (:parent-id component-shape))
parent-shape (d/seek #(cp/is-master-of component-parent-shape %)
@@ -904,6 +908,7 @@
(as-> {:type :add-obj
:id (:id shape')
:parent-id (:parent-id shape')
:index index
:ignore-touched true
:obj shape'} $
(cond-> $
@@ -929,7 +934,7 @@
[rchanges uchanges])))
(defn- add-shape-to-master
[shape component page root-instance root-master]
[shape index component page root-instance root-master]
(log/info :msg (str "ADD [C] " (:name shape)))
(let [parent-shape (cp/get-shape page (:parent-id shape))
component-parent-shape (d/seek #(cp/is-master-of % parent-shape)
@@ -963,6 +968,7 @@
:id (:id shape')
:component-id (:id component)
:parent-id (:parent-id shape')
:index index
:ignore-touched true
:obj shape'})
new-shapes)

View File

@@ -200,13 +200,16 @@
(ptk/reify ::handle-file-change
ptk/WatchEvent
(watch [_ state stream]
(let [page-ids (into #{} (comp (map :page-id)
(filter identity))
changes)]
(let [changes-by-pages (group-by :page-id changes)
process-page-changes
(fn [[page-id changes]]
(dwc/update-indices page-id changes))]
(rx/merge
(rx/of (dwp/shapes-changes-persisted file-id msg))
(when (seq page-ids)
(rx/from (map dwc/update-indices page-ids changes))))))))
(when-not (empty? changes-by-pages)
(rx/from (map process-page-changes changes-by-pages))))))))
(s/def ::library-change-event
(s/keys :req-un [::type

View File

@@ -9,9 +9,8 @@
(ns app.main.data.workspace.persistence
(:require
[cuerdas.core :as str]
[app.util.http :as http]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[app.common.media :as cm]
[app.common.pages :as cp]
@@ -21,21 +20,22 @@
[app.main.data.media :as di]
[app.main.data.messages :as dm]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.svg-upload :as svg]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.svg-upload :as svg]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.avatars :as avatars]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.time :as dt]
[app.util.transit :as t]
[app.util.avatars :as avatars]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]
[app.main.store :as st]))
[cuerdas.core :as str]
[potok.core :as ptk]))
(declare persist-changes)
(declare persist-sychronous-changes)
@@ -417,24 +417,36 @@
(defn- handle-upload-error [on-error stream]
(->> stream
(rx/catch
(fn [error]
(cond
(= (:code error) :media-type-not-allowed)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(fn on-error* [error]
(if (ex/ex-info? error)
(on-error* (ex-data error))
(cond
(= (:code error) :invalid-svg-file)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-type-mismatch)
(rx/of (dm/error (tr "errors.media-type-mismatch")))
(= (:code error) :media-type-not-allowed)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :unable-to-optimize)
(rx/of (dm/error (:hint error)))
(= (:code error) :ubable-to-access-to-url)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(fn? on-error)
(do
(= (:code error) :invalid-image)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-too-large)
(rx/of (dm/error (tr "errors.media-too-large")))
(= (:code error) :media-type-mismatch)
(rx/of (dm/error (tr "errors.media-type-mismatch")))
(= (:code error) :unable-to-optimize)
(rx/of (dm/error (:hint error)))
(fn? on-error)
(on-error error)
(rx/empty))
:else
(rx/throw error))))))
:else
(rx/throw error)))))))
(defn- upload-uris [file-id local? name uris mtype on-image on-svg]
(letfn [(svg-url? [url]
@@ -490,7 +502,7 @@
(rx/map #(assoc (first %) :name (.-name (second %))))
(rx/do on-svg)))))
(defn upload-media-objects
(defn- upload-media-objects
[{:keys [file-id local? data name uris mtype svg-as-images] :as params}]
(us/assert ::upload-media-objects params)
(ptk/reify ::upload-media-objects
@@ -499,7 +511,6 @@
(let [{:keys [on-image on-svg on-error]
:or {on-image identity
on-svg identity}} (meta params)]
(rx/concat
(rx/of (dm/show {:content (tr "media.loading")
:type :info
@@ -515,7 +526,8 @@
(handle-upload-error on-error)
(rx/finalize (st/emitf (dm/hide-tag :media-loading)))))))))
(defn upload-media-asset [params]
(defn upload-media-asset
[params]
(let [params (-> params
(assoc :svg-as-images true)
(assoc :local? false)
@@ -525,13 +537,12 @@
(defn upload-media-workspace
[params position]
(let [{:keys [x y]} position
params (-> params
(assoc :local? true)
(with-meta
{:on-image
#(st/emit! (dwc/image-uploaded % x y))
:on-svg
#(st/emit! (svg/svg-uploaded % x y))}))]
mdata {:on-image #(st/emit! (dwc/image-uploaded % x y))
:on-svg #(st/emit! (svg/svg-uploaded % x y))}
params (-> (assoc params :local? true)
(with-meta mdata))]
(upload-media-objects params)))

View File

@@ -105,6 +105,14 @@
objects (dwc/lookup-page-objects state page-id)]
(rx/of (dwc/expand-all-parents [id] objects)))))))
(defn deselect-shape
[id]
(us/verify ::us/uuid id)
(ptk/reify ::select-shape
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :selected] disj id))))
(defn shift-select-shapes
([id]
(ptk/reify ::shift-select-shapes
@@ -156,11 +164,10 @@
(not= (:id common-frame-id) uuid/zero))
(-> (get objects common-frame-id)
:shapes)
(let [frames (cp/select-frames objects)]
(->> (if (seq frames)
frames
(cp/select-toplevel-shapes objects))
(map :id)))))
(->> (cp/select-toplevel-shapes objects
{:include-frames? true
:include-frame-children? false})
(map :id))))
is-not-blocked (fn [shape-id] (not (get-in state [:workspace-data
:pages-index page-id

View File

@@ -0,0 +1,262 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.workspace.shortcuts
(:require
[app.config :as cfg]
[app.main.data.colors :as mdc]
[app.main.data.shortcuts :as ds]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.transforms :as dwt]
[app.main.store :as st]
[app.util.dom :as dom]
[beicon.core :as rx]
[potok.core :as ptk]))
;; \u2318P
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shortcuts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shortcuts impl https://github.com/ccampbell/mousetrap
(defn esc-pressed []
(ptk/reify :esc-pressed
ptk/WatchEvent
(watch [_ state stream]
;; Not interrupt when we're editing a path
(let [edition-id (or (get-in state [:workspace-drawing :object :id])
(get-in state [:workspace-local :edition]))
path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])]
(if-not (= :draw path-edit-mode)
(rx/of :interrupt (dw/deselect-all true))
(rx/empty))))))
(def shortcuts
{:toggle-layers {:tooltip (ds/meta "L")
:command (ds/c-mod "l")
:fn #(st/emit! (dw/go-to-layout :layers))}
:toggle-assets {:tooltip (ds/meta "I")
:command (ds/c-mod "i")
:fn #(st/emit! (dw/go-to-layout :assets))}
:toggle-history {:tooltip (ds/meta "H")
:command (ds/c-mod "h")
:fn #(st/emit! (dw/go-to-layout :document-history))}
:toggle-palette {:tooltip (ds/meta "P")
:command (ds/c-mod "p")
:fn #(st/emit! (dw/toggle-layout-flags :colorpalette))}
:toggle-rules {:tooltip (ds/meta-shift "R")
:command (ds/c-mod "shift+r")
:fn #(st/emit! (dw/toggle-layout-flags :rules))}
:select-all {:tooltip (ds/meta "A")
:command (ds/c-mod "a")
:fn #(st/emit! (dw/select-all))}
:toggle-grid {:tooltip (ds/meta "'")
:command (ds/c-mod "'")
:fn #(st/emit! (dw/toggle-layout-flags :display-grid))}
:toggle-snap-grid {:tooltip (ds/meta-shift "'")
:command (ds/c-mod "shift+'")
:fn #(st/emit! (dw/toggle-layout-flags :snap-grid))}
:toggle-alignment {:tooltip (ds/meta "\\")
:command (ds/c-mod "\\")
:fn #(st/emit! (dw/toggle-layout-flags :dynamic-alignment))}
:increase-zoom {:tooltip "+"
:command "+"
:fn #(st/emit! (dw/increase-zoom nil))}
:decrease-zoom {:tooltip "-"
:command "-"
:fn #(st/emit! (dw/decrease-zoom nil))}
:group {:tooltip (ds/meta "G")
:command (ds/c-mod "g")
:fn #(st/emit! dw/group-selected)}
:ungroup {:tooltip (ds/shift "G")
:command "shift+g"
:fn #(st/emit! dw/ungroup-selected)}
:mask {:tooltip (ds/meta "M")
:command (ds/c-mod "m")
:fn #(st/emit! dw/mask-group)}
:unmask {:tooltip (ds/meta-shift "M")
:command (ds/c-mod "shift+m")
:fn #(st/emit! dw/unmask-group)}
:create-component {:tooltip (ds/meta "K")
:command (ds/c-mod "k")
:fn #(st/emit! dwl/add-component)}
:flip-vertical {:tooltip (ds/shift "V")
:command "shift+v"
:fn #(st/emit! (dw/flip-vertical-selected))}
:flip-horizontal {:tooltip (ds/shift "V")
:command "shift+h"
:fn #(st/emit! (dw/flip-horizontal-selected))}
:reset-zoom {:tooltip (ds/shift "0")
:command "shift+0"
:fn #(st/emit! dw/reset-zoom)}
:fit-all {:tooltip (ds/shift "1")
:command "shift+1"
:fn #(st/emit! dw/zoom-to-fit-all)}
:zoom-selected {:tooltip (ds/shift "2")
:command "shift+2"
:fn #(st/emit! dw/zoom-to-selected-shape)}
:duplicate {:tooltip (ds/meta "D")
:command (ds/c-mod "d")
:fn #(st/emit! dw/duplicate-selected)}
:undo {:tooltip (ds/meta "Z")
:command (ds/c-mod "z")
:fn #(st/emit! dwc/undo)}
:redo {:tooltip (ds/meta "Y")
:command [(ds/c-mod "shift+z") (ds/c-mod "y")]
:fn #(st/emit! dwc/redo)}
:clear-undo {:tooltip (ds/meta "Q")
:command (ds/c-mod "q")
:fn #(st/emit! dwc/reinitialize-undo)}
:draw-frame {:tooltip "A"
:command "a"
:fn #(st/emit! (dwd/select-for-drawing :frame))}
:draw-rect {:tooltip "R"
:command "r"
:fn #(st/emit! (dwd/select-for-drawing :rect))}
:draw-ellipse {:tooltip "E"
:command "e"
:fn #(st/emit! (dwd/select-for-drawing :circle))}
:draw-text {:tooltip "T"
:command "t"
:fn #(st/emit! dwtxt/start-edit-if-selected
(dwd/select-for-drawing :text))}
:draw-path {:tooltip "P"
:command "p"
:fn #(st/emit! (dwd/select-for-drawing :path))}
:draw-curve {:tooltip (ds/shift "C")
:command "shift+c"
:fn #(st/emit! (dwd/select-for-drawing :curve))}
:add-comment {:tooltip "C"
:command "c"
:fn #(st/emit! (dwd/select-for-drawing :comments))}
:insert-image {:tooltip "K"
:command "k"
:fn #(-> "image-upload" dom/get-element dom/click)}
:copy {:tooltip (ds/meta "C")
:command (ds/c-mod "c")
:fn #(st/emit! (dw/copy-selected))}
:cut {:tooltip (ds/meta "X")
:command (ds/c-mod "x")
:fn #(st/emit! (dw/copy-selected) dw/delete-selected)}
:paste {:tooltip (ds/meta "V")
:disabled true
:command (ds/c-mod "v")}
:delete {:tooltip (ds/supr)
:command ["del" "backspace"]
:fn #(st/emit! dw/delete-selected)}
:bring-forward {:tooltip (ds/meta ds/up-arrow)
:command (ds/c-mod "up")
:fn #(st/emit! (dw/vertical-order-selected :up))}
:bring-backward {:tooltip (ds/meta ds/down-arrow)
:command (ds/c-mod "down")
:fn #(st/emit! (dw/vertical-order-selected :down))}
:bring-front {:tooltip (ds/meta-shift ds/up-arrow)
:command (ds/c-mod "shift+up")
:fn #(st/emit! (dw/vertical-order-selected :top))}
:bring-back {:tooltip (ds/meta-shift ds/down-arrow)
:command (ds/c-mod "shift+down")
:fn #(st/emit! (dw/vertical-order-selected :bottom))}
:move-fast-up {:tooltip (ds/shift ds/up-arrow)
:command "shift+up"
:fn #(st/emit! (dwt/move-selected :up true))}
:move-fast-down {:tooltip (ds/shift ds/down-arrow)
:command "shift+down"
:fn #(st/emit! (dwt/move-selected :down true))}
:move-fast-right {:tooltip (ds/shift ds/right-arrow)
:command "shift+right"
:fn #(st/emit! (dwt/move-selected :right true))}
:move-fast-left {:tooltip (ds/shift ds/left-arrow)
:command "shift+left"
:fn #(st/emit! (dwt/move-selected :left true))}
:move-unit-up {:tooltip ds/up-arrow
:command "up"
:fn #(st/emit! (dwt/move-selected :up false))}
:move-unit-down {:tooltip ds/down-arrow
:command "down"
:fn #(st/emit! (dwt/move-selected :down false))}
:move-unit-left {:tooltip ds/right-arrow
:command "right"
:fn #(st/emit! (dwt/move-selected :right false))}
:move-unit-right {:tooltip ds/left-arrow
:command "left"
:fn #(st/emit! (dwt/move-selected :left false))}
:open-color-picker {:tooltip "I"
:command "i"
:fn #(st/emit! (mdc/picker-for-selected-shape ))}
:open-viewer {:tooltip "G V"
:command "g v"
:fn #(st/emit! (dw/go-to-viewer))}
:open-dashboard {:tooltip "G D"
:command "g d"
:fn #(st/emit! (dw/go-to-dashboard))}
:escape {:tooltip (ds/esc)
:command "escape"
:fn #(st/emit! (esc-pressed))}})
(defn get-tooltip [shortcut]
(assert (contains? shortcuts shortcut) (str shortcut))
(get-in shortcuts [shortcut :tooltip]))

View File

@@ -249,7 +249,7 @@
(assoc :overflow-text true)
(and (= :fixed grow-type) overflow-text (<= new-height shape-height))
(assoc :overflow-text true)
(assoc :overflow-text false)
(and (not-changed? shape-width new-width) (= grow-type :auto-width))
(-> (assoc :modifiers modifier-width)

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