Compare commits

...

148 Commits

Author SHA1 Message Date
Andrey Antukh
e369b70aeb Merge pull request #5279 from penpot/alotor-hotfix-grid-problem
🐛 Fix problem with grid layout ordering after moving
2024-11-11 12:48:40 +01:00
alonso.torres
c3970255e6 🐛 Fix problem with grid layout ordering after moving 2024-11-11 12:34:07 +01:00
Andrey Antukh
7823eaf890 📎 Update changelog 2024-11-11 12:08:49 +01:00
Andrey Antukh
ec0079461e Merge pull request #5262 from penpot/palba-add-libraries-dialog-event
 Send event when an user opens a modal
2024-11-07 20:52:52 +01:00
Pablo Alba
70a1a7a5ea Send event when an user opens a modal 2024-11-07 16:27:16 +01:00
Andrey Antukh
c3dc165c4c Merge pull request #5241 from penpot/bameda-docs-kubernetes-setup
📚 Add documentation to install with Kubernetes
2024-11-07 10:33:49 +01:00
David Barragán Merino
5a3619c737 📚 Add documentation to install with Kubernetes 2024-11-06 14:55:41 +01:00
Pablo Alba
227f06c1ec Merge pull request #5255 from penpot/niwinz-bugfix-1
🐛 Fix null pointer exception on validating nil with number schema
2024-11-06 09:36:14 +01:00
Andrey Antukh
946dac3c9f 🐛 Fix NPE on number schemas
Mainly, without this fix, happens the following:

user=> (sm/validate [::sm/int {:min 0}] nil)
Execution error (NullPointerException) at app.common.schema/fn$fn (schema.cljc:692).
Cannot invoke "Object.getClass()" because "x" is null

And it should return `false` without an exception.
2024-11-06 09:15:06 +01:00
Andrey Antukh
b160ba1793 📎 Update .gitignore 2024-11-06 09:14:47 +01:00
Andrey Antukh
33d51a51d1 📚 Update changelog 2024-11-04 17:29:52 +01:00
Andrey Antukh
ab4be85669 Merge pull request #5250 from penpot/alotor-hotfix-plugins-api-problem
🐛 Fix problem with promises in plugins
2024-11-04 17:27:41 +01:00
alonso.torres
6c0dce580d 🐛 Fix problem with promises in plugins 2024-11-04 17:12:45 +01:00
Andrey Antukh
59050a7bc6 📎 Backport frontend/package.json changes
Mainly for compatibility with the upcoming devenv changes
2024-11-04 16:16:28 +01:00
Andrey Antukh
3334fb0e99 🐛 Add migration to fix invalid pages 2024-11-04 15:34:09 +01:00
Andrey Antukh
24268bbf33 Merge pull request #5248 from penpot/palba-add-event-for-add-frame
 Add event for add-frame
2024-11-04 14:25:19 +01:00
Pablo Alba
cd3f8f0c43 Add event for add-frame 2024-11-04 12:56:34 +01:00
Andrey Antukh
d3a8954605 Merge pull request #5247 from penpot/alotor-fix-plugin-problem
🐛 Fix problem with plugins path positioning
2024-11-04 12:16:25 +01:00
alonso.torres
1cda61e230 🐛 Fix problem with plugins path positioning 2024-11-04 11:02:04 +01:00
Alejandro
aca3e3db4f Merge pull request #5237 from penpot/niwinz-hotfix-2
🐛 Fix incorrect thumbnail lookup on dashboard project view
2024-10-31 16:12:22 +01:00
Andrey Antukh
74f9166f3d Merge pull request #5238 from penpot/bameda-manage-build-docs-bundle
🎉 add command to build docs bundle
2024-10-30 23:15:22 +01:00
David Barragán Merino
977a2090fb 🎉 add command to build docs bundle 2024-10-30 19:17:04 +01:00
Andrey Antukh
14e6ea9393 Merge pull request #5236 from penpot/palba-testab-templates-link2
🎉 Add test A/B for add a link to the libraries page
2024-10-30 16:44:17 +01:00
Andrey Antukh
3eb35f0aa6 🐛 Fix incorrect thumbnail lookup on dashboard project view
That causes a repeated generation of thumbnails on each page
view instead of reusing already generated thumbnails.
2024-10-30 16:19:16 +01:00
Pablo Alba
92b7a35c58 🎉 Add test A/B for add a link to the libraries page 2024-10-30 16:13:05 +01:00
Andrey Antukh
99807b4cd4 Merge pull request #5231 from penpot/bameda-merge-docs-repo
📚 Merge repository penpot/penpot-docs
2024-10-30 13:46:37 +01:00
Andrey Antukh
bff415c7cd 📎 Set yarn 4.3.1 for docs
The same as the rest of packages
2024-10-30 13:33:16 +01:00
Andrey Antukh
1d84835fd5 📎 Move .nvmrc file to the repo root 2024-10-30 13:31:24 +01:00
David Barragán Merino
88296480ec 📚 Merge penpot/penpot-docs repository 2024-10-30 13:30:02 +01:00
Pablo Alba
4f5bc77379 Update libraries links to new versions 2024-10-30 13:28:37 +01:00
Alejandro
3932054ea6 Merge pull request #5222 from penpot/niwinz-bugfix-8
🐛 Backport bugfixes from develop to staging
2024-10-30 11:33:56 +01:00
Andrey Antukh
243fd17305 Merge pull request #5227 from penpot/palba-update-readme
🎉 Update readme with plugins info
2024-10-29 14:50:34 +01:00
Pablo Alba
4b8febd7dc 🎉 Update readme with plugins info 2024-10-29 13:14:15 +01:00
Andrey Antukh
7c73e44ab8 Add minor improvement on error reporting on shape validation 2024-10-29 11:47:47 +01:00
Andrey Antukh
e533762f33 📎 Show version on dbg header 2024-10-29 11:47:45 +01:00
Andrey Antukh
40c118df55 🐛 Fix incorrect pred composition on number schema types
Fixes the following:

  => (sm/validate (sm/schema [::sm/int {:max 10}]) nil)
  Cannot invoke "Object.getClass()" because "x" is null
2024-10-29 11:47:06 +01:00
Andrey Antukh
f43fc282d3 Increase internal s3 http client limits
Tries to improve performance of accidental spikes/bursts of
requests to s3 service. This is not a final solution to all issues
caused by unexpected burst, is a simple improvement to the current
apprach.
2024-10-29 11:47:06 +01:00
Andrey Antukh
8616e2f25c Use penpot own executor for s3 response completion executor 2024-10-29 11:47:06 +01:00
Andrey Antukh
4299fd28f0 Expose ::wrk/executor as ExecutorService instance
Instead of a plain Executor instance
2024-10-29 11:47:06 +01:00
Andrey Antukh
302ff92b31 🐛 Fix incorrect handling of EOF on s3 upload thread 2024-10-29 11:47:06 +01:00
Andrey Antukh
b62cc9c8e9 📎 Update backend scripts/repl with a default config 2024-10-29 11:47:06 +01:00
Andrey Antukh
225c2ca6e6 Add better reporting for s3 storage backend errors 2024-10-29 11:47:06 +01:00
Andrey Antukh
e5bdd852ca 🐛 Fix corner case on selection storage backend from settings
Related to how backward compatibility is handled with previous
settings.
2024-10-29 11:47:06 +01:00
Andrey Antukh
591788403a Add safer mechanism for tempfile naming
Using a uuidv8 that has strong guarranties about councurrent
ids generation that a simple random long
2024-10-29 11:47:06 +01:00
Andrey Antukh
f1b82e289d 🐛 Add retry mechanism for internal tmp file handling on s3 backend 2024-10-29 11:47:06 +01:00
Alejandro
f4ae8ea5ac Merge pull request #5218 from penpot/niwinz-bugfix-10
🐛 Fix issues with invalid fills
2024-10-29 08:57:50 +01:00
Andrey Antukh
d9310d651a 🐛 Fix exception on adding animation to an interraction
Happens when an animation is added to a just created interaction
and then changed to other animation type. Bug is caused by missing
dependency on react handlers.
2024-10-28 18:21:02 +01:00
Andrey Antukh
6b817d102b 🐛 Add migration for a fix of invalid fills 2024-10-28 18:04:27 +01:00
Andrey Antukh
08a9371322 🐛 Use proper ::sm/int schema type on color and shape schemas 2024-10-28 18:04:25 +01:00
Yamila Moreno
f96da090d6 📎 Fix readme badges 2024-10-28 11:07:40 +01:00
Andrey Antukh
8d8f203b8a Merge pull request #5204 from penpot/alotor-bugfix-import
🐛 Fix problem with imports
2024-10-25 14:32:41 +02:00
alonso.torres
f40ffacfbd 🐛 Fix problem with imports 2024-10-25 13:34:40 +02:00
luisddm
ae435f67a5 🐛 Fix intentation and ellipsis in the left sidebar when inspector is activated in viewer mode 2024-10-24 14:37:39 +02:00
Andrey Antukh
d89dfc5e30 Merge pull request #5194 from penpot/alotor-bugfix-grid
🐛 Fix problem with swap components on grid
2024-10-22 17:31:31 +02:00
alonso.torres
cd586c81ee 🐛 Fix problem with swap components on grid 2024-10-22 16:14:14 +02:00
Andrey Antukh
16e1e01234 Merge pull request #5171 from penpot/palba-fix-rulers-on-view-only
🐛 Fix you can manage rulers on view mode
2024-10-21 17:29:40 +02:00
Belén Albeza
fe6c9f24d3 🐛 Fix edit grid unit dropdown being clipped 2024-10-21 10:15:57 +02:00
Pablo Alba
fe314cf146 🐛 Fix you can manage rulers on view mode 2024-10-18 14:03:46 +02:00
Andrey Antukh
97a880c946 Merge pull request #5179 from penpot/alotor-bugfixing-5
Alotor bugfixing 5
2024-10-18 14:00:53 +02:00
alonso.torres
df66955594 🐛 Fix problem with shadows and frames in Safari 2024-10-18 11:52:41 +02:00
alonso.torres
07f055bd49 🐛 Fix problem when duplicating board with guide 2024-10-18 11:52:41 +02:00
alonso.torres
22d5b125bd 🐛 Fix problem with layers overflowing panel 2024-10-18 11:52:40 +02:00
alonso.torres
ef3b4a5895 🐛 Fix problem with plugins icons 2024-10-17 14:51:01 +02:00
Andrey Antukh
02611029fb Merge pull request #5176 from penpot/juan-relesae-notes-2.3
Relesae notes 2.3 on onboarding slides
2024-10-16 16:31:19 +02:00
Elhombretecla
14e4e6d6ea 🎉 Add release note slides for 2.3 2024-10-16 14:33:20 +02:00
Andrey Antukh
9170c70f2a Merge pull request #5169 from penpot/alotor-bugfixing4
🐛 Fix problem with inner strokes bounds
2024-10-15 13:02:53 +02:00
alonso.torres
83d8bf37a6 🐛 Fix problem with inner strokes bounds 2024-10-15 12:28:23 +02:00
Andrey Antukh
1fb21d537c 🐛 Send thread-id on create-comment-thread rpc method 2024-10-15 09:29:40 +02:00
Andrey Antukh
ac80e9a1ac Respect overrides of jvm_opts on devenv bashrc file 2024-10-15 09:13:46 +02:00
Andrey Antukh
dbbb8e76ab Allow override java opts for build scripts 2024-10-15 09:13:46 +02:00
Andrey Antukh
916f055aec Merge pull request #5165 from penpot/alotor-bugfixing-3
Alotor bugfixing 3
2024-10-14 19:16:39 +02:00
alonso.torres
6d8c183160 Add plugins whitelisting for removing the disclaimer 2024-10-14 15:25:37 +02:00
alonso.torres
9d2f484aa3 🐛 Fix problem with horizontal/vertical lines and shadows 2024-10-14 15:25:37 +02:00
alonso.torres
2dc0cfdee3 🐛 Fix problem with caps and inner shadows 2024-10-14 15:25:37 +02:00
alonso.torres
a25abd0ca4 🐛 Fix percent calculation on grid layout tracks 2024-10-14 15:25:37 +02:00
alonso.torres
3a9119cf29 🐛 Add visual feedback when moving an element into a board 2024-10-14 15:25:37 +02:00
alonso.torres
c236e0765b 🐛 Fix problems with show in viewer and interactions 2024-10-14 15:25:37 +02:00
alonso.torres
f8fad95fef 🐛 Fix problem with shortcuts in text editor 2024-10-14 11:45:50 +02:00
alonso.torres
97ae295cb9 🐛 Fix problem updating layout when toggle visibility in component copy 2024-10-14 11:45:27 +02:00
Eva Marco
bd888dcde2 🐛 Fix constraints buttons 2024-10-14 11:41:26 +02:00
Andrey Antukh
784274f8ae Merge pull request #5163 from penpot/palba-bugfixing-011
Palba bugfixing 011
2024-10-14 11:40:45 +02:00
Pablo Alba
eda6c6a4c3 🐛 Fix "Done" button on toolbar on inspect mode should go to design mode 2024-10-11 14:36:57 +02:00
Pablo Alba
7d7594818c 🐛 Fix Internal Error page: "go to your penpot" wrong design 2024-10-11 14:35:56 +02:00
Andrey Antukh
7cc8f67e24 Merge pull request #5161 from penpot/niwinz-bugfix-6
🐛 Fix storybook build
2024-10-11 12:28:41 +02:00
Andrey Antukh
87fc3bbb8e 🐛 Fix storybook build 2024-10-11 12:11:37 +02:00
Andrey Antukh
bbb2cc972f Merge pull request #5159 from penpot/alotor-plugins-fixes-2
Plugins improvements
2024-10-11 09:04:29 +02:00
alonso.torres
6a07e6ae01 Add update plugin permission dialog 2024-10-10 17:12:39 +02:00
Andrey Antukh
87dfd2b3c8 🐛 Force sync update on storage before immediate refresh 2024-10-10 16:04:15 +02:00
Andrey Antukh
b0bfb8006d 💄 Add cosmetic changes to dashboard templates layer 2024-10-10 16:04:15 +02:00
Andrey Antukh
d46274abf2 Add better error reporting on zip file importation 2024-10-10 16:04:15 +02:00
Andrey Antukh
23f7889cff 💄 Add cosmetic change to create-temp-file rpc method 2024-10-10 16:04:15 +02:00
Andrey Antukh
534659cdc6 🐛 Fix flows import and export on zip format 2024-10-10 16:04:15 +02:00
alonso.torres
1e68d4ec87 Close plugin on esc button 2024-10-10 16:03:45 +02:00
alonso.torres
1779fd3e8b Fix zero case for plugins 2024-10-10 16:03:45 +02:00
alonso.torres
3c496ddd9d ⬆️ Update plugins runtime 2024-10-10 16:03:45 +02:00
Andrey Antukh
47bc9d8ef1 Merge pull request #5157 from penpot/alotor-bugfixing-2
Alotor bugfixing 2
2024-10-10 11:45:48 +02:00
alonso.torres
a3a5fe056d 📚 Update changelog 2024-10-10 11:45:16 +02:00
Eero Pitkänen
fbb3271c81 🐛 Fix dragging path points by returning closest point instead of only the distance 2024-10-10 11:45:16 +02:00
alonso.torres
ecc93d9246 🐛 Fix problem with precision on boolean calculation 2024-10-10 11:45:16 +02:00
alonso.torres
302672f5b0 🐛 Fix problem with hover layers when hidden/blocked 2024-10-10 11:45:16 +02:00
alonso.torres
4f16ea2d2d 🐛 Fix problem with stroke and filter ordering in frames 2024-10-10 11:45:12 +02:00
Andrey Antukh
b7a0b7d629 🐛 Increase feedback limits to reasonable values 2024-10-10 11:27:04 +02:00
Andrey Antukh
bd6f1bef10 🐛 Don't raise an unexpected exception on multiple-input enter
When a enter is pressed and field is empty
2024-10-10 11:27:04 +02:00
Andrey Antukh
c4941bb102 🐛 Fix unexpected exception on handling audit log on team invitations
A regression introduced in previous commits of this release
2024-10-10 11:27:04 +02:00
Andrey Antukh
b8a606a35f 🐛 Fix incorrect dependency for log-emails and smtp flags 2024-10-10 11:23:04 +02:00
Andrey Antukh
370eebeb64 🐛 Remove unused shadow config from exporter 2024-10-10 11:23:04 +02:00
Andrey Antukh
35bcb082a0 🐛 Remove data-testid usage from shape 2024-10-10 11:23:04 +02:00
Andrey Antukh
dd220e228e Merge pull request #5152 from penpot/alotor-fix-selection
🐛 Fix problem with selection
2024-10-09 13:50:51 +02:00
alonso.torres
7b63aa4a4f 🐛 Fix problem with selection 2024-10-09 13:34:33 +02:00
Andrey Antukh
33a07346dd 💄 Add minor cmd naming change for e2e test commands 2024-10-09 13:09:01 +02:00
Andrey Antukh
abd77559ab 🐛 Fix svg exportation with shapes with svg-unsafe characters in the name 2024-10-09 13:09:01 +02:00
Andrey Antukh
28878caca9 🐛 Fix cache issues with plugin runtime import uri 2024-10-09 13:09:01 +02:00
Andrey Antukh
74f3379b5d Merge pull request #5150 from penpot/alotor-bugfixing
Alotor bugfixing
2024-10-09 12:16:26 +02:00
alonso.torres
379770343a 🐛 Close plugin if open when installed 2024-10-09 10:50:56 +02:00
alonso.torres
6327286328 ⬆️ Update runtime 2024-10-09 09:39:47 +02:00
alonso.torres
3a2677a91a 🐛 Fix problem with shadows in text for Safari 2024-10-08 15:40:20 +02:00
alonso.torres
fcd232aa35 🐛 Fix problem with go back button on error page 2024-10-08 15:40:20 +02:00
alonso.torres
f194e2c1c6 📚 Updates changelog 2024-10-08 15:34:41 +02:00
Andrey Antukh
ea6731e22b Add EOF handling on sse response helper 2024-10-08 15:30:33 +02:00
Andrey Antukh
002b1679c3 ♻️ Clean assertion and schema chechking API 2024-10-08 15:30:33 +02:00
Andrey Antukh
45f3a67950 Relax transaction requeriments for team invitation creation 2024-10-08 14:51:14 +02:00
Andrey Antukh
c6917bb0cf Relax transaction requirements on create-team rpc method 2024-10-08 14:51:14 +02:00
Andrey Antukh
f777845d14 Relax transaction requirement on comment thread creation rpc method 2024-10-08 14:51:14 +02:00
Andrey Antukh
a1f5bcae80 ♻️ Add better ergonomics for the internal quotes API 2024-10-08 14:51:14 +02:00
Andrey Antukh
3e11b4aa74 Add facility for wrap a rpc method in a db transaction 2024-10-08 14:51:14 +02:00
Aitor Moreno
4f48236fee Merge pull request #5141 from penpot/niwinz-enhancements-text-editor-v2-2
 Add minor improvements to text editor v2 events handling
2024-10-07 12:58:17 +02:00
Andrey Antukh
ffadf29ad7 Add minor improvements to text editor v2 events handling
Also updates the editor code to the latest version
2024-10-07 10:13:21 +02:00
Aitor Moreno
352efcb610 Merge pull request #5139 from penpot/niwinz-enhancements-text-editor-v2
 Add minor improvements for text-editor-v2
2024-10-04 09:38:50 +02:00
Andrey Antukh
334e83479f Add minor improvements for text-editor-v2 2024-10-03 09:51:04 +02:00
Alejandro Alonso
476eedbd2c Merge remote-tracking branch 'origin/staging' into develop 2024-10-03 07:19:53 +02:00
Alejandro
ae7e28b71b Merge pull request #5137 from penpot/niwinz-enhancements-1
 Add limits for invitation creation RPC method
2024-10-03 07:18:18 +02:00
Andrey Antukh
be30174a49 Add limits for team invitations 2024-10-02 16:05:33 +02:00
Alejandro
8373654f80 Merge pull request #5134 from penpot/alotor-hotfix-2.3
Alotor hotfix 2.3
2024-10-02 13:57:05 +02:00
alonso.torres
471c636580 🐛 Fix visual problem with the font-size dropdown in assets 2024-10-02 13:45:50 +02:00
alonso.torres
635c6efe42 🐛 Fix problem with Ctrl+F shortcut on the dashboard 2024-10-02 13:45:30 +02:00
Alejandro
d570048f78 Merge pull request #5132 from penpot/niwinz-bugfix-1
🐛 Fix issues on migration 55
2024-10-02 13:36:43 +02:00
Andrey Antukh
dcc49dafd3 Merge pull request #5029 from penpot/azazeln28-refactor-text-editor
♻️ Refactor text editor
2024-10-02 11:05:26 +02:00
AzazelN28
7398f7ce0d ♻️ Replace Draft.js with custom editor 2024-10-01 22:31:16 +02:00
Andrey Antukh
76479a2486 🐛 Fix page background migration 2024-10-01 16:44:54 +02:00
Andrey Antukh
31f62dcc12 🐛 Fix incorrect flows conversion on migration 55 2024-10-01 16:34:22 +02:00
Andrey Antukh
3d7df5b005 Merge pull request #5115 from penpot/alotor-plugins
Plugins update
2024-10-01 12:53:03 +02:00
alonso.torres
c16a116707 Modifications after review 2024-10-01 11:57:52 +02:00
alonso.torres
f7f06f59ce ⬆️ Upgrade plugin runtime 2024-10-01 09:34:45 +02:00
alonso.torres
d1277afee6 New plugin install workflow 2024-09-30 16:03:40 +02:00
alonso.torres
a510d01136 Plugins api changes 2024-09-30 15:49:46 +02:00
alonso.torres
0e651df65f Updates permissions for comments 2024-09-30 15:20:34 +02:00
alonso.torres
758e0458bc 🐛 Fix problem when returning parent proxy 2024-09-30 15:20:34 +02:00
alonso.torres
e18b4666ba Update permissions dialog 2024-09-30 15:20:34 +02:00
825 changed files with 25779 additions and 3329 deletions

View File

@@ -111,7 +111,7 @@ jobs:
yarn run build:app:assets
clojure -M:dev:shadow-cljs release main
yarn playwright install --with-deps chromium
yarn e2e:test
yarn test:e2e
- run:
name: "backend tests"

2
.gitignore vendored
View File

@@ -74,3 +74,5 @@ node_modules
/playwright-report/
/blob-report/
/playwright/.cache/
/render-wasm/target/
/**/.yarn/*

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v20.11.1

View File

@@ -1,17 +1,83 @@
# CHANGELOG
## 2.3.2
### :bug: Bugs fixed
- Fix null pointer exception on number checking functions
- Fix problem with grid layout ordering after moving [Taiga #9179](https://tree.taiga.io/project/penpot/issue/9179)
### :books: Documentation
- Add initial documentation for Kubernetes
## 2.3.1
### :bug: Bugs fixed
- Fix unexpected issue on interaction between plugins sandbox and
internal impl of promise
## 2.3.0
### :rocket: Epics and highlights
- **New plugin system.**
Penpot now supports custom plugins. Read everything about developing your plugins [HERE](https://help.penpot.app/plugins/)
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
- All our plugins beta testers :heart:.
- Fix problem when translating multiple path points by @eeropic [#4459](https://github.com/penpot/penpot/issues/4459)
### :sparkles: New features
- **Replace Draft.js completely with a custom editor** [Taiga #7706](https://tree.taiga.io/project/penpot/us/7706)
This refactor adds better IME support, more performant text editing
experience and a better clipboard support while keeping full
retrocompatibility with previous editor.
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
### :bug: Bugs fixed
- Fix problem with constraints buttons [Taiga #8465](https://tree.taiga.io/project/penpot/issue/8465)
- Fix problem with go back button on error page [Taiga #8887](https://tree.taiga.io/project/penpot/issue/8887)
- Fix problem with shadows in text for Safari [Taiga #8770](https://tree.taiga.io/project/penpot/issue/8770)
- Fix a regression with feedback form subject and content limits [Taiga #8908](https://tree.taiga.io/project/penpot/issue/8908)
- Fix problem with stroke and filter ordering in frames [Github #5058](https://github.com/penpot/penpot/issues/5058)
- Fix problem with hover layers when hidden/blocked [Github #5074](https://github.com/penpot/penpot/issues/5074)
- Fix problem with precision on boolean calculation [Taiga #8482](https://tree.taiga.io/project/penpot/issue/8482)
- Fix problem when translating multiple path points [Github #4459](https://github.com/penpot/penpot/issues/4459)
- Fix problem on importing (and exporting) files with flows [Taiga #8914](https://tree.taiga.io/project/penpot/issue/8914)
- Fix Internal Error page: "go to your penpot" wrong design [Taiga #8922](https://tree.taiga.io/project/penpot/issue/8922)
- Fix problem updating layout when toggle visibility in component copy [Github #5143](https://github.com/penpot/penpot/issues/5143)
- Fix "Done" button on toolbar on inspect mode should go to design mode [Taiga #8933](https://tree.taiga.io/project/penpot/issue/8933)
- Fix problem with shortcuts in text editor [Github #5078](https://github.com/penpot/penpot/issues/5078)
- Fix problems with show in viewer and interactions [Github #4868](https://github.com/penpot/penpot/issues/4868)
- Add visual feedback when moving an element into a board [Github #3210](https://github.com/penpot/penpot/issues/3210)
- Fix percent calculation on grid layout tracks [Github #4688](https://github.com/penpot/penpot/issues/4688)
- Fix problem with caps and inner shadows [Github #4517](https://github.com/penpot/penpot/issues/4517)
- Fix problem with horizontal/vertical lines and shadows [Github #4516](https://github.com/penpot/penpot/issues/4516)
- Fix problem with layers overflowing panel [Taiga #9021](https://tree.taiga.io/project/penpot/issue/9021)
- Fix in workspace you can manage rulers on view mode [Taiga #8966](https://tree.taiga.io/project/penpot/issue/8966)
- Fix problem with swap components in grid layout [Taiga #9066](https://tree.taiga.io/project/penpot/issue/9066)
## 2.2.1
### :bug: Bugs fixed
- Fix problem with Ctrl+F shortcut on the dashboard [Taiga #8876](https://tree.taiga.io/project/penpot/issue/8876)
- Fix visual problem with the font-size dropdown in assets [Taiga #8872](https://tree.taiga.io/project/penpot/issue/8872)
- Add limits for invitation RPC methods (hard limit 25 emails per request)
## 2.2.0
### :rocket: Epics and highlights
@@ -148,7 +214,7 @@ time being.
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
### :heart: Communityq contributions (Thank you!)
### :sparkles: New features

View File

@@ -8,10 +8,12 @@
<img alt="penpot header image" src="https://penpot.app/images/readme/github-light-mode.png">
</picture>
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
<a href="https://gitter.im/penpot/community" rel="nofollow"><img src="https://camo.githubusercontent.com/5b0aecb33434f82a7b158eab7247544235ada0cf7eeb9ce8e52562dd67f614b7/68747470733a2f2f6261646765732e6769747465722e696d2f736572656e6f2d78797a2f636f6d6d756e6974792e737667" alt="Gitter" data-canonical-src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img src="https://camo.githubusercontent.com/4a1d1112f0272e3393b1e8da312ff4435418e9e2eb4c0964881e3680f90a653c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d616e61676564253230776974682d54414947412e696f2d3730396631342e737667" alt="Managed with Taiga.io" data-canonical-src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
<p align="center">
<a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img alt="License: MPL-2.0" src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
<a href="https://gitter.im/penpot/community" rel="nofollow"><img alt="Gitter" src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img alt="Managed with Taiga.io" src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img alt="Gitpod ready-to-code" src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a>
</p>
<p align="center">
<a href="https://penpot.app/"><b>Website</b></a> •
@@ -58,6 +60,9 @@ Penpots latest [huge release 2.0](https://penpot.app/dev-diaries), takes the
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
### Plugin system ###
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
### Designed for developers ###
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".

View File

@@ -1,15 +1,15 @@
[{:id "wireframing-kit"
:name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/main/wireframing-kit.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}
{:id "prototype-examples"
:name "Prototype template"
:file-uri "https://github.com/penpot/penpot-files/raw/main/prototype-examples.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Prototype%20examples%20v1.1.penpot"}
{:id "plants-app"
:name "UI mockup example"
:file-uri "https://github.com/penpot/penpot-files/raw/main/Plants-app.penpot"}
{:id "penpot-design-system"
:name "Design system example"
:file-uri "https://github.com/penpot/penpot-files/raw/main/Penpot-Design-system.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Penpot%20-%20Design%20System%20v2.1.penpot"}
{:id "tutorial-for-beginners"
:name "Tutorial for beginners"
:file-uri "https://github.com/penpot/penpot-files/raw/main/tutorial-for-beginners.penpot"}
@@ -36,7 +36,7 @@
:file-uri "https://github.com/penpot/penpot-files/raw/main/Open%20Color%20Scheme%20(v1.9.1).penpot"}
{:id "flex-layout-playground"
:name "Flex Layout Playground"
:file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Flex%20Layout%20Playground%20v2.0.penpot"}
{:id "welcome"
:name "Welcome"
:file-uri "https://github.com/penpot/penpot-files/raw/main/welcome.penpot"}]

View File

@@ -7,7 +7,7 @@ Debug Main Page
{% block content %}
<nav>
<div class="title">
<h1>ADMIN DEBUG INTERFACE</h1>
<h1>ADMIN DEBUG INTERFACE (VERSION: {{version}})</h1>
</div>
</nav>
<main class="dashboard">

View File

@@ -23,6 +23,7 @@ export PENPOT_FLAGS="\
enable-urepl-server \
enable-rpc-climit \
enable-rpc-rlimit \
enable-quotes \
enable-soft-rpc-rlimit \
enable-auto-file-snapshot \
enable-webhooks \
@@ -67,6 +68,7 @@ export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
export PENPOT_OBJECTS_STORAGE_FS_DIRECTORY="assets"
export OPTIONS="
-A:jmx-remote -A:dev \

View File

@@ -17,6 +17,7 @@ export PENPOT_FLAGS="\
disable-secure-session-cookies \
enable-rpc-climit \
enable-smtp \
enable-quotes \
enable-file-snapshot \
enable-access-tokens \
enable-tiered-file-data-storage \

View File

@@ -315,15 +315,13 @@
(l/dbg :hint "sendmail"
:id (:id params)
:to (:to params)
:subject (str/trim (:subject params))
:body (str/join "," (map :type (:body params))))
:subject (str/trim (:subject params)))
(.sendMessage ^Transport transport
^MimeMessage message
(.getAllRecipients message))))))
(when (or (contains? cf/flags :log-emails)
(not (contains? cf/flags :smtp)))
(when (contains? cf/flags :log-emails)
(send-to-logger! cfg params))))
(defmethod ig/pre-init-spec ::handler [_]

View File

@@ -47,7 +47,7 @@
{::rres/status 200
::rres/headers {"content-type" "text/html"}
::rres/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {}))})
(tmpl/render {:version (:full cf/version)}))})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE CHANGES

View File

@@ -60,6 +60,9 @@
(try
(let [result (handler)]
(events/tap :end result))
(catch java.io.EOFException cause
(events/tap :error (errors/handle' cause request)))
(catch Throwable cause
(l/err :hint "unexpected error on processing sse response"
:cause cause)

View File

@@ -278,25 +278,18 @@
:inc 1)
message)
(def ^:private schema:params
[:map {:title "params"}
[:session-id ::sm/uuid]])
(def ^:private decode-params
(sm/decoder schema:params sm/json-transformer))
(def ^:private validate-params!
(sm/validate-fn schema:params))
(defn- http-handler
[cfg {:keys [params ::session/profile-id] :as request}]
(let [{:keys [session-id]} (-> params
decode-params
validate-params!)]
(let [session-id (some-> params :session-id sm/parse-uuid)]
(when-not (uuid? session-id)
(ex/raise :type :validation
:code :missing-session-id
:hint "missing or invalid session-id found"))
(cond
(not profile-id)
(ex/raise :type :authentication
:hint "Authentication required.")
:hint "authentication required")
;; WORKAROUND: we use the adapter specific predicate for
;; performance reasons; for now, the ring default impl for

View File

@@ -63,7 +63,7 @@
(ex/format-throwable cause :data? false :explain? false :header? false :summary? false))}
(when-let [params (or (:request/params context) (:params context))]
{:params (pp/pprint-str params :length 30 :level 12)})
{:params (pp/pprint-str params :length 30 :level 13)})
(when-let [value (:value context)]
{:value (pp/pprint-str value :length 30 :level 12)})

View File

@@ -135,7 +135,7 @@
(l/dbg :hint "run webhook"
:event-name (:name event)
:webhook-id (:id whook)
:webhook-id (str (:id whook))
:webhook-uri (:uri whook)
:webhook-mtype (:mtype whook))

View File

@@ -475,7 +475,8 @@
::sto.s3/bucket (or (cf/get :storage-assets-s3-bucket)
(cf/get :objects-storage-s3-bucket))
::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads)
(cf/get :objects-storage-s3-io-threads))}
(cf/get :objects-storage-s3-io-threads))
::wrk/executor (ig/ref ::wrk/executor)}
:app.storage.fs/backend
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)

View File

@@ -149,6 +149,13 @@
:hint "authentication required for this endpoint")
(f cfg params)))))
(defn- wrap-db-transaction
[_ f mdata]
(if (::db/transaction mdata)
(fn [cfg params]
(db/tx-run! cfg f params))
f))
(defn- wrap-audit
[_ f mdata]
(if (or (contains? cf/flags :webhooks)
@@ -196,6 +203,7 @@
(defn- wrap-all
[cfg f mdata]
(as-> f $
(wrap-db-transaction cfg $ mdata)
(cond/wrap cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
(climit/wrap cfg $ mdata)

View File

@@ -30,18 +30,17 @@
:tid token-id
:iat created-at})
expires-at (some-> expiration dt/in-future)]
(db/insert! conn :access-token
{:id token-id
:name name
:token token
:profile-id profile-id
:created-at created-at
:updated-at created-at
:expires-at expires-at
:perms (db/create-array conn "text" [])})))
expires-at (some-> expiration dt/in-future)
token (db/insert! conn :access-token
{:id token-id
:name name
:token token
:profile-id profile-id
:created-at created-at
:updated-at created-at
:expires-at expires-at
:perms (db/create-array conn "text" [])})]
(decode-row token)))
(defn repl:create-access-token
[{:keys [::db/pool] :as system} profile-id name expiration]
@@ -60,14 +59,12 @@
(sv/defmethod ::create-access-token
{::doc/added "1.18"
::sm/params schema:create-access-token}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg ::db/conn conn)]
(quotes/check-quote! conn
{::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id})
(-> (create-access-token cfg profile-id name expiration)
(decode-row)))))
[cfg {:keys [::rpc/profile-id name expiration]}]
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id})
(db/tx-run! cfg create-access-token profile-id name expiration))
(def ^:private schema:delete-access-token
[:map {:title "delete-access-token"}

View File

@@ -71,10 +71,15 @@
[conn comment-id & {:as opts}]
(db/get-by-id conn :comment comment-id opts))
(def ^:private sql:get-next-seqn
"SELECT (f.comment_thread_seqn + 1) AS next_seqn
FROM file AS f
WHERE f.id = ?
FOR UPDATE")
(defn- get-next-seqn
[conn file-id]
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
res (db/exec-one! conn [sql file-id])]
(let [res (db/exec-one! conn [sql:get-next-seqn file-id])]
(:next-seqn res)))
(def sql:upsert-comment-thread-status
@@ -304,38 +309,43 @@
::rtry/when rtry/conflict-exception?
::sm/params schema:create-comment-thread}
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
(files/check-comment-permissions! cfg profile-id file-id share-id)
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-comment-permissions! cfg profile-id file-id share-id)
(let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)]
(let [{:keys [team-id project-id page-name]} (get-file cfg file-id page-id)]
(run! (partial quotes/check-quote! cfg)
(list {::quotes/id ::quotes/comment-threads-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id}
{::quotes/id ::quotes/comments-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id}))
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id team-id)
(assoc ::quotes/project-id project-id)
(assoc ::quotes/file-id file-id)
(quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
{::quotes/id ::quotes/comments-per-file}))
(create-comment-thread conn {:created-at request-at
:profile-id profile-id
:file-id file-id
:page-id page-id
:page-name page-name
:position position
:content content
:frame-id frame-id})))))
(let [params {:created-at request-at
:profile-id profile-id
:file-id file-id
:page-id page-id
:page-name page-name
:position position
:content content
:frame-id frame-id}
thread (db/tx-run! cfg create-comment-thread params)]
(vary-meta thread assoc ::audit/props thread))))
(defn- create-comment-thread
[conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
[{:keys [::db/conn] :as cfg}
{:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
(let [;; NOTE: we take the next seq number from a separate query
;; because we need to lock the file for avoid race conditions
;; FIXME: this method touches and locks the file table,which
;; is already heavy-update tablel; we need to think on move
;; the sequence state management to a different table or
;; different storage (example: redis) for alivate the update
;; pression on the file table
(let [;; NOTE: we take the next seq number from a separate query because the whole
;; operation can be retried on conflict, and in this case the new seq shold be
;; retrieved from the database.
seqn (get-next-seqn conn file-id)
thread-id (uuid/next)
thread (db/insert! conn :comment-thread
@@ -364,7 +374,8 @@
;; Optimistic update of current seq number on file.
(db/update! conn :file
{:comment-thread-seqn seqn}
{:id file-id})
{:id file-id}
{::db/return-keys false})
(-> thread
(select-keys [:id :file-id :page-id])
@@ -387,7 +398,6 @@
(files/check-comment-permissions! conn profile-id file-id share-id)
(upsert-comment-thread-status! conn profile-id id)))))
;; --- COMMAND: Update Comment Thread
(def ^:private
@@ -432,12 +442,11 @@
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(quotes/check-quote! conn
{::quotes/id ::quotes/comments-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id})
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id})
;; Update the page-name cached attribute on comment thread table.
(when (not= page-name (:page-name thread))

View File

@@ -21,8 +21,8 @@
(def ^:private schema:send-user-feedback
[:map {:title "send-user-feedback"}
[:subject [:string {:max 250}]]
[:content [:string {:max 250}]]])
[:subject [:string {:max 400}]]
[:content [:string {:max 2500}]]])
(sv/defmethod ::send-user-feedback
{::doc/added "1.18"

View File

@@ -356,7 +356,7 @@
f.name,
f.revn,
f.is_shared,
ft.media_id
ft.media_id AS thumbnail_id
from file as f
left join file_thumbnail as ft on (ft.file_id = f.id
and ft.revn = f.revn
@@ -367,13 +367,7 @@
(defn get-project-files
[conn project-id]
(->> (db/exec! conn [sql:project-files project-id])
(mapv (fn [row]
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(dissoc row :media-id))))))
(db/exec! conn [sql:project-files project-id]))
(def schema:get-project-files
[:map {:title "get-project-files"}

View File

@@ -98,46 +98,49 @@
{::doc/added "1.17"
::doc/module :files
::webhooks/event? true
::sm/params schema:create-file}
[cfg {:keys [::rpc/profile-id project-id] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn
:profile-id profile-id
:project-id project-id)
team-id (:id team)
::sm/params schema:create-file
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn
:profile-id profile-id
:project-id project-id)
team-id (:id team)
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)))
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)))
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features (-> (:features params #{})
(set/intersection cfeat/no-migration-features)
(set/union features))
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features (-> (:features params #{})
(set/intersection cfeat/no-migration-features)
(set/union features))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))]
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
::quotes/profile-id profile-id
::quotes/project-id project-id}))
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
::quotes/profile-id profile-id
::quotes/project-id project-id})
;; When newly computed features does not match exactly with
;; the features defined on team row, we update it.
(when (not= features (:features team))
(let [features (db/create-array conn "text" features)]
(db/update! conn :team
{:features features}
{:id team-id})))
;; FIXME: IMPORTANT: this code can have race
;; conditions, because we have no locks for updating
;; team so, creating two files concurrently can lead
;; to lost team features updating
(-> (create-file cfg params)
(vary-meta assoc ::audit/props {:team-id team-id}))))))
;; When newly computed features does not match exactly with
;; the features defined on team row, we update it.
(when (not= features (:features team))
(let [features (db/create-array conn "text" features)]
(db/update! conn :team
{:features features}
{:id team-id})))
(-> (create-file cfg params)
(vary-meta assoc ::audit/props {:team-id team-id}))))

View File

@@ -45,37 +45,38 @@
(sv/defmethod ::create-temp-file
{::doc/added "1.17"
::doc/module :files
::sm/params schema:create-temp-file}
[cfg {:keys [::rpc/profile-id project-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
::sm/params schema:create-temp-file
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
input-features
(:features params #{})
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
input-features (:features params #{})
;; If the imported project doesn't contain v2 we need to remove it
team-features
(cond-> (cfeat/get-team-enabled-features cf/flags team)
(not (contains? input-features "components/v2"))
(disj "components/v2"))
;; If the imported project doesn't contain v2 we need to remove it
team-features
(cond-> (cfeat/get-team-enabled-features cf/flags team)
(not (contains? input-features "components/v2"))
(disj "components/v2"))
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features
(-> input-features
(set/intersection cfeat/no-migration-features)
(set/union team-features))
params
(-> params
(assoc :profile-id profile-id)
(assoc :deleted-at (dt/in-future {:days 1}))
(assoc :features features))]
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features (-> input-features
(set/intersection cfeat/no-migration-features)
(set/union team-features))
params (-> params
(assoc :profile-id profile-id)
(assoc :deleted-at (dt/in-future {:days 1}))
(assoc :features features))]
(files.create/create-file cfg params)))))
(files.create/create-file cfg params)))
;; --- MUTATION COMMAND: update-temp-file

View File

@@ -86,6 +86,9 @@
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
[:font-style [::sm/one-of {:format "string"} valid-style]]])
;; FIXME: IMPORTANT: refactor this, we should not hold a whole db
;; connection around the font creation
(sv/defmethod ::create-font-variant
{::doc/added "1.18"
::climit/id [[:process-font/by-profile ::rpc/profile-id]
@@ -96,9 +99,9 @@
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(quotes/check! cfg {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(create-font-variant cfg (assoc params :profile-id profile-id)))))
(defn create-font-variant

View File

@@ -168,6 +168,17 @@
;; --- MUTATION: Create Project
(defn- create-project
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}]
(let [project (teams/create-project conn params)]
(teams/create-project-role conn profile-id (:id project) :owner)
(db/insert! conn :team-project-profile-rel
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned false})
(assoc project :is-pinned false)))
(def ^:private schema:create-project
[:map {:title "create-project"}
[:team-id ::sm/uuid]
@@ -178,23 +189,15 @@
{::doc/added "1.18"
::webhooks/event? true
::sm/params schema:create-project}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
[cfg {:keys [::rpc/profile-id team-id] :as params}]
(let [params (assoc params :profile-id profile-id)
project (teams/create-project conn params)]
(teams/create-project-role conn profile-id (:id project) :owner)
(db/insert! conn :team-project-profile-rel
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned false})
(assoc project :is-pinned false))))
(teams/check-edition-permissions! cfg profile-id team-id)
(quotes/check! cfg {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(let [params (assoc params :profile-id profile-id)]
(db/tx-run! cfg create-project params)))
;; --- MUTATION: Toggle Project Pin

View File

@@ -82,19 +82,17 @@
(cond-> row
(some? features) (assoc :features (db/decode-pgarray features #{}))))
(defn- check-valid-email-muted
"Check if the member's email is part of the global bounce report."
(defn- check-profile-muted
"Check if the member's email is part of the global bounce report"
[conn member]
(let [email (profile/clean-email (:email member))]
(let [email (profile/clean-email (:email member))]
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
:code :member-is-muted
:email email
:hint "the profile has reported repeatedly as spam or has bounces"))))
(defn- check-valid-email-bounce
(defn- check-email-bounce
"Check if the email is part of the global complain report"
[conn email show?]
(when (eml/has-bounce-reports? conn email)
@@ -103,7 +101,7 @@
:email (if show? email "private")
:hint "this email has been repeatedly reported as bounce")))
(defn- check-valid-email-spam
(defn- check-email-spam
"Check if the member email is part of the global complain report"
[conn email show?]
(when (eml/has-complaint-reports? conn email)
@@ -227,16 +225,16 @@
;; --- Query: Team Members
(def sql:team-members
"select tp.*,
"SELECT tp.*,
p.id,
p.email,
p.fullname as name,
p.fullname as fullname,
p.fullname AS name,
p.fullname AS fullname,
p.photo_id,
p.is_active
from team_profile_rel as tp
join profile as p on (p.id = tp.profile_id)
where tp.team_id = ?")
FROM team_profile_rel AS tp
JOIN profile AS p ON (p.id = tp.profile_id)
WHERE tp.team_id = ?")
(defn get-team-members
[conn team-id]
@@ -403,17 +401,19 @@
{::doc/added "1.17"
::sm/params schema:create-team}
[cfg {:keys [::rpc/profile-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id})
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
team (create-team cfg (assoc params
:profile-id profile-id
:features features))]
(with-meta team
{::audit/props {:id (:id team)}})))))
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id})
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
team (db/tx-run! cfg create-team params)]
(with-meta team
{::audit/props {:id (:id team)}})))
(defn create-team
"This is a complete team creation process, it creates the team
@@ -767,21 +767,51 @@
:member-id member-id}))
(defn- create-profile-identity-token
[cfg profile]
[cfg profile-id]
(dm/assert!
"expected valid uuid for profile-id"
(uuid? profile-id))
(tokens/generate (::setup/props cfg)
{:iss :profile-identity
:profile-id (:id profile)
:profile-id profile-id
:exp (dt/in-future {:days 30})}))
(def ^:private schema:create-invitation
[:map {:title "params:create-invitation"}
[::rpc/profile-id ::sm/uuid]
[:team
[:map
[:id ::sm/uuid]
[:name :string]]]
[:profile
[:map
[:id ::sm/uuid]
[:fullname :string]]]
[:role [::sm/one-of valid-roles]]
[:email ::sm/email]])
(def ^:private check-create-invitation-params!
(sm/check-fn schema:create-invitation))
(defn- create-invitation
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
(dm/assert!
"expected valid connection on cfg parameter"
(db/connection? conn))
(dm/assert!
"expected valid params for `create-invitation` fn"
(check-create-invitation-params! params))
(let [email (profile/clean-email email)
member (profile/get-profile-by-email conn email)]
(check-valid-email-muted conn member)
(check-valid-email-bounce conn email true)
(check-valid-email-spam conn email true)
(check-profile-muted conn member)
(check-email-bounce conn email true)
(check-email-spam conn email true)
;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the
@@ -815,7 +845,8 @@
(name role) expire
(name role) expire])
updated? (not= id (:id invitation))
tprops {:profile-id (:id profile)
profile-id (:id profile)
tprops {:profile-id profile-id
:invitation-id (:id invitation)
:valid-until expire
:team-id (:id team)
@@ -823,12 +854,11 @@
:member-id (:id member)
:role role}
itoken (create-invitation-token cfg tprops)
ptoken (create-profile-identity-token cfg profile)]
ptoken (create-profile-identity-token cfg profile-id)]
(when (contains? cf/flags :log-invitation-tokens)
(l/info :hint "invitation token" :token itoken))
(let [props (-> (dissoc tprops :profile-id)
(audit/clean-props))
evname (if updated?
@@ -851,26 +881,27 @@
itoken))))
(defn- add-user-to-team
[conn profile team email role]
[conn profile team role email]
(let [team-id (:id team)
member (db/get* conn :profile
{:email (str/lower email)}
{::sql/columns [:id :email]})
params (merge
{:team-id team-id
:profile-id (:id member)}
(role->params role))]
member (db/get* conn :profile
{:email (str/lower email)}
{::sql/columns [:id :email]})
params (merge
{:team-id team-id
:profile-id (:id member)}
(role->params role))]
;; Do not allow blocked users to join teams.
(when (:is-blocked member)
(ex/raise :type :restriction
:code :profile-blocked))
(quotes/check-quote! conn
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
(quotes/check!
{::db/conn conn
::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
;; Insert the member to the team
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
@@ -902,68 +933,89 @@
[conn team-id]
(db/exec! conn [sql:valid-requests-email team-id]))
(def ^:private xf:map-email
(map :email))
(defn- create-team-invitations
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
(let [join-requests (into #{} xf:map-email
(get-valid-requests-email conn (:id team)))
team-members (into #{} xf:map-email
(get-team-members conn (:id team)))
invitations (into #{}
(comp
;; We don't re-send inviation to
;; already existing members
(remove team-members)
;; We don't send invitations to
;; join-requested members
(remove join-requests)
(map (fn [email] (assoc params :email email)))
(keep (partial create-invitation cfg)))
emails)]
;; For requested invitations, do not send invitation emails, add
;; the user directly to the team
(->> (filter join-requests emails)
(run! (partial add-user-to-team conn profile team role)))
invitations))
(def ^:private schema:create-team-invitations
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
[:role schema:role]
[:emails [::sm/set ::sm/email]]])
(def ^:private max-invitations-by-request-threshold
"The number of invitations can be sent in a single rpc request"
25)
(sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to
join the team."
{::doc/added "1.17"
::sm/params schema:create-team-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)
emails (into #{} (map profile/clean-email) emails)]
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
(let [perms (get-permissions cfg profile-id team-id)
profile (db/get-by-id cfg :profile profile-id)
emails (into #{} (map profile/clean-email) emails)]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
;; Check if the current profile is allowed to send emails.
(check-valid-email-muted conn profile)
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id team-id)
(assoc ::quotes/incr (count emails))
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
;; Check if the current profile is allowed to send emails
(check-profile-muted cfg profile)
(let [requested (into #{} (map :email) (get-valid-requests-email conn team-id))
emails-to-add (filter #(contains? requested %) emails)
emails (remove #(contains? requested %) emails)
cfg (assoc cfg ::db/conn conn)
members (->> (db/exec! conn [sql:team-members team-id])
(into #{} (map :email)))
(let [team (db/get-by-id cfg :team team-id)
;; NOTE: Is important pass RPC method params down to the
;; `create-team-invitations` because it uses the implicit
;; RPC properties from params for fill necessary data on
;; emiting an entry to the audit-log
invitations (db/tx-run! cfg create-team-invitations
(-> params
(assoc :profile profile)
(assoc :team team)
(assoc :emails emails)))]
invitations (into #{}
(comp
;; We don't re-send inviation to already existing members
(remove (partial contains? members))
(map (fn [email]
(-> params
(assoc :email email)
(assoc :team team)
(assoc :profile profile)
(assoc :role role))))
(keep (partial create-invitation cfg)))
emails)]
;; For requested invitations, do not send invitation emails, add the user directly to the team
(doseq [email emails-to-add]
(add-user-to-team conn profile team email role))
(with-meta {:total (count invitations)
:invitations invitations}
{::audit/props {:invitations (count invitations)}})))))
(with-meta {:total (count invitations)
:invitations invitations}
{::audit/props {:invitations (count invitations)}}))))
;; --- Mutation: Create Team & Invite Members
@@ -977,52 +1029,50 @@
(sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"
::sm/params schema:create-team-with-invitations}
[cfg {:keys [::rpc/profile-id emails role name] :as params}]
::sm/params schema:create-team-with-invitations
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
team (create-team cfg params)
emails (into #{} (map profile/clean-email) emails)]
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)
emails (into #{} (map profile/clean-email) emails)]
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id (:id team))
(assoc ::quotes/incr (count emails))
(quotes/check! {::quotes/id ::quotes/teams-per-profile}
{::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
(let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team")
(assoc ::audit/props props))]
(audit/submit! cfg event))
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
;; Create invitations for all provided emails.
(->> emails
(map (fn [email]
(-> params
(assoc :team team)
(assoc :profile profile)
(assoc :email email)
(assoc :role role))))
(run! (partial create-invitation cfg)))
(let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team")
(assoc ::audit/props props))]
(audit/submit! cfg event))
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}
{::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
;; Create invitations for all provided emails.
(let [profile (db/get-by-id conn :profile profile-id)
params (-> params
(assoc :team team)
(assoc :profile profile)
(assoc :role role))
invitations (->> emails
(map (fn [email] (assoc params :email email)))
(map (partial create-invitation cfg)))]
(vary-meta team assoc ::audit/props {:invitations (count emails)})))))
(vary-meta team assoc ::audit/props {:invitations (count invitations)}))))
;; --- Query: get-team-invitation-token
@@ -1215,11 +1265,11 @@
:code :invalid-parameters))
;; Check that the requester is not muted
(check-valid-email-muted conn requester)
(check-profile-muted conn requester)
;; Check that the owner is not marked as bounce nor spam
(check-valid-email-bounce conn (:email team-owner) false)
(check-valid-email-spam conn (:email team-owner) true)
(check-email-bounce conn (:email team-owner) false)
(check-email-spam conn (:email team-owner) true)
(let [request (create-team-access-request
cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})]

View File

@@ -37,14 +37,12 @@
::doc/added "1.15"
::doc/module :auth
::sm/params schema:verify-token}
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool]
(let [claims (tokens/verify (::setup/props cfg) {:token token})
cfg (assoc cfg :conn conn)]
(process-token cfg params claims))))
[cfg {:keys [token] :as params}]
(let [claims (tokens/verify (::setup/props cfg) {:token token})]
(db/tx-run! cfg process-token params claims)))
(defmethod process-token :change-email
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id email] :as claims}]
(let [email (profile/clean-email email)]
(when (profile/get-profile-by-email conn email)
(ex/raise :type :validation
@@ -60,7 +58,7 @@
::audit/profile-id profile-id})))
(defmethod process-token :verify-email
[{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}]
[{:keys [::db/conn] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id)
claims (assoc claims :profile profile)]
@@ -81,14 +79,14 @@
::audit/profile-id (:id profile)}))))
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id)]
(assoc claims :profile profile)))
;; --- Team Invitation
(defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
[{:keys [::db/conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
(let [;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role)
params (merge
@@ -101,10 +99,9 @@
(ex/raise :type :restriction
:code :profile-blocked))
(quotes/check-quote! conn
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
@@ -140,7 +137,7 @@
(sm/lazy-validator schema:team-invitation-claims))
(defmethod process-token :team-invitation
[{:keys [conn] :as cfg}
[{:keys [::db/conn] :as cfg}
{:keys [::rpc/profile-id token] :as params}
{:keys [member-id team-id member-email] :as claims}]

View File

@@ -7,16 +7,13 @@
(ns app.rpc.quotes
"Penpot resource usage quotes."
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(defmulti check-quote ::id)
@@ -34,6 +31,9 @@
[::id :keyword]
[::profile-id ::sm/uuid]])
(def valid-quote?
(sm/lazy-validator schema:quote))
(def ^:private enabled (volatile! true))
(defn enable!
@@ -46,20 +46,31 @@
[]
(vswap! enabled (constantly false)))
(defn check-quote!
[ds quote]
(dm/assert!
"expected valid quote map"
(sm/validate schema:quote quote))
(defn- check
[cfg quote]
(let [quote (merge cfg quote)
id (::id quote)]
(when (contains? cf/flags :quotes)
(when @enabled
;; This approach add flexibility on how and where the
;; check-quote! can be called (in or out of transaction)
(db/run! ds (fn [cfg]
(-> (merge cfg quote)
(assoc ::target (name (::id quote)))
(check-quote)))))))
(when-not (valid-quote? quote)
(ex/raise :type :internal
:code :invalid-quote-definition
:hint "found invalid data for quote schema"
:quote (name id)))
(-> (assoc quote ::target (name id))
(check-quote))))
(defn check!
([cfg]
(when (contains? cf/flags :quotes)
(when @enabled
(db/run! cfg check {}))))
([cfg & others]
(when (contains? cf/flags :quotes)
(when @enabled
(db/run! cfg (fn [cfg]
(run! (partial check cfg) others)))))))
(defn- send-notification!
[{:keys [::db/conn] :as params}]
@@ -100,7 +111,7 @@
(map :quote)
(reduce max (- Integer/MAX_VALUE)))
quote (if (pos? quote) quote default)
total (->> (db/exec! conn count-sql) first :total)]
total (:total (db/exec-one! conn count-sql))]
(when (> (+ total incr) quote)
(if (contains? cf/flags :soft-quotes)
@@ -112,72 +123,81 @@
:count total)))))
(def ^:private sql:get-quotes-1
"select id, quote from usage_quote
where target = ?
and profile_id = ?
and team_id is null
and project_id is null
and file_id is null;")
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND profile_id = ?
AND team_id IS NULL
AND project_id IS NULL
AND file_id IS NULL;")
(def ^:private sql:get-quotes-2
"select id, quote from usage_quote
where target = ?
and ((team_id = ? and (profile_id = ? or profile_id is null)) or
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND ((team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
(def ^:private sql:get-quotes-3
"select id, quote from usage_quote
where target = ?
and ((project_id = ? and (profile_id = ? or profile_id is null)) or
(team_id = ? and (profile_id = ? or profile_id is null)) or
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND ((project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
(def ^:private sql:get-quotes-4
"select id, quote from usage_quote
where target = ?
and ((file_id = ? and (profile_id = ? or profile_id is null)) or
(project_id = ? and (profile_id = ? or profile_id is null)) or
(team_id = ? and (profile_id = ? or profile_id is null)) or
(profile_id = ? and team_id is null and project_id is null and file_id is null));")
"SELECT id, quote
FROM usage_quote
WHERE target = ?
AND ((file_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: TEAMS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-teams-per-profile
"select count(*) as total
from team_profile_rel
where profile_id = ?")
(def ^:private schema:teams-per-profile
[:map [::profile-id ::sm/uuid]])
(s/def ::profile-id ::us/uuid)
(s/def ::teams-per-profile
(s/keys :req [::profile-id ::target]))
(def ^:private valid-teams-per-profile-quote?
(sm/lazy-validator schema:teams-per-profile))
(def ^:private sql:get-teams-per-profile
"SELECT count(*) AS total
FROM team_profile_rel
WHERE profile_id = ?")
(defmethod check-quote ::teams-per-profile
[{:keys [::profile-id ::target] :as quote}]
(us/assert! ::teams-per-profile quote)
(assert (valid-teams-per-profile-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
(assoc ::count-sql [sql:get-teams-per-profile profile-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: ACCESS-TOKENS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-access-tokens-per-profile
"select count(*) as total
from access_token
where profile_id = ?")
(def ^:private schema:access-tokens-per-profile
[:map [::profile-id ::sm/uuid]])
(s/def ::access-tokens-per-profile
(s/keys :req [::profile-id ::target]))
(def ^:private valid-access-tokens-per-profile-quote?
(sm/lazy-validator schema:access-tokens-per-profile))
(def ^:private sql:get-access-tokens-per-profile
"SELECT count(*) AS total
FROM access_token
WHERE profile_id = ?")
(defmethod check-quote ::access-tokens-per-profile
[{:keys [::profile-id ::target] :as quote}]
(us/assert! ::access-tokens-per-profile quote)
(assert (valid-access-tokens-per-profile-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
@@ -188,40 +208,51 @@
;; QUOTE: PROJECTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-projects-per-team
"select count(*) as total
from project as p
where p.team_id = ?
and p.deleted_at is null")
(def ^:private schema:projects-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::team-id ::us/uuid)
(s/def ::projects-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(def ^:private valid-projects-per-team-quote?
(sm/lazy-validator schema:projects-per-team))
(def ^:private sql:get-projects-per-team
"SELECT count(*) AS total
FROM project AS p
WHERE p.team_id = ?
AND p.deleted_at IS NULL")
(defmethod check-quote ::projects-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-projects-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-projects-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FONT-VARIANTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-font-variants-per-team
"select count(*) as total
from team_font_variant as v
where v.team_id = ?")
(def ^:private schema:font-variants-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::font-variants-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(def ^:private valid-font-variant-per-team-quote?
(sm/lazy-validator schema:font-variants-per-team))
(def ^:private sql:get-font-variants-per-team
"SELECT count(*) AS total
FROM team_font_variant AS v
WHERE v.team_id = ?")
(defmethod check-quote ::font-variants-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::font-variants-per-team quote)
(assert (valid-font-variant-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
@@ -233,70 +264,86 @@
;; QUOTE: INVITATIONS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-invitations-per-team
"select count(*) as total
from team_invitation
where team_id = ?")
(def ^:private schema:invitations-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::invitations-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(def ^:private valid-invitations-per-team-quote?
(sm/lazy-validator schema:invitations-per-team))
(def ^:private sql:get-invitations-per-team
"SELECT count(*) AS total
FROM team_invitation
WHERE team_id = ?")
(defmethod check-quote ::invitations-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::invitations-per-team quote)
(assert (valid-invitations-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-invitations-per-team team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: PROFILES-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:profiles-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-profiles-per-team-quote?
(sm/lazy-validator schema:profiles-per-team))
(def ^:private sql:get-profiles-per-team
"select (select count(*)
from team_profile_rel
where team_id = ?) +
(select count(*)
from team_invitation
where team_id = ?
and valid_until > now()) as total;")
"SELECT (SELECT count(*)
FROM team_profile_rel
WHERE team_id = ?) +
(SELECT count(*)
FROM team_invitation
WHERE team_id = ?
AND valid_until > now()) AS total;")
;; NOTE: the total number of profiles is determined by the number of
;; effective members plus ongoing valid invitations.
(s/def ::profiles-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(defmethod check-quote ::profiles-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::profiles-per-team quote)
(assert (valid-profiles-per-team-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FILES-PER-PROJECT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-files-per-project
"select count(*) as total
from file as f
where f.project_id = ?
and f.deleted_at is null")
(def ^:private schema:files-per-project
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::project-id ::us/uuid)
(s/def ::files-per-project
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(def ^:private valid-files-per-project-quote?
(sm/lazy-validator schema:files-per-project))
(def ^:private sql:get-files-per-project
"SELECT count(*) AS total
FROM file AS f
WHERE f.project_id = ?
AND f.deleted_at IS NULL")
(defmethod check-quote ::files-per-project
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
(us/assert! ::files-per-project quote)
(assert (valid-files-per-project-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
@@ -307,17 +354,24 @@
;; QUOTE: COMMENT-THREADS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comment-threads-per-file
"select count(*) as total
from comment_thread as ct
where ct.file_id = ?")
(def ^:private schema:comment-threads-per-file
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::comment-threads-per-file
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(def ^:private valid-comment-threads-per-file-quote?
(sm/lazy-validator schema:comment-threads-per-file))
(def ^:private sql:get-comment-threads-per-file
"SELECT count(*) AS total
FROM comment_thread AS ct
WHERE ct.file_id = ?")
(defmethod check-quote ::comment-threads-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(us/assert! ::files-per-project quote)
(assert (valid-comment-threads-per-file-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
@@ -325,23 +379,28 @@
(assoc ::count-sql [sql:get-comment-threads-per-file file-id])
(generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: COMMENTS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comments-per-file
"select count(*) as total
from comment as c
join comment_thread as ct on (ct.id = c.thread_id)
where ct.file_id = ?")
(def ^:private schema:comments-per-file
[:map
[::profile-id ::sm/uuid]
[::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::comments-per-file
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(def ^:private valid-comments-per-file-quote?
(sm/lazy-validator schema:comments-per-file))
(def ^:private sql:get-comments-per-file
"SELECT count(*) AS total
FROM comment AS c
JOIN comment_thread AS ct ON (ct.id = c.thread_id)
WHERE ct.file_id = ?")
(defmethod check-quote ::comments-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(us/assert! ::files-per-project quote)
(assert (valid-comments-per-file-quote? quote) "invalid quote parameters")
(-> quote
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id

View File

@@ -10,6 +10,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.http.client :as http]
@@ -19,26 +20,26 @@
[datoteka.fs :as fs]
[integrant.core :as ig]))
(def ^:private
schema:template
(def ^:private schema:template
[:map {:title "Template"}
[:id ::sm/word-string]
[:name ::sm/word-string]
[:file-uri ::sm/word-string]])
(def ^:private
schema:templates
(def ^:private schema:templates
[:vector schema:template])
(def check-templates!
(sm/check-fn schema:templates
:code :invalid-templates
:hint "invalid templates"))
(defmethod ig/init-key ::setup/templates
[_ _]
(let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string)
templates (check-templates! templates)
dest (fs/join fs/*cwd* "builtin-templates")]
(dm/verify!
"expected a valid templates file"
(sm/check! schema:templates templates))
(doseq [{:keys [id path] :as template} templates]
(let [path (or path (fs/join dest id))]
(if (fs/exists? path)
@@ -58,9 +59,9 @@
(let [resp (http/req! cfg
{:method :get :uri (:file-uri template)}
{:response-type :input-stream :sync? true})]
(dm/verify!
"unexpected response found on fetching template"
(= 200 (:status resp)))
(when-not (= 200 (:status resp))
(ex/raise :type :internal
:code :unexpected-status-code
:hint (str "unable to download template, recevied status " (:status resp))))
(io/input-stream (:body resp)))))))

View File

@@ -155,9 +155,10 @@
(defn enable-team-feature!
[team-id feature]
(dm/verify!
"feature should be supported"
(contains? cfeat/supported-features feature))
(when-not (contains? cfeat/supported-features feature)
(ex/raise :type :assertion
:code :feature-not-supported
:hint (str "feature '" feature "' not supported")))
(let [team-id (h/parse-uuid team-id)]
(db/tx-run! main/system
@@ -173,9 +174,11 @@
(defn disable-team-feature!
[team-id feature]
(dm/verify!
"feature should be supported"
(contains? cfeat/supported-features feature))
(when-not (contains? cfeat/supported-features feature)
(ex/raise :type :assertion
:code :feature-not-supported
:hint (str "feature '" feature "' not supported")))
(let [team-id (h/parse-uuid team-id)]
(db/tx-run! main/system
@@ -203,9 +206,11 @@
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
:or {code :generic level :info}
:as params}]
(dm/verify!
["invalid level %" level]
(contains? #{:success :error :info :warning} level))
(when-not (contains? #{:success :error :info :warning} level)
(ex/raise :type :assertion
:code :incorrect-level
:hint (str "level '" level "' not supported")))
(letfn [(send [dest]
(l/inf :hint "sending notification" :dest (str dest))

View File

@@ -10,6 +10,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -19,6 +20,7 @@
[app.storage.s3 :as ss3]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[integrant.core :as ig])
(:import
@@ -30,7 +32,7 @@
(case name
:assets-fs :fs
:assets-s3 :s3
:fs)))
nil)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Storage Module State
@@ -52,11 +54,19 @@
(defmethod ig/init-key ::storage
[_ {:keys [::backends ::db/pool] :as cfg}]
(-> (d/without-nils cfg)
(assoc ::backends (d/without-nils backends))
(assoc ::backend (or (get-legacy-backend)
(cf/get :objects-storage-backend :fs)))
(assoc ::db/connectable pool)))
(let [backend (or (get-legacy-backend)
(cf/get :objects-storage-backend)
:fs)
backends (d/without-nils backends)]
(l/dbg :hint "initialize"
:default (d/name backend)
:available (str/join "," (map d/name (keys backends))))
(-> (d/without-nils cfg)
(assoc ::backends backends)
(assoc ::backend backend)
(assoc ::db/connectable pool))))
(s/def ::backend keyword?)
(s/def ::storage

View File

@@ -17,6 +17,7 @@
[app.storage.impl :as impl]
[app.storage.tmp :as tmp]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[datoteka.fs :as fs]
@@ -27,17 +28,15 @@
java.io.FilterInputStream
java.io.InputStream
java.net.URI
java.nio.ByteBuffer
java.nio.file.Path
java.time.Duration
java.util.Collection
java.util.Optional
java.util.concurrent.Semaphore
org.reactivestreams.Subscriber
org.reactivestreams.Subscription
software.amazon.awssdk.core.ResponseBytes
software.amazon.awssdk.core.async.AsyncRequestBody
software.amazon.awssdk.core.async.AsyncResponseTransformer
software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
@@ -59,6 +58,20 @@
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest))
(def ^:private max-retries
"A maximum number of retries on internal operations"
3)
(def ^:private max-concurrency
"Maximum concurrent request to S3 service"
128)
(def ^:private max-pending-connection-acquires
20000)
(def default-timeout
(dt/duration {:seconds 30}))
(declare put-object)
(declare get-object-bytes)
(declare get-object-data)
@@ -80,7 +93,7 @@
(s/def ::io-threads ::us/integer)
(defmethod ig/pre-init-spec ::backend [_]
(s/keys :opt [::region ::bucket ::prefix ::endpoint ::io-threads]))
(s/keys :opt [::region ::bucket ::prefix ::endpoint ::io-threads ::wrk/executor]))
(defmethod ig/prep-key ::backend
[_ {:keys [::prefix ::region] :as cfg}]
@@ -128,18 +141,29 @@
[backend object]
(us/assert! ::backend backend)
(let [result (p/await (get-object-data backend object))]
(if (ex/exception? result)
(cond
(ex/instance? NoSuchKeyException result)
(ex/raise :type :not-found
:code :object-not-found
:hint "s3 object not found"
:cause result)
:else
(throw result))
(loop [result (get-object-data backend object)
retryn 0]
result)))
(let [result (p/await result)]
(if (ex/exception? result)
(cond
(ex/instance? NoSuchKeyException result)
(ex/raise :type :not-found
:code :object-not-found
:hint "s3 object not found"
:object-id (:id object)
:object-path (impl/id->path (:id object))
:cause result)
(and (ex/instance? java.nio.file.FileAlreadyExistsException result)
(< retryn max-retries))
(recur (get-object-data backend object)
(inc retryn))
:else
(throw result))
result))))
(defmethod impl/get-object-bytes :s3
[backend object]
@@ -163,18 +187,14 @@
;; --- HELPERS
(def default-timeout
(dt/duration {:seconds 30}))
(defn- lookup-region
^Region
[region]
(Region/of (name region)))
(defn- build-s3-client
[{:keys [::region ::endpoint ::io-threads]}]
(let [executor (px/resolve-executor :virtual)
aconfig (-> (ClientAsyncConfiguration/builder)
[{:keys [::region ::endpoint ::io-threads ::wrk/executor]}]
(let [aconfig (-> (ClientAsyncConfiguration/builder)
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
(.build))
@@ -190,6 +210,8 @@
(.connectionTimeout default-timeout)
(.readTimeout default-timeout)
(.writeTimeout default-timeout)
(.maxConcurrency (int max-concurrency))
(.maxPendingConnectionAcquires (int max-pending-connection-acquires))
(.build))
client (let [builder (S3AsyncClient/builder)
@@ -223,69 +245,38 @@
(.serviceConfiguration ^S3Configuration config)
(.build))))
(defn- upload-thread
[id subscriber sem content]
(px/thread
{:name "penpot/s3/uploader"
:virtual true
:daemon true}
(l/trace :hint "start upload thread"
:object-id (str id)
:size (impl/get-size content)
::l/sync? true)
(let [stream (io/input-stream content)
bsize (* 1024 64)
tpoint (dt/tpoint)]
(try
(loop []
(.acquire ^Semaphore sem 1)
(let [buffer (byte-array bsize)
readed (.read ^InputStream stream buffer)]
(when (pos? readed)
(let [data (ByteBuffer/wrap ^bytes buffer 0 readed)]
(.onNext ^Subscriber subscriber ^ByteBuffer data)
(when (= readed bsize)
(recur))))))
(.onComplete ^Subscriber subscriber)
(catch InterruptedException _
(l/trace :hint "interrupted upload thread"
:object-:id (str id)
::l/sync? true)
nil)
(catch Throwable cause
(.onError ^Subscriber subscriber cause))
(finally
(l/trace :hint "end upload thread"
:object-id (str id)
:elapsed (dt/format-duration (tpoint))
::l/sync? true)
(.close ^InputStream stream))))))
(defn- write-input-stream
[delegate input]
(try
(.writeInputStream ^BlockingInputStreamAsyncRequestBody delegate
^InputStream input)
(catch Throwable cause
(l/error :hint "encountered error while writing input stream to service"
:cause cause))
(finally
(.close ^InputStream input))))
(defn- make-request-body
[id content]
(reify
AsyncRequestBody
(contentLength [_]
(Optional/of (long (impl/get-size content))))
(^void subscribe [_ ^Subscriber subscriber]
(let [sem (Semaphore. 0)
thr (upload-thread id subscriber sem content)]
(.onSubscribe subscriber
(reify Subscription
(cancel [_]
(px/interrupt! thr)
(.release sem 1))
(request [_ n]
(.release sem (int n)))))))))
[executor content]
(let [size (impl/get-size content)]
(reify
AsyncRequestBody
(contentLength [_]
(Optional/of (long size)))
(^void subscribe [_ ^Subscriber subscriber]
(let [delegate (AsyncRequestBody/forBlockingInputStream (long size))
input (io/input-stream content)]
(px/run! executor (partial write-input-stream delegate input))
(.subscribe ^BlockingInputStreamAsyncRequestBody delegate
^Subscriber subscriber))))))
(defn- put-object
[{:keys [::client ::bucket ::prefix]} {:keys [id] :as object} content]
[{:keys [::client ::bucket ::prefix ::wrk/executor]} {:keys [id] :as object} content]
(let [path (dm/str prefix (impl/id->path id))
mdata (meta object)
mtype (:content-type mdata "application/octet-stream")
rbody (make-request-body id content)
rbody (make-request-body executor content)
request (.. (PutObjectRequest/builder)
(bucket bucket)
(contentType mtype)

View File

@@ -11,13 +11,16 @@
permanently delete these files (look at systemd-tempfiles)."
(:require
[app.common.logging :as l]
[app.common.uuid :as uuid]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[datoteka.fs :as fs]
[integrant.core :as ig]
[promesa.exec :as px]
[promesa.exec.csp :as sp]))
[promesa.exec.csp :as sp])
(:import
java.nio.file.Files))
(def default-tmp-dir "/tmp/penpot")
@@ -76,11 +79,9 @@
[& {:keys [suffix prefix min-age]
:or {prefix "penpot."
suffix ".tmp"}}]
(let [path (fs/create-tempfile
:perms "rw-r--r--"
:dir default-tmp-dir
:suffix suffix
:prefix prefix)]
(let [attrs (fs/make-permissions "rw-r--r--")
path (fs/join default-tmp-dir (str prefix (uuid/next) suffix))
path (Files/createFile path attrs)]
(fs/delete-on-exit! path)
(sp/offer! queue [path (some-> min-age dt/duration)])
path))

View File

@@ -17,12 +17,11 @@
[integrant.core :as ig]
[promesa.exec :as px])
(:import
java.util.concurrent.Executor
java.util.concurrent.ThreadPoolExecutor))
(set! *warn-on-reflection* true)
(s/def ::wrk/executor #(instance? Executor %))
(s/def ::wrk/executor #(instance? ThreadPoolExecutor %))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EXECUTOR
@@ -36,30 +35,22 @@
(let [factory (px/thread-factory :prefix "penpot/default/")
executor (px/cached-executor :factory factory :keepalive 60000)]
(l/inf :hint "executor started")
(reify
java.lang.AutoCloseable
(close [_]
(l/inf :hint "stoping executor")
(px/shutdown! executor))
clojure.lang.IDeref
(deref [_]
{:active (.getPoolSize ^ThreadPoolExecutor executor)
:running (.getActiveCount ^ThreadPoolExecutor executor)
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
Executor
(execute [_ runnable]
(.execute ^Executor executor ^Runnable runnable)))))
executor))
(defmethod ig/halt-key! ::wrk/executor
[_ instance]
(.close ^java.lang.AutoCloseable instance))
(px/shutdown! instance))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MONITOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- get-stats
[^ThreadPoolExecutor executor]
{:active (.getPoolSize ^ThreadPoolExecutor executor)
:running (.getActiveCount ^ThreadPoolExecutor executor)
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
(s/def ::name ::us/keyword)
(defmethod ig/pre-init-spec ::wrk/monitor [_]
@@ -74,7 +65,7 @@
[_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}]
(letfn [(monitor! [executor prev-completed]
(let [labels (into-array String [(d/name name)])
stats (deref executor)
stats (get-stats executor)
completed (:completed stats)
completed-inc (- completed prev-completed)

View File

@@ -21,7 +21,7 @@
(t/use-fixtures :each th/database-reset)
(t/deftest ttf-font-upload-1
(with-mocks [mock {:target 'app.rpc.quotes/check-quote! :return nil}]
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)

View File

@@ -108,14 +108,6 @@
`(do ~@body)
(reverse (partition 2 bindings))))
(defmacro check
"Applies a predicate to the value, if result is true, return the
value if not, returns nil."
[pred-fn value]
`(if (~pred-fn ~value)
~value
nil))
(defmacro get-prop
"A macro based, optimized variant of `get` that access the property
directly on CLJS, on CLJ works as get."
@@ -124,47 +116,32 @@
(list 'js* (c/str "(~{}?." (str/snake prop) "?? ~{})") obj (list 'cljs.core/get obj prop))
(list `c/get obj prop)))
(def ^:dynamic *assert-context* nil)
(defn runtime-assert
[hint f]
(try
(when-not (f)
(throw (ex-info hint {:type :assertion
:code :expr-validation
:hint hint})))
(catch #?(:clj Throwable :cljs :default) cause
(let [data (-> (ex-data cause)
(assoc :type :assertion)
(assoc :code :expr-validation)
(assoc :hint hint))]
(throw (ex-info hint data cause))))))
(defmacro assert!
([expr]
`(assert! nil ~expr))
([hint expr]
(let [hint (cond
(vector? hint)
`(str/ffmt ~@hint)
(let [hint (cond
(vector? hint)
`(str/ffmt ~@hint)
(some? hint)
hint
(some? hint)
hint
:else
(str "expr assert: " (pr-str expr)))]
:else
(str "expr assert: " (pr-str expr)))]
(when *assert*
`(binding [*assert-context* ~hint]
(when-not ~expr
(let [hint# ~hint
params# {:type :assertion
:code :expr-validation
:hint hint#}]
(throw (ex-info hint# params#)))))))))
(defmacro verify!
([expr]
`(verify! nil ~expr))
([hint expr]
(let [hint (cond
(vector? hint)
`(str/ffmt ~@hint)
(some? hint)
hint
:else
(str "expr assert: " (pr-str expr)))]
`(binding [*assert-context* ~hint]
(when-not ~expr
(let [hint# ~hint
params# {:type :assertion
:code :expr-validation
:hint hint#}]
(throw (ex-info hint# params#))))))))
`(runtime-assert ~hint (fn [] ~expr))))))

View File

@@ -49,7 +49,8 @@
"components/v2"
"styles/v2"
"layout/grid"
"plugins/runtime"})
"plugins/runtime"
"text-editor/v2"})
;; A set of features enabled by default
(def default-features
@@ -64,7 +65,8 @@
;; team feature field
(def frontend-only-features
#{"styles/v2"
"plugins/runtime"})
"plugins/runtime"
"text-editor/v2"})
;; Features that are mainly backend only or there are a proper
;; fallback when frontend reports no support for it
@@ -81,7 +83,8 @@
"fdata/pointer-map"
"layout/grid"
"fdata/shape-data-type"
"plugins/runtime"}
"plugins/runtime"
"text-editor/v2"}
(into frontend-only-features)))
(sm/register! ::features
@@ -101,6 +104,7 @@
:feature-fdata-objects-map "fdata/objects-map"
:feature-fdata-pointer-map "fdata/pointer-map"
:feature-plugins "plugins/runtime"
:feature-text-editor-v2 "text-editor/v2"
nil))
(defn migrate-legacy-features

View File

@@ -414,10 +414,12 @@
;; If object has changed or is new verify is correct
(when (and (some? shape-new)
(not= shape-old shape-new))
(dm/verify!
"expected valid shape"
(and (cts/valid-shape? shape-new)
(cts/shape? shape-new))))))]
(when-not (and (cts/valid-shape? shape-new)
(cts/shape? shape-new))
(ex/raise :type :assertion
:code :data-validation
:hint "invalid shape found after applying changes"
::sm/explain (cts/explain-shape shape-new))))))]
(->> (into #{} (map :page-id) items)
(mapcat (fn [page-id]
@@ -465,7 +467,7 @@
#?(:clj (validate-shapes! data result items))
result))))
;; DEPRECATED: remove before 2.3 release
;; DEPRECATED: remove after 2.3 release
(defmethod process-change :set-option
[data _]
data)

View File

@@ -6,4 +6,4 @@
(ns app.common.files.defaults)
(def version 55)
(def version 57)

View File

@@ -13,6 +13,7 @@
[app.common.files.defaults :as cfd]
[app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.path :as gsp]
@@ -1056,9 +1057,14 @@
(not (contains? page :default-grids)))
(assoc :default-grids (:saved-grids options))
(and (some? (:background options))
(not (contains? page :background)))
(assoc :background (:background options))
(and (some? (:flows options))
(not (contains? page :flows)))
(assoc :flows (:flows options))
(or (not (contains? page :flows))
(not (map? (:flows page)))))
(assoc :flows (d/index-by :id (:flows options)))
(and (some? (:guides options))
(not (contains? page :guides)))
@@ -1070,6 +1076,60 @@
(update data :pages-index d/update-vals update-page)))
(defn migrate-up-56
[data]
(letfn [(fix-fills [object]
(d/update-when object :fills (partial filterv valid-fill?)))
(update-object [object]
(-> object
(fix-fills)
;; If shape contains shape-ref but has a nil value, we
;; should remove it from shape object
(cond-> (and (contains? object :shape-ref)
(nil? (get object :shape-ref)))
(dissoc :shape-ref))
;; The text shape also can has fills on the text
;; fragments so we need to fix fills there
(cond-> (cfh/text-shape? object)
(update :content (partial txt/transform-nodes identity fix-fills)))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(defn migrate-up-57
[data]
(letfn [(fix-thread-positions [positions]
(reduce-kv (fn [result id {:keys [position] :as data}]
(let [data (cond
(gpt/point? position)
data
(and (map? position)
(gpt/valid-point-attrs? position))
(assoc data :position (gpt/point position))
:else
(assoc data :position (gpt/point 0 0)))]
(assoc result id data)))
positions
positions))
(update-page [page]
(d/update-when page :comment-thread-positions fix-thread-positions))]
(-> data
(update :pages (fn [pages] (into [] (remove nil?) pages)))
(update :pages-index dissoc nil)
(update :pages-index update-vals update-page))))
(def migrations
"A vector of all applicable migrations"
[{:id 2 :migrate-up migrate-up-2}
@@ -1116,4 +1176,7 @@
{:id 52 :migrate-up migrate-up-52}
{:id 53 :migrate-up migrate-up-26}
{:id 54 :migrate-up migrate-up-54}
{:id 55 :migrate-up migrate-up-55}])
{:id 55 :migrate-up migrate-up-55}
{:id 56 :migrate-up migrate-up-56}
{:id 57 :migrate-up migrate-up-57}])

View File

@@ -56,6 +56,9 @@
[:x ::sm/safe-number]
[:y ::sm/safe-number]])
(def valid-point-attrs?
(sm/validator schema:point-attrs))
(def valid-point?
(sm/validator
[:and [:fn point?] schema:point-attrs]))

View File

@@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]))
(defn shape-stroke-margin
@@ -60,6 +61,7 @@
filter-y (mth/min y (+ y offset-y (- spread) (- blur) -5))
filter-w (+ w (mth/abs offset-x) (* spread 2) (* blur 2) 10)
filter-h (+ h (mth/abs offset-y) (* spread 2) (* blur 2) 10)]
(grc/make-rect filter-x filter-y filter-w filter-h)))
(defn get-rect-filter-bounds
@@ -96,12 +98,15 @@
([shape ignore-margin?]
(let [strokes (:strokes shape)
open-path? (and ^boolean (cfh/path-shape? shape)
^boolean (gsh/open-path? shape))
stroke-width
(->> strokes
(map #(case (get % :stroke-alignment :center)
:center (/ (:stroke-width % 0) 2)
:outer (:stroke-width % 0)
0))
(if open-path? (:stroke-width % 0) 0)))
(reduce d/max 0))
stroke-margin

View File

@@ -852,8 +852,10 @@
(defn ray-overlaps?
[ray-point {selrect :selrect}]
(and (>= (:y ray-point) (:y1 selrect))
(<= (:y ray-point) (:y2 selrect))))
(and (or (> (:y ray-point) (:y1 selrect))
(mth/almost-zero? (- (:y ray-point) (:y1 selrect))))
(or (< (:y ray-point) (:y2 selrect))
(mth/almost-zero? (- (:y ray-point) (:y2 selrect))))))
(defn content->geom-data
[content]

View File

@@ -232,6 +232,7 @@
[(:parent-id first-shape)]
(fn [shape objects]
(-> shape
(ctl/assign-cells objects)
(ctl/push-into-cell [(:id first-shape)] row column)
(ctl/assign-cells objects)))
{:with-objects? true})
@@ -1988,7 +1989,8 @@
(+ (:position guide) (- (:y new-frame) (:y frame))))
guide {:id guide-id
:frame-id new-id
:position position}]
:position position
:axis (:axis guide)}]
(pcb/set-guide changes guide-id guide))
changes))
changes

View File

@@ -124,7 +124,7 @@
;; All parents of any deleted shape must be resized.
(into res (cfh/get-parent-ids objects id)))
(d/ordered-set)
ids-to-delete)
(concat ids-to-delete ids-to-hide))
all-children
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
@@ -391,13 +391,14 @@
(-> (pcb/update-shapes
[parent-id]
(fn [frame objects]
(-> frame
;; Assign the cell when pushing into a specific grid cell
(cond-> (some? cell)
(-> (ctl/free-cell-shapes ids)
(ctl/push-into-cell ids (:row cell) (:column cell))
(ctl/assign-cells objects)))
(ctl/assign-cell-positions objects)))
(let [[row column] cell]
(-> frame
;; Assign the cell when pushing into a specific grid cell
(cond-> (some? cell)
(-> (ctl/free-cell-shapes ids)
(ctl/push-into-cell ids row column)
(ctl/assign-cells objects)))
(ctl/assign-cell-positions objects))))
{:with-objects? true})
(pcb/reorder-grid-children [parent-id])))
@@ -408,17 +409,12 @@
;; Resize parent containers that need to
(pcb/resize-parents parents))))
(defn change-show-in-viewer [shape hide?]
(cond-> (assoc shape :hide-in-viewer hide?)
;; When a frame is no longer shown in view mode, it cannot have interactions
hide?
(dissoc :interactions)))
(assoc shape :hide-in-viewer hide?))
(defn add-new-interaction [shape interaction]
(-> shape
(update :interactions ctsi/add-interaction interaction)
;; When a interaction is created, the frame must be shown in view mode
(dissoc :hide-in-viewer)))
(update :interactions ctsi/add-interaction interaction)))
(defn show-in-viewer [shape]
(dissoc shape :hide-in-viewer))

View File

@@ -9,7 +9,6 @@
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.pprint :as pp]
[app.common.schema.generators :as sg]
[app.common.schema.openapi :as-alias oapi]
@@ -243,59 +242,35 @@
(defn- fast-check!
"A fast path for checking process, assumes the ILazySchema protocol
implemented on the provided `s` schema. Sould not be used directly."
[s value]
[s type code hint value]
(when-not ^boolean (-validate s value)
(let [hint (d/nilv dm/*assert-context* "check error")
explain (-explain s value)]
(throw (ex-info hint {:type :assertion
:code :data-validation
(let [explain (-explain s value)]
(throw (ex-info hint {:type type
:code code
:hint hint
::explain explain}))))
true)
value)
(declare ^:private lazy-schema)
(defn check-fn
"Create a predefined check function"
[s]
(let [schema (if (lazy-schema? s) s (lazy-schema s))]
(partial fast-check! schema)))
[s & {:keys [hint type code]}]
(let [schema (if (lazy-schema? s) s (lazy-schema s))
hint (or ^boolean hint "check error")
type (or ^boolean type :assertion)
code (or ^boolean code :data-validation)]
(partial fast-check! schema type code hint)))
(defn check!
"A helper intended to be used on assertions for validate/check the
schema over provided data. Raises an assertion exception, should be
used together with `dm/assert!` or `dm/verify!`."
[s value]
(let [s (if (lazy-schema? s) s (lazy-schema s))]
(fast-check! s value)))
(defn- fast-validate!
"A fast path for validation process, assumes the ILazySchema protocol
implemented on the provided `s` schema. Sould not be used directly."
([s value] (fast-validate! s value nil))
([s value options]
(when-not ^boolean (-validate s value)
(let [explain (-explain s value)
options (into {:type :validation
:code :data-validation
::explain explain}
options)
hint (get options :hint "schema validation error")]
(throw (ex-info hint options))))
value))
(defn validate-fn
"Create a predefined validate function that raises an expception"
[s]
(let [schema (if (lazy-schema? s) s (lazy-schema s))]
(partial fast-validate! schema)))
(defn validate!
"A generic validation function for predefined schemas."
([s value] (validate! s value nil))
([s value options]
(let [s (if (lazy-schema? s) s (lazy-schema s))]
(fast-validate! s value options))))
schema over provided data. Raises an assertion exception."
[s value & {:keys [hint type code]}]
(let [s (if (lazy-schema? s) s (lazy-schema s))
hint (or ^boolean hint "check error")
type (or ^boolean type :assertion)
code (or ^boolean code :data-validation)]
(fast-check! s type code hint value)))
(defn register! [type s]
(let [s (if (map? s) (m/-simple-schema s) s)]
@@ -391,7 +366,7 @@
(defn parse-email
[s]
(if (string? s)
(re-matches email-re s)
(first (re-seq email-re s))
nil))
(defn email-string?
@@ -706,13 +681,13 @@
(let [pred int?
pred (if (some? min)
(fn [v]
(and (>= v min)
(pred v)))
(and (pred v)
(>= v min)))
pred)
pred (if (some? max)
(fn [v]
(and (>= max v)
(pred v)))
(and (pred v)
(>= max v)))
pred)]
{:pred pred
@@ -744,13 +719,13 @@
(let [pred double?
pred (if (some? min)
(fn [v]
(and (>= v min)
(pred v)))
(and (pred v)
(>= v min)))
pred)
pred (if (some? max)
(fn [v]
(and (>= max v)
(pred v)))
(and (pred v)
(>= max v)))
pred)]
{:pred pred
@@ -774,13 +749,13 @@
(let [pred number?
pred (if (some? min)
(fn [v]
(and (>= v min)
(pred v)))
(and (pred v)
(>= v min)))
pred)
pred (if (some? max)
(fn [v]
(and (>= max v)
(pred v)))
(and (pred v)
(>= max v)))
pred)
gen (sg/one-of
@@ -1005,6 +980,12 @@
(def check-email!
(check-fn ::email))
(def check-uuid!
(check-fn ::uuid :hint "expected valid uuid instance"))
(def check-string!
(check-fn :string :hint "expected string"))
(def check-coll-of-uuid!
(check-fn ::coll-of-uuid))

View File

@@ -10,6 +10,7 @@
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
@@ -29,12 +30,12 @@
{:x 0 :y 0 :width 1 :height 1})
(defn- assert-valid-num [attr num]
(dm/verify!
["%1 attribute has invalid value: %2" (d/name attr) num]
(and (d/num? num)
(<= num max-safe-int)
(>= num min-safe-int)))
(when-not (and (d/num? num)
(<= num max-safe-int)
(>= num min-safe-int))
(ex/raise :type :assertion
:code :data-validation
:hint (str "invalid numeric value for `" attr "`: " num)))
(cond
(and (> num 0) (< num 1)) 1
(and (< num 0) (> num -1)) -1
@@ -43,19 +44,21 @@
(defn- assert-valid-pos-num
[attr num]
(dm/verify!
["%1 attribute should be positive" (d/name attr)]
(pos? num))
(when-not (pos? num)
(ex/raise :type :assertion
:code :data-validation
:hint (str "invalid numeric value for `" attr "`: " num " (should be positive)")))
num)
(defn- assert-valid-blend-mode
[mode]
(let [clean-value (-> mode str/trim str/lower keyword)]
(dm/verify!
["%1 is not a valid blend mode" clean-value]
(contains? cts/blend-modes clean-value))
clean-value))
(let [value (-> mode str/trim str/lower keyword)]
(when-not (contains? cts/blend-modes value)
(ex/raise :type :assertion
:code :data-validation
:hint (str "unexpected blend mode: " value)))
value))
(defn- svg-dimensions
[{:keys [attrs] :as data}]

View File

@@ -78,6 +78,12 @@
(def text-all-attrs (d/concat-set shape-attrs root-attrs paragraph-attrs text-node-attrs))
(def text-style-attrs
(d/concat-vec root-attrs paragraph-attrs text-node-attrs))
(def default-root-attrs
{:vertical-align "top"})
(def default-text-attrs
{:typography-ref-file nil
:typography-ref-id nil
@@ -92,9 +98,13 @@
:text-transform "none"
:text-align "left"
:text-decoration "none"
:text-direction "ltr"
:fills [{:fill-color clr/black
:fill-opacity 1}]})
(def default-attrs
(merge default-root-attrs default-text-attrs))
(def typography-fields
[:font-id
:font-family

View File

@@ -56,8 +56,8 @@
(def schema:image-color
[:map {:title "ImageColor"}
[:name {:optional true} :string]
[:width :int]
[:height :int]
[:width ::sm/int]
[:height ::sm/int]
[:mtype {:optional true} [:maybe :string]]
[:id ::sm/uuid]
[:keep-aspect-ratio {:optional true} :boolean]])
@@ -116,7 +116,7 @@
(sm/register! ::color-attrs schema:color-attrs)
(def check-color!
(sm/check-fn schema:color))
(sm/check-fn schema:color :hint "expected valid color struct"))
(def check-recent-color!
(sm/check-fn schema:recent-color))

View File

@@ -224,8 +224,8 @@
[:map {:title "ImageAttrs"}
[:metadata
[:map
[:width {:gen/gen (sg/small-int :min 1)} :int]
[:height {:gen/gen (sg/small-int :min 1)} :int]
[:width {:gen/gen (sg/small-int :min 1)} ::sm/int]
[:height {:gen/gen (sg/small-int :min 1)} ::sm/int]
[:mtype {:optional true
:gen/gen (sg/elements ["image/jpeg"
"image/png"])}
@@ -355,11 +355,15 @@
(sm/check-fn schema:shape-attrs))
(def check-shape!
(sm/check-fn schema:shape))
(sm/check-fn schema:shape
:hint "expected valid shape"))
(def valid-shape?
(sm/lazy-validator schema:shape))
(def explain-shape
(sm/lazy-explainer schema:shape))
(defn has-images?
[{:keys [fills strokes]}]
(or (some :fill-image fills)

View File

@@ -1,75 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.logic.hide-in-viewer-test
(:require
[app.common.files.changes-builder :as pcb]
[app.common.logic.shapes :as cls]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.types.shape.interactions :as ctsi]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(t/deftest test-remove-show-in-view-mode-delete-interactions
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame :frame-dest)
(tho/add-frame :frame-origin)
(ths/add-interaction :frame-origin :frame-dest))
frame-origin (ths/get-shape file :frame-origin)
page (thf/current-page file)
;; ==== Action
changes (-> (pcb/empty-changes nil (:id page))
(pcb/with-objects (:objects page))
(pcb/update-shapes [(:id frame-origin)] #(cls/change-show-in-viewer % true)))
file' (thf/apply-changes file changes)
;; ==== Get
frame-origin' (ths/get-shape file' :frame-origin)]
;; ==== Check
(t/is (some? (:interactions frame-origin)))
(t/is (nil? (:interactions frame-origin')))))
(t/deftest test-add-new-interaction-updates-show-in-view-mode
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-frame :frame-dest :hide-in-viewer true)
(tho/add-frame :frame-origin :hide-in-viewer true))
frame-dest (ths/get-shape file :frame-dest)
frame-origin (ths/get-shape file :frame-origin)
page (thf/current-page file)
;; ==== Action
new-interaction (-> ctsi/default-interaction
(ctsi/set-destination (:id frame-dest))
(assoc :position-relative-to (:id frame-dest)))
changes (-> (pcb/empty-changes nil (:id page))
(pcb/with-objects (:objects page))
(pcb/update-shapes [(:id frame-origin)] #(cls/add-new-interaction % new-interaction)))
file' (thf/apply-changes file changes)
;; ==== Get
frame-origin' (ths/get-shape file' :frame-origin)]
;; ==== Check
(t/is (true? (:hide-in-viewer frame-origin)))
(t/is (nil? (:hide-in-viewer frame-origin')))))

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
export JAVA_OPTS="-Xmx1000m -Xms50m"
export JAVA_OPTS=${JAVA_OPTS:-"-Xmx1000m -Xms200m"};
alias l='ls --color -GFlh'
alias rm='rm -r'

9
docs/.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8

144
docs/.eleventy.js Normal file
View File

@@ -0,0 +1,144 @@
const { DateTime } = require("luxon");
const fs = require("fs");
const pluginNavigation = require("@11ty/eleventy-navigation");
const pluginRss = require("@11ty/eleventy-plugin-rss");
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
const pluginAncestry = require("@tigersway/eleventy-plugin-ancestry");
const metagen = require('eleventy-plugin-metagen');
const pluginTOC = require('eleventy-plugin-nesting-toc');
const markdownIt = require("markdown-it");
const markdownItAnchor = require("markdown-it-anchor");
const markdownItPlantUML = require("markdown-it-plantuml");
const elasticlunr = require("elasticlunr");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(pluginNavigation);
eleventyConfig.addPlugin(pluginRss);
eleventyConfig.addPlugin(pluginSyntaxHighlight);
eleventyConfig.addPlugin(pluginAncestry);
eleventyConfig.addPlugin(metagen);
eleventyConfig.addPlugin(pluginTOC, {
tags: ['h1', 'h2', 'h3']
});
eleventyConfig.setDataDeepMerge(true);
eleventyConfig.addLayoutAlias("post", "layouts/post.njk");
eleventyConfig.addFilter("readableDate", dateObj => {
return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat("dd LLL yyyy");
});
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string
eleventyConfig.addFilter('htmlDateString', (dateObj) => {
return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat('yyyy-LL-dd');
});
// Remove trailing # in automatic generated toc, because of
// anchors added at the end of the titles.
eleventyConfig.addFilter('stripHash', (toc) => {
return toc.replace(/ #\<\/a\>/g, "</a>");
});
// Get the first `n` elements of a collection.
eleventyConfig.addFilter("head", (array, n) => {
if( n < 0 ) {
return array.slice(n);
}
return array.slice(0, n);
});
// Get the lowest in a list of numbers.
eleventyConfig.addFilter("min", (...numbers) => {
return Math.min.apply(null, numbers);
});
// Build a search index
eleventyConfig.addFilter("search", (collection) => {
// What fields we'd like our index to consist of
// TODO: remove html tags from content
var index = elasticlunr(function () {
this.addField("title");
this.addField("content");
this.setRef("id");
});
// loop through each page and add it to the index
collection.forEach((page) => {
index.addDoc({
id: page.url,
title: page.template.frontMatter.data.title,
content: page.template.frontMatter.content,
});
});
return index.toJSON();
});
eleventyConfig.addPassthroughCopy("img");
eleventyConfig.addPassthroughCopy("css");
eleventyConfig.addPassthroughCopy("js");
/* Markdown Overrides */
let markdownLibrary = markdownIt({
html: true,
breaks: false,
linkify: true
}).use(markdownItAnchor, {
permalink: true,
permalinkClass: "direct-link",
permalinkSymbol: "#"
}).use(markdownItPlantUML, {
});
eleventyConfig.setLibrary("md", markdownLibrary);
// Browsersync Overrides
eleventyConfig.setBrowserSyncConfig({
callbacks: {
ready: function(err, browserSync) {
const content_404 = fs.readFileSync('_dist/404.html');
browserSync.addMiddleware("*", (req, res) => {
// Provides the 404 content without redirect.
res.write(content_404);
res.end();
});
},
},
ui: false,
ghostMode: false
});
return {
templateFormats: [
"md",
"njk",
"html",
"liquid"
],
// If your site lives in a different subdirectory, change this.
// Leading or trailing slashes are all normalized away, so dont worry about those.
// If you dont have a subdirectory, use "" or "/" (they do the same thing)
// This is only used for link URLs (it does not affect your file structure)
// Best paired with the `url` filter: https://www.11ty.dev/docs/filters/url/
// You can also pass this in on the command line using `--pathprefix`
// pathPrefix: "/",
markdownTemplateEngine: "liquid",
htmlTemplateEngine: "njk",
dataTemplateEngine: "njk",
// These are all optional, defaults are shown:
dir: {
input: ".",
includes: "_includes",
data: "_data",
output: "_dist"
}
};
};

1
docs/.eleventyignore Normal file
View File

@@ -0,0 +1 @@
README.md

118
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,118 @@
# Distribution files
_dist/*
# yarn
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
.idea

5
docs/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"editor.rulers": [
80
]
}

11
docs/.yarnrc.yml Normal file
View File

@@ -0,0 +1,11 @@
enableGlobalCache: true
enableImmutableCache: false
enableImmutableInstalls: false
enableTelemetry: false
httpTimeout: 600000
nodeLinker: node-modules

17
docs/404.md Normal file
View File

@@ -0,0 +1,17 @@
---
layout: layouts/home.njk
permalink: 404.html
eleventyExcludeFromCollections: true
---
# Content not found.
Go <a href="{{ '/' | url }}">home</a>.
{% comment %}
Read more: https://www.11ty.dev/docs/quicktips/not-found/
This will work for both GitHub pages and Netlify:
* https://help.github.com/articles/creating-a-custom-404-page-for-your-github-pages-site/
* https://www.netlify.com/docs/redirects/#custom-404
{% endcomment %}

38
docs/README.md Normal file
View File

@@ -0,0 +1,38 @@
# Penpot Docs
Penpot documentation website.
## Usage
To view this site locally, first set up the environment:
```sh
# only if necessary
nvm install
nvm use
# only if necessary
corepack enable
yarn install
```
And launch a development server:
```sh
yarn start
```
You can then point a browser to [http://localhost:8080](http://localhost:8080).
## Tooling
* [Eleventy (11ty)](https://www.11ty.dev/docs)
* [Diagrams](https://github.com/gmunguia/markdown-it-plantuml) with
[plantuml](https://plantuml.com). See also
[real-world-plantuml](https://real-world-plantuml.com).
* [Diagrams](https://github.com/agoose77/markdown-it-diagrams) with
[svgbob](https://github.com/ivanceras/svgbob) and
[mermaid](https://github.com/mermaid-js/mermaid).
* [arc42](https://arc42.org/overview) template.
* [c4model](https://c4model.com) for software architecture, and an
[implementation in plantuml](https://github.com/plantuml-stdlib/C4-PlantUML).

21
docs/_data/metadata.json Executable file
View File

@@ -0,0 +1,21 @@
{
"title": "Help center",
"url": "https://docs.penpot.app/",
"description": "Design freedom for teams.",
"feed": {
"subtitle": "Penpot: design freedom for teams.",
"filename": "feed.xml",
"path": "/feed/feed.xml",
"id": "https://docs.penpot.app/"
},
"jsonfeed": {
"path": "/feed/feed.json",
"url": "https://docs.penpot.app/feed/feed.json"
},
"author": {
"name": "Penpot",
"email": "hello@penpot.app",
"url": "https://penpot.app"
},
"twitter": "@penpotapp"
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
---
layout: layouts/base.njk
templateClass: tmpl-contributing-guide
---
{%- macro show_children(item) -%}
{%- for child in item | children | sorted('data.title') %}
{%- if loop.first -%}<ul>{%- endif -%}
<li>
<a href="{{ child.url }}">{{ child.data.title }}</a>
{%- if page.url.includes(child.url) -%}
{{ show_children(child) }}
{%- endif -%}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}
{%- endfor %}
{%- endmacro -%}
<div class="main-container with-sidebar">
<aside id="stickySidebar" class="sidebar">
{%- set root = '/contributing-guide/index' | find -%}
<div id="toc">
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
{{ show_children(root) }}
</div>
</aside>
<content class="main-content">
{{ content | safe }}
</content>
</div>

View File

@@ -0,0 +1,7 @@
---
layout: layouts/base.njk
templateClass: tmpl-home
---
<div class="main-container">
{{ content | safe }}
</div>

View File

@@ -0,0 +1,27 @@
---
layout: layouts/base.njk
templateClass: tmpl-plugins-guide
---
{%- macro show_children(item) -%}
{%- for child in item | children | sorted('data.title') %}
{%- if loop.first -%}<ul>{%- endif -%}
<li>
<a href="{{ child.url }}">{{ child.data.title }}</a>
{%- if page.url.includes(child.url) -%}
{{ show_children(child) }}
{%- endif -%}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}
{%- endfor %}
{%- endmacro -%}
<div class="main-container">
<content class="main-content plugins">
{{ content | safe }}
</content>
</div>

View File

@@ -0,0 +1,28 @@
---
layout: layouts/base.njk
templateClass: tmpl-plugins-guide
---
{%- macro show_children(item) -%}
{%- for child in item | children | sorted('data.title') %}
{%- if loop.first -%}<ul>{%- endif -%}
<li>
<a href="{{ child.url }}">{{ child.data.title }}</a>
{%- if page.url.includes(child.url) -%}
{{ show_children(child) }}
{%- endif -%}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}
{%- endfor %}
{%- endmacro -%}
<div class="main-container with-sidebar">
<content class="main-content">
{{ content | safe }}
</content>
</div>

View File

@@ -0,0 +1,36 @@
---
layout: layouts/base.njk
templateClass: tmpl-plugins-guide
---
{%- macro show_children(item) -%}
{%- for child in item | children | sorted('data.title') %}
{%- if loop.first -%}<ul>{%- endif -%}
<li>
<a href="{{ child.url }}">{{ child.data.title }}</a>
{%- if page.url.includes(child.url) -%}
{{ show_children(child) }}
{%- endif -%}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}
{%- endfor %}
{%- endmacro -%}
<div class="main-container with-sidebar">
<aside id="stickySidebar" class="sidebar">
{%- set root = '/plugins/index' | find -%}
<div id="toc">
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
{{ show_children(root) }}
</div>
</aside>
<content class="main-content">
{{ content | safe }}
</content>
</div>

View File

@@ -0,0 +1,36 @@
---
layout: layouts/base.njk
templateClass: tmpl-developer-guide
---
{%- macro show_children(item) -%}
{%- for child in item | children | sorted('data.title') %}
{%- if loop.first -%}<ul>{%- endif -%}
<li>
<a href="{{ child.url }}">{{ child.data.title }}</a>
{%- if page.url.includes(child.url) -%}
{{ show_children(child) }}
{%- endif -%}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}
{%- endfor %}
{%- endmacro -%}
<div class="main-container with-sidebar">
<aside id="stickySidebar" class="sidebar">
{%- set root = '/technical-guide/index' | find -%}
<div id="toc">
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
{{ show_children(root) }}
</div>
</aside>
<content class="main-content">
{{ content | safe }}
</content>
</div>

View File

@@ -0,0 +1,36 @@
---
layout: layouts/base.njk
templateClass: tmpl-user-guide
---
{%- macro show_children(item) -%}
{%- for child in item | children | sorted('data.title') %}
{%- if loop.first -%}<ul>{%- endif -%}
<li>
<a href="{{ child.url }}">{{ child.data.title }}</a>
{%- if page.url.includes(child.url) -%}
{{ show_children(child) }}
{%- endif -%}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}
{%- endfor %}
{%- endmacro -%}
<div class="main-container with-sidebar">
<aside id="stickySidebar" class="sidebar">
{%- set root = '/user-guide/index' | find -%}
<div id="toc">
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
{{ show_children(root) }}
</div>
</aside>
<content class="main-content">
{{ content | safe }}
</content>
</div>

View File

@@ -0,0 +1,13 @@
---
title: 04· Code of Conduct
---
<h1 id="coc">Code of conduct</h1>
<p>As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.</p>
<p>We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.</p>
<p>Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.</p>
<p>Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.</p>
<p>This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.</p>
<p>Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.</p>
<p>This Code of Conduct is adapted from the Contributor Covenant, version 1.1.0, available from <a href="http://contributor-covenant.org/version/1/1/0/" target="_blank">http://contributor-covenant.org/version/1/1/0/</a></p>

View File

@@ -0,0 +1,108 @@
---
title: 03· Core code contributions
---
<h1 id="code-contributions">Core code contributions</h1>
<p class="main-paragraph">Details to know how to improve Penpot's core code</p>
<p class="advice">
Thinking of contributing to Penpot core but not sure where to start? Weve made a curated selection of enhancements to help you with that. We believe that these tasks should be a great way to get started with Penpot development and quickly become an active contributor.
<br><br>
<a href="https://github.com/penpot/penpot/contribute" target="_blank">Heres the list of enhancements labeled as "good first issue"</a>
</p>
<h3 id="code-contributions-techguide">Technical guide</h3>
<p>Go to the <a href="/technical-guide">Technical guide</a> to get detailed explanations about how to get Penpot application and run it locally, to test it or make changes to it.</p>
<h3 id="code-contributions-pull-requests">Pull requests</h3>
<p>If you want propose a change or bug fix with the Pull-Request system firstly you should carefully read the <a href="#code-contributions-dco">DCO section</a> and format your commits accordingly.</p>
<p>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 fixing. This is helpful in case we don't accept that specific fix but want to keep track of the issue.</p>
<p>If you want to implement or start working in a new feature, please open a <b>question</b> / <b>discussion</b> issue for it. No pull-request will be accepted without previous chat about the changes, independently if it is a new feature, already planned feature or small quick win.</p>
<p>If is going to be your first pull request, You can learn how from <a href="https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github" target="_blank">this free video series</a>.</p>
<p>We will use the <code class="language-bash">easy fix</code> mark for tag for indicate issues that are easy for beginners.</p>
<h3 id="code-contributions-commits">Commit message guidelines</h3>
<p>We have very precise rules over how our git commit messages can be formatted.</p>
<p>The commit message format is:</p>
<pre>
<code class="language-md">
&lt;type&gt; &lt;subject&gt;
[body]
[footer]
</code>
</pre>
<p>Where type is:</p>
<ul>
<li><g-emoji class="g-emoji" alias="bug" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f41b.png"><img class="emoji" alt="bug" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f41b.png"></g-emoji> <code>:bug:</code> a commit that fixes a bug</li>
<li><g-emoji class="g-emoji" alias="sparkles" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2728.png"><img class="emoji" alt="sparkles" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/2728.png"></g-emoji> <code>:sparkles:</code> a commit that adds an improvement</li>
<li><g-emoji class="g-emoji" alias="tada" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f389.png"><img class="emoji" alt="tada" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f389.png"></g-emoji> <code>:tada:</code> a commit with new feature</li>
<li><g-emoji class="g-emoji" alias="recycle" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/267b.png"><img class="emoji" alt="recycle" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/267b.png"></g-emoji> <code>:recycle:</code> a commit that introduces a refactor</li>
<li><g-emoji class="g-emoji" alias="lipstick" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f484.png"><img class="emoji" alt="lipstick" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f484.png"></g-emoji> <code>:lipstick:</code> a commit with cosmetic changes</li>
<li><g-emoji class="g-emoji" alias="ambulance" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f691.png"><img class="emoji" alt="ambulance" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f691.png"></g-emoji> <code>:ambulance:</code> a commit that fixes critical bug</li>
<li><g-emoji class="g-emoji" alias="books" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4da.png"><img class="emoji" alt="books" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4da.png"></g-emoji> <code>:books:</code> a commit that improves or adds documentation</li>
<li><g-emoji class="g-emoji" alias="construction" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f6a7.png"><img class="emoji" alt="construction" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f6a7.png"></g-emoji> <code>:construction:</code> a wip commit</li>
<li><g-emoji class="g-emoji" alias="construction_worker" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f477.png"><img class="emoji" alt="construction_worker" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f477.png"></g-emoji> <code>:construction_worker:</code> a commit with CI related stuff</li>
<li><g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"></g-emoji> <code>:boom:</code> a commit with breaking changes</li>
<li><g-emoji class="g-emoji" alias="wrench" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f527.png"><img class="emoji" alt="wrench" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f527.png"></g-emoji> <code>:wrench:</code> a commit for config updates</li>
<li><g-emoji class="g-emoji" alias="zap" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/26a1.png"><img class="emoji" alt="zap" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/26a1.png"></g-emoji> <code>:zap:</code> a commit with performance improvements</li>
<li><g-emoji class="g-emoji" alias="whale" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f433.png"><img class="emoji" alt="whale" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f433.png"></g-emoji> <code>:whale:</code> a commit for docker related stuff</li>
<li><g-emoji class="g-emoji" alias="rewind" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/23ea.png"><img class="emoji" alt="rewind" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/23ea.png"></g-emoji> <code>:rewind:</code> a commit that reverts changes</li>
<li><g-emoji class="g-emoji" alias="paperclip" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4ce.png"><img class="emoji" alt="paperclip" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4ce.png"></g-emoji> <code>:paperclip:</code> a commit with other not relevant changes</li>
<li><g-emoji class="g-emoji" alias="arrow_up" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2b06.png"><img class="emoji" alt="arrow_up" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/2b06.png"></g-emoji> <code>:arrow_up:</code> a commit with dependencies updates</li>
</ul>
<p>More info:</p>
<ul>
<li><a href="https://gist.github.com/parmentf/035de27d6ed1dce0b36a">https://gist.github.com/parmentf/035de27d6ed1dce0b36a</a></li>
<li><a href="https://gist.github.com/rxaviers/7360908">https://gist.github.com/rxaviers/7360908</a></li>
</ul>
<p>The subject should be:</p>
<ul>
<li>Use the imperative mood.</li>
<li>Capitalize the first letter.</li>
<li>Don't put a period at the end of the subject line.</li>
<li>Put a blank line between the subject line and the body.</li>
</ul>
<h3 id="code-contributions-dco">Developer's Certificate of Origin (DCO)</h3>
<p>By submitting code you are agree and can certify the below:</p>
<pre>
<code class="language-markdown">
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
</code>
</pre>
<p>Then, all your code patches (<b>documentation are excluded</b>) should contain a sign-off at the end of the patch/commit description body. It can be automatically added on adding <code class="language-bash">-s</code> parameter to <code class="language-bash">git commit</code>.</p>
<p>This is an example of the aspect of the line:</p>
<pre>
<code class="language-markdown">
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
</code>
</pre>
<p>Please, use your real name (sorry, no pseudonyms or anonymous contributions are allowed).</p>

View File

@@ -0,0 +1,4 @@
{
"layout": "layouts/contributing-guide.njk",
"tags": "contributing-guide"
}

View File

@@ -0,0 +1,47 @@
---
title: Contributing
eleventyNavigation:
key: Contributing
order: 3
---
<div class="main-illus">
<img src="/img/home-contributing.png" alt="User guide" border="0">
</div>
<h1 id="contributing-guide">Contributing guide.</h1>
<p class="main-paragraph">In this documentation you will find (almost) everything you need to know about how to contribute at Penpot.</p>
<ul class="intro-sections">
<li>
<a href="/contributing-guide/reporting-bugs">
<h2>Reporting bugs</h2>
<p>Easy steps to bug hunting</p>
</a>
</li>
<li>
<a href="/contributing-guide/translations">
<h2>Translations</h2>
<p>How to become a Penpot translator</p>
</a>
</li>
<li>
<a href="/contributing-guide/code-contributions">
<h2>Core code contributions</h2>
<p>Help Penpot improve its code</p>
</a>
</li>
<li>
<a href="/contributing-guide/coc">
<h2>Code of conduct</h2>
<p>Rules, values and principles</p>
</a>
</li>
<li class="illus illus-libraries">
<a href="https://penpot.app/libraries-templates" target="_blank">
<h2>Libraries & templates</h2>
<p>Share your libraries and templates or download the ones you like.</p>
</a>
</li>
</ul>

View File

@@ -0,0 +1,15 @@
---
title: 05· Libraries & Templates
---
<h1 id="libraries">Libraries & templates</h1>
<img src="/img/contributing-libraries.png" alt="libraries and templates" border="0">
<p>There are published Penpot files ready to use made by community members and Penpot core team members.</p>
<ul>
<li>Here you can find the complete list of <a href="https://penpot.app/libraries-templates" target="_blank">available Libraries & Templates</a>.</li>
<li>Are you willing to contribute sharing a Penpot file with the Penpot community? Here's a <a href="https://penpot.app/how-to-contribute" target="_blank">how you can do it</a>.</li>
<li>If you are in doubt about how to use one of these files, here you can <a href="https://penpot.app/libraries-templates#how-to-use" target="_blank">watch a video explanation</a>.</li>
</ul>

View File

@@ -0,0 +1,23 @@
---
title: 01· Reporting bugs
---
<h1 id="reporting-bugs">Reporting bugs</h1>
<p class="main-paragraph">Bug hunting is not difficult if you know how.</p>
<h2 id="reporting-bugs-howto">How to report a bug</h2>
<p>We are using <a href="https://github.com/penpot/penpot/issues" target="_blank">GitHub Issues</a> for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist.</p>
<p>If you found a bug, please report it, as far as possible including:</p>
<ul>
<li>a detailed explanation of steps to reproduce the error.</li>
<li>a browser and the browser version used.</li>
<li>a dev tools console exception stack trace (if it is available).</li>
</ul>
<p>Consider sending us an email first at <a href="mailto:support@penpot.app">support@penpot.app</a> if you discovered a bug that you'd prefer to discuss in confidence (such as a security bug).</p>
<p><strong>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.</strong></p>

View File

@@ -0,0 +1,58 @@
---
title: 02· Translations
---
<h1 id="translations">Translations</h1>
<p class="main-paragraph">Thank you for interest in contribute translating Penpot. Here you will find ways to do it.</p>
<h2 id="translations-howto">How to become a Penpot translator</h2>
<p>We are using <a href="https://hosted.weblate.org/projects/penpot/frontend/" target="_blank">Weblate</a> as translation platform, so the first thing you need to be a Penpot translator is to have a Weblate account (you can <a href="https://hosted.weblate.org/accounts/register/" target="_blank">register here</a>).</p>
<p>To start translating at Penpot:</p>
<ol>
<li>Open a <a href="https://github.com/penpot/penpot/issues" target="_blank">github issue</a> giving details about the language you want to translate (language), the type of translation (new language, new translation or change an existing translation) and your Weblate user.</li>
<li>If everything is correct we will get back to you providing you permissions to the actions needed.</li>
<li>You also might want to take a look at the guide for <a href="https://docs.weblate.org/en/latest/user/translating.html" target="_blank">Translating using Weblate</a>.</li>
</ol>
<h2 id="translations-howto">Add a new language</h2>
<p>To add a language that is still not among the Penpot language options:</p>
<ol>
<li>Go to the <a href="https://hosted.weblate.org/projects/penpot/frontend/" target="_blank">languages list</a>.</li>
<li>Press the "Start new translation" button.</li>
<li>Choose the language you want to translate to.</li>
<li>Press the "Start new translation" button at the start new translation page.</li>
<li>Start translating strings for the new language :)</li>
</ol>
<p><img src="/img/translations-start.png" alt="translations" /></p>
<p><img src="/img/translations-start-translation.png" alt="translations" /></p>
<h2 id="translations-howto">Add a new translation</h2>
<p>To add a new translation (a string with a lacking translation for a certain language) follow the next steps:</p>
<ol>
<li>Go to the <a href="https://hosted.weblate.org/projects/penpot/frontend/" target="_blank">languages list</a>.</li>
<li>Click the edit button (pencil icon) close to the name of the language where you want to add the missing translation or translations.</li>
<li>Find and select the translation/s to complete.</li>
<li>Complete the translation in the required input field.</li>
<li>Press the "Save· button.</li>
<li>Repeat the action with as many translation strings you can / you want ;)</li>
</ol>
<p>Saved new translations will automatically get the status "waiting for review". Our team will periodically check strings waiting for review and, if considered correct, will approve them.</p>
<p><img src="/img/translations-lang-list.png" alt="translations" /></p>
<p><img src="/img/translations-strings-list.png" alt="translations" /></p>
<h2 id="translations-howto">Change an approved translation</h2>
<p>To edit an already approved translation string follow the next steps:</p>
<ol>
<li>Go to the <a href="https://hosted.weblate.org/projects/penpot/frontend/" target="_blank">languages list</a>.</li>
<li>Click the name of the language where is the translation you want to change.</li>
<li>Click the Browse button.</li>
<li>Find and select the translation/s to complete.</li>
<li>Change the translation in the input field.</li>
<li>Press the "Save" button if you have permissions.</li>
<li>If you don't have permissions to Save you can still press "Suggest" to make a suggestion.</li>
</ol>
<p>Saved editions will get the status "Waiting for review". Suggestions will get the status "Approved strings with suggestions". Our team will periodically check strings waiting for review and, if considered correct, will approve them.</p>
<p><img src="/img/translations-lang-state.png" alt="translations" /></p>

1210
docs/css/index.css Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
/* Styles for syntax highlighting inside code blocks */
code[class*="language-"], pre[class*="language-"] {
font-size: 14px;
line-height: 1.375;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
background: #272822;
color: #f8f8f2;
max-width: 40rem;
}
pre[class*="language-"] {
padding: 1.5em 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment, .token.prolog, .token.doctype, .token.cdata {
color: #75715e;
}
.token.punctuation {
color: #f8f8f2;
}
.token.namespace {
opacity: .7;
}
.token.operator, .token.boolean, .token.number {
color: #fd971f;
}
.token.property {
color: #f4bf75;
}
.token.tag {
color: #66d9ef;
}
.token.string {
color: #a1efe4;
}
.token.selector {
color: #ae81ff;
}
.token.attr-name {
color: #fd971f;
}
.token.entity, .token.url, .language-css .token.string, .style .token.string {
color: #a1efe4;
}
.token.attr-value, .token.keyword, .token.control, .token.directive, .token.unit {
color: #a6e22e;
}
.token.statement, .token.regex, .token.atrule {
color: #a1efe4;
}
.token.placeholder, .token.variable {
color: #66d9ef;
}
.token.deleted {
text-decoration: line-through;
}
.token.inserted {
border-bottom: 1px dotted #f9f8f5;
text-decoration: none;
}
.token.italic {
font-style: italic;
}
.token.important, .token.bold {
font-weight: bold;
}
.token.important {
color: #f92672;
}
.token.entity {
cursor: help;
}
pre > code.highlight {
outline: 0.4em solid #f92672;
outline-offset: .4em;
}

122
docs/css/prism.css Normal file
View File

@@ -0,0 +1,122 @@
/**
* GHColors theme by Avi Aryan (http://aviaryan.in)
* Inspired by Github syntax coloring
*/
code[class*="language-"],
pre[class*="language-"] {
color: #393A34;
font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
font-size: .9em;
line-height: 1.2em;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre > code[class*="language-"] {
font-size: 1em;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
background: #b3d4fc;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
background: #b3d4fc;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border: 1px solid #dddddd;
background-color: white;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .2em;
padding-top: 1px;
padding-bottom: 1px;
background: #f8f8f8;
border: 1px solid #dddddd;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #999988;
font-style: italic;
}
.token.namespace {
opacity: .7;
}
.token.string,
.token.attr-value {
color: #e3116c;
}
.token.punctuation,
.token.operator {
color: #393A34; /* no highlight */
}
.token.entity,
.token.url,
.token.symbol,
.token.number,
.token.boolean,
.token.variable,
.token.constant,
.token.property,
.token.regex,
.token.inserted {
color: #36acaa;
}
.token.atrule,
.token.keyword,
.token.attr-name,
.language-autohotkey .token.selector {
color: #00a4db;
}
.token.function,
.token.deleted,
.language-autohotkey .token.tag {
color: #9a050f;
}
.token.tag,
.token.selector,
.language-autohotkey .token.keyword {
color: #00009f;
}
.token.important,
.token.function,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}

29
docs/feed/feed.njk Executable file
View File

@@ -0,0 +1,29 @@
---
# Metadata comes from _data/metadata.json
permalink: "{{ metadata.feed.path }}"
eleventyExcludeFromCollections: true
---
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>{{ metadata.title }}</title>
<subtitle>{{ metadata.feed.subtitle }}</subtitle>
{% set absoluteUrl %}{{ metadata.feed.path | url | absoluteUrl(metadata.url) }}{% endset %}
<link href="{{ absoluteUrl }}" rel="self"/>
<link href="{{ metadata.url }}"/>
<updated>{{ collections.posts | rssLastUpdatedDate }}</updated>
<id>{{ metadata.feed.id }}</id>
<author>
<name>{{ metadata.author.name }}</name>
<email>{{ metadata.author.email }}</email>
</author>
{%- for post in collections.posts | reverse %}
{% set absolutePostUrl %}{{ post.url | url | absoluteUrl(metadata.url) }}{% endset %}
<entry>
<title>{{ post.data.title }}</title>
<link href="{{ absolutePostUrl }}"/>
<updated>{{ post.date | rssDate }}</updated>
<id>{{ absolutePostUrl }}</id>
<content type="html">{{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</content>
</entry>
{%- endfor %}
</feed>

6
docs/feed/htaccess.njk Normal file
View File

@@ -0,0 +1,6 @@
---
permalink: feed/.htaccess
eleventyExcludeFromCollections: true
---
# For Apache, to show `{{ metadata.feed.filename }}` when browsing to directory /feed/ (hide the file!)
DirectoryIndex {{ metadata.feed.filename }}

31
docs/feed/json.njk Normal file
View File

@@ -0,0 +1,31 @@
---
# Metadata comes from _data/metadata.json
permalink: "{{ metadata.jsonfeed.path }}"
eleventyExcludeFromCollections: true
---
{
"version": "https://jsonfeed.org/version/1",
"title": "{{ metadata.title }}",
"home_page_url": "{{ metadata.url }}",
"feed_url": "{{ metadata.jsonfeed.url }}",
"description": "{{ metadata.description }}",
"author": {
"name": "{{ metadata.author.name }}",
"url": "{{ metadata.author.url }}"
},
"items": [
{%- for post in collections.posts | reverse %}
{%- set absolutePostUrl %}{{ post.url | url | absoluteUrl(metadata.url) }}{% endset -%}
{
"id": "{{ absolutePostUrl }}",
"url": "{{ absolutePostUrl }}",
"title": "{{ post.data.title }}",
"content_html": {% if post.templateContent %}{{ post.templateContent | dump | safe }}{% else %}""{% endif %},
"date_published": "{{ post.date | rssDate }}"
}
{%- if not loop.last -%}
,
{%- endif -%}
{%- endfor %}
]
}

0
docs/img/.gitkeep Normal file
View File

BIN
docs/img/a11y-tree-btn.webp Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/img/artboards-move.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/img/assets-add.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
docs/img/assets-edit.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
docs/img/assets-filter.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
docs/img/assets-order.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
docs/img/assets-search.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/img/assets-use.gif Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

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