Compare commits

...
807 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
Alejandro Alonso 864088eecd Merge remote-tracking branch 'origin/staging' into develop 2024-09-30 09:38:11 +02:00
Pablo Alba 0b39318b33 🐛 Fix request dialog is shown in all errors 2024-09-30 09:36:41 +02:00
Alejandro d5a9961ec8 Merge pull request #5124 from penpot/eva-move-tab-switcher
♻️  Move tab-switcher to its own folder inside DS
2024-09-30 09:29:15 +02:00
Alejandro 7dac7de365 Merge pull request #5123 from penpot/palba-change-emails-footer
 Update emails footer
2024-09-30 07:11:27 +02:00
Alejandro dd0721e91e Merge pull request #5126 from penpot/palba-fix-show-request-on-all-errors
🐛 Fix request dialog is shown in all errors
2024-09-30 06:19:58 +02:00
Pablo Alba 21fde2e991 🐛 Fix request dialog is shown in all errors 2024-09-27 10:38:24 +02:00
Eva Marco ca1893164d Add the undefied option to props schema enums 2024-09-26 17:39:52 +02:00
Eva Marco b619ac3e08 ♻️ Move tab-switcher to its own folder inside DS 2024-09-26 17:39:45 +02:00
Belén Albeza d7eb86c86d Merge pull request #5095 from penpot/eva-update-ds-components
♻️ Update colors and icons
2024-09-26 12:00:45 +02:00
Pablo Alba 6c4f216da8 Update emails footer 2024-09-26 09:45:42 +02:00
Alejandro Alonso f786a00e89 Merge remote-tracking branch 'origin/staging' into develop 2024-09-25 11:35:41 +02:00
Alejandro 47cecb2ac4 Merge pull request #5119 from penpot/eva-add-props
  Add shema prop
2024-09-25 11:31:57 +02:00
Eva Marco 5d6ceec803 Add shema prop 2024-09-25 11:07:40 +02:00
Alejandro Alonso bec11220e3 🐛 Fix storage typo 2024-09-25 10:41:55 +02:00
Alejandro Alonso 9b802e1c7d Merge remote-tracking branch 'origin/staging' into develop 2024-09-24 14:27:20 +02:00
Alejandro 16cf16c422 Merge pull request #5114 from penpot/superalex-fix-fetch-profile-exception
🐛 Fix fetch profile exception
2024-09-24 14:24:56 +02:00
Alejandro Alonso 4e1eee197e 🐛 Fix fetch profile exception 2024-09-24 14:13:46 +02:00
Alejandro 91c8af9e38 Merge pull request #5108 from penpot/palba-fix-login-redirect
🐛 Fix bad redirect on new oops page with penpot login
2024-09-24 11:27:04 +02:00
Pablo Alba 58593a9428 🐛 Fix bad redirect on new oops page with penpot login 2024-09-24 10:32:57 +02:00
Alejandro Alonso 21aa8b0703 Merge remote-tracking branch 'origin/staging' into develop 2024-09-24 09:42:56 +02:00
Eva Marco 17cf57f7ca Merge pull request #5113 from penpot/superalex-fix-show-in-assets-panel
🐛 Fix show in assets panel
2024-09-24 09:38:32 +02:00
Alejandro Alonso f7cfe36f37 🐛 Fix show in assets panel 2024-09-24 08:49:52 +02:00
Alejandro c26f909565 Merge pull request #5110 from penpot/eva-fix-code-block-height
🐛 Fix code block height
2024-09-24 06:54:44 +02:00
Eva Marco 6db7fe5f7b 🐛 Fix code block height 2024-09-23 17:03:25 +02:00
Pablo Alba a207114d95 Merge pull request #5109 from penpot/hiru-fix-swap-inside-group
🐛 Fix error when swapping a copy that is the only child of a group
2024-09-23 15:12:55 +02:00
Eva Marco b8299a5ea5 🐛 Fix create team without invitations on onboarding 2024-09-23 15:09:43 +02:00
Andrés Moya 1fa461e996 🐛 Fix error when swapping a copy that is the only child of a group 2024-09-23 15:01:01 +02:00
Alejandro 2e3745099b Merge pull request #5107 from penpot/alotor-remove-export-option
🐛 Removed "merge assets" option from export
2024-09-23 13:22:56 +02:00
Pablo Alba 03ebeb0657 🐛 Fix session storage entry name 2024-09-23 12:15:49 +02:00
alonso.torres 6892cffe54 🐛 Removed "merge assets" option from export 2024-09-23 12:13:33 +02:00
Alejandro 19a613e90c Merge pull request #5105 from penpot/superalex-merge-conflicts-2
Merge conflicts
2024-09-23 10:57:33 +02:00
Alejandro Alonso 7fe95f218b 🐛 Fix logged in redirect 2024-09-23 10:36:59 +02:00
Alejandro Alonso a1fc785771 Merge remote-tracking branch 'origin/staging' into superalex-merge-conflicts-2 2024-09-23 10:08:06 +02:00
Alejandro Alonso e0034dc205 🐛 Fix onboarding edn urls 2024-09-23 06:30:19 +02:00
Alejandro bd9eab08b7 Merge pull request #5101 from penpot/palba-migrate-templates-v2
 Update templates links to binary v2
2024-09-23 06:12:15 +02:00
Pablo Alba b5121657ee Update templates links to binary v2 2024-09-18 16:23:12 +02:00
Andrey Antukh 4f04dbc294 🐛 Fix frame flows issues related to the refactor 2024-09-18 12:15:12 +02:00
alonso.torres 2b2a84da64 🐛 Fix problem with frame guides 2024-09-18 12:15:12 +02:00
Andrey Antukh 21dd9a260c 📎 Rename common files changes test namespace 2024-09-18 12:15:12 +02:00
Andrey Antukh 7b9b5bafc1 🔥 Remove duplicated test 2024-09-18 12:15:12 +02:00
Andrey Antukh 41ebba6ce0 Add generative tests for flows and saved-grids 2024-09-18 12:15:12 +02:00
Andrey Antukh 61446592b3 Move generative test related code to a separated ns 2024-09-18 12:15:12 +02:00
Andrey Antukh b82c6326cf Add better error reporting on changes validation 2024-09-18 12:15:12 +02:00
Andrey Antukh a2f466810b ♻️ Add minor refactor on set-plugin-data change 2024-09-18 12:15:12 +02:00
Andrey Antukh 1bd1782d66 ♻️ Add better reporting for generative tests 2024-09-18 12:15:12 +02:00
Andrey Antukh ea6a1c05fa 🐛 Fix incorrect assignation of plugin data on page data structure 2024-09-18 12:15:12 +02:00
Andrey Antukh 4f84e77b10 Add generative tests for set-guide change 2024-09-18 12:15:12 +02:00
Andrey Antukh fa75a3539f 📎 Rename shape decode encode test file 2024-09-18 12:15:12 +02:00
Andrey Antukh fa12d9785a Add tests for basic obj crud change operations
Restored and adapted from already existing commented code
2024-09-18 12:15:12 +02:00
Andrey Antukh c578e31ae2 📎 Update some docstrings on common/schema ns 2024-09-18 12:15:12 +02:00
Andrey Antukh 749c369080 Add less verbose shape validation 2024-09-18 12:15:12 +02:00
Andrey Antukh 4ad4057878 ♻️ Refactor page options data structure 2024-09-18 12:15:12 +02:00
Andrey Antukh 2dea0b52ed Merge pull request #5077 from penpot/ladybenko-8638-docker-rust
Set up devenv for Rust
2024-09-17 17:30:38 +02:00
Aitor Moreno ca257d1caf Merge pull request #5097 from penpot/eva-fix-arrow-keys-tabs
🐛  Fix arrow key movement on tabs
2024-09-17 17:23:32 +02:00
Aitor Moreno e164692391 Merge pull request #5098 from penpot/eva-fix-desing-panel
🐛  Fix path side panel options
2024-09-17 17:22:21 +02:00
Eva Marco b58edea544 🐛 Fix path side panel options 2024-09-17 17:00:40 +02:00
Eva Marco 9a587c91a8 🐛 Fix arrow key movement on tabs 2024-09-17 16:36:44 +02:00
Belén Albeza 7590a7ce4d Merge pull request #5072 from penpot/eva-add-schema-to-ds-components
 Add schema validation to all DS components
2024-09-17 16:22:22 +02:00
Aitor Moreno aae1571a5c Merge pull request #5096 from penpot/alotor-bugfixes
Bugfixes
2024-09-17 16:14:59 +02:00
alonso.torres ebaf30727c 🐛 Fix copy/paste images in Safari 2024-09-17 15:38:18 +02:00
Belén Albeza 884ceb052b Use dynamic import for wasm module 2024-09-17 14:52:27 +02:00
Belén Albeza cc7ed497e8 🎉 Enable conditional use of wasm module 2024-09-17 14:51:55 +02:00
Belén Albeza cd6a739abb 🔧 Add dummy rust project + build scripts with wasm-pack 2024-09-17 14:51:55 +02:00
Belén Albeza f0cecfd517 🔧 Install Rust+Cargo in devenv 2024-09-17 14:51:55 +02:00
alonso.torres f5f255e2d5 🐛 Fix problem with comments max length 2024-09-17 14:18:51 +02:00
Eva Marco 5ffa56be3d ♻️ Update select background color on input 2024-09-17 14:11:50 +02:00
Alejandro e65c0d9f48 Merge pull request #5088 from penpot/niwinz-bugfix-2
🐛 Fix issues related to invalid colors inserted on shape shadow
2024-09-17 13:58:43 +02:00
Eva Marco 076cb0e35b Add schema validation to all DS components 2024-09-17 13:43:35 +02:00
Andrey Antukh 2a90ca6546 Merge pull request #5094 from penpot/alotor-fix-plugins
🐛 Fix small problems in plugins
2024-09-17 12:37:43 +02:00
Eva Marco a26deafa75 ♻️ Update the colors and icon of some toast notification 2024-09-17 12:08:17 +02:00
alonso.torres cf705e352b 🐛 Fix small problems in plugins 2024-09-17 10:19:37 +02:00
Andrey Antukh 86c5ca4213 🐛 Fix incorrect redirect handling on request-access go-home button 2024-09-16 18:53:56 +02:00
Andrey Antukh 179d534237 🐛 Fix several issues related to invalid colors inserted on shadows 2024-09-16 18:32:40 +02:00
Andrey Antukh 162507264c 🐛 Reexecute file migration 26 again for shapes that has transform prop as nil 2024-09-16 18:32:40 +02:00
Andrey Antukh 7e0a8b6227 Merge pull request #5092 from penpot/palbs-fix-request-acces-dont-go-your-team
🐛 Fix request access to the Team don't go to Your Penpot team
2024-09-16 18:32:19 +02:00
Andrey Antukh b50fcee079 Merge pull request #5090 from penpot/alotor-new-apis
Plugins - API's modifications
2024-09-16 18:29:51 +02:00
alonso.torres 9bca42c14a Fixed plugin registration props 2024-09-16 15:46:02 +02:00
Pablo Alba 475d14edf4 🐛 Fix request access to the Team don't go to Your Penpot team 2024-09-16 14:42:52 +02:00
alonso.torres 214733c880 ⬆️ Update plugins runtime 2024-09-16 09:48:56 +02:00
alonso.torres 979828ffe3 🐛 Fix issue when exporting libraries when merging libraries 2024-09-16 09:08:51 +02:00
alonso.torres 65bb795199 🐛 Fix visual problem with stroke cap menu 2024-09-16 09:07:57 +02:00
Alejandro a0546b2e63 Merge pull request #5086 from penpot/niwinz-bugfix-1
🐛 Ignore object thumbnail requests if file is already marked as deleted
2024-09-13 12:30:27 +02:00
alonso.torres d6f6d78b1e New viewport functions 2024-09-13 12:29:07 +02:00
alonso.torres 8c1fba5160 Add api methods to align, distribute and flatten shapes 2024-09-13 12:29:07 +02:00
alonso.torres fb39dd5440 Methods for comments 2024-09-13 12:29:07 +02:00
alonso.torres dd0c5b7806 Add support to guides for plugins 2024-09-13 11:30:59 +02:00
Andrey Antukh 9e94cf7b99 ♻️ Simplify internal implementation of sm/schema namespace 2024-09-13 11:30:55 +02:00
Andrey Antukh b882b9e283 🔥 Remove usage of public usage of sm/define funcion 2024-09-13 11:30:55 +02:00
Andrey Antukh cdcff62232 Store some profile props on browser global storage 2024-09-13 11:30:55 +02:00
Andrey Antukh c8caca77a3 Add storage namespacing
Allows separate global properties from user specific properties
2024-09-13 11:30:55 +02:00
Andrey Antukh f291125377 🐛 Add migration for invalid value on layout-wrap-type on shape prop 2024-09-12 21:33:01 +02:00
Andrey Antukh 0ce981a68c 🐛 Add missing ref-id unsassign on srepl helpers for process file 2024-09-12 21:32:34 +02:00
Andrey Antukh a8814dcaba 🐛 Add missing fields on file-gc libraries fetching sql 2024-09-12 21:07:19 +02:00
Pablo Alba 229eeae6db 🐛 Fix bad redirect on new oops page with social login 2024-09-12 16:35:49 +02:00
Andrey Antukh d03788af93 🐛 Ignore object thumbnail requests if file is already marked as deleted 2024-09-12 15:23:31 +02:00
Alejandro Alonso 017aad6454 🐛 Fix export failed error when exporting multiple shapes 2024-09-12 12:55:32 +02:00
Alejandro Alonso 042b3a71d8 Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 12:46:04 +02:00
Alejandro Alonso 767ec37b83 Merge remote-tracking branch 'origin/main' into staging 2024-09-11 12:45:39 +02:00
Alejandro Alonso 89f64e0c49 🐛 Fix challenge redirect with parameters 2024-09-11 12:30:12 +02:00
Alejandro Alonso eadae5e2cd Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 12:05:45 +02:00
Alejandro Alonso d108ad904e Merge remote-tracking branch 'origin/main' into staging 2024-09-11 12:05:21 +02:00
Alejandro Alonso 6564736d3e 🐛 Fix challenge redirect with parameters 2024-09-11 12:03:32 +02:00
Alejandro Alonso 7f9c4df284 Merge remote-tracking branch 'origin/staging' into develop 2024-09-11 11:34:35 +02:00
Alejandro Alonso d01cd70c6b Merge remote-tracking branch 'origin/main' into staging 2024-09-11 11:34:04 +02:00
Pablo Alba 29f1c2bdad Merge pull request #5080 from penpot/niwinz-oidc-fix-limits-issues
🐛 Fix oidc auth internal limits issue
2024-09-10 16:41:44 +02:00
Andrey Antukh e79f9ba40f 🐛 Increase token limit 2024-09-10 12:39:54 +02:00
Andrey Antukh 452aabdec6 🐛 Don't send user props on auth token after oidc login 2024-09-10 12:39:54 +02:00
Alejandro Alonso 9e3f8e7827 Merge remote-tracking branch 'origin/staging' into develop 2024-09-09 11:09:53 +02:00
Eva Marco 3a4e9ccc5a 👷 Fix CI error 2024-09-09 10:32:50 +02:00
Aitor Moreno 860e32d965 Merge pull request #5070 from penpot/superalex-fix-onboarding
🐛 Fix onboarding questions
2024-09-09 10:19:03 +02:00
Andrey Antukh 495f9dfa84 Merge pull request #5064 from penpot/superalex-release-notes-2.2
 Release notes for 2.2
2024-09-09 09:54:50 +02:00
Alejandro 133ca33cb5 Merge pull request #5076 from penpot/niwinz-exporter-config-parse
🐛 Fix issues on parsing configuration on exporter
2024-09-09 09:54:12 +02:00
Andrey Antukh 1c69a9fd8a 🐛 Fix config parsing on exporter 2024-09-09 09:47:55 +02:00
Andrey Antukh 15faa57e01 🐛 Fix decoding on sm/set schema 2024-09-09 09:46:50 +02:00
Alejandro f5510234cf Merge pull request #5066 from penpot/eva-fix-frame-row-menu
🐛 Fix guides submenu visualization
2024-09-09 06:49:05 +02:00
Belén Albeza eb720b053a Merge pull request #5057 from penpot/eva-fix-css-compilation
🔧 Rearrange css files for compilation
2024-09-06 14:45:52 +02:00
Alejandro Alonso efc61241a0 Merge remote-tracking branch 'origin/staging' into develop 2024-09-06 13:50:37 +02:00
Alejandro Alonso 5b0331611d Merge remote-tracking branch 'origin/main' into staging 2024-09-06 13:50:09 +02:00
Alejandro f8f1c58f61 Merge pull request #5069 from penpot/niwinz-storybook-build-fix
🐛 Fix storybook build related to commonjs to esm module conversion issue
2024-09-06 13:43:04 +02:00
Alejandro Alonso c99102e49b 🐛 Fix onboarding questions 2024-09-06 13:18:39 +02:00
Andrey Antukh d583661e58 🐛 Fix storybook build related to commonjs to esm module conversion issue 2024-09-06 12:31:48 +02:00
Andrey Antukh cfad1d178f Merge pull request #5068 from penpot/alotor-plugins-install-profile
 Change installation data to profile
2024-09-06 12:05:52 +02:00
Andrey Antukh ffa326e08f Merge pull request #5067 from penpot/alotor-fix-validation
🐛 Fix problem when dismissing shared library update
2024-09-06 11:31:22 +02:00
alonso.torres c24b2dadec Change installation data to profile 2024-09-06 11:10:32 +02:00
alonso.torres 03040ed40b 🐛 Fix problem when dismissing shared library update 2024-09-06 11:02:02 +02:00
Alejandro Alonso 5e89cd1cb3 Release notes for 2.2 2024-09-06 10:49:20 +02:00
Eva Marco bf202473e9 🐛 Fix guides submenu visualization 2024-09-06 09:47:09 +02:00
Andrey Antukh 9a3b5337d7 Merge pull request #5062 from penpot/alotor-plugins-fix-interactions
🐛 Fix plugins add interaction
2024-09-05 16:22:46 +02:00
alonso.torres 396cbb27b2 🐛 Fix plugins add interaction 2024-09-05 16:00:04 +02:00
Alejandro b4e6f8bc73 Merge pull request #5061 from penpot/niwinz-challenge
 Add support for optional human challenge
2024-09-05 15:49:50 +02:00
Andrey Antukh d88f28f5c2 Add support for optional human challenge 2024-09-05 15:35:39 +02:00
Andrey Antukh 886c0c596f Merge pull request #5060 from penpot/alotor-bugfixes
Bugfixes
2024-09-05 15:32:44 +02:00
alonso.torres b15b394c65 🐛 Fix problem when creating a component instance from grid layout 2024-09-05 15:11:04 +02:00
Eva Marco e36cf1d963 🐛 Fix onboarding slide after rearrange 2024-09-05 14:46:49 +02:00
alonso.torres caf78a6b4d 🐛 Fix layer panel overflowing 2024-09-05 11:50:16 +02:00
alonso.torres 6a161267ba 🐛 Fix problem with overlay positions in viewer 2024-09-05 10:48:00 +02:00
Eva Marco a0bb5e5ef3 ♻️ Remove unnecesary code 2024-09-05 09:41:11 +02:00
Eva Marco 34cc211912 🔧 Rearrange css files for compilation 2024-09-05 09:39:43 +02:00
Eva Marco e95713c1df 🐛 Fix visual integration test 2024-09-05 09:39:43 +02:00
Alejandro Alonso e189dc965d Merge remote-tracking branch 'origin/staging' into develop 2024-09-05 09:37:16 +02:00
alonso.torres a180c33a32 🐛 Fix problem with SVG import 2024-09-05 09:26:22 +02:00
Alejandro ea8febdb7d Merge pull request #5056 from penpot/niwinz-refactor-recent-colors
♻️ Refactor recent colors and local storage abstraction
2024-09-05 09:07:26 +02:00
Alejandro f765cc8dbc Merge pull request #5011 from penpot/palba-testab-start-workspace
A/B test start directly at the workspace
2024-09-05 07:05:57 +02:00
Pablo Alba 81b7972347 🎉 Test A/B for start in workspace 2024-09-04 17:19:39 +02:00
Andrey Antukh 1281670c61 Clear storage on user logout 2024-09-04 16:20:00 +02:00
Andrey Antukh b8c6103858 Add performance enhancements for util/storage abstraction layer 2024-09-04 16:20:00 +02:00
Andrey Antukh b2c0bed84c Add efficiency improvements to use-resize-hook 2024-09-04 16:20:00 +02:00
Andrey Antukh 9619fcbc1f Make efficiency improvements to use-shared-state hook 2024-09-04 16:20:00 +02:00
Andrey Antukh e9c55e9eb4 Make recent colors to be stored locally instead of on file 2024-09-04 16:20:00 +02:00
Belén Albeza 53f580ad40 Merge pull request #5017 from penpot/eva-add-select-to-ds
 Add select component to the DS
2024-09-04 15:51:10 +02:00
Andrey Antukh cf0045681e Merge pull request #5054 from penpot/alotor-plugins-fixes
Update API types
2024-09-04 14:16:01 +02:00
Andrey Antukh 488d034a58 Merge pull request #5055 from penpot/eva-fix-webhook-checkbox
🐛  Fix webhook checkbox position
2024-09-04 14:15:37 +02:00
alonso.torres 762a883b39 🐛 Fix problem with font weight and style 2024-09-04 13:52:48 +02:00
alonso.torres a63ded1ba1 Change type names in plugins 2024-09-04 13:29:56 +02:00
Eva Marco 8d66275187 🐛 Fix webhook checkbox position 2024-09-04 12:51:30 +02:00
alonso.torres f812b28892 ⬆️ Update plugin dependencies 2024-09-04 12:38:50 +02:00
Alejandro 59063e861c Merge pull request #5053 from penpot/niwinz-update-file-refactor
♻️ Refactor file-update for make it more reusable
2024-09-04 12:30:36 +02:00
Andrey Antukh 873c9b1903 Merge pull request #5050 from penpot/hiru-ordered-maps
🔧 Add serializable ordered collections
2024-09-04 12:26:21 +02:00
Andrey Antukh 9da891e9b0 📎 Enable auto-file-snapshot feature scripts/repl 2024-09-04 12:18:31 +02:00
Andrey Antukh a6de12323e ♻️ Refactor file-update for make it more reusable 2024-09-04 12:18:31 +02:00
Alejandro Alonso edeb16bc26 Merge remote-tracking branch 'origin/staging' into develop 2024-09-04 12:02:31 +02:00
Alejandro Alonso 52d099c80e Merge remote-tracking branch 'origin/main' into staging 2024-09-04 12:02:04 +02:00
Alejandro 5808bd3743 Merge pull request #5045 from penpot/niwinz-fix-file-gc
🐛 Fix issues on file-gc task
2024-09-04 11:11:13 +02:00
Alejandro Alonso 90d947391a Merge remote-tracking branch 'origin/staging' into develop 2024-09-04 08:59:05 +02:00
Alejandro Alonso 729f679c0f Merge remote-tracking branch 'origin/main' into staging 2024-09-04 08:58:51 +02:00
Andrés Moya 47cc80a93f 🔧 Add serializable ordered collections 2024-09-03 23:35:53 +02:00
Andrey Antukh 50df2279a7 🐛 Make the media cleaning on file-gc task aware of snapshots
It now takes in account the snapshots, and prevents
deletion of media files used in snapshots.
2024-09-03 14:50:17 +02:00
Andrey Antukh 71ba0242c7 🐛 Add missing type decoding on changes schema 2024-09-03 14:42:00 +02:00
Andrey Antukh 1f8cfde1cf Merge pull request #5046 from penpot/alotor-plugins-fixes
Plugins small fixes
2024-09-03 14:36:59 +02:00
Alejandro Alonso 5f2ec595cb 📎 Update changelog 2024-09-03 13:15:48 +02:00
alonso.torres 37a6446e32 🐛 Fix problem with font style 2024-09-03 13:10:28 +02:00
alonso.torres be84b1cb01 🐛 Change place for circular dependency workaround 2024-09-03 13:10:28 +02:00
Alejandro Alonso 9fb91b3052 Merge remote-tracking branch 'origin/staging' into develop 2024-09-03 13:06:40 +02:00
Eva Marco f3525b9ff2 Merge pull request #5044 from penpot/ladybenko-update-changes-ds
📚 Update changelog with the latest DS features
2024-09-03 10:53:00 +02:00
Belén Albeza d2509f4b97 📚 Update changelog with the latest DS features 2024-09-03 10:38:40 +02:00
Andrey Antukh 6d8c424710 📎 Fix linter issues on ui.auth ns 2024-09-03 10:38:10 +02:00
Andrey Antukh 93e8657f73 Merge remote-tracking branch 'origin/staging' into develop 2024-09-03 08:02:57 +02:00
Alejandro Alonso 91f6c001c0 📚 Update changelog 2024-09-03 06:47:53 +02:00
Andrey Antukh 0dd8300a80 Merge pull request #5037 from penpot/ladybenko-8615-ds-translations
Enable translations for the DS / Storybook
2024-09-02 19:45:09 +02:00
Eva Marco 298db46722 Add documentation to select on storybook 2024-09-02 16:56:53 +02:00
Alejandro c2b97b13a1 Merge pull request #5042 from penpot/niwinz-update-changelog
📚 Update changelog
2024-09-02 15:22:27 +02:00
Belén Albeza 8feb5dabb0 Use translations in a DS component story 2024-09-02 14:51:41 +02:00
Belén Albeza 8aaa04b1f8 Add English translations to storybook template 2024-09-02 14:51:41 +02:00
Andrey Antukh a28117b301 📚 Add missing backend enhancements on the changelog 2024-09-02 13:21:02 +02:00
Andrey Antukh 0117a4767d 📎 Rename file-snapshot to auto-file-snapshot 2024-09-02 11:52:19 +02:00
Eva Marco 0c6b0598fa Add new select ds component to storybook 2024-08-29 14:14:12 +02:00
Eva Marco f2a2d772b0 Add new select component to the ds 2024-08-29 14:14:08 +02:00
Alejandro d703205921 Merge pull request #5028 from penpot/niwinz-path-changes
🐛 Add missing safechecks and schema validations
2024-08-28 12:41:18 +02:00
Alejandro 1e73bec2b9 Merge pull request #5031 from penpot/niwinz-fire-version-txt
🔥 Remove the usage of version.txt
2024-08-28 12:30:57 +02:00
Andrey Antukh 1abbeb0273 Merge pull request #5015 from penpot/yamila-moreno-patch-1
Update manage.sh
2024-08-28 11:15:07 +02:00
Andrey Antukh d25424d325 🔥 Remove the usage of version.txt 2024-08-28 11:14:28 +02:00
Andrey Antukh 19d6f4381a Merge pull request #5023 from penpot/eva-add-tooltips-to-typography
🐛 Add missing tooltips to typography options
2024-08-27 17:54:41 +02:00
Andrey Antukh 25e9129a8e 🔥 Remove unused and deprecated helpers from app.util.object ns 2024-08-27 16:44:14 +02:00
Andrey Antukh 569674452a Add label and id support for debug snapshot helpers 2024-08-27 15:43:31 +02:00
Andrey Antukh 2643dae0a7 🐛 Fix json encoding on zip encoding decoding 2024-08-27 15:43:31 +02:00
Andrey Antukh 1f53e48032 🐛 Add schema validation for color changes 2024-08-27 10:48:38 +02:00
Andrey Antukh ef3a47b492 Add efficiency changes to get-segments helper 2024-08-27 10:48:38 +02:00
Andrey Antukh f302c724c4 Add efficiency changes on make-curve-point helper 2024-08-27 10:48:37 +02:00
Andrey Antukh 4ce654511b Ensure plain map on path params in several functions 2024-08-27 10:48:37 +02:00
Andrey Antukh 2f68310a7c 📎 Add some fixmes for future path changes 2024-08-27 10:48:37 +02:00
Alejandro 67cd855e97 Merge pull request #5027 from penpot/niwinz-backend-improvements
 Add efficiency improvements to xlog task
2024-08-27 09:59:59 +02:00
Andrey Antukh ceaafdbb1c Add offload mechanism for file snapshots 2024-08-26 13:52:42 +02:00
Andrey Antukh 8dea5d5158 ♻️ Make file-xlog-gc task more scalable 2024-08-26 11:15:59 +02:00
Alejandro 7ea5c79393 Merge pull request #5019 from penpot/niwinz-static-error-duplication
 Add improvements on how UI (React) errors are handled
2024-08-26 09:26:28 +02:00
Andrey Antukh 215148ca81 Add better fillfactor setting for storage_object and task tables 2024-08-23 15:09:58 +02:00
Eva Marco 897960194e 🐛 Add missing tooltips to typography options 2024-08-23 13:42:33 +02:00
Alejandro 5d97f4b924 Merge pull request #5020 from penpot/eva-fix-color-picker
🐛  Fix color picker
2024-08-23 12:37:25 +02:00
Alejandro e7b663749a Merge pull request #5021 from penpot/eva-fix-design-tab-layout
🐛 Fix design tab layout after new tab component added
2024-08-23 12:25:38 +02:00
Eva Marco aa5999b2e0 🐛 Fix design tab layout after new tab component added 2024-08-23 11:59:23 +02:00
Alejandro 736d75a93c Merge pull request #5009 from penpot/niwinz-json-decoder-2
🎉 Add `:assign` operation as alternative to `:set`
2024-08-23 11:53:07 +02:00
Eva Marco c81a17ada5 🐛 Fix color picker 2024-08-23 11:52:05 +02:00
Andrey Antukh 401a28f317 ⬆️ Update rumext to v2.14
Adds some improvements on compiler
2024-08-23 11:21:54 +02:00
Andrey Antukh f16caa2b98 💄 Add cosmetic changes on sidebar/options ns 2024-08-23 11:21:54 +02:00
Andrey Antukh 868af29d14 💄 Add some cosmetic changes to sidebar ns 2024-08-23 11:21:54 +02:00
Andrey Antukh a091c9c910 ♻️ Refactor how UI error reporting is handled 2024-08-23 11:21:54 +02:00
Anonymous 1d1d4d9371 🌐 Add translations for: Serbian.
Currently translated at 96.0% (1395 of 1453 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sr/
2024-08-23 11:16:13 +02:00
Anonymous d578659b21 🌐 Add translations for: Dutch.
Currently translated at 96.2% (1399 of 1453 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-08-23 11:16:13 +02:00
Anonymous 2be9cebb0e 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 96.2% (1399 of 1453 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-08-23 11:16:13 +02:00
Anonymous 982b900066 🌐 Add translations for: Hebrew.
Currently translated at 96.2% (1399 of 1453 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-08-23 11:16:12 +02:00
Anonymous 4c1bc7c3c1 🌐 Add translations for: German.
Currently translated at 96.2% (1399 of 1453 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-08-23 11:16:12 +02:00
Anonymous fb09959459 🌐 Add translations for: Turkish.
Currently translated at 96.2% (1399 of 1453 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-08-23 11:16:12 +02:00
Anonymous 5601ed7071 🌐 Add translations for: Russian.
Currently translated at 96.1% (1397 of 1453 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2024-08-23 11:16:12 +02:00
Anonymous 5ab282c344 🌐 Add translations for: French.
Currently translated at 96.2% (1398 of 1453 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2024-08-23 11:16:12 +02:00
Anonymous 5eb4b28834 🌐 Add translations for: Spanish.
Currently translated at 99.1% (1440 of 1453 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2024-08-23 11:16:11 +02:00
Andrey Antukh 4cdea36b7c 📎 Rehash & sort all translations 2024-08-23 11:09:45 +02:00
Andrey Antukh f2c1d4d83d Merge branch 'translations' into develop 2024-08-23 11:04:10 +02:00
Andrey Antukh 9dd0cd57ce Merge remote-tracking branch 'weblate/develop' into translations 2024-08-23 11:03:39 +02:00
Andrey Antukh 3026bd8aaf Merge remote-tracking branch 'origin/staging' into develop 2024-08-23 10:59:14 +02:00
Andrey Antukh 2ec27de353 🎉 Add :assing operation as altenative to :set 2024-08-23 10:55:10 +02:00
Andrey Antukh 9a3c953f0f Merge pull request #5014 from penpot/juanfran-update-plugins-runtime
⬆️ Update plugins runtime
2024-08-22 12:47:02 +02:00
Yamila Moreno 02c031a0ef 📎 Delete unnecessary option in manage.sh script
Delete `-x` option as it gives an annoying output when we're not debugging.
2024-08-22 12:45:18 +02:00
Juanfran a5114369ba ⬆️ Update plugins runtime 2024-08-22 12:13:20 +02:00
Andrey Antukh 4765685440 Merge pull request #5010 from penpot/eva-replace-tabs-component
♻️ Replace tabs component
2024-08-22 10:31:22 +02:00
Eva Marco 3f3c3a3df4 🐛 Fix labelled by on tab-panel component 2024-08-21 13:39:16 +02:00
Alejandro baa52d432f Merge pull request #5003 from penpot/niwinz-json-decoder
🎉 Add json encode/decode mechanism for schemas
2024-08-21 12:48:41 +02:00
Andrey Antukh 89562d0231 Add schema validation for tabs 2024-08-21 11:55:12 +02:00
Eva Marco 3df9c88bb7 ♻️ Replace layer tabs component with the new tab switcher component 2024-08-21 11:54:39 +02:00
Andrey Antukh cacee40d11 🎉 Add proper schema encoding/decoding mechanism
this allows almost all api operations to success usin application/json
encoding with the exception of the update-file, which we need to
approach a bit differently;

the reason update-file is different, is because the operations vector
is right now defined without the context of shape type, so we are just
unable to properly parse the value to correct type using the schema
decoding mechanism
2024-08-21 11:27:36 +02:00
Eva Marco 1782837a38 ♻️ Replace colorpicker modal tab component 2024-08-20 14:01:29 +02:00
Eva Marco c0cd980f5f ♻️ Replace libraries modal tab component 2024-08-20 09:55:18 +02:00
Eva Marco 2f99d17885 ♻️ Replace tab switcher on design tab 2024-08-19 16:26:17 +02:00
Eva Marco 63ffa704f5 ♻️ Replace tab switcher on viewer 2024-08-19 16:26:10 +02:00
Eva Marco 129b7afda9 ♻️ Remove components preview 2024-08-19 16:25:46 +02:00
Црнобог 1c09670e3c 🌐 Add translations for: Serbian.
Currently translated at 98.6% (1389 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sr/
2024-08-17 13:09:32 +02:00
Andrey Antukh 0db1eed87f Improve word-string schema generator
make it to generate a more readable strings
2024-08-16 14:43:06 +02:00
Andrey Antukh db52d98595 📎 Update user ns on common and backend 2024-08-16 14:43:06 +02:00
Andrey Antukh e46b5b3f57 🎉 Add json module to common 2024-08-16 14:43:06 +02:00
Alejandro d1d3b4353a Merge pull request #4987 from penpot/eva-add-selection-colors-to-ds
  Add selection colors to ds
2024-08-16 07:59:18 +02:00
Црнобог 3c0944ebfc 🌐 Add translations for: Serbian.
Currently translated at 98.5% (1388 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sr/
2024-08-15 17:09:28 +02:00
Црнобог a2725ed8fe 🌐 Add translations for: Russian.
Currently translated at 99.6% (1403 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2024-08-15 17:09:18 +02:00
Alejandro 84bfef9938 Merge pull request #4920 from penpot/palba-request-access
Oops page and Request access
2024-08-14 15:53:45 +02:00
Pablo Alba 6169f5c2e8 🎉 New oops page with login and request access 2024-08-14 15:32:04 +02:00
Andrey Antukh fc14871d01 Merge pull request #4993 from penpot/eva-fix-fill-menu
🐛 Fix collapsing fill menu
2024-08-14 13:09:03 +02:00
Andrey Antukh bcfa1d4394 Merge pull request #4996 from penpot/eltociear-patch-2
🐛 Update time.cljc
2024-08-14 13:08:40 +02:00
Ikko Eltociear Ashimine 2361b2b63a 🐛 Update time.cljc
prefered -> preferred
2024-08-14 12:58:44 +02:00
Alejandro d9a941cb32 Merge pull request #4992 from penpot/juanfran-invalid-content-path-arguments
🐛 Fix typo in command parsing (paras to params)
2024-08-14 09:40:37 +02:00
Eva Marco f43ca1869b 🐛 Fix collapsing fill menu 2024-08-14 09:24:26 +02:00
Juanfran a34b06cfb4 🐛 Fix typo in command parsing (paras to params)
Signed-off-by: Juanfran <juanfran.ag@gmail.com>
2024-08-14 09:23:23 +02:00
Madalena Melo f5b910d391 🌐 Added translation for: Serbian. 2024-08-14 09:06:16 +02:00
Eva Marco b57e68f4b6 Add selection colors to ds 2024-08-13 12:06:46 +02:00
Alejandro d2311f066a Merge pull request #4988 from penpot/niwinz-fix-notifications
🐛 Fix incorrect params for notifications
2024-08-13 11:17:58 +02:00
Andrey Antukh aa39de4ea8 🐛 Fix incorrect params for notifications 2024-08-13 11:04:26 +02:00
Eva Marco 215f6fc0ab Merge pull request #4973 from penpot/ladybenko-8255-focus-mode
🐛 Fix layer sidebar in focus mode for long names
2024-08-13 10:34:41 +02:00
Belén Albeza fc333ae098 Merge pull request #4941 from penpot/eva-tabs-component-ds
  Add tabs component to DS
2024-08-12 17:46:59 +02:00
Belén Albeza d3e891eec3 Enable focusing on tab-switcher* panel 2024-08-12 17:35:28 +02:00
Eva Marco 66d30e35c7 Add tab switcher component documentation 2024-08-12 17:35:28 +02:00
Eva Marco b8693c3f85 Add tab component to the DS 2024-08-12 17:35:26 +02:00
Eva Marco 4b2742efca Merge pull request #4972 from penpot/ladybenko-7887-toast
Implement `toast*` component
2024-08-12 16:28:57 +02:00
Andrey Antukh c1435fba95 Merge pull request #4980 from rasom/fix-link-contributing-guide
📚 Fix dead link in contributing guide
2024-08-12 12:25:23 +02:00
Alejandro b13148265e Merge pull request #4982 from penpot/niwinz-bugfix-3
🐛 Bugfixes
2024-08-12 12:12:15 +02:00
Andrey Antukh c0174ab501 🐛 Fix unhandled exception on try to reuse registration token 2024-08-12 11:55:19 +02:00
Andrey Antukh 77c45ed109 Add better error reporting on offload-file-data task 2024-08-12 11:55:19 +02:00
Andrey Antukh 7df68bb8bd Add :params prop to :not-found exception 2024-08-12 11:55:19 +02:00
Andrey Antukh ab461ba560 🐛 Launch offload only if file-gc has processed the file 2024-08-12 11:55:19 +02:00
Alejandro 280252d40e Merge pull request #4958 from penpot/niwinz-fdata-storage-offload
🎉 Add file-data offload mechanism
2024-08-12 10:59:29 +02:00
Alhassan Atama 09b6989491 🌐 Add translations for: Igbo.
Currently translated at 37.1% (523 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2024-08-11 20:09:13 +00:00
Alhassan Atama f96bbb38b0 🌐 Add translations for: Hausa.
Currently translated at 89.9% (1267 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2024-08-11 20:09:13 +00:00
Roman Volosovskyi f0e3dc2f0e 📚 Fix dead link in contributing guide 2024-08-11 22:03:41 +02:00
Andrey Antukh d2937a76d9 🐛 Fix error handling issue on login with oidc
happens when no oidc backend is configured on backend
2024-08-09 14:28:18 +02:00
Andrey Antukh 3219c150d4 Add better internal fillfactor setting for file table
Increasing the change for HOT updates on db for this heavy-update
table
2024-08-09 14:28:18 +02:00
Andrey Antukh ba167f256b Add performance enhancements on telemetry related queries 2024-08-09 14:28:18 +02:00
Andrey Antukh 0e92bcc0de 🎉 Add file-data offload mechanism 2024-08-09 14:28:18 +02:00
Andrey Antukh f6bfe3931c Add performance enhacements to storage/gc-touched task 2024-08-09 14:13:33 +02:00
Andrey Antukh 253b9e5bd8 Split file-gc task in two separated tasks
Add a new file-gc-scheduler task for analizing all files for
elegibility and leave file-gc task with the responsability to
performn the GC operation.
2024-08-09 14:13:33 +02:00
Belén Albeza 604f80de20 📚 Add documentation for toast* 2024-08-09 13:07:55 +02:00
Belén Albeza 0aa8f7bfc6 🐛 Fix layer sidebar in focus mode for long names 2024-08-09 12:54:42 +02:00
Dário 4cc2c736e9 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 100.0% (1408 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-08-09 04:09:16 +02:00
Belén Albeza 9455b56c07 Implement toast* component 2024-08-08 15:46:57 +02:00
Andrey Antukh d6f528acd2 Merge pull request #4955 from penpot/eva-bugfixing-1
🐛 Fix several bugs
2024-08-08 11:38:18 +02:00
Alejandro Alonso 9344fb958a Merge remote-tracking branch 'origin/staging' into develop 2024-08-08 07:43:10 +02:00
Alejandro 6ee9025f13 Merge pull request #4971 from penpot/niwinz-fdata-storage-offload-2
 Add naming consistency changes for file_data_fragment table
2024-08-08 07:41:54 +02:00
Alejandro 3a2e868d80 Merge pull request #4915 from penpot/niwinz-mini-refactor-notifications
♻️ Mini refactor of notification components locations
2024-08-08 07:18:13 +02:00
Andrey Antukh 86a732600b Add naming consistency changes for file_data_fragment table 2024-08-07 16:34:39 +02:00
Andrey Antukh 22f3dba842 💄 Update run-store helper 2024-08-07 16:07:59 +02:00
Andrey Antukh bde1cd3f5f 🐛 Fix unexpected exception on frontend test
caused by unexpected return nil on a merge-map
2024-08-07 16:07:59 +02:00
Andrey Antukh f187012469 ♻️ Refactor naming and location of flash notifications 2024-08-07 15:04:52 +02:00
Belén Albeza 3917215952 Merge pull request #4969 from penpot/superalex-fix-storybook-css-path
🐛 Fix storybook css path
2024-08-07 09:31:00 +02:00
Alejandro Alonso 5f0c036ad5 🐛 Fix storybook css path 2024-08-07 06:46:06 +02:00
Andrey Antukh f0342f25ea Merge pull request #4967 from penpot/superalex-fix-missing-env-variable-for-building
🐛 Fix missing storybook env variable for building
2024-08-06 17:32:22 +02:00
Alejandro Alonso f5a8c806d9 🐛 Fix missing storybook env variable for building 2024-08-06 16:34:09 +02:00
Alejandro f8cbe1dfbf Merge pull request #4965 from penpot/niwinz-storybook-fixes
⬆️ Update beicon version to v2.2
2024-08-06 12:52:58 +02:00
Andrey Antukh c6c9e7d179 ⬆️ Update beicon version to v2.2
Fixes the esm build compatibility issues
2024-08-06 12:29:18 +02:00
Andrey Antukh 36ac81bb43 Merge pull request #4964 from penpot/superalex-fix-storybook-build
🐛 Fix storybook build
2024-08-06 10:47:54 +02:00
Alejandro Alonso 1515498a23 🐛 Fix storybook build 2024-08-06 10:46:37 +02:00
Andrey Antukh d0059cbe29 Merge pull request #4952 from penpot/ladybenko-8439-storybook-build
Storybook build
2024-08-06 10:25:18 +02:00
sangpil hwang 34cf6e9e58 🌐 Add translations for: Korean.
Currently translated at 15.8% (223 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2024-08-05 21:09:25 +02:00
Nima K 05e283baec 🌐 Add translations for: Persian.
Currently translated at 50.0% (705 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2024-08-05 21:09:24 +02:00
Belén Albeza 457da6f23e Append timestamp to CSS import in storybook 2024-08-05 17:39:57 +02:00
Andrey Antukh 0f16f65d30 🔧 Update script names and conditionally build storybook 2024-08-05 17:39:37 +02:00
Eva Marco 1c2b6f5ab5 Merge pull request #4938 from penpot/ladybenko-7879-ds-input
🎉 Implement input* component
2024-08-05 08:58:53 +02:00
Eva Marco f011d94339 🐛 Fix error on padding input props 2024-08-02 14:14:51 +02:00
Eva Marco d527bbd8b5 🐛 Fix fill collapsed options 2024-08-02 11:12:28 +02:00
Belén Albeza 2015c484d0 📚 Add Input component documentation 2024-08-02 11:08:20 +02:00
Belén Albeza 480d5ba7c3 🎉 Implement input* component 2024-08-02 11:08:20 +02:00
Eva Marco 6b3bfc85b3 🐛 Fix scroll on color picker modal 2024-08-02 09:30:42 +02:00
Belén Albeza 2b2bc73564 🔧 Update build script to include storybook 2024-08-01 16:11:01 +02:00
Belén Albeza 7cdfd5a6d3 Watch css assets specifically for storybook 2024-08-01 15:31:23 +02:00
Belén Albeza 8bcc2a4932 Compile storybook target in release and a separate DS stylesheet 2024-08-01 15:29:02 +02:00
Belén Albeza deef8abca5 🔧 Enable storybook to be served from a subdir 2024-08-01 15:29:02 +02:00
Belén Albeza f683d65990 Merge pull request #4949 from penpot/niwinz-update-beicon
⬆️ Update dependencies
2024-07-31 16:54:04 +02:00
Andrey Antukh 93ec352f4a ⬆️ Update shadown-cljs and beicon dependency
Fixes compatibility issues between shadow-cljs :esm build and beicon
library.
2024-07-31 16:30:22 +02:00
Andrey Antukh e0b009c538 ⬆️ Update yarn version to 4.3.1 2024-07-31 16:30:22 +02:00
Andrey Antukh 5c61d874be Merge remote-tracking branch 'origin/develop' into develop 2024-07-31 12:47:05 +02:00
Andrey Antukh 5cf64c1440 Merge remote-tracking branch 'origin/staging' into develop 2024-07-31 12:46:47 +02:00
Alejandro ed91c7ca32 Merge pull request #4689 from penpot/niwinz-devenv-upgrade
⬆️ Upgrade devenv linux distribution version
2024-07-31 10:46:27 +02:00
Andrey Antukh fdbec9917c Merge pull request #4946 from june128/fix-stuck-progress-bar
Revert "🐛 Set proper default tenant on exporter"
2024-07-31 10:15:28 +02:00
Julian Schacher 5ae28da709 Revert "🐛 Set proper default tenant on exporter"
This reverts commit 86b2ce4dab.
2024-07-31 00:32:48 +02:00
Belén Albeza dba94237dc Merge pull request #4912 from penpot/eva-remove-old-tokens
♻️ Remove unused tokens
2024-07-30 12:03:07 +02:00
Alejandro 802846a838 Merge pull request #4929 from penpot/niwinz-snapshoting-improvements
 Improve file snapshoting mechanism
2024-07-30 08:15:09 +02:00
Andrey Antukh c2aa4f4893 Merge pull request #4936 from n-stha/n-stha-fix-spelling
📎 Correct a spelling in onboarding.edn
2024-07-29 15:23:18 +02:00
n-stha 25c875dc55 📎 Correct a spelling in onboarding.edn 2024-07-29 18:52:40 +05:45
Eva Marco 2c5289d338 ♻️ Remove duplicated color tokens 2024-07-29 11:05:14 +02:00
Alejandro 00c5d58203 Merge pull request #4903 from penpot/palba-bugfixing-010
🐛 Fix Components are not dragged from the group to the assets tab
2024-07-29 10:34:28 +02:00
Andrey Antukh 5cf54c6384 Improve file snapshoting mechanism 2024-07-29 10:19:34 +02:00
Eva Marco 7c75af83b3 Merge pull request #4904 from penpot/eva-substitude-loader-component
♻️  Replacing the old Pencil loader with the new Loader component
2024-07-29 10:08:08 +02:00
Belén Albeza 39b2c1722a ♻️ Refactor loader* component props & usage 2024-07-29 09:20:06 +02:00
Andrey Antukh 40910703ee Merge pull request #4930 from penpot/ladybenko-8413-asset-ids
Assert icon and svg IDs in DS components
2024-07-29 08:45:53 +02:00
Belén Albeza cfc01e03f6 Assert existing icon ID for button* and icon-button* 2024-07-26 14:33:11 +02:00
Belén Albeza 10ef9d696c Assert valid svg id in raw-svg* 2024-07-26 14:24:47 +02:00
Belén Albeza 138ece085e Assert existing icon ID for icon* 2024-07-26 14:24:47 +02:00
alonso.torres 19b2f330dd Merge remote-tracking branch 'origin/plugins-beta-test' into develop 2024-07-26 13:47:12 +02:00
Andrey Antukh e48aa909da Merge pull request #4919 from penpot/ladybenko-7869-ds-buttons
Implement design system buttons
2024-07-26 13:18:21 +02:00
Andrey Antukh b122db447a Merge pull request #4928 from penpot/alotor-plugins-10
Alotor plugins 10
2024-07-26 13:17:16 +02:00
alonso.torres 642c4fc9d1 ⬆️ Update plugins runtime 2024-07-26 12:42:31 +02:00
alonso.torres 4d57f33371 Change local storage access 2024-07-26 12:42:31 +02:00
alonso.torres 60c63e4558 Add undo block api to plugins 2024-07-26 12:42:31 +02:00
alonso.torres aa90232bf9 Add export method into API 2024-07-26 12:42:31 +02:00
Belén Albeza cf7439b1b4 📚 Add buttons documentation for the design system 2024-07-26 11:49:51 +02:00
Belén Albeza 60cba6c9f3 Implement button* and icon-button* for the design system 2024-07-26 11:49:51 +02:00
Andrey Antukh 3eaa997145 Merge remote-tracking branch 'origin/staging' into develop 2024-07-26 08:36:30 +02:00
AlexTECPlayz 28cca332e7 🌐 Add translations for: Romanian.
Currently translated at 90.4% (1274 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2024-07-25 19:09:17 +02:00
Andrey Antukh 025034cb71 Merge remote-tracking branch 'origin/staging' into develop 2024-07-25 11:23:42 +02:00
Andrey Antukh 7fd75336a5 Merge pull request #4918 from penpot/alotor-plugins-9
Alotor plugins 9
2024-07-25 11:04:22 +02:00
Eva Marco af5a189d04 ♻️ Replacing the old Pencil loader with the new Loader component 2024-07-24 17:06:13 +02:00
AlexTECPlayz 7ec80a375a 🌐 Add translations for: Romanian.
Currently translated at 90.1% (1269 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2024-07-24 16:09:21 +02:00
alonso.torres 285119b2e5 ⬆️ Update plugins runtime 2024-07-24 13:43:50 +02:00
Pablo Alba 0539337121 🐛 Fix user cannot close comment creation tool 2024-07-24 09:22:53 +02:00
Pablo Alba 50fe715fba 🐛 Fix Components are not dragged from the group to the assets tab 2024-07-24 09:20:54 +02:00
Eva Marco e5daa00d73 Add elevation tokens to ds 2024-07-24 09:00:03 +02:00
Belén Albeza fd58813ec9 Merge pull request #4908 from penpot/eva-add-zindex-to-ds
 Add z-index tokens to the ds
2024-07-24 08:56:35 +02:00
Andrey Antukh 111add1ed6 🐛 Fix issue on merging stanging to develop 2024-07-24 08:40:31 +02:00
Eva Marco 12d65c7743 Add z-index tokens to the ds 2024-07-24 08:30:46 +02:00
Andrey Antukh a100d1d11a Merge remote-tracking branch 'origin/staging' into develop 2024-07-24 08:20:36 +02:00
Andrey Antukh 28c4053ad3 Merge pull request #4914 from penpot/bameda-patch-1
📎 Add Contributing to the TOC
2024-07-23 21:16:59 +02:00
David Barragán Merino e84d9358d1 📎 Add Contributing to the TOC 2024-07-23 21:15:39 +02:00
alonso.torres 2d25df33ce Add write methods to prototype API 2024-07-23 16:55:32 +02:00
Andrey Antukh c51778f391 Merge pull request #4910 from penpot/ladybenko-8394-fix-sourcesans
🐛 Fix font declarations for Source Sans
2024-07-23 16:04:36 +02:00
Belén Albeza 20333d8179 🐛 Fix font declarations for Source Sans 2024-07-23 14:24:04 +02:00
alonso.torres 6454e878dd Page functions for plugins api 2024-07-22 13:10:10 +02:00
alonso.torres d13b9ef3ea 🐛 Fix wrap not working in plugins 2024-07-22 11:27:29 +02:00
alonso.torres 26fa2a71ea 🐛 Fix problem with reset alignment on change text 2024-07-22 11:27:12 +02:00
Andrey Antukh 46ed61f070 Merge pull request #4878 from penpot/alotor-plugins-8
Alotor plugins 8
2024-07-22 11:17:03 +02:00
Eva Marco 09cd45fd8b Merge pull request #4876 from penpot/ladybenko-7914-spacing-tokens
 Add spacing tokens to the design system
2024-07-22 08:11:54 +02:00
Eva Marco d162e3e11b Merge pull request #4879 from penpot/ladybenko-8349-storybook-theme
 Enable themes in Storybook
2024-07-22 07:47:46 +02:00
Belén Albeza a5dd2683cd 💄 Remove deprecated StoryWrapper helper component 2024-07-12 14:05:44 +02:00
Belén Albeza ebda46f748 Support theme switching within Storybook 2024-07-12 14:05:44 +02:00
Belén Albeza 0d8c98dcfe ⬆️ Upgrade storybook (patch version) 2024-07-12 14:05:44 +02:00
Belén Albeza 174ca6bed5 Merge pull request #4870 from penpot/eva-add-loader-component-to-ds
  Add loader component to ds
2024-07-12 14:04:10 +02:00
Eva Marco 271be57c99 Add loader component to the ds 2024-07-12 13:51:33 +02:00
alonso.torres 423d2fbb92 ⬆️ Update plugins runtime 2024-07-12 13:36:00 +02:00
alonso.torres 21b15167dd Add detach method to plugins 2024-07-12 13:36:00 +02:00
alonso.torres 59005e3bb8 Changes to plugin events 2024-07-12 13:36:00 +02:00
alonso.torres 60f637e947 Add parent property to shapes 2024-07-12 13:36:00 +02:00
alonso.torres 8ded4811bb Internal refactor of plugin installs 2024-07-12 13:36:00 +02:00
Eva Marco 3900b37f5c Merge pull request #4874 from penpot/ladybenko-8008-upgrade-storybook8
Upgrade to Storybook 8
2024-07-12 10:07:14 +02:00
TheScientistPT cfb5a98cac 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 99.5% (1402 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-07-11 22:09:23 +02:00
Nima K 03ddb32556 🌐 Add translations for: Persian.
Currently translated at 48.7% (686 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2024-07-11 22:09:22 +02:00
Belén Albeza 1398bcbc8c Add spacing tokens to the design system 2024-07-11 18:08:33 +02:00
Belén Albeza 93d9438f6c 📚 Adapts Heading docs/stories to be as the Text component 2024-07-11 17:06:15 +02:00
Belén Albeza 0fce6c5ebb 🐛 Fix broken typography docs after upgrading to v8 2024-07-11 17:01:04 +02:00
Belén Albeza a03eeb63e7 ⬆️ Upgrade to storybook v8 2024-07-11 15:41:24 +02:00
Alejandro Alonso 4dd7880744 Add create-devenv command to manage.sh 2024-07-11 13:24:20 +02:00
Eva Marco 5ab7123566 Merge pull request #4865 from penpot/ladybenko-8347-storybook-controls
Use Storybook controls for stories
2024-07-11 13:06:56 +02:00
Belén Albeza ba4732c526 🐛 Fix text component misbehaving when prop is empty string 2024-07-11 12:56:25 +02:00
Alejandro 73fb95976c Merge pull request #4863 from penpot/niwinz-refactor-backend-config
♻️ Refactor configuration validation
2024-07-11 12:27:59 +02:00
Belén Albeza b473b7905d 🐛 Fix Text docs re: accessibility 2024-07-11 12:24:19 +02:00
Belén Albeza ba36023ae6 Revamp RawSvg stories and update prop name 2024-07-11 10:06:46 +02:00
Alejandro Alonso 48e7cd28b3 Merge remote-tracking branch 'origin/staging' into develop 2024-07-11 09:24:17 +02:00
Alejandro 54711b0d25 Merge pull request #4864 from penpot/niwinz-kondo-fix
🐛 Fix exception on clj-kondo extension hook fn
2024-07-11 09:22:25 +02:00
Belén Albeza 7e61acc4da 💄 Update icon prop name 2024-07-10 16:15:07 +02:00
Belén Albeza d7ca4d49dc Revamp icon stories 2024-07-10 15:53:27 +02:00
Belén Albeza df858c2c7d 📎 Move icon and raw-svg components to a common subdir 2024-07-10 15:42:19 +02:00
Belén Albeza 9d3a282c0a 📎 Move typography-related components to their own folder 2024-07-10 15:33:38 +02:00
Andrey Antukh 9174bb140b ♻️ Refactor configuration validation
Replace spec with schema
2024-07-10 15:16:28 +02:00
Belén Albeza 54da6832f3 Revamp Text stories to display controls + rename tag prop to as 2024-07-10 13:34:09 +02:00
Belén Albeza 508f4fcd3c Revamp Heading stories to display controls 2024-07-10 13:31:56 +02:00
Andrey Antukh eaaff76aad 🐛 Fix exception on clj-kondo extension hook fn
The exception is hidden on normal cli invocatin of clj-kondo
and hapens when component with empty params is defined.
2024-07-10 13:27:12 +02:00
Belén Albeza 0d0b5ead86 📎 Remove unused storybook addons + other libs 2024-07-10 12:44:15 +02:00
Belén Albeza 7f6bfacff1 Merge pull request #4857 from penpot/eva-add-text-component-to-storybook
 Add text component to storybook
2024-07-10 12:31:42 +02:00
Eva Marco 7bd5d31094 Add text component to storybook 2024-07-10 12:18:37 +02:00
Eva Marco 645bc32121 Add new heading component to the DS 2024-07-10 12:03:55 +02:00
Belén Albeza 6e55260160 Merge pull request #4851 from penpot/eva-add-heading-component-to-storybook
 Add heading component to storybook
2024-07-10 12:01:57 +02:00
Eva Marco 2abbb0d359 Add new heading component to the DS 2024-07-10 11:46:06 +02:00
Nima K b13238eb30 🌐 Add translations for: Persian.
Currently translated at 47.9% (675 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2024-07-09 18:09:34 +02:00
Alejandro Alonso f09cfedebc Merge remote-tracking branch 'origin/staging' into develop 2024-07-09 14:12:57 +02:00
Andrey Antukh 0a86d9d515 Merge remote-tracking branch 'origin/staging' into develop 2024-07-09 12:16:45 +02:00
Eva Marco 808ed6a98b Merge pull request #4853 from penpot/ladybenko-8291-update-assets
Update icons and svg assets (design system)
2024-07-09 08:12:36 +02:00
Eva Marco 2bb59671dd Merge pull request #4856 from penpot/ladybenko-8337-enable-ds-css-override
Enable design system CSS override + fix debug css compiling
2024-07-09 08:11:07 +02:00
Belén Albeza fb6ebcd074 🐛 Fix debug css being included in prod builds 2024-07-08 15:57:35 +02:00
Belén Albeza 44a2a63fb8 Ensure DS scss modules are compiled before the app css modules 2024-07-08 15:57:35 +02:00
Belén Albeza eae19e8252 📎 Remove leftover code 2024-07-08 15:57:35 +02:00
Andrey Antukh 1b05e7aa36 Merge pull request #4852 from penpot/alotor-plugins-7
 Small improvements over plugin manager
2024-07-08 09:04:44 +02:00
Belén Albeza be9a2767ea 📎 Remove unused rocket icon 2024-07-05 15:28:25 +02:00
Belén Albeza 6a18791c30 Add character icons 2024-07-05 15:26:45 +02:00
alonso.torres 2cc3f65323 Small improvements over plugin manager 2024-07-05 15:16:09 +02:00
Belén Albeza 63d1b558d1 Add percentage icon and update puzzle icon 2024-07-05 15:09:01 +02:00
Belén Albeza cfbfda925b 📚 Fix typo in RawSvg storybook docs 2024-07-05 14:57:22 +02:00
Belén Albeza fd1ab29920 Update v2-related svg assets 2024-07-05 14:34:59 +02:00
alonso.torres 35f4a07d27 Merge remote-tracking branch 'origin/plugins-beta-test' into develop 2024-07-05 11:12:58 +02:00
Eva Marco 9ce3f6da45 Merge pull request #4845 from penpot/ladybenko-8265-raw-svg-component
RawSVG component (design system) + generate SVG sprite for assets
2024-07-05 09:44:35 +02:00
Belén Albeza b8b199c5ec Merge pull request #4844 from penpot/eva-fix-visual-test
🐛  Fix workspace visual test for assets modal
2024-07-05 09:23:22 +02:00
Belén Albeza e492284abe Merge pull request #4843 from penpot/eva-remove-unused-locale-fn
♻️  Remove unused locale fn
2024-07-05 09:22:24 +02:00
Eva Marco 3d5e064358 ♻️ Remove unused locale function 2024-07-05 09:04:34 +02:00
Andrey Antukh 6f8ce1fc5a Merge pull request #4849 from penpot/alotor-plugins-6
 Add events for plugins
2024-07-04 17:17:38 +02:00
Andrey Antukh 5ae30ea9bc Merge pull request #4850 from penpot/alotor-bug-api-svg
🐛 Fix problem when creating SVG images
2024-07-04 17:15:35 +02:00
Belén Albeza 8959d39356 Merge pull request #4837 from penpot/eva-ds-foundations-typography
 Add typography ds tokens
2024-07-04 16:11:25 +02:00
alonso.torres aded9f1a36 🐛 Fix problem when creating SVG images 2024-07-04 16:01:02 +02:00
Belén Albeza 3a34eb1357 🐛 Fix flakiness of visual workspace test (assets tab) 2024-07-04 15:45:50 +02:00
alonso.torres c2564eaf65 Change spec location 2024-07-04 15:16:58 +02:00
alonso.torres 53d3b2abbc Add events for plugins 2024-07-04 15:16:34 +02:00
Belén Albeza 5d90c463a3 📚 Add docs for RawSvg component 2024-07-04 14:48:57 +02:00
Belén Albeza 5309da2eee 📎 Add storybook helper components and improve current stories 2024-07-04 14:11:03 +02:00
Andrey Antukh d1dd13fde9 Merge pull request #4842 from penpot/alotor-plugins-5
Alotor plugins 5
2024-07-04 13:59:48 +02:00
alonso.torres 0a83306015 ⬆️ Update plugins runtime 2024-07-04 13:57:04 +02:00
Belén Albeza 203a39f07c ♻️ Rename IconGrid to StoryGrid (storybook helper component) 2024-07-04 13:05:23 +02:00
Alejandro 85579b253f Merge pull request #4681 from penpot/niwinz-enhancements-2
🔥 Replace spec usage on RPC methods with schema
2024-07-04 12:56:41 +02:00
Eva Marco 1ca751bc42 🐛 Fix workspace visual test for assets modal 2024-07-04 12:41:48 +02:00
Andrey Antukh f344eee778 🐛 Fix backend test initialization code 2024-07-04 12:30:28 +02:00
Andrey Antukh 28c2197ba7 ♻️ Add string length validation to backend RPC methods fields 2024-07-04 12:30:28 +02:00
Andrey Antukh 0721760900 Add better schema for shape export property 2024-07-04 12:30:28 +02:00
Andrey Antukh 45c77f97ce 📎 Fix code style issue not catched by the linter
Comment indentantion
2024-07-04 12:30:28 +02:00
Andrey Antukh 05fb46a573 Accept uri instances on http client 2024-07-04 12:30:28 +02:00
Andrey Antukh ad6a864478 🔥 Replace spec with schema on search RPC methods 2024-07-04 12:30:28 +02:00
Andrey Antukh 37fcc74ef8 🔥 Replace spec with schema on media RPC methods 2024-07-04 12:30:28 +02:00
Andrey Antukh c4cf745d77 🔥 Replace spec with schema on ldap RPC methods 2024-07-04 12:30:28 +02:00
Andrey Antukh 85108672bf 🔥 Replace spec with schema on file-thumbnails RPC methods 2024-07-04 12:30:28 +02:00
Andrey Antukh 969c9105fd 🔥 Replace spec with schema on feedback RPC methods 2024-07-04 12:30:28 +02:00
Andrey Antukh 073621f29a 🔥 Replace spec with schema on demo RPC methods 2024-07-04 12:30:28 +02:00
Andrey Antukh bbc0089166 🔥 Replace spec with schema on verify-token RPC methods 2024-07-04 12:30:28 +02:00
Andrey Antukh 268f1d40aa 🔥 Replace spec with schema on file-share RPC methods 2024-07-04 12:30:28 +02:00
Andrey Antukh e9a28b034f 🔥 Replace spec with schema on file RPC methods 2024-07-04 12:30:28 +02:00
Andrey Antukh bfca324623 🔥 Replace spec with schema on project rpc methods 2024-07-04 12:30:28 +02:00
Andrey Antukh 279c6337e4 🔥 Replace spec with schema on access-token rpc methods 2024-07-04 12:30:28 +02:00
Andrey Antukh 368917f7f5 🔥 Replace spec with schema on webhooks rpc methods 2024-07-04 12:30:28 +02:00
Andrey Antukh f973faa409 📎 Add missing SPEC tag on doc entry template 2024-07-04 12:30:28 +02:00
Andrey Antukh a0339132dd 📎 Update cljfmt config 2024-07-04 12:30:28 +02:00
Andrey Antukh 40f947fc9f 🐛 Fix issue with schema uri decoder 2024-07-04 12:30:28 +02:00
Andrey Antukh d2bedec59c Unify naming of schema registering functions 2024-07-04 12:30:28 +02:00
alonso.torres 1b93ccdec9 Remove duplicate permissions in confirm dialog 2024-07-04 12:07:54 +02:00
Alejandro Alonso 2921b62b37 Merge remote-tracking branch 'origin/staging' into develop 2024-07-04 11:56:21 +02:00
alonso.torres 83f3ae1fbd Change plugins url 2024-07-04 11:52:31 +02:00
Eva Marco 66d97cb2e0 Add typography ds tokens 2024-07-04 11:18:05 +02:00
Belén Albeza 4ecaaba1e5 Add RawSvg component to the design system 2024-07-04 10:11:12 +02:00
Belén Albeza c52da573c5 📎 Fix react warning on icon* component 2024-07-04 09:47:37 +02:00
Belén Albeza 4ac18e2ef0 🐛 Fix cursors svg duplicating icons sprites 2024-07-04 09:20:18 +02:00
Belén Albeza f05e1354ff 🔧 Add assets svg sprite generation 2024-07-04 09:20:18 +02:00
Andrey Antukh bafe2ab985 Merge pull request #4832 from penpot/ladybenko-7866-icon-component
Implement icon component (design system)
2024-07-03 19:33:16 +02:00
Andrey Antukh 0d13a12d64 Merge pull request #4840 from penpot/alotor-plugins-4
Alotor plugins 4
2024-07-03 16:49:47 +02:00
alonso.torres deb0fab156 🐛 Fix small visual problems 2024-07-03 15:35:43 +02:00
Belén Albeza c181887266 📎 Remove unused example simple_button component 2024-07-03 15:26:11 +02:00
Belén Albeza a624a10c85 🐛 Fix scrolling not working on storybook mdx files 2024-07-03 15:26:11 +02:00
Belén Albeza 6295fbf7e2 Implement icon* component 2024-07-03 15:26:11 +02:00
Belén Albeza c6a7ad0520 🐛 Fix template generation for storybook 2024-07-03 15:25:30 +02:00
Belén Albeza de89dfe27f 📎 Remove unused legacy icons preview 2024-07-03 15:25:30 +02:00
alonso.torres a8463f349a New plugin attributes 2024-07-03 15:23:05 +02:00
Belén Albeza 3b6ed823b9 Merge pull request #4839 from penpot/niwinz-rumext-linter
 Add better linter for rumext defc macro
2024-07-03 15:22:55 +02:00
alonso.torres 80e17f8cfc Add new signature for the run-store test helper 2024-07-03 15:22:42 +02:00
Andrey Antukh 8fae4550c3 Add better linter for rumext defc macro 2024-07-03 14:36:29 +02:00
Alejandro Alonso 9125b46ca5 Merge remote-tracking branch 'origin/staging' into develop 2024-07-03 08:46:26 +02:00
Alejandro 7ed25d2dba Merge pull request #4765 from penpot/niwinz-refactor-forms
♻️ Refactor forms (spec -> schema)
2024-07-03 08:45:42 +02:00
Andrey Antukh 0fa8aca6e2 Add minor improvements to common.schema ns 2024-07-03 08:25:51 +02:00
Andrey Antukh 7be79c10fd ♻️ Refactor forms
Mainly replace spec with schema with better
and more reusable validations
2024-07-03 08:25:51 +02:00
Andrey Antukh f095e1b29f Replace custom all-spaces? fn with generic str/blank? 2024-07-02 17:47:58 +02:00
Andrey Antukh 96822082bd Revert " Rehash and sync translations"
This reverts commit 5b6fc19e00.
2024-07-02 17:47:29 +02:00
Aitor Moreno c637912337 Merge pull request #4822 from penpot/niwinz-translation-management-script
🎉 Add new translations management script
2024-07-02 16:20:25 +02:00
AzazelN28 5b6fc19e00 Rehash and sync translations 2024-07-02 16:04:57 +02:00
Andrey Antukh f6435461a1 Merge pull request #4835 from penpot/alotor-plugins-3
🐛 Fix some problems in plugins
2024-07-02 15:44:50 +02:00
AzazelN28 83e51abd35 🐛 Fix package.json translation scripts 2024-07-02 15:41:00 +02:00
Andrey Antukh dab88404c0 Merge pull request #4834 from penpot/ladybenko-fix-layer-width
🐛 Fix layer width on the left sidebar
2024-07-02 15:37:22 +02:00
alonso.torres c66ff74f21 🐛 Fix some problems in plugins 2024-07-02 15:30:35 +02:00
Belén Albeza 769655f565 🐛 Fix layer width on the left sidebar 2024-07-02 15:18:27 +02:00
Andrey Antukh 139dd7d80f 🎉 Add new translations management script 2024-07-02 15:11:56 +02:00
Andrey Antukh 62cea62356 Merge pull request #4816 from penpot/alotor-plugins-2
Improvements over plugins subsystem
2024-07-02 13:43:15 +02:00
alonso.torres 58fa10a0d5 ⬆️ Update plugins runtime 2024-07-02 11:08:56 +02:00
alonso.torres 395a91c00c Plugins permissions review 2024-07-02 11:01:43 +02:00
alonso.torres 741bf3b666 New plugins permissions dialog 2024-07-02 11:01:43 +02:00
alonso.torres fbce59e81f Improved text handling in plugins 2024-07-02 10:41:06 +02:00
alonso.torres ac58a5b8fa Improved transformation from and to JS for plugins 2024-07-02 10:41:06 +02:00
alonso.torres 42230f2630 Change shapes color 2024-07-02 10:41:06 +02:00
Belén Albeza 9a188fd792 Merge pull request #4830 from penpot/eva-ds-color-foundations
 Add DS foundation colors
2024-07-01 16:04:24 +02:00
Eva Marco 6c68a34d63 Add DS foundation colors 2024-07-01 15:10:46 +02:00
Eva Marco 746b448f30 Merge pull request #4825 from penpot/ladybenko-8224-setup-ds
Setup design system
2024-07-01 12:49:22 +02:00
Belén Albeza b5aba58aac Make storybook wrapper to render both dark and light themes 2024-07-01 11:14:42 +02:00
Belén Albeza 2bb7726180 📎 Remove unneeded icons export in the storybook target 2024-07-01 11:14:42 +02:00
Belén Albeza 24b607cad3 💄 Move the stub design system component to its own ds folder 2024-07-01 11:14:42 +02:00
Eva Marco 087d0e90e6 Merge pull request #4821 from penpot/ladybenko-8182-prettier
Add prettier for JS files
2024-07-01 10:48:01 +02:00
Belén Albeza e448951d6f Add CI rule for checking JS format 2024-07-01 10:29:57 +02:00
Belén Albeza ecbedf847f 💄 Reformat affected JS files 2024-07-01 10:29:57 +02:00
Belén Albeza 3efd5cb9e8 Add prettier JS commands to package.json 2024-07-01 10:29:57 +02:00
Belén Albeza e1dc964c4c Upgrade prettier and use specific settings for scss files only 2024-07-01 10:23:19 +02:00
The_BadUser 295c932646 🌐 Add translations for: Russian.
Currently translated at 99.6% (1403 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2024-06-30 21:10:08 +02:00
The_BadUser 5b9f4128f3 🌐 Add translations for: Russian.
Currently translated at 89.3% (1258 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2024-06-29 01:26:20 +02:00
The_BadUser 100f1229ce 🌐 Add translations for: Russian.
Currently translated at 75.9% (1070 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2024-06-29 00:09:23 +02:00
Aryiu adf24cd274 🌐 Add translations for: Catalan.
Currently translated at 77.1% (1086 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2024-06-29 00:09:22 +02:00
Andrey Antukh de1d154e9c Merge pull request #4824 from penpot/alotor-fix-plugins
🐛 Fix problem with recursive properties
2024-06-28 16:21:58 +02:00
alonso.torres 4345584384 🐛 Fix problem with recursive properties 2024-06-28 14:57:14 +02:00
Alejandro Alonso ba1adf91ca Merge remote-tracking branch 'origin/staging' into develop 2024-06-28 11:09:37 +02:00
Andrey Antukh 024b08de06 Merge remote-tracking branch 'origin/staging' into develop 2024-06-28 10:37:22 +02:00
Alejandro 4e5eabbf05 Merge pull request #4781 from penpot/ladybenko-8110-clean-global-css
 Clean global css styles
2024-06-28 08:48:21 +02:00
Belén Albeza c9258b5526 Remove workspace partial scss stylesheet 2024-06-27 15:35:57 +02:00
Belén Albeza ce9fb80558 📎 Remove unused css for loader 2024-06-27 15:35:57 +02:00
Belén Albeza cd8e2540de Use CSS modules for styling the debug icons preview page 2024-06-27 15:35:57 +02:00
Belén Albeza 9a2ee806e4 Clean up base scss 2024-06-27 15:35:57 +02:00
Belén Albeza b192887d19 Remove framework scss stylesheet 2024-06-27 15:35:57 +02:00
Belén Albeza b6e918b024 Remove unused legacy sass files 2024-06-27 15:35:57 +02:00
Belén Albeza ed174d16d1 Remove unused animation mixin 2024-06-27 15:35:57 +02:00
Belén Albeza 6b8108afda Make reset css to be included first thing in our styles 2024-06-27 15:35:57 +02:00
Belén Albeza 49fd000217 Clean up fonts scss files 2024-06-27 15:35:57 +02:00
Belén Albeza 0c3f47b0c3 Clean up unused selectors / mixins / sass vars 2024-06-27 15:35:57 +02:00
Belén Albeza 2d53b96a15 Initial clean up of framework scss 2024-06-27 15:35:57 +02:00
Belén Albeza 540875c41e Remove unused css var colors 2024-06-27 15:35:57 +02:00
Belén Albeza 595f153d85 Remove importing animate.css 2024-06-27 15:35:57 +02:00
Belén Albeza 0cbc3487b0 🔧 Tweak the pixel diff ratio of playwright 2024-06-27 15:35:57 +02:00
Alejandro Alonso 978ac68474 Merge remote-tracking branch 'origin/staging' into develop 2024-06-27 09:54:48 +02:00
Alejandro Alonso 045e83e871 Merge remote-tracking branch 'origin/staging' into develop 2024-06-27 08:44:23 +02:00
Andrey Antukh 0239139b4b Merge pull request #4802 from penpot/jordisala1991-feature/no-direct-download
 Return blob when exporting using the lib-penpot
2024-06-27 07:59:38 +02:00
Alejandro 8c411e7727 Merge pull request #4808 from penpot/ladybenko-fix-playwright-test-mac-shortcut
🐛 Fix playwright test mac shortcut
2024-06-27 07:48:36 +02:00
Belén Albeza 16d0b925fa 🐛 Fix workspace 'make group' test on Mac 2024-06-26 17:02:24 +02:00
Belén Albeza f306ddb51f 🐛 Fix onboarding playwright test on CI mode 2024-06-26 17:01:58 +02:00
Andrey Antukh 4faf9bbff1 📎 Add minor change on how promise is created from observable 2024-06-26 11:27:30 +02:00
Jordi Sala Morales 44b6d1a516 Return blob when exporting using the lib-penpot 2024-06-26 11:19:09 +02:00
Alejandro Alonso 4ed6e1e8ec Merge remote-tracking branch 'origin/staging' into develop 2024-06-26 07:42:50 +02:00
Alejandro 4437fc43e4 Merge pull request #4799 from penpot/ladybenko-fix-visual-regression-tests
Fix broken visual regression test + refactor Dashboard POM
2024-06-26 07:01:49 +02:00
Andrey Antukh 253e5a11fc Merge remote-tracking branch 'origin/staging' into develop 2024-06-25 16:33:52 +02:00
Alejandro Alonso 6e734f2eac Merge remote-tracking branch 'origin/staging' into develop 2024-06-25 15:54:32 +02:00
Belén Albeza 30edca024a Remove unneeded draftsFile and newFile locators in dashboard POM 2024-06-25 15:26:49 +02:00
Belén Albeza c14f783d94 Remove redundant locators for Dashboard POM 2024-06-25 15:16:13 +02:00
Belén Albeza 8f3452c0af Avoid using unneeded test ids in dashboard POM 2024-06-25 14:57:37 +02:00
Belén Albeza 97a1b59861 💄 Reformat DashboardPage.js according to prettier rules 2024-06-25 11:56:31 +02:00
Alejandro Alonso 41aad7558b Merge remote-tracking branch 'origin/staging' into develop 2024-06-25 10:49:32 +02:00
Alejandro Alonso d48616d510 Merge remote-tracking branch 'origin/staging' into develop 2024-06-25 10:18:42 +02:00
Andrey Antukh 8296e72887 Merge pull request #4793 from penpot/alotor-plugins-2
 Add some missing text properties in plugins
2024-06-25 07:47:34 +02:00
Oğuz Ersen 2648e71f5b 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1408 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-06-24 21:09:19 +02:00
alonso.torres 5771f2f8aa Plugins retrieve selection colors 2024-06-24 15:26:47 +02:00
alonso.torres f86156b619 Plugins support for code generation 2024-06-24 15:26:21 +02:00
alonso.torres 8ff0015458 Add to plugins connect and fetch libraries 2024-06-24 12:34:35 +02:00
alonso.torres 84ecb99400 Add some missing text properties in plugins 2024-06-24 09:57:57 +02:00
Stas Haas 5160ae364d 🌐 Add translations for: German.
Currently translated at 100.0% (1408 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-06-22 10:09:13 +02:00
Andrey Antukh b9e40b4d82 Merge pull request #4788 from penpot/alotor-plugins-2
Fix problem with plugins initialization
2024-06-21 14:58:42 +02:00
alonso.torres 4b67c0593d Upload media with data in plugins 2024-06-21 12:46:31 +02:00
alonso.torres fb1429956a Changed order of plugins initialization 2024-06-21 12:44:35 +02:00
Andrey Antukh c70d20f95d Merge pull request #4783 from penpot/alotor-plugins-texts
Update plugins
2024-06-21 10:03:52 +02:00
alonso.torres 47d211cd87 ⬆️ Update plugin runtime 2024-06-21 09:29:09 +02:00
alonso.torres 7fd223893b Expose component properties in components 2024-06-21 09:29:09 +02:00
alonso.torres 1794859468 Review input validation for plugins 2024-06-21 09:29:09 +02:00
alonso.torres c5c8be4b4a Improve input validation in plugins 2024-06-21 09:29:09 +02:00
alonso.torres e13d543dcd Add geometry utils 2024-06-21 09:29:09 +02:00
alonso.torres 69fad7a920 Add some utilities for fonts in plugins 2024-06-21 09:29:09 +02:00
alonso.torres 2da5dcb619 Add text ranges support in plugins 2024-06-21 09:29:09 +02:00
Stas Haas 3260130042 🌐 Add translations for: German.
Currently translated at 99.8% (1406 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-06-21 07:09:15 +00:00
Unreal Vision c77316f91e 🌐 Add translations for: French.
Currently translated at 100.0% (1408 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2024-06-21 07:09:14 +00:00
Pablo Alba 018464aedf ♻️ Unify move shape on workspace and relocate on layers panel 2024-06-20 16:00:39 +02:00
Andrey Antukh 2982dde7df Merge pull request #4776 from penpot/SudoVanilla-patch-1
🔥 Remove version specification from docker-compose.yaml file
2024-06-20 08:28:04 +02:00
SudoVanilla acb7ca5440 🔥 Remove version specification from docker-compose.yaml file
It is deprecated
2024-06-20 08:16:02 +02:00
Yaron Shahrabani 8a7fdaa6ca 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1408 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-06-19 22:09:19 +02:00
Stas Haas 27c574f0a7 🌐 Add translations for: German.
Currently translated at 99.5% (1401 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-06-19 22:09:18 +02:00
Unreal Vision 6d82d54560 🌐 Add translations for: French.
Currently translated at 96.6% (1361 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2024-06-19 22:09:17 +02:00
Alejandro Alonso cba62c0172 Merge remote-tracking branch 'origin/staging' into develop 2024-06-19 08:12:19 +02:00
Alejandro Alonso 47b455ba87 Merge remote-tracking branch 'origin/staging' into develop 2024-06-18 16:57:51 +02:00
Stephan Paternotte 3f29071139 🌐 Add translations for: Dutch.
Currently translated at 100.0% (1408 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-06-18 15:09:24 +02:00
Yaron Shahrabani ccdfac7b8e 🌐 Add translations for: Hebrew.
Currently translated at 96.2% (1355 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-06-18 15:09:23 +02:00
Stas Haas be959ca90c 🌐 Add translations for: German.
Currently translated at 98.7% (1391 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-06-18 15:09:22 +02:00
Brendan Bell 2929783d35 📎 Update docker-compose.yaml 2024-06-17 13:30:51 +02:00
Andrey Antukh 838843be45 Merge branch 'translations' into develop 2024-06-17 10:09:05 +02:00
Anonymous 1225d72917 🌐 Add translations for: Yoruba.
Currently translated at 85.2% (1200 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2024-06-17 10:07:39 +02:00
Anonymous 9c4a310d84 🌐 Add translations for: Igbo.
Currently translated at 37.1% (523 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2024-06-17 10:07:39 +02:00
Anonymous d3e9fd9a36 🌐 Add translations for: Malay.
Currently translated at 48.1% (678 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ms/
2024-06-17 10:07:39 +02:00
Anonymous d2c63e0857 🌐 Add translations for: Spanish (Latin America).
Currently translated at 9.5% (134 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es_419/
2024-06-17 10:07:39 +02:00
Anonymous 52c00367d7 🌐 Add translations for: Hausa.
Currently translated at 89.9% (1267 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2024-06-17 10:07:39 +02:00
Anonymous 6ee844b48f 🌐 Add translations for: Afrikaans.
Currently translated at 6.8% (97 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/af/
2024-06-17 10:07:39 +02:00
Anonymous 542ad58c32 🌐 Add translations for: Dutch.
Currently translated at 96.7% (1362 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-06-17 10:07:38 +02:00
Anonymous 4781cf1037 🌐 Add translations for: Latvian.
Currently translated at 96.3% (1357 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2024-06-17 10:07:38 +02:00
Anonymous 5aa3b432bf 🌐 Add translations for: Korean.
Currently translated at 15.5% (219 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2024-06-17 10:07:38 +02:00
Anonymous 47401aae99 🌐 Add translations for: Bengali.
Currently translated at 1.0% (15 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/bn/
2024-06-17 10:07:37 +02:00
Anonymous 27473003b7 🌐 Add translations for: Faroese.
Currently translated at 11.2% (159 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fo/
2024-06-17 10:07:37 +02:00
Anonymous fa8bc7d40d 🌐 Add translations for: Ukrainian (ukr_UA).
Currently translated at 14.9% (210 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2024-06-17 10:07:37 +02:00
Anonymous f352cdf9a3 🌐 Add translations for: Croatian.
Currently translated at 72.2% (1017 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2024-06-17 10:07:37 +02:00
Anonymous 3d68d454fe 🌐 Add translations for: Tamil.
Currently translated at 3.2% (46 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ta/
2024-06-17 10:07:37 +02:00
Anonymous 9a9eca7813 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 94.6% (1332 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-06-17 10:07:36 +02:00
Anonymous f3d82c915b 🌐 Add translations for: Finnish.
Currently translated at 3.9% (55 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fi/
2024-06-17 10:07:36 +02:00
Anonymous 5185d1eaa5 🌐 Add translations for: Czech.
Currently translated at 92.8% (1307 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2024-06-17 10:07:36 +02:00
Anonymous 6abf792e0f 🌐 Add translations for: Basque.
Currently translated at 83.4% (1175 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2024-06-17 10:07:36 +02:00
Anonymous 4d56f86719 🌐 Add translations for: Japanese.
Currently translated at 16.6% (235 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ja/
2024-06-17 10:07:35 +02:00
Anonymous f033814f96 🌐 Add translations for: Galician.
Currently translated at 26.7% (376 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/gl/
2024-06-17 10:07:35 +02:00
Anonymous 2e3dbfeb27 🌐 Add translations for: Lithuanian.
Currently translated at 8.4% (119 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lt/
2024-06-17 10:07:35 +02:00
Anonymous a50e431c7a 🌐 Add translations for: Polish.
Currently translated at 81.3% (1146 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2024-06-17 10:07:35 +02:00
Anonymous 59e18e8d4c 🌐 Add translations for: Italian.
Currently translated at 32.2% (454 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2024-06-17 10:07:34 +02:00
Anonymous 731299fe60 🌐 Add translations for: Persian.
Currently translated at 47.8% (674 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2024-06-17 10:07:34 +02:00
Anonymous 5cb2c1bbe4 🌐 Add translations for: Malayalam.
Currently translated at 3.7% (53 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ml/
2024-06-17 10:07:34 +02:00
Anonymous 5362d9f5e9 🌐 Add translations for: Chinese (Traditional).
Currently translated at 45.8% (646 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2024-06-17 10:07:34 +02:00
Anonymous 981cd92b26 🌐 Add translations for: Hebrew.
Currently translated at 96.0% (1353 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-06-17 10:07:34 +02:00
Anonymous 77b70cac60 🌐 Add translations for: Indonesian.
Currently translated at 96.0% (1352 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2024-06-17 10:07:33 +02:00
Anonymous ac8775565b 🌐 Add translations for: Arabic.
Currently translated at 80.0% (1127 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2024-06-17 10:07:33 +02:00
Anonymous 5651b72c0f 🌐 Add translations for: Romanian.
Currently translated at 90.0% (1268 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2024-06-17 10:07:33 +02:00
Anonymous 3e54ab1c3c 🌐 Add translations for: Danish.
Currently translated at 7.4% (105 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/da/
2024-06-17 10:07:33 +02:00
Anonymous 81482c8350 🌐 Add translations for: German.
Currently translated at 97.0% (1367 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-06-17 10:07:32 +02:00
Anonymous 9f25c6eb09 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 81.3% (1145 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2024-06-17 10:07:32 +02:00
Anonymous 4d778e71fe 🌐 Add translations for: Chinese (Simplified).
Currently translated at 95.8% (1349 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2024-06-17 10:07:32 +02:00
Anonymous 36d0e6eb36 🌐 Add translations for: Turkish.
Currently translated at 97.1% (1368 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-06-17 10:07:32 +02:00
Anonymous 492a975a3a 🌐 Add translations for: Russian.
Currently translated at 56.1% (790 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2024-06-17 10:07:31 +02:00
Anonymous ac14a6315b 🌐 Add translations for: Greek.
Currently translated at 36.9% (520 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/el/
2024-06-17 10:07:31 +02:00
Anonymous 37abe7d7f1 🌐 Add translations for: French.
Currently translated at 96.3% (1356 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2024-06-17 10:07:31 +02:00
Anonymous 0cebd89c01 🌐 Add translations for: Catalan.
Currently translated at 75.0% (1057 of 1408 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2024-06-17 10:07:30 +02:00
Hosted Weblate 2f49b419bd Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2024-06-17 10:02:54 +02:00
Stephan Paternotte c144a20012 🌐 Add translations for: Dutch.
Currently translated at 100.0% (1390 of 1390 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-06-17 10:02:52 +02:00
Stas Haas 1cf79fb56e 🌐 Add translations for: German.
Currently translated at 99.9% (1389 of 1390 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-06-17 10:02:52 +02:00
Oğuz Ersen 68724e6236 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1390 of 1390 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-06-17 10:02:52 +02:00
Andrey Antukh f529b339c6 Merge remote-tracking branch 'origin/staging' into develop 2024-06-17 10:02:34 +02:00
Alejandro Alonso b991391667 Merge remote-tracking branch 'origin/staging' into develop 2024-06-14 08:10:51 +02:00
Andrey Antukh 88540eeedd Merge remote-tracking branch 'origin/staging' into develop 2024-06-14 07:45:53 +02:00
Alejandro Alonso 7e87362a39 Merge remote-tracking branch 'origin/staging' into develop 2024-06-13 11:04:58 +02:00
Belén Albeza c6da42ee35 Merge pull request #4721 from penpot/alotor-bugfix
🐛 Fix problem moving layout to frame
2024-06-11 15:06:08 +02:00
alonso.torres f88bb4e204 🐛 Fix problem moving layout to frame 2024-06-11 14:55:00 +02:00
Andrey Antukh c0919aff51 Merge remote-tracking branch 'origin/staging' into develop 2024-06-11 11:27:16 +02:00
Alejandro Alonso 3bb5db6490 Merge remote-tracking branch 'origin/staging' into develop 2024-06-11 07:34:48 +02:00
alonso.torres 33166032f1 ⬆️ Update plugins runtime 2024-06-10 15:12:51 +02:00
alonso.torres 5233654da2 Add support for plugin data into penpot objects 2024-06-10 15:12:51 +02:00
alonso.torres 4d4a3a512d 💄 Style changes to the plugins modal 2024-06-10 15:12:51 +02:00
alonso.torres 411fe5448b 🐛 Fix rename layers for plugins 2024-06-10 15:12:51 +02:00
Pablo Alba f052c81ee1 🐛 Fix issue with annotation menu rerendering (2) 2024-06-07 13:08:43 +02:00
Pablo Alba b170a619cd Merge pull request #4695 from penpot/niwinz-bugfix-annotations
🐛 Fix issue with annotation menu rerendering
2024-06-07 13:06:40 +02:00
Andrey Antukh 4d2f82e03a 🐛 Fix issue with annotation menu rerendering 2024-06-07 12:55:33 +02:00
alonso.torres da738ba1e9 Merge remote-tracking branch 'origin/staging' into develop 2024-06-07 12:21:57 +02:00
Belén Albeza 2f84978274 Merge pull request #4687 from penpot/superalex-e2e-tests-color-palette
 Add e2e tests for fix color palette default library
2024-06-06 15:27:48 +02:00
Andrey Antukh 1d88c7e92d ⬆️ Upgrade devenv linux distribution version
We pass from ubuntu 22.04 LTS to Debian 12 (bookworm)
Update postgresql from 15 to 16
Update JVM21 to the latest minor version
2024-06-06 15:06:16 +02:00
Alejandro Alonso 5af9bb1cdb Add e2e tests for fix color palette default library 2024-06-06 12:54:31 +02:00
Andrey Antukh c8130e9453 Send profile zoom and vport/vbox on presence notifications 2024-06-06 12:02:43 +02:00
Andrés Moya ccd687cbf3 Merge branch 'staging' into develop 2024-06-06 11:21:42 +02:00
Alejandro Alonso ca73a79cfe 📎 Prepare new development cycle 2024-06-05 12:46:11 +02:00
1401 changed files with 112376 additions and 59610 deletions

No files matched your search

+10 -10
View File
@@ -22,10 +22,10 @@ jobs:
# Download and cache dependencies # Download and cache dependencies
- restore_cache: - restore_cache:
keys: keys:
- v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} - v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
# fallback to using the latest cache if no exact match is found # fallback to using the latest cache if no exact match is found
- v1-dependencies- - v1-dependencies-
- run: cd .clj-kondo && cat config.edn - run: cd .clj-kondo && cat config.edn
- run: cat .cljfmt.edn - run: cat .cljfmt.edn
@@ -58,6 +58,7 @@ jobs:
command: | command: |
yarn install yarn install
yarn run fmt:clj:check yarn run fmt:clj:check
yarn run fmt:js:check
- run: - run:
name: "common linter check" name: "common linter check"
@@ -107,10 +108,10 @@ jobs:
working_directory: "./frontend" working_directory: "./frontend"
command: | command: |
yarn install yarn install
yarn run compile yarn run build:app:assets
clojure -M:dev:shadow-cljs release main clojure -M:dev:shadow-cljs release main
yarn playwright install --with-deps chromium yarn playwright install --with-deps chromium
yarn e2e:test yarn test:e2e
- run: - run:
name: "backend tests" name: "backend tests"
@@ -125,7 +126,6 @@ jobs:
PENPOT_TEST_REDIS_URI: "redis://localhost/1" PENPOT_TEST_REDIS_URI: "redis://localhost/1"
- save_cache: - save_cache:
paths: paths:
- ~/.m2 - ~/.m2
key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
+1 -1
View File
@@ -3,7 +3,6 @@
promesa.core/->> clojure.core/->> promesa.core/->> clojure.core/->>
promesa.core/-> clojure.core/-> promesa.core/-> clojure.core/->
promesa.exec.csp/go-loop clojure.core/loop promesa.exec.csp/go-loop clojure.core/loop
rumext.v2/defc clojure.core/defn
promesa.util/with-open clojure.core/with-open promesa.util/with-open clojure.core/with-open
app.common.schema.generators/let clojure.core/let app.common.schema.generators/let clojure.core/let
app.common.data/export clojure.core/def app.common.data/export clojure.core/def
@@ -20,6 +19,7 @@
app.db/with-atomic hooks.export/penpot-with-atomic app.db/with-atomic hooks.export/penpot-with-atomic
potok.v2.core/reify hooks.export/potok-reify potok.v2.core/reify hooks.export/potok-reify
rumext.v2/fnc hooks.export/rumext-fnc rumext.v2/fnc hooks.export/rumext-fnc
rumext.v2/defc hooks.export/rumext-defc
rumext.v2/lazy-component hooks.export/rumext-lazycomponent rumext.v2/lazy-component hooks.export/rumext-lazycomponent
shadow.lazy/loadable hooks.export/rumext-lazycomponent shadow.lazy/loadable hooks.export/rumext-lazycomponent
}} }}
+80 -6
View File
@@ -67,12 +67,86 @@
(let [[cname mdata params & body] (rest (:children node)) (let [[cname mdata params & body] (rest (:children node))
[params body] (if (api/vector-node? mdata) [params body] (if (api/vector-node? mdata)
[mdata (cons params body)] [mdata (cons params body)]
[params body])] [params body])
(let [result (api/list-node
(into [(api/token-node 'fn) result (api/list-node
params] (into [(api/token-node 'fn) params]
(cons mdata body)))] (cons mdata body)))]
{:node result})))
{:node result}))
(defn- parse-defc
[{:keys [children] :as node}]
(let [args (rest children)
[cname args]
(if (api/token-node? (first args))
[(first args) (rest args)]
(throw (ex-info "unexpected1" {})))
[docs args]
(if (api/string-node? (first args))
[(first args) (rest args)]
["" args])
[mdata args]
(if (api/map-node? (first args))
[(first args) (rest args)]
[(api/map-node []) args])
[params body]
(if (api/vector-node? (first args))
[(first args) (rest args)]
(throw (ex-info "unexpected2" {})))]
[cname docs mdata params body]))
(defn rumext-defc
[{:keys [node]}]
(let [[cname docs mdata params body] (parse-defc node)
param1 (first (:children params))
paramN (rest (:children params))
param1 (if (api/map-node? param1)
(let [param1 (into {} (comp
(partition-all 2)
(map (fn [[k v]]
[(if (api/keyword-node? k)
(:k k)
k)
(if (api/vector-node? v)
(vec (:children v))
v)])))
(:children param1))
binding (:rest param1)
param1 (if binding
(if (contains? param1 :as)
(update param1 :keys (fnil conj []) binding)
(assoc param1 :as binding))
param1)]
(->> (dissoc param1 :rest)
(mapcat (fn [[k v]]
[(if (keyword? k)
(api/keyword-node k)
k)
(if (vector? v)
(api/vector-node v)
v)]))
(api/map-node)))
param1)
result (api/list-node
(into [(api/token-node 'defn)
cname
(api/vector-node (filter some? (cons param1 paramN)))]
(cons mdata body)))]
;; (prn (api/sexpr result))
{:node result}))
(defn rumext-lazycomponent (defn rumext-lazycomponent
+1
View File
@@ -4,6 +4,7 @@
:remove-consecutive-blank-lines? false :remove-consecutive-blank-lines? false
:extra-indents {rumext.v2/fnc [[:inner 0]] :extra-indents {rumext.v2/fnc [[:inner 0]]
cljs.test/async [[:inner 0]] cljs.test/async [[:inner 0]]
app.common.schema/register! [[:inner 0] [:inner 1]]
promesa.exec/thread [[:inner 0]] promesa.exec/thread [[:inner 0]]
specify! [[:inner 0] [:inner 1]]} specify! [[:inner 0] [:inner 1]]}
} }
+4 -1
View File
@@ -48,6 +48,8 @@
/deploy /deploy
/docker/images/bundle* /docker/images/bundle*
/exporter/target /exporter/target
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/cypress/fixtures/validuser.json /frontend/cypress/fixtures/validuser.json
/frontend/cypress/videos/*/ /frontend/cypress/videos/*/
/frontend/cypress/videos/*/ /frontend/cypress/videos/*/
@@ -68,8 +70,9 @@
/web /web
clj-profiler/ clj-profiler/
node_modules node_modules
frontend/.storybook/preview-body.html
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/render-wasm/target/
/**/.yarn/*
+1
View File
@@ -0,0 +1 @@
v20.11.1
+171 -4
View File
@@ -1,5 +1,174 @@
# CHANGELOG # 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
### :boom: Breaking changes & Deprecations
- Removed "merge assets" option when exporting ".svg + .json" files. After the components changes the option wasn't
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
time being.
### :heart: Community contributions (Thank you!)
- Set proper default tenant on exporter (by @june128) [#4946](https://github.com/penpot/penpot/pull/4946)
- Correct a spelling in onboarding.edn (by @n-stha) [#4936](https://github.com/penpot/penpot/pull/4936)
### :sparkles: New features
- **Tiered File Data Storage** [Taiga #8376](https://tree.taiga.io/project/penpot/us/8376)
This feature allows offloading file data that is not actively used
from the database to object storage (e.g., filesystem, S3), thereby
freeing up space in the database. It can be enabled with the
`enable-enable-tiered-file-data-storage` flag.
*(On-Premise feature, EXPERIMENTAL).*
- **JSON Interoperability for HTTP API** [Taiga #8372](https://tree.taiga.io/project/penpot/us/8372)
Enables full JSON interoperability for our HTTP API. Previously,
JSON was only barely supported for output when the
`application/json` media type was specified in the `Accept` header,
or when `_fmt=json` was passed as a query parameter. With this
update, we now offer proper bi-directional support for using our API
with plain JSON, instead of Transit.
- **Automatic File Snapshotting**
Adds the ability to automatically take and maintain a limited set of
snapshots of active files without explicit user intervention. This
feature allows on-premise administrators to recover the state of a
file from a past point in time in a limited manner.
It can be enabled with the `enable-auto-file-snapshot` flag and
configured with the following settings:
```bash
# Take snapshots every 10 update operations
PENPOT_AUTO_FILE_SNAPSHOT_EVERY=10
# Take a snapshot if it has been more than 3 hours since the file was last modified
PENPOT_AUTO_FILE_SNAPSHOT_TIMEOUT=3h
# The total number of snapshots to keep
PENPOT_AUTO_FILE_SNAPSHOT_TOTAL=10
```
Snapshots are only taken during update operations; there is NO
active background process for this.
- Add separated flag `enable-oidc-registration` for enable the
registration only for OIDC authentication backend [Github
#4882](https://github.com/penpot/penpot/issues/4882)
- Update templates in libraries & templates in dashboard modal [Taiga #8145](https://tree.taiga.io/project/penpot/us/8145)
- **Design System**
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
- **Storybook** [Taiga #6329](https://tree.taiga.io/project/penpot/us/6329)
The Design System components are now published in a Storybook, available at `/storybook`.
### :bug: Bugs fixed
- Fix webhook checkbox position [Taiga #8634](https://tree.taiga.io/project/penpot/issue/8634)
- Fix wrong props on padding input [Taiga #8254](https://tree.taiga.io/project/penpot/issue/8254)
- Fix fill collapsed options [Taiga #8351](https://tree.taiga.io/project/penpot/issue/8351)
- Fix scroll on color picker modal [Taiga #8353](https://tree.taiga.io/project/penpot/issue/8353)
- Fix components are not dragged from the group to the assets tab [Taiga #8273](https://tree.taiga.io/project/penpot/issue/8273)
- Fix problem with SVG import [Github #4888](https://github.com/penpot/penpot/issues/4888)
- Fix problem with overlay positions in viewer [Taiga #8464](https://tree.taiga.io/project/penpot/issue/8464)
- Fix layer panel overflowing [Taiga #8665](https://tree.taiga.io/project/penpot/issue/8665)
- Fix problem when creating a component instance from grid layout [Github #4881](https://github.com/penpot/penpot/issues/4881)
- Fix problem when dismissing shared library update [Taiga #8669](https://tree.taiga.io/project/penpot/issue/8669)
- Fix visual problem with stroke cap menu [Taiga #8730](https://tree.taiga.io/project/penpot/issue/8730)
- Fix issue when exporting libraries when merging libraries [Taiga #8758](https://tree.taiga.io/project/penpot/issue/8758)
- Fix problem with comments max length [Taiga #8778](https://tree.taiga.io/project/penpot/issue/8778)
- Fix copy/paste images in Safari [Taiga #8771](https://tree.taiga.io/project/penpot/issue/8771)
- Fix swap when the copy is the only child of a group [#5075](https://github.com/penpot/penpot/issues/5075)
## 2.1.5 ## 2.1.5
### :bug: Bugs fixed ### :bug: Bugs fixed
@@ -16,9 +185,7 @@
## 2.1.3 ## 2.1.3
### :bug: Bugs fixed - Don't allow registration when registration is disabled and invitation token is used [Github #4975](https://github.com/penpot/penpot/issues/4975)
- Don't allow registry with email and password, if password login is disabled (invitation workflow) [Github #4975](https://github.com/penpot/penpot/issues/4975)
## 2.1.2 ## 2.1.2
@@ -47,7 +214,7 @@
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!) ### :heart: Communityq contributions (Thank you!)
### :sparkles: New features ### :sparkles: New features
+1 -1
View File
@@ -48,7 +48,7 @@ quick win.
If is going to be your first pull request, You can learn how from this If is going to be your first pull request, You can learn how from this
free video series: free video series:
https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github
We will use the `easy fix` mark for tag for indicate issues that are We will use the `easy fix` mark for tag for indicate issues that are
easy for beginners. easy for beginners.
+10 -4
View File
@@ -8,10 +8,12 @@
<img alt="penpot header image" src="https://penpot.app/images/readme/github-light-mode.png"> <img alt="penpot header image" src="https://penpot.app/images/readme/github-light-mode.png">
</picture> </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> <p align="center">
<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://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://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://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://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> <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"> <p align="center">
<a href="https://penpot.app/"><b>Website</b></a> • <a href="https://penpot.app/"><b>Website</b></a> •
@@ -50,6 +52,7 @@ Penpots latest [huge release 2.0](https://penpot.app/dev-diaries), takes the
- [Why Penpot](#why-penpot) - [Why Penpot](#why-penpot)
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Community](#community) - [Community](#community)
- [Contributing](#contributing)
- [Resources](#resources) - [Resources](#resources)
- [License](#license) - [License](#license)
@@ -57,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. 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 ### ### 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". 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".
+1 -1
View File
@@ -20,6 +20,7 @@
[app.common.schema.desc-native :as smdn] [app.common.schema.desc-native :as smdn]
[app.common.schema.generators :as sg] [app.common.schema.generators :as sg]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.json :as json]
[app.common.transit :as t] [app.common.transit :as t]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@@ -29,7 +30,6 @@
[app.srepl.helpers :as srepl.helpers] [app.srepl.helpers :as srepl.helpers]
[app.srepl.main :as srepl] [app.srepl.main :as srepl]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.json :as json]
[app.util.time :as dt] [app.util.time :as dt]
[clj-async-profiler.core :as prof] [clj-async-profiler.core :as prof]
[clojure.contrib.humanize :as hum] [clojure.contrib.humanize :as hum]
+1 -1
View File
@@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC", "author": "Kaleidos INC",
"private": true, "private": true,
"packageManager": "yarn@4.2.2", "packageManager": "yarn@4.3.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "https://github.com/penpot/penpot"
+48 -268
View File
@@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<title> <title>
@@ -110,15 +111,20 @@
class="" style="vertical-align:top;width:600px;" class="" style="vertical-align:top;width:600px;"
> >
<![endif]--> <![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> <div class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody> <tbody>
<tr> <tr>
<td style="width:97px;"> <td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" /> <img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -151,7 +157,8 @@
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"> <td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]--> <![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;"> <div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;"> <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody> <tbody>
<tr> <tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"> <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
@@ -164,29 +171,43 @@
class="" style="vertical-align:top;width:600px;" class="" style="vertical-align:top;width:600px;"
> >
<![endif]--> <![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> <div class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
Hello {{name|abbreviate:25}}!</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">We received a request to change your current email to {{ pending-email }}.</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
We received a request to change your current email to {{ pending-email }}.</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Click to the link below to confirm the change:</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Click to the link below to confirm the change:</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="center" vertical-align="middle"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%;">
<tr> <tr>
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle"> <td align="center" bgcolor="#31EFB8" role="presentation"
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Confirm email change </a> style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Confirm email change </a>
</td> </td>
</tr> </tr>
</table> </table>
@@ -194,17 +215,24 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">If you received this email by mistake, please consider changing your password for security reasons.</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you received this email by mistake, please consider changing your password for security
reasons.</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Enjoy!</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td> </td>
</tr> </tr>
</table> </table>
@@ -221,258 +249,10 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table {% include "app/email/includes/footer.html" %}
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
<![endif]-->
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://twitter.com/penpotapp" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://github.com/penpot/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.instagram.com/penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/penpot" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with &lt;3 and Open Source</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div> </div>
</body> </body>
</html> </html>
@@ -0,0 +1,323 @@
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
<![endif]-->
<div class="mj-column-px-425 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
Penpot is the first Open Source design and prototyping platform meant for
cross-domain teams.
</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-radius:3px;width:24px;">
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://penpot.app/" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-uxbox.png"
style="border-radius:3px;display:block;"
width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-radius:3px;width:24px;">
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://x.com/penpotapp" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-x.png"
style="border-radius:3px;display:block;"
width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-radius:3px;width:24px;">
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://github.com/penpot/" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-github.png"
style="border-radius:3px;display:block;"
width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-radius:3px;width:24px;">
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.linkedin.com/company/penpotdesign/"
target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-linkedin.png"
style="border-radius:3px;display:block;"
width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-radius:3px;width:24px;">
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://fosstodon.org/@penpot/" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-mastodon.png"
style="border-radius:3px;display:block;"
width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-radius:3px;width:24px;">
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/penpot"
target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-taiga.png"
style="border-radius:3px;display:block;"
width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
Penpot | Made with &lt;3 and Open Source</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
@@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<title> <title>
@@ -110,15 +111,20 @@
class="" style="vertical-align:top;width:600px;" class="" style="vertical-align:top;width:600px;"
> >
<![endif]--> <![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> <div class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody> <tbody>
<tr> <tr>
<td style="width:97px;"> <td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" /> <img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -151,7 +157,8 @@
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"> <td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]--> <![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;"> <div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;"> <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody> <tbody>
<tr> <tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"> <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
@@ -164,24 +171,36 @@
class="" style="vertical-align:top;width:600px;" class="" style="vertical-align:top;width:600px;"
> >
<![endif]--> <![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> <div class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello!</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
Hello!</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="center" vertical-align="middle"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%;">
<tr> <tr>
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle"> <td align="center" bgcolor="#31EFB8" role="presentation"
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Accept invite </a> style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Accept invite </a>
</td> </td>
</tr> </tr>
</table> </table>
@@ -189,12 +208,16 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Enjoy!</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td> </td>
</tr> </tr>
</table> </table>
@@ -211,258 +234,10 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table {% include "app/email/includes/footer.html" %}
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
<![endif]-->
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://twitter.com/penpotapp" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://github.com/penpot/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.instagram.com/penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/penpot" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with &lt;3 and Open Source</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div> </div>
</body> </body>
</html> </html>
@@ -0,0 +1,244 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-px-425 {
width: 425px !important;
max-width: 425px;
}
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="background-color:#E5E5E5;">
<div style="background-color:#E5E5E5;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
Hello!</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
As you requested, {{invited-by|abbreviate:25}} has added you to the team “{{
team|abbreviate:25}}”.</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#31EFB8" role="presentation"
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/projects"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Go to the Team </a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Enjoy!</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
{% include "app/email/includes/footer.html" %}
</div>
</body>
</html>
@@ -0,0 +1 @@
You have joined {{team}}
@@ -0,0 +1,10 @@
Hello!
As you requested, {{invited-by|abbreviate:25}} has added you to the team “{{ team|abbreviate:25}}”.
Go to the team with this link:
{{ public-uri }}/#/dashboard/team/{{team-id}}
Enjoy!
The Penpot team.
@@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<title> <title>
@@ -110,15 +111,20 @@
class="" style="vertical-align:top;width:600px;" class="" style="vertical-align:top;width:600px;"
> >
<![endif]--> <![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> <div class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody> <tbody>
<tr> <tr>
<td style="width:97px;"> <td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" /> <img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -151,7 +157,8 @@
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"> <td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]--> <![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;"> <div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;"> <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody> <tbody>
<tr> <tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"> <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
@@ -164,24 +171,37 @@
class="" style="vertical-align:top;width:600px;" class="" style="vertical-align:top;width:600px;"
> >
<![endif]--> <![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> <div class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
Hello {{name|abbreviate:25}}!</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">We have received a request to reset your password. Click the link below to choose a new one:</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
We have received a request to reset your password. Click the link below to choose a new one:
</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="center" vertical-align="middle"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%;">
<tr> <tr>
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle"> <td align="center" bgcolor="#31EFB8" role="presentation"
<a href="{{ public-uri }}/#/auth/recovery?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Reset password </a> style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/auth/recovery?token={{token}}"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Reset password </a>
</td> </td>
</tr> </tr>
</table> </table>
@@ -189,17 +209,24 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">If you received this email by mistake, you can safely ignore it. Your password won't be changed.</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you received this email by mistake, you can safely ignore it. Your password won't be changed.
</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Enjoy!</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td> </td>
</tr> </tr>
</table> </table>
@@ -216,258 +243,10 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table {% include "app/email/includes/footer.html" %}
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
<![endif]-->
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://twitter.com/penpotapp" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://github.com/penpot/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.instagram.com/penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/penpot" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with &lt;3 and Open Source</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div> </div>
</body> </body>
</html> </html>
+42 -266
View File
@@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
<title> <title>
@@ -110,15 +111,20 @@
class="" style="vertical-align:top;width:600px;" class="" style="vertical-align:top;width:600px;"
> >
<![endif]--> <![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> <div class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"> <table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody> <tbody>
<tr> <tr>
<td style="width:97px;"> <td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" /> <img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -151,7 +157,8 @@
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"> <td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]--> <![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;"> <div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;"> <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody> <tbody>
<tr> <tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"> <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
@@ -164,24 +171,37 @@
class="" style="vertical-align:top;width:600px;" class="" style="vertical-align:top;width:600px;"
> >
<![endif]--> <![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> <div class="mj-column-per-100 mj-outlook-group-fix"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
Hello {{name|abbreviate:25}}!</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below and get started building mockups and prototypes today!</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Thanks for signing up for your Penpot account! Please verify your email using the link below and
get started building mockups and prototypes today!</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="center" vertical-align="middle"
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%;">
<tr> <tr>
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle"> <td align="center" bgcolor="#31EFB8" role="presentation"
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Verify email </a> style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Verify email </a>
</td> </td>
</tr> </tr>
</table> </table>
@@ -189,12 +209,16 @@
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Enjoy!</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div> <div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td> </td>
</tr> </tr>
</table> </table>
@@ -211,258 +235,10 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table {% include "app/email/includes/footer.html" %}
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:425px;"
>
<![endif]-->
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://twitter.com/penpotapp" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://github.com/penpot/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.instagram.com/penpot.app/" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/penpot" target="_blank">
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with &lt;3 and Open Source</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div> </div>
</body> </body>
</html> </html>
@@ -0,0 +1,254 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-px-425 {
width: 425px !important;
max-width: 425px;
}
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="background-color:#E5E5E5;">
<div style="background-color:#E5E5E5;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
Hello!</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<p>
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the
file named “{{file-name|abbreviate:25}}”.
</p>
<p>
Since this file is in your Penpot team, you can provide access by sending a view-only link.
This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
</p>
<p>To proceed, please click the button below to generate and send the view-only link:</p>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%">
<tr>
<td align="center" bgcolor="#31EFB8" role="presentation"
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Send a View-Only link </a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<p>If you do not wish to grant access at this time, you can simply disregard this email.</p>
<p>Thank you</p>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
{% include "app/email/includes/footer.html" %}
</div>
</body>
</html>
@@ -0,0 +1 @@
Request View-Only Access to “{{file-name|abbreviate:25}}”
@@ -0,0 +1,17 @@
Hello!
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the file named “{{file-name|abbreviate:25}}”.
Since this file is in your Penpot team, you can provide access by sending a view-only link. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
To proceed, please click the link below to generate and send the view-only link:
{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true
If you do not wish to grant access at this time, you can simply disregard this email.
Thank you
The Penpot team.
@@ -0,0 +1,277 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-px-425 {
width: 425px !important;
max-width: 425px;
}
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="background-color:#E5E5E5;">
<div style="background-color:#E5E5E5;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
Hello!</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<p>
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named
“{{file-name|abbreviate:25}}”.
</p>
<p>
Please note that the file is currently in Your Penpot 's team, so direct access cannot be
granted. However, you have two options to provide the requested access:
</p>
<ul>
<li>
<p>Move the File to Another Team:</p>
<p>You can move the file to another team and then give access to that team, inviting
{{requested-by|abbreviate:25}}.</p>
</li>
</ul>
</p>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<ul>
<li>
<p>Send a View-Only Link:</p>
<p>Alternatively, you can create and share a view-only link to the file. This will allow
{{requested-by|abbreviate:25}} to view the content without making any changes.</p>
<p>Click the button below to generate and send the link:</p>
</li>
</ul>
</p>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%">
<tr>
<td align="center" bgcolor="#31EFB8" role="presentation"
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Send a View-Only link </a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<p>If you do not wish to grant access at this time, you can simply disregard this email.</p>
<p>Thank you</p>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
{% include "app/email/includes/footer.html" %}
</div>
</body>
</html>
@@ -0,0 +1 @@
Request Access to “{{file-name|abbreviate:25}}”
@@ -0,0 +1,30 @@
Hello!
Hello!
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”.
Please note that the file is currently in Your Penpot 's team, so direct access cannot be granted. However, you have two options to provide the requested access:
- Move the File to Another Team:
You can move the file to another team and then give access to that team, inviting {{requested-by|abbreviate:25}}.
- Send a View-Only Link:
Alternatively, you can create and share a view-only link to the file. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
Click the link below to generate and send the link:
{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true
If you do not wish to grant access at this time, you can simply disregard this email.
Thank you
The Penpot team.
@@ -0,0 +1,295 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-px-425 {
width: 425px !important;
max-width: 425px;
}
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="background-color:#E5E5E5;">
<div style="background-color:#E5E5E5;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
Hello!</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<p>
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named
“{{file-name|abbreviate:25}}”.
</p>
<p>
To provide this access, you have the following options:
</p>
<ul>
<li>
<p>Give Access to the “{{team-name|abbreviate:25}}” Team:</p>
<p>This will automatically include {{requested-by|abbreviate:25}} in the team, so the user
can see all the projects and files in it.</p>
<p>Click the button below to provide team access:</p>
</li>
</ul>
</p>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%">
<tr>
<td align="center" bgcolor="#31EFB8" role="presentation"
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape }}"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Give access to “{{team-name|abbreviate:25}}” Team </a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<ul>
<li>
<p>Send a View-Only Link:</p>
<p>Alternatively, you can create and share a view-only link to the file. This will allow
{{requested-by|abbreviate:25}} to view the content without making any changes.</p>
<p>Click the button below to generate and send the link:</p>
</li>
</ul>
</p>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%">
<tr>
<td align="center" bgcolor="#31EFB8" role="presentation"
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Send a View-Only link </a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<p>If you do not wish to grant access at this time, you can simply disregard this email.</p>
<p>Thank you</p>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
{% include "app/email/includes/footer.html" %}
</div>
</body>
</html>
@@ -0,0 +1 @@
Request Access to “{{file-name|abbreviate:25}}”
@@ -0,0 +1,34 @@
Hello!
Hello!
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”.
To provide this access, you have the following options:
- Give Access to the “{{team-name|abbreviate:25}}” Team:
This will automatically include {{requested-by|abbreviate:25}} in the team, so the user can see all the projects and files in it.
Click the link below to provide team access:
{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}
- Send a View-Only Link:
Alternatively, you can create and share a view-only link to the file. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
Click the link below to generate and send the link:
{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true
If you do not wish to grant access at this time, you can simply disregard this email.
Thank you
The Penpot team.
@@ -0,0 +1,252 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-px-425 {
width: 425px !important;
max-width: 425px;
}
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="background-color:#E5E5E5;">
<div style="background-color:#E5E5E5;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
Hello!</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<p>
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have access to the
“{{team-name|abbreviate:25}}” Team.
</p>
<p>
To provide access, please click the button below:
</p>
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#31EFB8" role="presentation"
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
valign="middle">
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}"
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
target="_blank"> Give access to “{{team-name|abbreviate:25}}” </a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<p>If you do not wish to grant access at this time, you can simply disregard this email.</p>
<p>Thank you</p>
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
{% include "app/email/includes/footer.html" %}
</div>
</body>
</html>
@@ -0,0 +1 @@
Request Access to “{{team-name|abbreviate:25}}”
@@ -0,0 +1,14 @@
Hello!
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have access to the “{{team-name|abbreviate:25}}” Team.
To provide access, please click the link below:
{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}
If you do not wish to grant access at this time, you can simply disregard this email.
Thank you
The Penpot team.
+17 -14
View File
@@ -1,39 +1,42 @@
[{:id "wireframing-kit" [{:id "wireframing-kit"
:name "Wireframe library" :name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}
{:id "prototype-examples" {:id "prototype-examples"
:name "Prototipe template" :name "Prototype template"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Prototype%20examples%20v1.1.penpot"}
{:id "plants-app" {:id "plants-app"
:name "UI mockup example" :name "UI mockup example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/main/Plants-app.penpot"}
{:id "penpot-design-system" {:id "penpot-design-system"
:name "Design system example" :name "Design system example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/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" {:id "tutorial-for-beginners"
:name "Tutorial for beginners" :name "Tutorial for beginners"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/main/tutorial-for-beginners.penpot"}
{:id "lucide-icons" {:id "lucide-icons"
:name "Lucide Icons" :name "Lucide Icons"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Lucide-icons.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/main/Lucide-icons.penpot"}
{:id "font-awesome" {:id "font-awesome"
:name "Font Awesome" :name "Font Awesome"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/main/FontAwesome.penpot"}
{:id "black-white-mobile-templates" {:id "black-white-mobile-templates"
:name "Black & White Mobile Templates" :name "Black & White Mobile Templates"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Black-White-Mobile-Templates.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/main/Black-&-White-Mobile-Templates.penpot"}
{:id "avataaars" {:id "avataaars"
:name "Avataaars" :name "Avataaars"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Avataaars-by-Pablo-Stanley.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/main/Avataaars-by-Pablo-Stanley.penpot"}
{:id "ux-notes" {:id "ux-notes"
:name "UX Notes" :name "UX Notes"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/UX-Notes.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/main/UX-Notes.penpot"}
{:id "whiteboarding-kit" {:id "whiteboarding-kit"
:name "Whiteboarding Kit" :name "Whiteboarding Kit"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/main/Whiteboarding-mapping-kit.penpot"}
{:id "open-color-scheme" {:id "open-color-scheme"
:name "Open Color Scheme" :name "Open Color Scheme"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/main/Open%20Color%20Scheme%20(v1.9.1).penpot"}
{:id "flex-layout-playground" {:id "flex-layout-playground"
:name "Flex Layout Playground" :name "Flex Layout Playground"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.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"}]
@@ -20,12 +20,19 @@
<span>WEBHOOK</span> <span>WEBHOOK</span>
</span> </span>
{% endif %} {% endif %}
{% if item.params-schema-js %} {% if item.params-schema-js %}
<span class="tag"> <span class="tag">
<span>SCHEMA</span> <span>SCHEMA</span>
</span> </span>
{% endif %} {% endif %}
{% if item.spec %}
<span class="tag">
<span>SPEC</span>
</span>
{% endif %}
{% if item.sse %} {% if item.sse %}
<span class="tag"> <span class="tag">
<span>SSE</span> <span>SSE</span>
+1 -1
View File
@@ -7,7 +7,7 @@ Debug Main Page
{% block content %} {% block content %}
<nav> <nav>
<div class="title"> <div class="title">
<h1>ADMIN DEBUG INTERFACE</h1> <h1>ADMIN DEBUG INTERFACE (VERSION: {{version}})</h1>
</div> </div>
</nav> </nav>
<main class="dashboard"> <main class="dashboard">
+8 -3
View File
@@ -23,9 +23,12 @@ export PENPOT_FLAGS="\
enable-urepl-server \ enable-urepl-server \
enable-rpc-climit \ enable-rpc-climit \
enable-rpc-rlimit \ enable-rpc-rlimit \
enable-quotes \
enable-soft-rpc-rlimit \ enable-soft-rpc-rlimit \
enable-auto-file-snapshot \
enable-webhooks \ enable-webhooks \
enable-access-tokens \ enable-access-tokens \
enable-tiered-file-data-storage \
enable-file-validation \ enable-file-validation \
enable-file-schema-validation"; enable-file-schema-validation";
@@ -61,9 +64,11 @@ mc mb penpot-s3/penpot -p -q
export AWS_ACCESS_KEY_ID=penpot-devenv export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000 export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot 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=" export OPTIONS="
-A:jmx-remote -A:dev \ -A:jmx-remote -A:dev \
+6 -3
View File
@@ -17,7 +17,10 @@ export PENPOT_FLAGS="\
disable-secure-session-cookies \ disable-secure-session-cookies \
enable-rpc-climit \ enable-rpc-climit \
enable-smtp \ enable-smtp \
enable-quotes \
enable-file-snapshot \
enable-access-tokens \ enable-access-tokens \
enable-tiered-file-data-storage \
enable-file-validation \ enable-file-validation \
enable-file-schema-validation"; enable-file-schema-validation";
@@ -55,9 +58,9 @@ mc mb penpot-s3/penpot -p -q
export AWS_ACCESS_KEY_ID=penpot-devenv export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3 export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000 export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
entrypoint=${1:-app.main}; entrypoint=${1:-app.main};
+12 -12
View File
@@ -9,7 +9,6 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cf]
[clj-ldap.client :as ldap] [clj-ldap.client :as ldap]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.string] [clojure.string]
@@ -104,17 +103,17 @@
nil)))) nil))))
(s/def ::enabled? ::us/boolean) (s/def ::enabled? ::us/boolean)
(s/def ::host ::cf/ldap-host) (s/def ::host ::us/string)
(s/def ::port ::cf/ldap-port) (s/def ::port ::us/integer)
(s/def ::ssl ::cf/ldap-ssl) (s/def ::ssl ::us/boolean)
(s/def ::tls ::cf/ldap-starttls) (s/def ::tls ::us/boolean)
(s/def ::query ::cf/ldap-user-query) (s/def ::query ::us/string)
(s/def ::base-dn ::cf/ldap-base-dn) (s/def ::base-dn ::us/string)
(s/def ::bind-dn ::cf/ldap-bind-dn) (s/def ::bind-dn ::us/string)
(s/def ::bind-password ::cf/ldap-bind-password) (s/def ::bind-password ::us/string)
(s/def ::attrs-email ::cf/ldap-attrs-email) (s/def ::attrs-email ::us/string)
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname) (s/def ::attrs-fullname ::us/string)
(s/def ::attrs-username ::cf/ldap-attrs-username) (s/def ::attrs-username ::us/string)
(s/def ::provider-params (s/def ::provider-params
(s/keys :opt-un [::host ::port (s/keys :opt-un [::host ::port
@@ -126,6 +125,7 @@
::attrs-email ::attrs-email
::attrs-username ::attrs-username
::attrs-fullname])) ::attrs-fullname]))
(s/def ::provider (s/def ::provider
(s/nilable ::provider-params)) (s/nilable ::provider-params))
+11 -12
View File
@@ -567,7 +567,6 @@
(tokens/generate (::setup/props cfg) (tokens/generate (::setup/props cfg)
{:iss :auth {:iss :auth
:exp (dt/in-future "15m") :exp (dt/in-future "15m")
:props (:props info)
:profile-id (:id profile)})) :profile-id (:id profile)}))
props (audit/profile->props profile) props (audit/profile->props profile)
context (d/without-nils {:external-session-id (:external-session-id info)})] context (d/without-nils {:external-session-id (:external-session-id info)})]
@@ -656,17 +655,17 @@
:provider provider :provider provider
:hint "provider not configured"))))))}) :hint "provider not configured"))))))})
(s/def ::client-id ::cf/oidc-client-id) (s/def ::client-id ::us/string)
(s/def ::client-secret ::cf/oidc-client-secret) (s/def ::client-secret ::us/string)
(s/def ::base-uri ::cf/oidc-base-uri) (s/def ::base-uri ::us/string)
(s/def ::token-uri ::cf/oidc-token-uri) (s/def ::token-uri ::us/string)
(s/def ::auth-uri ::cf/oidc-auth-uri) (s/def ::auth-uri ::us/string)
(s/def ::user-uri ::cf/oidc-user-uri) (s/def ::user-uri ::us/string)
(s/def ::scopes ::cf/oidc-scopes) (s/def ::scopes ::us/set-of-strings)
(s/def ::roles ::cf/oidc-roles) (s/def ::roles ::us/set-of-strings)
(s/def ::roles-attr ::cf/oidc-roles-attr) (s/def ::roles-attr ::us/string)
(s/def ::email-attr ::cf/oidc-email-attr) (s/def ::email-attr ::us/string)
(s/def ::name-attr ::cf/oidc-name-attr) (s/def ::name-attr ::us/string)
(s/def ::provider (s/def ::provider
(s/keys :req-un [::client-id (s/keys :req-un [::client-id
+4 -5
View File
@@ -22,7 +22,6 @@
[app.db :as db] [app.db :as db]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -403,9 +402,9 @@
(write-obj! output rels))) (write-obj! output rels)))
(defmethod write-section :v1/sobjects (defmethod write-section :v1/sobjects
[{:keys [::sto/storage ::output]}] [{:keys [::output] :as cfg}]
(let [sids (-> bfc/*state* deref :sids) (let [sids (-> bfc/*state* deref :sids)
storage (media/configure-assets-storage storage)] storage (sto/resolve cfg)]
(l/dbg :hint "found sobjects" (l/dbg :hint "found sobjects"
:items (count sids) :items (count sids)
@@ -620,8 +619,8 @@
::l/sync? true)))))) ::l/sync? true))))))
(defmethod read-section :v1/sobjects (defmethod read-section :v1/sobjects
[{:keys [::sto/storage ::db/conn ::input ::bfc/overwrite ::bfc/timestamp]}] [{:keys [::db/conn ::input ::bfc/overwrite ::bfc/timestamp] :as cfg}]
(let [storage (media/configure-assets-storage storage) (let [storage (sto/resolve cfg)
ids (read-obj! input) ids (read-obj! input)
thumb? (into #{} (map :media-id) (:thumbnails @bfc/*state*))] thumb? (into #{} (map :media-id) (:thumbnails @bfc/*state*))]
+1 -5
View File
@@ -20,7 +20,6 @@
[app.db.sql :as sql] [app.db.sql :as sql]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
[app.storage :as sto] [app.storage :as sto]
[app.storage.tmp :as tmp] [app.storage.tmp :as tmp]
[app.util.events :as events] [app.util.events :as events]
@@ -347,9 +346,7 @@
[cfg team-id] [cfg team-id]
(let [id (uuid/next) (let [id (uuid/next)
tp (dt/tpoint) tp (dt/tpoint)
cfg (create-database cfg)]
cfg (-> (create-database cfg)
(update ::sto/storage media/configure-assets-storage))]
(l/inf :hint "start" (l/inf :hint "start"
:operation "export" :operation "export"
@@ -390,7 +387,6 @@
tp (dt/tpoint) tp (dt/tpoint)
cfg (-> (create-database cfg path) cfg (-> (create-database cfg path)
(update ::sto/storage media/configure-assets-storage)
(assoc ::bfc/timestamp (dt/now)))] (assoc ::bfc/timestamp (dt/now)))]
(l/inf :hint "start" (l/inf :hint "start"
+172 -267
View File
@@ -11,30 +11,17 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.flags :as flags] [app.common.flags :as flags]
[app.common.spec :as us] [app.common.schema :as sm]
[app.common.version :as v] [app.common.version :as v]
[app.util.overrides]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.core :as c] [clojure.core :as c]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.pprint :as pprint]
[clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.fs :as fs] [datoteka.fs :as fs]
[environ.core :refer [env]] [environ.core :refer [env]]
[integrant.core :as ig])) [integrant.core :as ig]))
(prefer-method print-method
clojure.lang.IRecord
clojure.lang.IDeref)
(prefer-method print-method
clojure.lang.IPersistentMap
clojure.lang.IDeref)
(prefer-method pprint/simple-dispatch
clojure.lang.IPersistentMap
clojure.lang.IDeref)
(defmethod ig/init-key :default (defmethod ig/init-key :default
[_ data] [_ data]
(d/without-nils data)) (d/without-nils data))
@@ -45,18 +32,19 @@
(d/without-nils data) (d/without-nils data)
data)) data))
(def defaults (def default
{:database-uri "postgresql://postgres/penpot" {:database-uri "postgresql://postgres/penpot"
:database-username "penpot" :database-username "penpot"
:database-password "penpot" :database-password "penpot"
:default-blob-version 5 :default-blob-version 4
:rpc-rlimit-config (fs/path "resources/rlimit.edn") :rpc-rlimit-config "resources/rlimit.edn"
:rpc-climit-config (fs/path "resources/climit.edn") :rpc-climit-config "resources/climit.edn"
:file-change-snapshot-every 5 :auto-file-snapshot-total 10
:file-change-snapshot-timeout "3h" :auto-file-snapshot-every 5
:auto-file-snapshot-timeout "3h"
:public-uri "http://localhost:3449" :public-uri "http://localhost:3449"
:host "localhost" :host "localhost"
@@ -64,8 +52,8 @@
:redis-uri "redis://redis/0" :redis-uri "redis://redis/0"
:assets-storage-backend :assets-fs :objects-storage-backend "fs"
:storage-assets-fs-directory "assets" :objects-storage-fs-directory "assets"
:assets-path "/internal/assets/" :assets-path "/internal/assets/"
:smtp-default-reply-to "Penpot <no-reply@example.com>" :smtp-default-reply-to "Penpot <no-reply@example.com>"
@@ -92,254 +80,151 @@
;; time to avoid email sending after profile modification ;; time to avoid email sending after profile modification
:email-verify-threshold "15m"}) :email-verify-threshold "15m"})
(s/def ::default-rpc-rlimit ::us/vector-of-strings) (def schema:config
(s/def ::rpc-rlimit-config ::fs/path) (do #_sm/optional-keys
(s/def ::rpc-climit-config ::fs/path) [:map {:title "config"}
[:flags {:optional true} [::sm/set :string]]
[:admins {:optional true} [::sm/set ::sm/email]]
[:secret-key {:optional true} :string]
(s/def ::media-max-file-size ::us/integer) [:tenant {:optional false} :string]
[:public-uri {:optional false} :string]
[:host {:optional false} :string]
(s/def ::flags ::us/vector-of-keywords) [:http-server-port {:optional true} ::sm/int]
(s/def ::telemetry-enabled ::us/boolean) [:http-server-host {:optional true} :string]
[:http-server-max-body-size {:optional true} ::sm/int]
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
[:http-server-io-threads {:optional true} ::sm/int]
[:http-server-worker-threads {:optional true} ::sm/int]
(s/def ::audit-log-archive-uri ::us/string) [:telemetry-uri {:optional true} :string]
(s/def ::audit-log-http-handler-concurrency ::us/integer) [:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
(s/def ::email-domain-blacklist ::fs/path) [:auto-file-snapshot-total {:optional true} ::sm/int]
(s/def ::email-domain-whitelist ::fs/path) [:auto-file-snapshot-every {:optional true} ::sm/int]
[:auto-file-snapshot-timeout {:optional true} ::dt/duration]
(s/def ::deletion-delay ::dt/duration) [:media-max-file-size {:optional true} ::sm/int]
[:deletion-delay {:optional true} ::dt/duration] ;; REVIEW
[:telemetry-enabled {:optional true} ::sm/boolean]
[:default-blob-version {:optional true} ::sm/int]
[:allow-demo-users {:optional true} ::sm/boolean]
[:error-report-webhook {:optional true} :string]
[:user-feedback-destination {:optional true} :string]
(s/def ::admins ::us/set-of-valid-emails) [:default-rpc-rlimit {:optional true} [::sm/vec :string]]
(s/def ::file-change-snapshot-every ::us/integer) [:rpc-rlimit-config {:optional true} ::fs/path]
(s/def ::file-change-snapshot-timeout ::dt/duration) [:rpc-climit-config {:optional true} ::fs/path]
(s/def ::default-executor-parallelism ::us/integer) [:audit-log-archive-uri {:optional true} :string]
(s/def ::scheduled-executor-parallelism ::us/integer) [:audit-log-http-handler-concurrency {:optional true} ::sm/int]
(s/def ::worker-default-parallelism ::us/integer) [:default-executor-parallelism {:optional true} ::sm/int] ;; REVIEW
(s/def ::worker-webhook-parallelism ::us/integer) [:scheduled-executor-parallelism {:optional true} ::sm/int] ;; REVIEW
[:worker-default-parallelism {:optional true} ::sm/int]
[:worker-webhook-parallelism {:optional true} ::sm/int]
(s/def ::auth-data-cookie-domain ::us/string) [:database-password {:optional true} [:maybe :string]]
(s/def ::auth-token-cookie-name ::us/string) [:database-uri {:optional true} :string]
(s/def ::auth-token-cookie-max-age ::dt/duration) [:database-username {:optional true} [:maybe :string]]
[:database-readonly {:optional true} ::sm/boolean]
[:database-min-pool-size {:optional true} ::sm/int]
[:database-max-pool-size {:optional true} ::sm/int]
(s/def ::secret-key ::us/string) [:quotes-teams-per-profile {:optional true} ::sm/int]
(s/def ::allow-demo-users ::us/boolean) [:quotes-access-tokens-per-profile {:optional true} ::sm/int]
(s/def ::assets-path ::us/string) [:quotes-projects-per-team {:optional true} ::sm/int]
(s/def ::database-password (s/nilable ::us/string)) [:quotes-invitations-per-team {:optional true} ::sm/int]
(s/def ::database-uri ::us/string) [:quotes-profiles-per-team {:optional true} ::sm/int]
(s/def ::database-username (s/nilable ::us/string)) [:quotes-files-per-project {:optional true} ::sm/int]
(s/def ::database-readonly ::us/boolean) [:quotes-files-per-team {:optional true} ::sm/int]
(s/def ::database-min-pool-size ::us/integer) [:quotes-font-variants-per-team {:optional true} ::sm/int]
(s/def ::database-max-pool-size ::us/integer) [:quotes-comment-threads-per-file {:optional true} ::sm/int]
[:quotes-comments-per-file {:optional true} ::sm/int]
(s/def ::quotes-teams-per-profile ::us/integer) [:auth-data-cookie-domain {:optional true} :string]
(s/def ::quotes-access-tokens-per-profile ::us/integer) [:auth-token-cookie-name {:optional true} :string]
(s/def ::quotes-projects-per-team ::us/integer) [:auth-token-cookie-max-age {:optional true} ::dt/duration]
(s/def ::quotes-invitations-per-team ::us/integer)
(s/def ::quotes-profiles-per-team ::us/integer)
(s/def ::quotes-files-per-project ::us/integer)
(s/def ::quotes-files-per-team ::us/integer)
(s/def ::quotes-font-variants-per-team ::us/integer)
(s/def ::quotes-comment-threads-per-file ::us/integer)
(s/def ::quotes-comments-per-file ::us/integer)
(s/def ::default-blob-version ::us/integer) [:registration-domain-whitelist {:optional true} [::sm/set :string]]
(s/def ::error-report-webhook ::us/string) [:email-verify-threshold {:optional true} ::dt/duration]
(s/def ::user-feedback-destination ::us/string)
(s/def ::github-client-id ::us/string)
(s/def ::github-client-secret ::us/string)
(s/def ::gitlab-base-uri ::us/string)
(s/def ::gitlab-client-id ::us/string)
(s/def ::gitlab-client-secret ::us/string)
(s/def ::google-client-id ::us/string)
(s/def ::google-client-secret ::us/string)
(s/def ::oidc-client-id ::us/string)
(s/def ::oidc-user-info-source ::us/keyword)
(s/def ::oidc-client-secret ::us/string)
(s/def ::oidc-base-uri ::us/string)
(s/def ::oidc-token-uri ::us/string)
(s/def ::oidc-auth-uri ::us/string)
(s/def ::oidc-user-uri ::us/string)
(s/def ::oidc-jwks-uri ::us/string)
(s/def ::oidc-scopes ::us/set-of-strings)
(s/def ::oidc-roles ::us/set-of-strings)
(s/def ::oidc-roles-attr ::us/string)
(s/def ::oidc-email-attr ::us/string)
(s/def ::oidc-name-attr ::us/string)
(s/def ::host ::us/string)
(s/def ::http-server-port ::us/integer)
(s/def ::http-server-host ::us/string)
(s/def ::http-server-max-body-size ::us/integer)
(s/def ::http-server-max-multipart-body-size ::us/integer)
(s/def ::http-server-io-threads ::us/integer)
(s/def ::http-server-worker-threads ::us/integer)
(s/def ::ldap-attrs-email ::us/string)
(s/def ::ldap-attrs-fullname ::us/string)
(s/def ::ldap-attrs-username ::us/string)
(s/def ::ldap-base-dn ::us/string)
(s/def ::ldap-bind-dn ::us/string)
(s/def ::ldap-bind-password ::us/string)
(s/def ::ldap-host ::us/string)
(s/def ::ldap-port ::us/integer)
(s/def ::ldap-ssl ::us/boolean)
(s/def ::ldap-starttls ::us/boolean)
(s/def ::ldap-user-query ::us/string)
(s/def ::media-directory ::us/string)
(s/def ::media-uri ::us/string)
(s/def ::profile-bounce-max-age ::dt/duration)
(s/def ::profile-bounce-threshold ::us/integer)
(s/def ::profile-complaint-max-age ::dt/duration)
(s/def ::profile-complaint-threshold ::us/integer)
(s/def ::public-uri ::us/string)
(s/def ::redis-uri ::us/string)
(s/def ::registration-domain-whitelist ::us/set-of-strings)
(s/def ::smtp-default-from ::us/string) [:github-client-id {:optional true} :string]
(s/def ::smtp-default-reply-to ::us/string) [:github-client-secret {:optional true} :string]
(s/def ::smtp-host ::us/string) [:gitlab-base-uri {:optional true} :string]
(s/def ::smtp-password (s/nilable ::us/string)) [:gitlab-client-id {:optional true} :string]
(s/def ::smtp-port ::us/integer) [:gitlab-client-secret {:optional true} :string]
(s/def ::smtp-ssl ::us/boolean) [:google-client-id {:optional true} :string]
(s/def ::smtp-tls ::us/boolean) [:google-client-secret {:optional true} :string]
(s/def ::smtp-username (s/nilable ::us/string)) [:oidc-client-id {:optional true} :string]
(s/def ::urepl-host ::us/string) [:oidc-user-info-source {:optional true} :keyword]
(s/def ::urepl-port ::us/integer) [:oidc-client-secret {:optional true} :string]
(s/def ::prepl-host ::us/string) [:oidc-base-uri {:optional true} :string]
(s/def ::prepl-port ::us/integer) [:oidc-token-uri {:optional true} :string]
(s/def ::assets-storage-backend ::us/keyword) [:oidc-auth-uri {:optional true} :string]
(s/def ::storage-assets-fs-directory ::us/string) [:oidc-user-uri {:optional true} :string]
(s/def ::storage-assets-s3-bucket ::us/string) [:oidc-jwks-uri {:optional true} :string]
(s/def ::storage-assets-s3-region ::us/keyword) [:oidc-scopes {:optional true} [::sm/set :string]]
(s/def ::storage-assets-s3-endpoint ::us/string) [:oidc-roles {:optional true} [::sm/set :string]]
(s/def ::storage-assets-s3-io-threads ::us/integer) [:oidc-roles-attr {:optional true} :string]
(s/def ::telemetry-uri ::us/string) [:oidc-email-attr {:optional true} :string]
(s/def ::telemetry-with-taiga ::us/boolean) [:oidc-name-attr {:optional true} :string]
(s/def ::tenant ::us/string)
(s/def ::email-verify-threshold ::dt/duration)
(s/def ::config [:ldap-attrs-email {:optional true} :string]
(s/keys :opt-un [::secret-key [:ldap-attrs-fullname {:optional true} :string]
::flags [:ldap-attrs-username {:optional true} :string]
::admins [:ldap-base-dn {:optional true} :string]
::deletion-delay [:ldap-bind-dn {:optional true} :string]
::allow-demo-users [:ldap-bind-password {:optional true} :string]
::audit-log-archive-uri [:ldap-host {:optional true} :string]
::audit-log-http-handler-concurrency [:ldap-port {:optional true} ::sm/int]
::auth-token-cookie-name [:ldap-ssl {:optional true} ::sm/boolean]
::auth-token-cookie-max-age [:ldap-starttls {:optional true} ::sm/boolean]
::authenticated-cookie-domain [:ldap-user-query {:optional true} :string]
::database-password
::database-uri
::database-username
::database-readonly
::database-min-pool-size
::database-max-pool-size
::default-blob-version
::default-rpc-rlimit
::email-domain-blacklist
::email-domain-whitelist
::error-report-webhook
::default-executor-parallelism
::scheduled-executor-parallelism
::worker-default-parallelism
::worker-webhook-parallelism
::file-change-snapshot-every
::file-change-snapshot-timeout
::user-feedback-destination
::github-client-id
::github-client-secret
::gitlab-base-uri
::gitlab-client-id
::gitlab-client-secret
::google-client-id
::google-client-secret
::oidc-client-id
::oidc-client-secret
::oidc-user-info-source
::oidc-base-uri
::oidc-token-uri
::oidc-auth-uri
::oidc-user-uri
::oidc-jwks-uri
::oidc-scopes
::oidc-roles-attr
::oidc-email-attr
::oidc-name-attr
::oidc-roles
::host
::http-server-host
::http-server-port
::http-server-max-body-size
::http-server-max-multipart-body-size
::http-server-io-threads
::http-server-worker-threads
::ldap-attrs-email
::ldap-attrs-fullname
::ldap-attrs-username
::ldap-base-dn
::ldap-bind-dn
::ldap-bind-password
::ldap-host
::ldap-port
::ldap-ssl
::ldap-starttls
::ldap-user-query
::local-assets-uri
::media-max-file-size
::profile-bounce-max-age
::profile-bounce-threshold
::profile-complaint-max-age
::profile-complaint-threshold
::public-uri
::quotes-teams-per-profile [:profile-bounce-max-age {:optional true} ::dt/duration]
::quotes-access-tokens-per-profile [:profile-bounce-threshold {:optional true} ::sm/int]
::quotes-projects-per-team [:profile-complaint-max-age {:optional true} ::dt/duration]
::quotes-invitations-per-team [:profile-complaint-threshold {:optional true} ::sm/int]
::quotes-profiles-per-team
::quotes-files-per-project
::quotes-files-per-team
::quotes-font-variants-per-team
::quotes-comment-threads-per-file
::quotes-comments-per-file
::redis-uri [:redis-uri {:optional true} :string]
::registration-domain-whitelist
::rpc-rlimit-config
::rpc-climit-config
::semaphore-process-font [:email-domain-blacklist {:optional true} ::fs/path]
::semaphore-process-image [:email-domain-whitelist {:optional true} ::fs/path]
::semaphore-update-file
::semaphore-auth
::smtp-default-from [:smtp-default-from {:optional true} :string]
::smtp-default-reply-to [:smtp-default-reply-to {:optional true} :string]
::smtp-host [:smtp-host {:optional true} :string]
::smtp-password [:smtp-password {:optional true} [:maybe :string]]
::smtp-port [:smtp-port {:optional true} ::sm/int]
::smtp-ssl [:smtp-ssl {:optional true} ::sm/boolean]
::smtp-tls [:smtp-tls {:optional true} ::sm/boolean]
::smtp-username [:smtp-username {:optional true} [:maybe :string]]
::urepl-host [:urepl-host {:optional true} :string]
::urepl-port [:urepl-port {:optional true} ::sm/int]
::prepl-host [:prepl-host {:optional true} :string]
::prepl-port [:prepl-port {:optional true} ::sm/int]
::assets-storage-backend [:media-directory {:optional true} :string] ;; REVIEW
::storage-assets-fs-directory [:media-uri {:optional true} :string]
::storage-assets-s3-bucket [:assets-path {:optional true} :string]
::storage-assets-s3-region
::storage-assets-s3-endpoint ;; Legacy, will be removed in 2.5
::storage-assets-s3-io-threads [:assets-storage-backend {:optional true} :keyword]
::telemetry-enabled [:storage-assets-fs-directory {:optional true} :string]
::telemetry-uri [:storage-assets-s3-bucket {:optional true} :string]
::telemetry-referer [:storage-assets-s3-region {:optional true} :keyword]
::telemetry-with-taiga [:storage-assets-s3-endpoint {:optional true} :string]
::tenant [:storage-assets-s3-io-threads {:optional true} ::sm/int]
::email-verify-threshold]))
[:objects-storage-backend {:optional true} :keyword]
[:objects-storage-fs-directory {:optional true} :string]
[:objects-storage-s3-bucket {:optional true} :string]
[:objects-storage-s3-region {:optional true} :keyword]
[:objects-storage-s3-endpoint {:optional true} :string]
[:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
(def default-flags (def default-flags
[:enable-backend-api-doc [:enable-backend-api-doc
@@ -367,20 +252,22 @@
{} {}
env))) env)))
(defn- read-config (def decode-config
[] (sm/decoder schema:config sm/string-transformer))
(try
(->> (read-env "penpot") (def validate-config
(merge defaults) (sm/validator schema:config))
(us/conform ::config))
(catch Throwable e (def explain-config
(when (ex/error? e) (sm/explainer schema:config))
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
(println "Error on validating configuration:") (defn read-config
(println (some-> e ex-data ex/explain)) "Reads the configuration from enviroment variables and decodes all
(println (ex/explain (ex-data e))) known values."
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")) [& {:keys [prefix default] :or {prefix "penpot"}}]
(throw e)))) (->> (read-env prefix)
(merge default)
(decode-config)))
(def version (def version
(v/parse (or (some-> (io/resource "version.txt") (v/parse (or (some-> (io/resource "version.txt")
@@ -388,10 +275,28 @@
(str/trim)) (str/trim))
"%version%"))) "%version%")))
(defonce ^:dynamic config (read-config)) (defonce ^:dynamic config (read-config :default default))
(defonce ^:dynamic flags (parse-flags config)) (defonce ^:dynamic flags (parse-flags config))
(def deletion-delay (defn validate!
"Validate the currently loaded configuration data."
[& {:keys [exit-on-error?] :or {exit-on-error? true}}]
(if (validate-config config)
true
(let [explain (explain-config config)]
(println "Error on validating configuration:")
(sm/pretty-explain explain
:variant ::sm/schemaless-explain
:message "Configuration Validation Error")
(flush)
(if exit-on-error?
(System/exit -1)
(ex/raise :type :validation
:code :config-validaton
::sm/explain explain)))))
(defn get-deletion-delay
[]
(or (c/get config :deletion-delay) (or (c/get config :deletion-delay)
(dt/duration {:days 7}))) (dt/duration {:days 7})))
+1 -1
View File
@@ -153,7 +153,7 @@
(s/def ::conn some?) (s/def ::conn some?)
(s/def ::nilable-pool (s/nilable ::pool)) (s/def ::nilable-pool (s/nilable ::pool))
(s/def ::pool pool?) (s/def ::pool pool?)
(s/def ::pool-or-conn some?) (s/def ::connectable some?)
(defn closed? (defn closed?
[pool] [pool]
+97 -26
View File
@@ -7,14 +7,18 @@
(ns app.email (ns app.email
"Main api for send emails." "Main api for send emails."
(:require (:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.pprint :as pp] [app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.email.invite-to-team :as-alias email.invite-to-team] [app.email.invite-to-team :as-alias email.invite-to-team]
[app.email.join-team :as-alias email.join-team]
[app.email.request-team-access :as-alias email.request-team-access]
[app.metrics :as mtx] [app.metrics :as mtx]
[app.util.template :as tmpl] [app.util.template :as tmpl]
[app.worker :as wrk] [app.worker :as wrk]
@@ -149,9 +153,27 @@
"mail.smtp.timeout" timeout "mail.smtp.timeout" timeout
"mail.smtp.connectiontimeout" timeout})) "mail.smtp.connectiontimeout" timeout}))
(def ^:private schema:smtp-config
[:map
[::username {:optional true} :string]
[::password {:optional true} :string]
[::tls {:optional true} ::sm/boolean]
[::ssl {:optional true} ::sm/boolean]
[::host {:optional true} :string]
[::port {:optional true} ::sm/int]
[::default-from {:optional true} :string]
[::default-reply-to {:optional true} :string]])
(def valid-smtp-config?
(sm/check-fn schema:smtp-config))
(defn- create-smtp-session (defn- create-smtp-session
^Session ^Session
[cfg] [cfg]
(dm/assert!
"expected valid smtp config"
(valid-smtp-config? cfg))
(let [props (opts->props cfg)] (let [props (opts->props cfg)]
(Session/getInstance props))) (Session/getInstance props)))
@@ -273,32 +295,10 @@
;; SENDMAIL FN / TASK HANDLER ;; SENDMAIL FN / TASK HANDLER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::username ::cf/smtp-username)
(s/def ::password ::cf/smtp-password)
(s/def ::tls ::cf/smtp-tls)
(s/def ::ssl ::cf/smtp-ssl)
(s/def ::host ::cf/smtp-host)
(s/def ::port ::cf/smtp-port)
(s/def ::default-reply-to ::cf/smtp-default-reply-to)
(s/def ::default-from ::cf/smtp-default-from)
(s/def ::smtp-config
(s/keys :opt [::username
::password
::tls
::ssl
::host
::port
::default-from
::default-reply-to]))
(declare send-to-logger!) (declare send-to-logger!)
(s/def ::sendmail fn?) (s/def ::sendmail fn?)
(defmethod ig/pre-init-spec ::sendmail [_]
(s/spec ::smtp-config))
(defmethod ig/init-key ::sendmail (defmethod ig/init-key ::sendmail
[_ cfg] [_ cfg]
(fn [params] (fn [params]
@@ -315,15 +315,13 @@
(l/dbg :hint "sendmail" (l/dbg :hint "sendmail"
:id (:id params) :id (:id params)
:to (:to params) :to (:to params)
:subject (str/trim (:subject params)) :subject (str/trim (:subject params)))
:body (str/join "," (map :type (:body params))))
(.sendMessage ^Transport transport (.sendMessage ^Transport transport
^MimeMessage message ^MimeMessage message
(.getAllRecipients message)))))) (.getAllRecipients message))))))
(when (or (contains? cf/flags :log-emails) (when (contains? cf/flags :log-emails)
(not (contains? cf/flags :smtp)))
(send-to-logger! cfg params)))) (send-to-logger! cfg params))))
(defmethod ig/pre-init-spec ::handler [_] (defmethod ig/pre-init-spec ::handler [_]
@@ -401,6 +399,79 @@
"Teams member invitation email." "Teams member invitation email."
(template-factory ::invite-to-team)) (template-factory ::invite-to-team))
(s/def ::email.join-team/invited-by ::us/string)
(s/def ::email.join-team/team ::us/string)
(s/def ::email.join-team/team-id ::us/uuid)
(s/def ::join-team
(s/keys :req-un [::email.join-team/invited-by
::email.join-team/team-id
::email.join-team/team]))
(def join-team
"Teams member joined after request email."
(template-factory ::join-team))
(s/def ::email.request-team-access/requested-by ::us/string)
(s/def ::email.request-team-access/requested-by-email ::us/string)
(s/def ::email.request-team-access/team-name ::us/string)
(s/def ::email.request-team-access/team-id ::us/uuid)
(s/def ::email.request-team-access/file-name ::us/string)
(s/def ::email.request-team-access/file-id ::us/uuid)
(s/def ::email.request-team-access/page-id ::us/uuid)
(s/def ::request-file-access
(s/keys :req-un [::email.request-team-access/requested-by
::email.request-team-access/requested-by-email
::email.request-team-access/team-name
::email.request-team-access/team-id
::email.request-team-access/file-name
::email.request-team-access/file-id
::email.request-team-access/page-id]))
(def request-file-access
"File access request email."
(template-factory ::request-file-access))
(s/def ::request-file-access-yourpenpot
(s/keys :req-un [::email.request-team-access/requested-by
::email.request-team-access/requested-by-email
::email.request-team-access/team-name
::email.request-team-access/team-id
::email.request-team-access/file-name
::email.request-team-access/file-id
::email.request-team-access/page-id]))
(def request-file-access-yourpenpot
"File access on Your Penpot request email."
(template-factory ::request-file-access-yourpenpot))
(s/def ::request-file-access-yourpenpot-view
(s/keys :req-un [::email.request-team-access/requested-by
::email.request-team-access/requested-by-email
::email.request-team-access/team-name
::email.request-team-access/team-id
::email.request-team-access/file-name
::email.request-team-access/file-id
::email.request-team-access/page-id]))
(def request-file-access-yourpenpot-view
"File access on Your Penpot view mode request email."
(template-factory ::request-file-access-yourpenpot-view))
(s/def ::request-team-access
(s/keys :req-un [::email.request-team-access/requested-by
::email.request-team-access/requested-by-email
::email.request-team-access/team-name
::email.request-team-access/team-id]))
(def request-team-access
"Team access request email."
(template-factory ::request-team-access))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; BOUNCE/COMPLAINS HELPERS ;; BOUNCE/COMPLAINS HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+5 -4
View File
@@ -62,6 +62,7 @@
[datoteka.io :as io] [datoteka.io :as io]
[promesa.util :as pu])) [promesa.util :as pu]))
(def ^:dynamic *stats* (def ^:dynamic *stats*
"A dynamic var for setting up state for collect stats globally." "A dynamic var for setting up state for collect stats globally."
nil) nil)
@@ -113,7 +114,7 @@
(sm/lazy-validator ::ctc/color)) (sm/lazy-validator ::ctc/color))
(def valid-fill? (def valid-fill?
(sm/lazy-validator ::cts/fill)) (sm/lazy-validator cts/schema:fill))
(def valid-stroke? (def valid-stroke?
(sm/lazy-validator ::cts/stroke)) (sm/lazy-validator ::cts/stroke))
@@ -134,10 +135,10 @@
(sm/lazy-validator ::ctc/rgb-color)) (sm/lazy-validator ::ctc/rgb-color))
(def valid-shape-points? (def valid-shape-points?
(sm/lazy-validator ::cts/points)) (sm/lazy-validator cts/schema:points))
(def valid-image-attrs? (def valid-image-attrs?
(sm/lazy-validator ::cts/image-attrs)) (sm/lazy-validator cts/schema:image-attrs))
(def valid-column-grid-params? (def valid-column-grid-params?
(sm/lazy-validator ::ctg/column-params)) (sm/lazy-validator ::ctg/column-params))
@@ -1742,7 +1743,7 @@
:validate validate? :validate validate?
:skip-on-graphic-error skip-on-graphic-error?) :skip-on-graphic-error skip-on-graphic-error?)
(db/tx-run! (update system ::sto/storage media/configure-assets-storage) (db/tx-run! system
(fn [system] (fn [system]
(binding [*system* system] (binding [*system* system]
(when (string? label) (when (string? label)
+27 -2
View File
@@ -12,10 +12,19 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.storage :as sto]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.objects-map :as omap] [app.util.objects-map :as omap]
[app.util.pointer-map :as pmap])) [app.util.pointer-map :as pmap]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OFFLOAD
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn offloaded?
[file]
(= "objects-storage" (:data-backend file)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OBJECTS-MAP ;; OBJECTS-MAP
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -55,12 +64,26 @@
;; POINTER-MAP ;; POINTER-MAP
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-file-data
"Get file data given a file instance."
[system file]
(if (offloaded? file)
(let [storage (sto/resolve system ::db/reuse-conn true)]
(->> (sto/get-object storage (:data-ref-id file))
(sto/get-object-bytes storage)))
(:data file)))
(defn resolve-file-data
[system file]
(let [data (get-file-data system file)]
(assoc file :data data)))
(defn load-pointer (defn load-pointer
"A database loader pointer helper" "A database loader pointer helper"
[system file-id id] [system file-id id]
(let [fragment (db/get* system :file-data-fragment (let [fragment (db/get* system :file-data-fragment
{:id id :file-id file-id} {:id id :file-id file-id}
{::sql/columns [:data]})] {::sql/columns [:data :data-backend :data-ref-id :id]})]
(l/trc :hint "load pointer" (l/trc :hint "load pointer"
:file-id (str file-id) :file-id (str file-id)
@@ -74,7 +97,9 @@
:file-id file-id :file-id file-id
:fragment-id id)) :fragment-id id))
(blob/decode (:data fragment)))) (let [data (get-file-data system fragment)]
;; FIXME: conditional thread scheduling for decoding big objects
(blob/decode data))))
(defn persist-pointers! (defn persist-pointers!
"Persist all currently tracked pointer objects" "Persist all currently tracked pointer objects"
+4 -5
View File
@@ -57,11 +57,10 @@
(defn- serve-object (defn- serve-object
"Helper function that returns the appropriate response depending on "Helper function that returns the appropriate response depending on
the storage object backend type." the storage object backend type."
[{:keys [::sto/storage] :as cfg} {:keys [backend] :as obj}] [cfg {:keys [backend] :as obj}]
(let [backend (sto/resolve-backend storage backend)] (case backend
(case (::sto/type backend) (:s3 :assets-s3) (serve-object-from-s3 cfg obj)
:s3 (serve-object-from-s3 cfg obj) (:fs :assets-fs) (serve-object-from-fs cfg obj)))
:fs (serve-object-from-fs cfg obj))))
(defn objects-handler (defn objects-handler
"Handler that servers storage objects by id." "Handler that servers storage objects by id."
+4 -3
View File
@@ -54,9 +54,10 @@
"A convencience toplevel function for gradual migration to a new API "A convencience toplevel function for gradual migration to a new API
convention." convention."
([cfg-or-client request] ([cfg-or-client request]
(let [client (resolve-client cfg-or-client)] (let [client (resolve-client cfg-or-client)
request (update request :uri str)]
(send! client request {:sync? true}))) (send! client request {:sync? true})))
([cfg-or-client request options] ([cfg-or-client request options]
(let [client (resolve-client cfg-or-client)] (let [client (resolve-client cfg-or-client)
request (update request :uri str)]
(send! client request (merge {:sync? true} options))))) (send! client request (merge {:sync? true} options)))))
+1 -1
View File
@@ -47,7 +47,7 @@
{::rres/status 200 {::rres/status 200
::rres/headers {"content-type" "text/html"} ::rres/headers {"content-type" "text/html"}
::rres/body (-> (io/resource "app/templates/debug.tmpl") ::rres/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {}))}) (tmpl/render {:version (:full cf/version)}))})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE CHANGES ;; FILE CHANGES
+15 -16
View File
@@ -7,11 +7,13 @@
(ns app.http.middleware (ns app.http.middleware
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.json :as json]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as-alias sm]
[app.common.transit :as t] [app.common.transit :as t]
[app.config :as cf] [app.config :as cf]
[app.http.errors :as errors] [app.http.errors :as errors]
[clojure.data.json :as json] [app.util.pointer-map :as pmap]
[cuerdas.core :as str] [cuerdas.core :as str]
[ring.request :as rreq] [ring.request :as rreq]
[ring.response :as rres] [ring.response :as rres]
@@ -39,16 +41,6 @@
(java.io.BufferedReader. (java.io.BufferedReader.
(java.io.InputStreamReader. body)))) (java.io.InputStreamReader. body))))
(defn- read-json-key
[k]
(-> k str/kebab keyword))
(defn- write-json-key
[k]
(if (or (keyword? k) (symbol? k))
(str/camel k)
(str k)))
(defn wrap-parse-request (defn wrap-parse-request
[handler] [handler]
(letfn [(process-request [request] (letfn [(process-request [request]
@@ -63,7 +55,7 @@
(str/starts-with? header "application/json") (str/starts-with? header "application/json")
(with-open [reader (get-reader request)] (with-open [reader (get-reader request)]
(let [params (json/read reader :key-fn read-json-key)] (let [params (json/read reader :key-fn json/read-kebab-key)]
(-> request (-> request
(assoc :body-params params) (assoc :body-params params)
(update :params merge params)))) (update :params merge params))))
@@ -113,6 +105,12 @@
(def ^:const buffer-size (:xnio/buffer-size yt/defaults)) (def ^:const buffer-size (:xnio/buffer-size yt/defaults))
(defn- write-json-value
[_ val]
(if (pmap/pointer-map? val)
[(pmap/get-id val) (meta val)]
val))
(defn wrap-format-response (defn wrap-format-response
[handler] [handler]
(letfn [(transit-streamable-body [data opts] (letfn [(transit-streamable-body [data opts]
@@ -134,10 +132,11 @@
(reify rres/StreamableResponseBody (reify rres/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream] (-write-body-to-stream [_ _ output-stream]
(try (try
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)] (let [encode (or (-> data meta :encode/json) identity)
(with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)] data (encode data)]
(json/write data writer :key-fn write-json-key))) (with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
(with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)]
(json/write writer data :key-fn json/write-camel-key :value-fn write-json-value))))
(catch java.io.IOException _) (catch java.io.IOException _)
(catch Throwable cause (catch Throwable cause
(binding [l/*context* {:value data}] (binding [l/*context* {:value data}]
+3
View File
@@ -60,6 +60,9 @@
(try (try
(let [result (handler)] (let [result (handler)]
(events/tap :end result)) (events/tap :end result))
(catch java.io.EOFException cause
(events/tap :error (errors/handle' cause request)))
(catch Throwable cause (catch Throwable cause
(l/err :hint "unexpected error on processing sse response" (l/err :hint "unexpected error on processing sse response"
:cause cause) :cause cause)
+7 -7
View File
@@ -278,18 +278,18 @@
:inc 1) :inc 1)
message) message)
(def ^:private schema:params
(sm/define
[:map {:title "params"}
[:session-id ::sm/uuid]]))
(defn- http-handler (defn- http-handler
[cfg {:keys [params ::session/profile-id] :as request}] [cfg {:keys [params ::session/profile-id] :as request}]
(let [{:keys [session-id]} (sm/conform! schema:params 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 (cond
(not profile-id) (not profile-id)
(ex/raise :type :authentication (ex/raise :type :authentication
:hint "Authentication required.") :hint "authentication required")
;; WORKAROUND: we use the adapter specific predicate for ;; WORKAROUND: we use the adapter specific predicate for
;; performance reasons; for now, the ring default impl for ;; performance reasons; for now, the ring default impl for
+1 -1
View File
@@ -63,7 +63,7 @@
(ex/format-throwable cause :data? false :explain? false :header? false :summary? false))} (ex/format-throwable cause :data? false :explain? false :header? false :summary? false))}
(when-let [params (or (:request/params context) (:params context))] (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)] (when-let [value (:value context)]
{:value (pp/pprint-str value :length 30 :level 12)}) {:value (pp/pprint-str value :length 30 :level 12)})
+1 -1
View File
@@ -135,7 +135,7 @@
(l/dbg :hint "run webhook" (l/dbg :hint "run webhook"
:event-name (:name event) :event-name (:name event)
:webhook-id (:id whook) :webhook-id (str (:id whook))
:webhook-uri (:uri whook) :webhook-uri (:uri whook)
:webhook-mtype (:mtype whook)) :webhook-mtype (:mtype whook))
+34 -13
View File
@@ -344,6 +344,8 @@
{:sendmail (ig/ref ::email/handler) {:sendmail (ig/ref ::email/handler)
:objects-gc (ig/ref :app.tasks.objects-gc/handler) :objects-gc (ig/ref :app.tasks.objects-gc/handler)
:file-gc (ig/ref :app.tasks.file-gc/handler) :file-gc (ig/ref :app.tasks.file-gc/handler)
:file-gc-scheduler (ig/ref :app.tasks.file-gc-scheduler/handler)
:offload-file-data (ig/ref :app.tasks.offload-file-data/handler)
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler) :file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler) :tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
:telemetry (ig/ref :app.tasks.telemetry/handler) :telemetry (ig/ref :app.tasks.telemetry/handler)
@@ -394,9 +396,17 @@
{::db/pool (ig/ref ::db/pool) {::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)} ::sto/storage (ig/ref ::sto/storage)}
:app.tasks.file-xlog-gc/handler :app.tasks.file-gc-scheduler/handler
{::db/pool (ig/ref ::db/pool)} {::db/pool (ig/ref ::db/pool)}
:app.tasks.offload-file-data/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.file-xlog-gc/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.telemetry/handler :app.tasks.telemetry/handler
{::db/pool (ig/ref ::db/pool) {::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client) ::http.client/client (ig/ref ::http.client/client)
@@ -448,19 +458,29 @@
::sto/storage ::sto/storage
{::db/pool (ig/ref ::db/pool) {::db/pool (ig/ref ::db/pool)
::sto/backends ::sto/backends
{:assets-s3 (ig/ref [::assets :app.storage.s3/backend]) {:s3 (ig/ref :app.storage.s3/backend)
:assets-fs (ig/ref [::assets :app.storage.fs/backend]) :fs (ig/ref :app.storage.fs/backend)
:s3 (ig/ref [::assets :app.storage.s3/backend])
:fs (ig/ref [::assets :app.storage.fs/backend])}}
[::assets :app.storage.s3/backend] ;; LEGACY (should not be removed, can only be removed after an
{::sto.s3/region (cf/get :storage-assets-s3-region) ;; explicit migration because the database objects/rows will
::sto.s3/endpoint (cf/get :storage-assets-s3-endpoint) ;; still reference the old names).
::sto.s3/bucket (cf/get :storage-assets-s3-bucket) :assets-s3 (ig/ref :app.storage.s3/backend)
::sto.s3/io-threads (cf/get :storage-assets-s3-io-threads)} :assets-fs (ig/ref :app.storage.fs/backend)}}
[::assets :app.storage.fs/backend] :app.storage.s3/backend
{::sto.fs/directory (cf/get :storage-assets-fs-directory)}}) {::sto.s3/region (or (cf/get :storage-assets-s3-region)
(cf/get :objects-storage-s3-region))
::sto.s3/endpoint (or (cf/get :storage-assets-s3-endpoint)
(cf/get :objects-storage-s3-endpoint))
::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))
::wrk/executor (ig/ref ::wrk/executor)}
:app.storage.fs/backend
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
(cf/get :objects-storage-fs-directory))}})
(def worker-config (def worker-config
@@ -487,7 +507,7 @@
:task :tasks-gc} :task :tasks-gc}
{:cron #app/cron "0 0 2 * * ?" ;; daily {:cron #app/cron "0 0 2 * * ?" ;; daily
:task :file-gc} :task :file-gc-scheduler}
{:cron #app/cron "0 30 */3,23 * * ?" {:cron #app/cron "0 30 */3,23 * * ?"
:task :telemetry} :task :telemetry}
@@ -526,6 +546,7 @@
(defn start (defn start
[] []
(cf/validate!)
(ig/load-namespaces (merge system-config worker-config)) (ig/load-namespaces (merge system-config worker-config))
(alter-var-root #'system (fn [sys] (alter-var-root #'system (fn [sys]
(when sys (ig/halt! sys)) (when sys (ig/halt! sys))
+2 -29
View File
@@ -11,7 +11,6 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.media :as cm] [app.common.media :as cm]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.schema.openapi :as-alias oapi] [app.common.schema.openapi :as-alias oapi]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.svg :as csvg] [app.common.svg :as csvg]
@@ -47,22 +46,10 @@
(s/keys :req-un [::path] (s/keys :req-un [::path]
:opt-un [::mtype])) :opt-un [::mtype]))
(sm/def! ::fs/path (sm/register! ::upload
{:type ::fs/path
:pred fs/path?
:type-properties
{:title "path"
:description "filesystem path"
:error/message "expected a valid fs path instance"
:gen/gen (sg/generator :string)
::oapi/type "string"
::oapi/format "unix-path"
::oapi/decode fs/path}})
(sm/def! ::upload
[:map {:title "Upload"} [:map {:title "Upload"}
[:filename :string] [:filename :string]
[:size :int] [:size ::sm/int]
[:path ::fs/path] [:path ::fs/path]
[:mtype {:optional true} :string] [:mtype {:optional true} :string]
[:headers {:optional true} [:headers {:optional true}
@@ -326,17 +313,3 @@
(= stype :ttf) (= stype :ttf)
(-> (assoc "font/otf" (ttf->otf sfnt)) (-> (assoc "font/otf" (ttf->otf sfnt))
(assoc "font/ttf" sfnt))))))))) (assoc "font/ttf" sfnt)))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Utility functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn configure-assets-storage
"Given storage map, returns a storage configured with the appropriate
backend for assets and optional connection attached."
([storage]
(assoc storage ::sto/backend (cf/get :assets-storage-backend :assets-fs)))
([storage pool-or-conn]
(-> (configure-assets-storage storage)
(assoc ::db/pool-or-conn pool-or-conn))))
+31 -1
View File
@@ -382,7 +382,37 @@
:fn (mg/resource "app/migrations/sql/0120-mod-audit-log-table.sql")} :fn (mg/resource "app/migrations/sql/0120-mod-audit-log-table.sql")}
{:name "0121-mod-file-data-fragment-table" {:name "0121-mod-file-data-fragment-table"
:fn (mg/resource "app/migrations/sql/0121-mod-file-data-fragment-table.sql")}]) :fn (mg/resource "app/migrations/sql/0121-mod-file-data-fragment-table.sql")}
{:name "0122-mod-file-table"
:fn (mg/resource "app/migrations/sql/0122-mod-file-table.sql")}
{:name "0122-mod-file-data-fragment-table"
:fn (mg/resource "app/migrations/sql/0122-mod-file-data-fragment-table.sql")}
{:name "0123-mod-file-change-table"
:fn (mg/resource "app/migrations/sql/0123-mod-file-change-table.sql")}
{:name "0124-mod-profile-table"
:fn (mg/resource "app/migrations/sql/0124-mod-profile-table.sql")}
{:name "0125-mod-file-table"
:fn (mg/resource "app/migrations/sql/0125-mod-file-table.sql")}
{:name "0126-add-team-access-request-table"
:fn (mg/resource "app/migrations/sql/0126-add-team-access-request-table.sql")}
{:name "0127-mod-storage-object-table"
:fn (mg/resource "app/migrations/sql/0127-mod-storage-object-table.sql")}
{:name "0128-mod-task-table"
:fn (mg/resource "app/migrations/sql/0128-mod-task-table.sql")}
{:name "0129-mod-file-change-table"
:fn (mg/resource "app/migrations/sql/0129-mod-file-change-table.sql")}
{:name "0130-mod-file-change-table"
:fn (mg/resource "app/migrations/sql/0130-mod-file-change-table.sql")}])
(defn apply-migrations! (defn apply-migrations!
[pool name migrations] [pool name migrations]
@@ -0,0 +1,6 @@
ALTER TABLE file_data_fragment
ADD COLUMN data_backend text NULL,
ADD COLUMN data_ref_id uuid NULL;
CREATE INDEX IF NOT EXISTS file_data_fragment__data_ref_id__idx
ON file_data_fragment (data_ref_id);
@@ -0,0 +1,6 @@
ALTER TABLE file_data_fragment
ADD COLUMN data_backend text NULL,
ADD COLUMN data_ref_id uuid NULL;
CREATE INDEX IF NOT EXISTS file_data_fragment__data_ref_id__idx
ON file_data_fragment (data_ref_id);
@@ -0,0 +1,4 @@
ALTER TABLE file ADD COLUMN data_ref_id uuid NULL;
CREATE INDEX IF NOT EXISTS file__data_ref_id__idx
ON file (data_ref_id);
@@ -0,0 +1,2 @@
CREATE INDEX IF NOT EXISTS file_change__created_at__label__idx
ON file_change (created_at, label);
@@ -0,0 +1,2 @@
CREATE INDEX profile__props__newsletter1__idx ON profile (email) WHERE props->>'~:newsletter-news' = 'true';
CREATE INDEX profile__props__newsletter2__idx ON profile (email) WHERE props->>'~:newsletter-updates' = 'true';
@@ -0,0 +1,3 @@
--- This setting allow to optimize the table for heavy write workload
--- leaving space on the page for HOT updates
ALTER TABLE file SET (FILLFACTOR=50);
@@ -0,0 +1,10 @@
CREATE TABLE team_access_request (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE DEFERRABLE,
requester_id uuid NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE,
valid_until timestamptz NOT NULL,
auto_join_until timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (team_id, requester_id)
);
@@ -0,0 +1,3 @@
--- This setting allow to optimize the table for heavy write workload
--- leaving space on the page for HOT updates
ALTER TABLE storage_object SET (FILLFACTOR=60);
@@ -0,0 +1,3 @@
--- This setting allow to optimize the table for heavy write workload
--- leaving space on the page for HOT updates
ALTER TABLE task SET (FILLFACTOR=60);
@@ -0,0 +1,6 @@
ALTER TABLE file_change
ADD COLUMN data_backend text NULL,
ADD COLUMN data_ref_id uuid NULL;
CREATE INDEX IF NOT EXISTS file_change__data_ref_id__idx
ON file_change (data_ref_id);
@@ -0,0 +1,2 @@
ALTER TABLE file_change
ADD COLUMN version integer NULL;
+14 -24
View File
@@ -149,6 +149,13 @@
:hint "authentication required for this endpoint") :hint "authentication required for this endpoint")
(f cfg params))))) (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 (defn- wrap-audit
[_ f mdata] [_ f mdata]
(if (or (contains? cf/flags :webhooks) (if (or (contains? cf/flags :webhooks)
@@ -178,41 +185,25 @@
(if-let [schema (::sm/params mdata)] (if-let [schema (::sm/params mdata)]
(let [validate (sm/validator schema) (let [validate (sm/validator schema)
explain (sm/explainer schema) explain (sm/explainer schema)
decode (sm/decoder schema)] decode (sm/decoder schema sm/json-transformer)
encode (sm/encoder schema sm/json-transformer)]
(fn [cfg params] (fn [cfg params]
(let [params (decode params)] (let [params (decode params)]
(if (validate params) (if (validate params)
(f cfg params) (let [result (f cfg params)]
(if (instance? clojure.lang.IObj result)
(vary-meta result assoc :encode/json encode)
result))
(let [params (d/without-qualified params)] (let [params (d/without-qualified params)]
(ex/raise :type :validation (ex/raise :type :validation
:code :params-validation :code :params-validation
::sm/explain (explain params))))))) ::sm/explain (explain params)))))))
f)) f))
(defn- wrap-output-validation
[_ f mdata]
(if (contains? cf/flags :rpc-output-validation)
(or (when-let [schema (::sm/result mdata)]
(let [schema (if (sm/lazy-schema? schema)
schema
(sm/define schema))
validate (sm/validator schema)
explain (sm/explainer schema)]
(fn [cfg params]
(let [response (f cfg params)]
(when (map? response)
(when-not (validate response)
(ex/raise :type :validation
:code :data-validation
::sm/explain (explain response))))
response))))
f)
f))
(defn- wrap-all (defn- wrap-all
[cfg f mdata] [cfg f mdata]
(as-> f $ (as-> f $
(wrap-db-transaction cfg $ mdata)
(cond/wrap cfg $ mdata) (cond/wrap cfg $ mdata)
(retry/wrap-retry cfg $ mdata) (retry/wrap-retry cfg $ mdata)
(climit/wrap cfg $ mdata) (climit/wrap cfg $ mdata)
@@ -220,7 +211,6 @@
(rlimit/wrap cfg $ mdata) (rlimit/wrap cfg $ mdata)
(wrap-audit cfg $ mdata) (wrap-audit cfg $ mdata)
(wrap-spec-conform cfg $ mdata) (wrap-spec-conform cfg $ mdata)
(wrap-output-validation cfg $ mdata)
(wrap-params-validation cfg $ mdata) (wrap-params-validation cfg $ mdata)
(wrap-authentication cfg $ mdata))) (wrap-authentication cfg $ mdata)))
+35 -39
View File
@@ -6,7 +6,7 @@
(ns app.rpc.commands.access-token (ns app.rpc.commands.access-token
(:require (:require
[app.common.spec :as us] [app.common.schema :as sm]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.main :as-alias main] [app.main :as-alias main]
@@ -16,8 +16,7 @@
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]))
[clojure.spec.alpha :as s]))
(defn- decode-row (defn- decode-row
[row] [row]
@@ -31,20 +30,19 @@
:tid token-id :tid token-id
:iat created-at}) :iat created-at})
expires-at (some-> expiration dt/in-future)] 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)))
(db/insert! conn :access-token (defn repl:create-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" [])})))
(defn repl-create-access-token
[{:keys [::db/pool] :as system} profile-id name expiration] [{:keys [::db/pool] :as system} profile-id name expiration]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [props (:app.setup/props system)] (let [props (:app.setup/props system)]
@@ -53,40 +51,38 @@
name name
expiration)))) expiration))))
(s/def ::name ::us/not-empty-string) (def ^:private schema:create-access-token
(s/def ::expiration ::dt/duration) [:map {:title "create-access-token"}
[:name [:string {:max 250 :min 1}]]
(s/def ::create-access-token [:expiration {:optional true} ::dt/duration]])
(s/keys :req [::rpc/profile-id]
:req-un [::name]
:opt-un [::expiration]))
(sv/defmethod ::create-access-token (sv/defmethod ::create-access-token
{::doc/added "1.18"} {::doc/added "1.18"
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}] ::sm/params schema:create-access-token}
(db/with-atomic [conn pool] [cfg {:keys [::rpc/profile-id name expiration]}]
(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)))))
(s/def ::delete-access-token (quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
(s/keys :req [::rpc/profile-id] ::quotes/profile-id profile-id})
:req-un [::us/id]))
(db/tx-run! cfg create-access-token profile-id name expiration))
(def ^:private schema:delete-access-token
[:map {:title "delete-access-token"}
[:id ::sm/uuid]])
(sv/defmethod ::delete-access-token (sv/defmethod ::delete-access-token
{::doc/added "1.18"} {::doc/added "1.18"
::sm/params schema:delete-access-token}
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
(db/delete! pool :access-token {:id id :profile-id profile-id}) (db/delete! pool :access-token {:id id :profile-id profile-id})
nil) nil)
(s/def ::get-access-tokens (def ^:private schema:get-access-tokens
(s/keys :req [::rpc/profile-id])) [:map {:title "get-access-tokens"}])
(sv/defmethod ::get-access-tokens (sv/defmethod ::get-access-tokens
{::doc/added "1.18"} {::doc/added "1.18"
::sm/params schema:get-access-tokens}
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
(->> (db/query pool :access-token (->> (db/query pool :access-token
{:profile-id profile-id} {:profile-id profile-id}
+19 -8
View File
@@ -27,9 +27,11 @@
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.setup.welcome-file :refer [create-welcome-file]]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(def schema:password (def schema:password
@@ -241,6 +243,7 @@
params (d/without-nils params) params (d/without-nils params)
token (tokens/generate (::setup/props cfg) params)] token (tokens/generate (::setup/props cfg) params)]
(with-meta {:token token} (with-meta {:token token}
{::audit/profile-id uuid/zero}))) {::audit/profile-id uuid/zero})))
@@ -350,7 +353,7 @@
:extra-data ptoken}))) :extra-data ptoken})))
(defn register-profile (defn register-profile
[{:keys [::db/conn] :as cfg} {:keys [token fullname theme] :as params}] [{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token fullname theme] :as params}]
(let [theme (when (= theme "light") theme) (let [theme (when (= theme "light") theme)
claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register}) claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
params (-> claims params (-> claims
@@ -380,8 +383,13 @@
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation})) (tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
props (audit/profile->props profile)] props (audit/profile->props profile)
create-welcome-file-when-needed
(fn []
(when (:create-welcome-file params)
(let [cfg (dissoc cfg ::db/conn)]
(wrk/submit! executor (create-welcome-file cfg profile)))))]
(cond (cond
;; When profile is blocked, we just ignore it and return plain data ;; When profile is blocked, we just ignore it and return plain data
(:is-blocked profile) (:is-blocked profile)
@@ -418,6 +426,7 @@
(if (:is-active profile) (if (:is-active profile)
(-> (profile/strip-private-attrs profile) (-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-defer create-welcome-file-when-needed)
(rph/with-meta (rph/with-meta
{::audit/replace-props props {::audit/replace-props props
::audit/context {:action "login"} ::audit/context {:action "login"}
@@ -427,10 +436,12 @@
(when-not (eml/has-reports? conn (:email profile)) (when-not (eml/has-reports? conn (:email profile))
(send-email-verification! cfg profile)) (send-email-verification! cfg profile))
(rph/with-meta {:email (:email profile)} (-> {:email (:email profile)}
{::audit/replace-props props (rph/with-defer create-welcome-file-when-needed)
::audit/context {:action "email-verification"} (rph/with-meta
::audit/profile-id (:id profile)}))) {::audit/replace-props props
::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)}))))
:else :else
(let [elapsed? (elapsed-verify-threshold? profile) (let [elapsed? (elapsed-verify-threshold? profile)
@@ -462,7 +473,8 @@
[:map {:title "register-profile"} [:map {:title "register-profile"}
[:token schema:token] [:token schema:token]
[:fullname [::sm/word-string {:max 100}]] [:fullname [::sm/word-string {:max 100}]]
[:theme {:optional true} [:string {:max 10}]]]) [:theme {:optional true} [:string {:max 10}]]
[:create-welcome-file {:optional true} :boolean]])
(sv/defmethod ::register-profile (sv/defmethod ::register-profile
{::rpc/auth false {::rpc/auth false
@@ -534,7 +546,6 @@
(create-recovery-token) (create-recovery-token)
(send-email-notification conn))))))) (send-email-notification conn)))))))
(def schema:request-profile-recovery (def schema:request-profile-recovery
[:map {:title "request-profile-recovery"} [:map {:title "request-profile-recovery"}
[:email ::sm/email]]) [:email ::sm/email]])
+6 -4
View File
@@ -30,9 +30,10 @@
;; --- Command: export-binfile ;; --- Command: export-binfile
(def ^:private schema:export-binfile (def ^:private
schema:export-binfile
[:map {:title "export-binfile"} [:map {:title "export-binfile"}
[:name :string] [:name [:string {:max 250}]]
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:include-libraries :boolean] [:include-libraries :boolean]
[:embed-assets :boolean]]) [:embed-assets :boolean]])
@@ -74,9 +75,10 @@
{:id project-id}) {:id project-id})
result)) result))
(def ^:private schema:import-binfile (def ^:private
schema:import-binfile
[:map {:title "import-binfile"} [:map {:title "import-binfile"}
[:name :string] [:name [:string {:max 250}]]
[:project-id ::sm/uuid] [:project-id ::sm/uuid]
[:file ::media/upload]]) [:file ::media/upload]])
+48 -39
View File
@@ -71,10 +71,15 @@
[conn comment-id & {:as opts}] [conn comment-id & {:as opts}]
(db/get-by-id conn :comment comment-id 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 (defn- get-next-seqn
[conn file-id] [conn file-id]
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?" (let [res (db/exec-one! conn [sql:get-next-seqn file-id])]
res (db/exec-one! conn [sql file-id])]
(:next-seqn res))) (:next-seqn res)))
(def sql:upsert-comment-thread-status (def sql:upsert-comment-thread-status
@@ -292,7 +297,7 @@
[:map {:title "create-comment-thread"} [:map {:title "create-comment-thread"}
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:position ::gpt/point] [:position ::gpt/point]
[:content :string] [:content [:string {:max 750}]]
[:page-id ::sm/uuid] [:page-id ::sm/uuid]
[:frame-id ::sm/uuid] [:frame-id ::sm/uuid]
[:share-id {:optional true} [:maybe ::sm/uuid]]]) [:share-id {:optional true} [:maybe ::sm/uuid]]])
@@ -304,38 +309,43 @@
::rtry/when rtry/conflict-exception? ::rtry/when rtry/conflict-exception?
::sm/params schema:create-comment-thread} ::sm/params schema:create-comment-thread}
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}] [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}] (let [{:keys [team-id project-id page-name]} (get-file cfg file-id page-id)]
(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)]
(run! (partial quotes/check-quote! cfg) (-> cfg
(list {::quotes/id ::quotes/comment-threads-per-file (assoc ::quotes/profile-id profile-id)
::quotes/profile-id profile-id (assoc ::quotes/team-id team-id)
::quotes/team-id team-id (assoc ::quotes/project-id project-id)
::quotes/project-id project-id (assoc ::quotes/file-id file-id)
::quotes/file-id file-id} (quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
{::quotes/id ::quotes/comments-per-file {::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}))
(create-comment-thread conn {:created-at request-at (let [params {:created-at request-at
:profile-id profile-id :profile-id profile-id
:file-id file-id :file-id file-id
:page-id page-id :page-id page-id
:page-name page-name :page-name page-name
:position position :position position
:content content :content content
:frame-id frame-id}))))) :frame-id frame-id}
thread (db/tx-run! cfg create-comment-thread params)]
(vary-meta thread assoc ::audit/props thread))))
(defn- create-comment-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) seqn (get-next-seqn conn file-id)
thread-id (uuid/next) thread-id (uuid/next)
thread (db/insert! conn :comment-thread thread (db/insert! conn :comment-thread
@@ -364,7 +374,8 @@
;; Optimistic update of current seq number on file. ;; Optimistic update of current seq number on file.
(db/update! conn :file (db/update! conn :file
{:comment-thread-seqn seqn} {:comment-thread-seqn seqn}
{:id file-id}) {:id file-id}
{::db/return-keys false})
(-> thread (-> thread
(select-keys [:id :file-id :page-id]) (select-keys [:id :file-id :page-id])
@@ -387,7 +398,6 @@
(files/check-comment-permissions! conn profile-id file-id share-id) (files/check-comment-permissions! conn profile-id file-id share-id)
(upsert-comment-thread-status! conn profile-id id))))) (upsert-comment-thread-status! conn profile-id id)))))
;; --- COMMAND: Update Comment Thread ;; --- COMMAND: Update Comment Thread
(def ^:private (def ^:private
@@ -418,7 +428,7 @@
schema:create-comment schema:create-comment
[:map {:title "create-comment"} [:map {:title "create-comment"}
[:thread-id ::sm/uuid] [:thread-id ::sm/uuid]
[:content :string] [:content [:string {:max 250}]]
[:share-id {:optional true} [:maybe ::sm/uuid]]]) [:share-id {:optional true} [:maybe ::sm/uuid]]])
(sv/defmethod ::create-comment (sv/defmethod ::create-comment
@@ -432,12 +442,11 @@
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)] {: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) (files/check-comment-permissions! conn profile-id file-id share-id)
(quotes/check-quote! conn (quotes/check! cfg {::quotes/id ::quotes/comments-per-file
{::quotes/id ::quotes/comments-per-file ::quotes/profile-id profile-id
::quotes/profile-id profile-id ::quotes/team-id team-id
::quotes/team-id team-id ::quotes/project-id project-id
::quotes/project-id project-id ::quotes/file-id file-id})
::quotes/file-id file-id})
;; Update the page-name cached attribute on comment thread table. ;; Update the page-name cached attribute on comment thread table.
(when (not= page-name (:page-name thread)) (when (not= page-name (:page-name thread))
@@ -477,7 +486,7 @@
schema:update-comment schema:update-comment
[:map {:title "update-comment"} [:map {:title "update-comment"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:content :string] [:content [:string {:max 250}]]
[:share-id {:optional true} [:maybe ::sm/uuid]]]) [:share-id {:optional true} [:maybe ::sm/uuid]]])
(sv/defmethod ::update-comment (sv/defmethod ::update-comment
+2 -5
View File
@@ -18,10 +18,7 @@
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[buddy.core.codecs :as bc] [buddy.core.codecs :as bc]
[buddy.core.nonce :as bn] [buddy.core.nonce :as bn]))
[clojure.spec.alpha :as s]))
(s/def ::create-demo-profile any?)
(sv/defmethod ::create-demo-profile (sv/defmethod ::create-demo-profile
"A command that is responsible of creating a demo purpose "A command that is responsible of creating a demo purpose
@@ -48,7 +45,7 @@
params {:email email params {:email email
:fullname fullname :fullname fullname
:is-active true :is-active true
:deleted-at (dt/in-future cf/deletion-delay) :deleted-at (dt/in-future (cf/get-deletion-delay))
:password (profile/derive-password cfg password) :password (profile/derive-password cfg password)
:props {}}] :props {}}]
+8 -12
View File
@@ -8,29 +8,25 @@
"A general purpose feedback module." "A general purpose feedback module."
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.schema :as sm]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.email :as eml] [app.email :as eml]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]))
[clojure.spec.alpha :as s]))
(declare ^:private send-feedback!) (declare ^:private send-feedback!)
(s/def ::content ::us/string) (def ^:private schema:send-user-feedback
(s/def ::from ::us/email) [:map {:title "send-user-feedback"}
(s/def ::subject ::us/string) [:subject [:string {:max 400}]]
[:content [:string {:max 2500}]]])
(s/def ::send-user-feedback
(s/keys :req [::rpc/profile-id]
:req-un [::subject
::content]))
(sv/defmethod ::send-user-feedback (sv/defmethod ::send-user-feedback
{::doc/added "1.18"} {::doc/added "1.18"
::sm/params schema:send-user-feedback}
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}] [{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
(when-not (contains? cf/flags :user-feedback) (when-not (contains? cf/flags :user-feedback)
(ex/raise :type :restriction (ex/raise :type :restriction
+75 -76
View File
@@ -15,7 +15,6 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.desc-js-like :as-alias smdj] [app.common.schema.desc-js-like :as-alias smdj]
[app.common.spec :as us]
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
[app.config :as cf] [app.config :as cf]
@@ -36,7 +35,6 @@
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
;; --- FEATURES ;; --- FEATURES
@@ -46,18 +44,6 @@
(when media-id (when media-id
(str (cf/get :public-uri) "/assets/by-id/" media-id))) (str (cf/get :public-uri) "/assets/by-id/" media-id)))
;; --- SPECS
(s/def ::features ::us/set-of-strings)
(s/def ::file-id ::us/uuid)
(s/def ::frame-id ::us/uuid)
(s/def ::id ::us/uuid)
(s/def ::is-shared ::us/boolean)
(s/def ::name ::us/string)
(s/def ::project-id ::us/uuid)
(s/def ::search-term ::us/string)
(s/def ::team-id ::us/uuid)
;; --- HELPERS ;; --- HELPERS
(def long-cache-duration (def long-cache-duration
@@ -82,6 +68,9 @@
:max-version fmg/version)) :max-version fmg/version))
file)) file))
;; --- FILE DATA
;; --- FILE PERMISSIONS ;; --- FILE PERMISSIONS
(def ^:private sql:file-permissions (def ^:private sql:file-permissions
@@ -185,38 +174,34 @@
;; --- COMMAND QUERY: get-file (by id) ;; --- COMMAND QUERY: get-file (by id)
(def schema:file (def schema:file
(sm/define [:map {:title "File"}
[:map {:title "File"} [:id ::sm/uuid]
[:id ::sm/uuid] [:features ::cfeat/features]
[:features ::cfeat/features] [:has-media-trimmed ::sm/boolean]
[:has-media-trimmed :boolean] [:comment-thread-seqn [::sm/int {:min 0}]]
[:comment-thread-seqn {:min 0} :int] [:name [:string {:max 250}]]
[:name :string] [:revn [::sm/int {:min 0}]]
[:revn {:min 0} :int] [:modified-at ::dt/instant]
[:modified-at ::dt/instant] [:is-shared ::sm/boolean]
[:is-shared :boolean] [:project-id ::sm/uuid]
[:project-id ::sm/uuid] [:created-at ::dt/instant]
[:created-at ::dt/instant] [:data {:optional true} :any]])
[:data {:optional true} :any]]))
(def schema:permissions-mixin (def schema:permissions-mixin
(sm/define [:map {:title "PermissionsMixin"}
[:map {:title "PermissionsMixin"} [:permissions ::perms/permissions]])
[:permissions ::perms/permissions]]))
(def schema:file-with-permissions (def schema:file-with-permissions
(sm/define [:merge {:title "FileWithPermissions"}
[:merge {:title "FileWithPermissions"} schema:file
schema:file schema:permissions-mixin])
schema:permissions-mixin]))
(def ^:private (def ^:private
schema:get-file schema:get-file
(sm/define [:map {:title "get-file"}
[:map {:title "get-file"} [:features {:optional true} ::cfeat/features]
[:features {:optional true} ::cfeat/features] [:id ::sm/uuid]
[:id ::sm/uuid] [:project-id {:optional true} ::sm/uuid]])
[:project-id {:optional true} ::sm/uuid]]))
(defn- migrate-file (defn- migrate-file
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}] [{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
@@ -272,18 +257,19 @@
(let [params (merge {:id id} (let [params (merge {:id id}
(when (some? project-id) (when (some? project-id)
{:project-id project-id})) {:project-id project-id}))
file (-> (db/get conn :file params file (->> (db/get conn :file params
{::db/check-deleted (not include-deleted?) {::db/check-deleted (not include-deleted?)
::db/remove-deleted (not include-deleted?) ::db/remove-deleted (not include-deleted?)
::sql/for-update lock-for-update?}) ::sql/for-update lock-for-update?})
(decode-row))] (feat.fdata/resolve-file-data cfg)
(decode-row))]
(if (and migrate? (fmg/need-migration? file)) (if (and migrate? (fmg/need-migration? file))
(migrate-file cfg file) (migrate-file cfg file)
file))) file)))
(defn get-minimal-file (defn get-minimal-file
[cfg id & {:as opts}] [cfg id & {:as opts}]
(let [opts (assoc opts ::sql/columns [:id :modified-at :revn])] (let [opts (assoc opts ::sql/columns [:id :modified-at :deleted-at :revn :data-ref-id :data-backend])]
(db/get cfg :file {:id id} opts))) (db/get cfg :file {:id id} opts)))
(defn get-file-etag (defn get-file-etag
@@ -342,8 +328,10 @@
(defn- get-file-fragment (defn- get-file-fragment
[cfg file-id fragment-id] [cfg file-id fragment-id]
(some-> (db/get cfg :file-data-fragment {:file-id file-id :id fragment-id}) (let [resolve-file-data (partial feat.fdata/resolve-file-data cfg)]
(update :data blob/decode))) (some-> (db/get cfg :file-data-fragment {:file-id file-id :id fragment-id})
(resolve-file-data)
(update :data blob/decode))))
(sv/defmethod ::get-file-fragment (sv/defmethod ::get-file-fragment
"Retrieve a file fragment by its ID. Only authenticated users." "Retrieve a file fragment by its ID. Only authenticated users."
@@ -368,7 +356,7 @@
f.name, f.name,
f.revn, f.revn,
f.is_shared, f.is_shared,
ft.media_id ft.media_id AS thumbnail_id
from file as f from file as f
left join file_thumbnail as ft on (ft.file_id = f.id left join file_thumbnail as ft on (ft.file_id = f.id
and ft.revn = f.revn and ft.revn = f.revn
@@ -379,13 +367,7 @@
(defn get-project-files (defn get-project-files
[conn project-id] [conn project-id]
(->> (db/exec! conn [sql:project-files 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))))))
(def schema:get-project-files (def schema:get-project-files
[:map {:title "get-project-files"} [:map {:title "get-project-files"}
@@ -416,7 +398,7 @@
"Checks if the file has libraries. Returns a boolean" "Checks if the file has libraries. Returns a boolean"
{::doc/added "1.15.1" {::doc/added "1.15.1"
::sm/params schema:has-file-libraries ::sm/params schema:has-file-libraries
::sm/result :boolean} ::sm/result ::sm/boolean}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! pool profile-id file-id) (check-read-permissions! pool profile-id file-id)
@@ -495,7 +477,7 @@
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid] [:page-id {:optional true} ::sm/uuid]
[:share-id {:optional true} ::sm/uuid] [:share-id {:optional true} ::sm/uuid]
[:object-id {:optional true} [:or ::sm/uuid ::sm/coll-of-uuid]] [:object-id {:optional true} [:or ::sm/uuid [::sm/set ::sm/uuid]]]
[:features {:optional true} ::cfeat/features]]) [:features {:optional true} ::cfeat/features]])
(sv/defmethod ::get-page (sv/defmethod ::get-page
@@ -737,6 +719,23 @@
[cfg {:keys [::rpc/profile-id] :as params}] [cfg {:keys [::rpc/profile-id] :as params}]
(db/tx-run! cfg get-file-summary (assoc params :profile-id profile-id))) (db/tx-run! cfg get-file-summary (assoc params :profile-id profile-id)))
;; --- COMMAND QUERY: get-file-info
(defn- get-file-info
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
(db/get* conn :file
{:id id}
{::sql/columns [:id]}))
(sv/defmethod ::get-file-info
"Retrieve minimal file info by its ID."
{::rpc/auth false
::doc/added "2.2.0"
::sm/params schema:get-file}
[cfg params]
(db/tx-run! cfg get-file-info params))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS ;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -759,19 +758,19 @@
[:map {:title "RenameFileEvent"} [:map {:title "RenameFileEvent"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:project-id ::sm/uuid] [:project-id ::sm/uuid]
[:name :string] [:name [:string {:max 250}]]
[:created-at ::dt/instant] [:created-at ::dt/instant]
[:modified-at ::dt/instant]] [:modified-at ::dt/instant]]
::sm/params ::sm/params
[:map {:title "RenameFileParams"} [:map {:title "RenameFileParams"}
[:name {:min 1} :string] [:name [:string {:min 1 :max 250}]]
[:id ::sm/uuid]] [:id ::sm/uuid]]
::sm/result ::sm/result
[:map {:title "SimplifiedFile"} [:map {:title "SimplifiedFile"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:name :string] [:name [:string {:max 250}]]
[:created-at ::dt/instant] [:created-at ::dt/instant]
[:modified-at ::dt/instant]]} [:modified-at ::dt/instant]]}
@@ -816,7 +815,8 @@
(db/update! cfg :file (db/update! cfg :file
{:revn (inc (:revn file)) {:revn (inc (:revn file))
:data (blob/encode (:data file)) :data (blob/encode (:data file))
:modified-at (dt/now)} :modified-at (dt/now)
:has-media-trimmed false}
{:id file-id}) {:id file-id})
(feat.fdata/persist-pointers! cfg file-id)))) (feat.fdata/persist-pointers! cfg file-id))))
@@ -904,10 +904,9 @@
(def ^:private (def ^:private
schema:set-file-shared schema:set-file-shared
(sm/define [:map {:title "set-file-shared"}
[:map {:title "set-file-shared"} [:id ::sm/uuid]
[:id ::sm/uuid] [:is-shared ::sm/boolean]])
[:is-shared :boolean]]))
(sv/defmethod ::set-file-shared (sv/defmethod ::set-file-shared
{::doc/added "1.17" {::doc/added "1.17"
@@ -934,9 +933,8 @@
(def ^:private (def ^:private
schema:delete-file schema:delete-file
(sm/define [:map {:title "delete-file"}
[:map {:title "delete-file"} [:id ::sm/uuid]])
[:id ::sm/uuid]]))
(defn- delete-file (defn- delete-file
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
@@ -968,10 +966,9 @@
(def ^:private (def ^:private
schema:link-file-to-library schema:link-file-to-library
(sm/define [:map {:title "link-file-to-library"}
[:map {:title "link-file-to-library"} [:file-id ::sm/uuid]
[:file-id ::sm/uuid] [:library-id ::sm/uuid]])
[:library-id ::sm/uuid]]))
(sv/defmethod ::link-file-to-library (sv/defmethod ::link-file-to-library
{::doc/added "1.17" {::doc/added "1.17"
@@ -1045,14 +1042,16 @@
{:id file-id} {:id file-id}
{::db/return-keys true})) {::db/return-keys true}))
(s/def ::ignore-file-library-sync-status (def ^:private schema:ignore-file-library-sync-status
(s/keys :req [::rpc/profile-id] [:map {:title "ignore-file-library-sync-status"}
:req-un [::file-id ::date])) [:file-id ::sm/uuid]
[:date ::dt/instant]])
;; TODO: improve naming ;; TODO: improve naming
(sv/defmethod ::ignore-file-library-sync-status (sv/defmethod ::ignore-file-library-sync-status
"Ignore updates in linked files" "Ignore updates in linked files"
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:ignore-file-library-sync-status}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id file-id)
+42 -39
View File
@@ -88,56 +88,59 @@
(def ^:private schema:create-file (def ^:private schema:create-file
[:map {:title "create-file"} [:map {:title "create-file"}
[:name :string] [:name [:string {:max 250}]]
[:project-id ::sm/uuid] [:project-id ::sm/uuid]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
[:is-shared {:optional true} :boolean] [:is-shared {:optional true} ::sm/boolean]
[:features {:optional true} ::cfeat/features]]) [:features {:optional true} ::cfeat/features]])
(sv/defmethod ::create-file (sv/defmethod ::create-file
{::doc/added "1.17" {::doc/added "1.17"
::doc/module :files ::doc/module :files
::webhooks/event? true ::webhooks/event? true
::sm/params schema:create-file} ::sm/params schema:create-file
[cfg {:keys [::rpc/profile-id project-id] :as params}] ::db/transaction true}
(db/tx-run! cfg [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(fn [{:keys [::db/conn] :as cfg}] (projects/check-edition-permissions! conn profile-id project-id)
(projects/check-edition-permissions! conn profile-id project-id) (let [team (teams/get-team conn
(let [team (teams/get-team conn :profile-id profile-id
:profile-id profile-id :project-id project-id)
:project-id project-id) team-id (:id team)
team-id (:id team)
;; When we create files, we only need to respect the team ;; When we create files, we only need to respect the team
;; features, because some features can be enabled ;; features, because some features can be enabled
;; globally, but the team is still not migrated properly. ;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team) features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))) (cfeat/check-client-features! (:features params)))
;; We also include all no migration features declared by ;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime ;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file ;; feature on frontend and make it permanent on file
features (-> (:features params #{}) features (-> (:features params #{})
(set/intersection cfeat/no-migration-features) (set/intersection cfeat/no-migration-features)
(set/union features)) (set/union features))
params (-> params params (-> params
(assoc :profile-id profile-id) (assoc :profile-id profile-id)
(assoc :features features))] (assoc :features features))]
(run! (partial quotes/check-quote! conn) (quotes/check! cfg {::quotes/id ::quotes/files-per-project
(list {::quotes/id ::quotes/files-per-project ::quotes/team-id team-id
::quotes/team-id team-id ::quotes/profile-id profile-id
::quotes/profile-id profile-id ::quotes/project-id project-id})
::quotes/project-id project-id}))
;; When newly computed features does not match exactly with ;; FIXME: IMPORTANT: this code can have race
;; the features defined on team row, we update it. ;; conditions, because we have no locks for updating
(when (not= features (:features team)) ;; team so, creating two files concurrently can lead
(let [features (db/create-array conn "text" features)] ;; to lost team features updating
(db/update! conn :team
{:features features}
{:id team-id})))
(-> (create-file cfg params) ;; When newly computed features does not match exactly with
(vary-meta assoc ::audit/props {:team-id team-id})))))) ;; 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}))))
+15 -18
View File
@@ -7,29 +7,24 @@
(ns app.rpc.commands.files-share (ns app.rpc.commands.files-share
"Share link related rpc mutation methods." "Share link related rpc mutation methods."
(:require (:require
[app.common.spec :as us] [app.common.schema :as sm]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]))
[clojure.spec.alpha :as s]))
;; --- Helpers & Specs
(s/def ::file-id ::us/uuid)
(s/def ::who-comment ::us/string)
(s/def ::who-inspect ::us/string)
(s/def ::pages (s/every ::us/uuid :kind set?))
;; --- MUTATION: Create Share Link ;; --- MUTATION: Create Share Link
(declare create-share-link) (declare create-share-link)
(s/def ::create-share-link (def ^:private schema:create-share-link
(s/keys :req [::rpc/profile-id] [:map {:title "create-share-link"}
:req-un [::file-id ::who-comment ::who-inspect ::pages])) [:file-id ::sm/uuid]
[:who-comment [:string {:max 250}]]
[:who-inspect [:string {:max 250}]]
[:pages [:set ::sm/uuid]]])
(sv/defmethod ::create-share-link (sv/defmethod ::create-share-link
"Creates a share-link object. "Creates a share-link object.
@@ -37,7 +32,8 @@
Share links are resources that allows external users access to specific Share links are resources that allows external users access to specific
pages of a file with specific permissions (who-comment and who-inspect)." pages of a file with specific permissions (who-comment and who-inspect)."
{::doc/added "1.18" {::doc/added "1.18"
::doc/module :files} ::doc/module :files
::sm/params schema:create-share-link}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)
@@ -58,13 +54,14 @@
;; --- MUTATION: Delete Share Link ;; --- MUTATION: Delete Share Link
(s/def ::delete-share-link (def ^:private schema:delete-share-link
(s/keys :req [::rpc/profile-id] [:map {:title "delete-share-link"}
:req-un [::us/id])) [:id ::sm/uuid]])
(sv/defmethod ::delete-share-link (sv/defmethod ::delete-share-link
{::doc/added "1.18" {::doc/added "1.18"
::doc/module ::files} ::doc/module ::files
::sm/params schema:delete-share-link}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [slink (db/get-by-id conn :share-link id)] (let [slink (db/get-by-id conn :share-link id)]
+73 -48
View File
@@ -13,13 +13,15 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata]
[app.main :as-alias main] [app.main :as-alias main]
[app.media :as media]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.storage :as sto] [app.storage :as sto]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@@ -34,20 +36,21 @@
:code :authentication-required :code :authentication-required
:hint "only admins allowed"))) :hint "only admins allowed")))
(def sql:get-file-snapshots
"SELECT id, label, revn, created_at
FROM file_change
WHERE file_id = ?
AND created_at < ?
AND label IS NOT NULL
ORDER BY created_at DESC
LIMIT ?")
(defn get-file-snapshots (defn get-file-snapshots
[{:keys [::db/conn]} {:keys [file-id limit start-at] [{:keys [::db/conn]} {:keys [file-id limit start-at]
:or {limit Long/MAX_VALUE}}] :or {limit Long/MAX_VALUE}}]
(let [query (str "select id, label, revn, created_at " (let [start-at (or start-at (dt/now))
" from file_change "
" where file_id = ? "
" and created_at < ? "
" and data is not null "
" order by created_at desc "
" limit ?")
start-at (or start-at (dt/now))
limit (min limit 20)] limit (min limit 20)]
(->> (db/exec! conn [sql:get-file-snapshots file-id start-at limit])
(->> (db/exec! conn [query file-id start-at limit])
(mapv (fn [row] (mapv (fn [row]
(update row :created-at dt/format-instant :rfc1123)))))) (update row :created-at dt/format-instant :rfc1123))))))
@@ -63,8 +66,8 @@
(db/run! cfg get-file-snapshots params)) (db/run! cfg get-file-snapshots params))
(defn restore-file-snapshot! (defn restore-file-snapshot!
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id id]}] [{:keys [::db/conn] :as cfg} {:keys [file-id id]}]
(let [storage (media/configure-assets-storage storage conn) (let [storage (sto/resolve cfg {::db/reuse-conn true})
file (files/get-minimal-file conn file-id {::db/for-update true}) file (files/get-minimal-file conn file-id {::db/for-update true})
snapshot (db/get* conn :file-change snapshot (db/get* conn :file-change
{:file-id file-id {:file-id file-id
@@ -78,43 +81,53 @@
:id id :id id
:file-id file-id)) :file-id file-id))
(when-not (:data snapshot) (let [snapshot (feat.fdata/resolve-file-data cfg snapshot)]
(ex/raise :type :precondition (when-not (:data snapshot)
:code :snapshot-without-data (ex/raise :type :precondition
:hint "snapshot has no data" :code :snapshot-without-data
:label (:label snapshot) :hint "snapshot has no data"
:file-id file-id)) :label (:label snapshot)
:file-id file-id))
(l/dbg :hint "restoring snapshot" (l/dbg :hint "restoring snapshot"
:file-id (str file-id) :file-id (str file-id)
:label (:label snapshot) :label (:label snapshot)
:snapshot-id (str (:id snapshot))) :snapshot-id (str (:id snapshot)))
(db/update! conn :file ;; If the file was already offloaded, on restring the snapshot
{:data (:data snapshot) ;; we are going to replace the file data, so we need to touch
:revn (inc (:revn file)) ;; the old referenced storage object and avoid possible leaks
:features (:features snapshot)} (when (feat.fdata/offloaded? file)
{:id file-id}) (sto/touch-object! storage (:data-ref-id file)))
;; clean object thumbnails (db/update! conn :file
(let [sql (str "update file_tagged_object_thumbnail " {:data (:data snapshot)
" set deleted_at = now() " :revn (inc (:revn file))
" where file_id=? returning media_id") :version (:version snapshot)
res (db/exec! conn [sql file-id])] :data-backend nil
:data-ref-id nil
:has-media-trimmed false
:features (:features snapshot)}
{:id file-id})
(doseq [media-id (into #{} (keep :media-id) res)] ;; clean object thumbnails
(sto/touch-object! storage media-id))) (let [sql (str "update file_tagged_object_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")
res (db/exec! conn [sql file-id])]
(doseq [media-id (into #{} (keep :media-id) res)]
(sto/touch-object! storage media-id)))
;; clean object thumbnails ;; clean file thumbnails
(let [sql (str "update file_thumbnail " (let [sql (str "update file_thumbnail "
" set deleted_at = now() " " set deleted_at = now() "
" where file_id=? returning media_id") " where file_id=? returning media_id")
res (db/exec! conn [sql file-id])] res (db/exec! conn [sql file-id])]
(doseq [media-id (into #{} (keep :media-id) res)] (doseq [media-id (into #{} (keep :media-id) res)]
(sto/touch-object! storage media-id))) (sto/touch-object! storage media-id)))
{:id (:id snapshot) {:id (:id snapshot)
:label (:label snapshot)})) :label (:label snapshot)})))
(defn- resolve-snapshot-by-label (defn- resolve-snapshot-by-label
[conn file-id label] [conn file-id label]
@@ -146,21 +159,33 @@
(merge (resolve-snapshot-by-label conn file-id label)))] (merge (resolve-snapshot-by-label conn file-id label)))]
(restore-file-snapshot! cfg params))))) (restore-file-snapshot! cfg params)))))
(defn- get-file
[cfg file-id]
(let [file (->> (db/get cfg :file {:id file-id})
(feat.fdata/resolve-file-data cfg))]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
(-> file
(update :data blob/decode)
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(update :data blob/encode)))))
(defn take-file-snapshot! (defn take-file-snapshot!
[cfg {:keys [file-id label]}] [cfg {:keys [file-id label ::rpc/profile-id]}]
(let [conn (db/get-connection cfg) (let [file (get-file cfg file-id)
file (db/get conn :file {:id file-id})
id (uuid/next)] id (uuid/next)]
(l/debug :hint "creating file snapshot" (l/debug :hint "creating file snapshot"
:file-id (str file-id) :file-id (str file-id)
:label label) :label label)
(db/insert! conn :file-change (db/insert! cfg :file-change
{:id id {:id id
:revn (:revn file) :revn (:revn file)
:data (:data file) :data (:data file)
:version (:version file)
:features (:features file) :features (:features file)
:profile-id profile-id
:file-id (:id file) :file-id (:id file)
:label label} :label label}
{::db/return-keys false}) {::db/return-keys false})
+32 -31
View File
@@ -35,47 +35,48 @@
(def ^:private schema:create-temp-file (def ^:private schema:create-temp-file
[:map {:title "create-temp-file"} [:map {:title "create-temp-file"}
[:name :string] [:name [:string {:max 250}]]
[:project-id ::sm/uuid] [:project-id ::sm/uuid]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
[:is-shared :boolean] [:is-shared ::sm/boolean]
[:features ::cfeat/features] [:features ::cfeat/features]
[:create-page :boolean]]) [:create-page ::sm/boolean]])
(sv/defmethod ::create-temp-file (sv/defmethod ::create-temp-file
{::doc/added "1.17" {::doc/added "1.17"
::doc/module :files ::doc/module :files
::sm/params schema:create-temp-file} ::sm/params schema:create-temp-file
[cfg {:keys [::rpc/profile-id project-id] :as params}] ::db/transaction true}
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(projects/check-edition-permissions! conn profile-id project-id) (projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn :profile-id profile-id :project-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 ;; If the imported project doesn't contain v2 we need to remove it
;; features, because some features can be enabled team-features
;; globally, but the team is still not migrated properly. (cond-> (cfeat/get-team-enabled-features cf/flags team)
input-features (:features params #{}) (not (contains? input-features "components/v2"))
(disj "components/v2"))
;; If the imported project doesn't contain v2 we need to remove it ;; We also include all no migration features declared by
team-features ;; client; that enables the ability to enable a runtime
(cond-> (cfeat/get-team-enabled-features cf/flags team) ;; feature on frontend and make it permanent on file
(not (contains? input-features "components/v2")) features
(disj "components/v2")) (-> 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 (files.create/create-file cfg params)))
;; 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)))))
;; --- MUTATION COMMAND: update-temp-file ;; --- MUTATION COMMAND: update-temp-file
@@ -83,7 +84,7 @@
(def ^:private schema:update-temp-file (def ^:private schema:update-temp-file
[:map {:title "update-temp-file"} [:map {:title "update-temp-file"}
[:changes [:vector ::cpc/change]] [:changes [:vector ::cpc/change]]
[:revn {:min 0} :int] [:revn [::sm/int {:min 0}]]
[:session-id ::sm/uuid] [:session-id ::sm/uuid]
[:id ::sm/uuid]]) [:id ::sm/uuid]])
@@ -33,7 +33,6 @@
[app.util.pointer-map :as pmap] [app.util.pointer-map :as pmap]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
;; --- FEATURES ;; --- FEATURES
@@ -86,8 +85,8 @@
::doc/module :files ::doc/module :files
::sm/params [:map {:title "get-file-object-thumbnails"} ::sm/params [:map {:title "get-file-object-thumbnails"}
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:tag {:optional true} :string]] [:tag {:optional true} [:string {:max 50}]]]
::sm/result [:map-of :string :string]} ::sm/result [:map-of [:string {:max 250}] [:string {:max 250}]]}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id tag] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id tag] :as params}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
@@ -180,18 +179,16 @@
(def ^:private (def ^:private
schema:get-file-data-for-thumbnail schema:get-file-data-for-thumbnail
(sm/define [:map {:title "get-file-data-for-thumbnail"}
[:map {:title "get-file-data-for-thumbnail"} [:file-id ::sm/uuid]
[:file-id ::sm/uuid] [:features {:optional true} ::cfeat/features]])
[:features {:optional true} ::cfeat/features]]))
(def ^:private (def ^:private
schema:partial-file schema:partial-file
(sm/define [:map {:title "PartialFile"}
[:map {:title "PartialFile"} [:id ::sm/uuid]
[:id ::sm/uuid] [:revn {:min 0} ::sm/int]
[:revn {:min 0} :int] [:page :any]])
[:page :any]]))
(sv/defmethod ::get-file-data-for-thumbnail (sv/defmethod ::get-file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used "Retrieves the data for generate the thumbnail of the file. Used
@@ -234,7 +231,7 @@
"INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id) "INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
ON CONFLICT (file_id, object_id, tag) ON CONFLICT (file_id, object_id, tag)
DO UPDATE SET updated_at=?, media_id=?, deleted_at=null DO UPDATE SET updated_at=?, media_id=?, deleted_at=?
RETURNING *") RETURNING *")
(defn- persist-thumbnail! (defn- persist-thumbnail!
@@ -252,17 +249,19 @@
:content-type mtype :content-type mtype
:bucket "file-object-thumbnail"}))) :bucket "file-object-thumbnail"})))
(defn- create-file-object-thumbnail! (defn- create-file-object-thumbnail!
[{:keys [::sto/storage] :as cfg} file-id object-id media tag] [{:keys [::sto/storage] :as cfg} file object-id media tag]
(let [tsnow (dt/now) (let [file-id (:id file)
media (persist-thumbnail! storage media tsnow) timestamp (dt/now)
media (persist-thumbnail! storage media timestamp)
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}] [th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
(let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag]) (let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag])
th2 (db/exec-one! conn [sql:create-file-object-thumbnail th2 (db/exec-one! conn [sql:create-file-object-thumbnail
file-id object-id tag (:id media) file-id object-id tag
tsnow (:id media)])] (:id media)
timestamp
(:id media)
(:deleted-at file)])]
[th1 th2])))] [th1 th2])))]
(when (and (some? th1) (when (and (some? th1)
@@ -276,9 +275,9 @@
schema:create-file-object-thumbnail schema:create-file-object-thumbnail
[:map {:title "create-file-object-thumbnail"} [:map {:title "create-file-object-thumbnail"}
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:object-id :string] [:object-id [:string {:max 250}]]
[:media ::media/upload] [:media ::media/upload]
[:tag {:optional true} :string]]) [:tag {:optional true} [:string {:max 50}]]])
(sv/defmethod ::create-file-object-thumbnail (sv/defmethod ::create-file-object-thumbnail
{::doc/added "1.19" {::doc/added "1.19"
@@ -295,9 +294,8 @@
(media/validate-media-size! media) (media/validate-media-size! media)
(db/run! cfg files/check-edition-permissions! profile-id file-id) (db/run! cfg files/check-edition-permissions! profile-id file-id)
(when-let [file (files/get-minimal-file cfg file-id {::db/check-deleted false})]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (create-file-object-thumbnail! cfg file object-id media (or tag "frame"))))
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))))
;; --- MUTATION COMMAND: delete-file-object-thumbnail ;; --- MUTATION COMMAND: delete-file-object-thumbnail
@@ -314,19 +312,21 @@
:object-id object-id :object-id object-id
:tag tag}))) :tag tag})))
(s/def ::delete-file-object-thumbnail (def ^:private schema:delete-file-object-thumbnail
(s/keys :req [::rpc/profile-id] [:map {:title "delete-file-object-thumbnail"}
:req-un [::file-id ::object-id])) [:file-id ::sm/uuid]
[:object-id [:string {:max 250}]]])
(sv/defmethod ::delete-file-object-thumbnail (sv/defmethod ::delete-file-object-thumbnail
{::doc/added "1.19" {::doc/added "1.19"
::doc/module :files ::doc/module :files
::sm/params schema:delete-file-object-thumbnail
::audit/skip true} ::audit/skip true}
[cfg {:keys [::rpc/profile-id file-id object-id]}] [cfg {:keys [::rpc/profile-id file-id object-id]}]
(files/check-edition-permissions! cfg profile-id file-id) (files/check-edition-permissions! cfg profile-id file-id)
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(-> cfg (-> cfg
(update ::sto/storage media/configure-assets-storage conn) (update ::sto/storage sto/configure conn)
(delete-file-object-thumbnail! file-id object-id)) (delete-file-object-thumbnail! file-id object-id))
nil))) nil)))
@@ -385,7 +385,7 @@
schema:create-file-thumbnail schema:create-file-thumbnail
[:map {:title "create-file-thumbnail"} [:map {:title "create-file-thumbnail"}
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:revn :int] [:revn ::sm/int]
[:media ::media/upload]]) [:media ::media/upload]])
(sv/defmethod ::create-file-thumbnail (sv/defmethod ::create-file-thumbnail
@@ -404,7 +404,6 @@
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn) (when-not (db/read-only? conn)
(let [cfg (update cfg ::sto/storage media/configure-assets-storage) (let [media (create-file-thumbnail! cfg params)]
media (create-file-thumbnail! cfg params)]
{:uri (files/resolve-public-uri (:id media)) {:uri (files/resolve-public-uri (:id media))
:id (:id media)}))))) :id (:id media)})))))
+280 -166
View File
@@ -29,6 +29,7 @@
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
[app.storage :as sto]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.pointer-map :as pmap] [app.util.pointer-map :as pmap]
[app.util.services :as sv] [app.util.services :as sv]
@@ -37,6 +38,20 @@
[clojure.set :as set] [clojure.set :as set]
[promesa.exec :as px])) [promesa.exec :as px]))
(declare ^:private get-lagged-changes)
(declare ^:private send-notifications!)
(declare ^:private update-file)
(declare ^:private update-file*)
(declare ^:private process-changes-and-validate)
(declare ^:private take-snapshot?)
(declare ^:private delete-old-snapshots!)
;; PUBLIC API; intended to be used outside of this module
(declare update-file!)
(declare update-file-data!)
(declare persist-file!)
(declare get-file)
;; --- SCHEMA ;; --- SCHEMA
(def ^:private (def ^:private
@@ -44,15 +59,15 @@
[:map {:title "update-file"} [:map {:title "update-file"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:session-id ::sm/uuid] [:session-id ::sm/uuid]
[:revn {:min 0} :int] [:revn {:min 0} ::sm/int]
[:features {:optional true} ::cfeat/features] [:features {:optional true} ::cfeat/features]
[:changes {:optional true} [:vector ::cpc/change]] [:changes {:optional true} [:vector ::cpc/change]]
[:changes-with-metadata {:optional true} [:changes-with-metadata {:optional true}
[:vector [:map [:vector [:map
[:changes [:vector ::cpc/change]] [:changes [:vector ::cpc/change]]
[:hint-origin {:optional true} :keyword] [:hint-origin {:optional true} :keyword]
[:hint-events {:optional true} [:vector :string]]]]] [:hint-events {:optional true} [:vector [:string {:max 250}]]]]]]
[:skip-validate {:optional true} :boolean]]) [:skip-validate {:optional true} ::sm/boolean]])
(def ^:private (def ^:private
schema:update-file-result schema:update-file-result
@@ -61,7 +76,7 @@
[:changes [:vector ::cpc/change]] [:changes [:vector ::cpc/change]]
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:id ::sm/uuid] [:id ::sm/uuid]
[:revn {:min 0} :int] [:revn {:min 0} ::sm/int]
[:session-id ::sm/uuid]]]) [:session-id ::sm/uuid]]])
;; --- HELPERS ;; --- HELPERS
@@ -96,40 +111,6 @@
(or (contains? library-change-types type) (or (contains? library-change-types type)
(contains? file-change-types type))) (contains? file-change-types type)))
(def ^:private sql:get-file
"SELECT f.*, p.team_id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
WHERE f.id = ?
AND (f.deleted_at IS NULL OR
f.deleted_at > now())
FOR KEY SHARE")
(defn get-file
[conn id]
(let [file (db/exec-one! conn [sql:get-file id])]
(when-not file
(ex/raise :type :not-found
:code :object-not-found
:hint (format "file with id '%s' does not exists" id)))
(update file :features db/decode-pgarray #{})))
(defn- wrap-with-pointer-map-context
[f]
(fn [cfg {:keys [id] :as file}]
(binding [pmap/*tracked* (pmap/create-tracked)
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(let [result (f cfg file)]
(feat.fdata/persist-pointers! cfg id)
result))))
(declare get-lagged-changes)
(declare send-notifications!)
(declare update-file)
(declare update-file*)
(declare update-file-data)
(declare take-snapshot?)
;; If features are specified from params and the final feature ;; If features are specified from params and the final feature
;; set is different than the persisted one, update it on the ;; set is different than the persisted one, update it on the
;; database. ;; database.
@@ -145,7 +126,8 @@
::sm/result schema:update-file-result ::sm/result schema:update-file-result
::doc/module :files ::doc/module :files
::doc/added "1.17"} ::doc/added "1.17"}
[cfg {:keys [::rpc/profile-id id] :as params}] [{:keys [::mtx/metrics] :as cfg}
{:keys [::rpc/profile-id id changes changes-with-metadata] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id id) (files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id) (db/xact-lock! conn id)
@@ -159,14 +141,30 @@
(cfeat/check-client-features! (:features params)) (cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params))) (cfeat/check-file-features! (:features file) (:features params)))
params (assoc params changes (if changes-with-metadata
:profile-id profile-id (->> changes-with-metadata (mapcat :changes) vec)
:features features (vec changes))
:team team
:file file) params (-> params
(assoc :profile-id profile-id)
(assoc :features features)
(assoc :team team)
(assoc :file file)
(assoc :changes changes))
cfg (assoc cfg ::timestamp (dt/now))
tpoint (dt/tpoint)] tpoint (dt/tpoint)]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
;; When newly computed features does not match exactly with ;; When newly computed features does not match exactly with
;; the features defined on team row, we update it. ;; the features defined on team row, we update it.
(when (not= features (:features team)) (when (not= features (:features team))
@@ -175,73 +173,124 @@
{:features features} {:features features}
{:id (:id team)}))) {:id (:id team)})))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
(binding [l/*context* (some-> (meta params) (binding [l/*context* (some-> (meta params)
(get :app.http/request) (get :app.http/request)
(errors/request->context))] (errors/request->context))]
(-> (update-file cfg params) (-> (update-file* cfg params)
(rph/with-defer #(let [elapsed (tpoint)] (rph/with-defer #(let [elapsed (tpoint)]
(l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))))) (l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))))
(defn update-file
[{:keys [::mtx/metrics] :as cfg}
{:keys [file features changes changes-with-metadata] :as params}]
(let [features (-> features
(set/difference cfeat/frontend-only-features)
(set/union (:features file)))
update-fn (cond-> update-file*
(contains? features "fdata/pointer-map")
(wrap-with-pointer-map-context))
changes (if changes-with-metadata
(->> changes-with-metadata (mapcat :changes) vec)
(vec changes))]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
(binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(let [file (assoc file :features features)
params (-> params
(assoc :file file)
(assoc :changes changes)
(assoc ::created-at (dt/now)))]
(-> (update-fn cfg params)
(vary-meta assoc ::audit/replace-props
{:id (:id file)
:name (:name file)
:features (:features file)
:project-id (:project-id file)
:team-id (:team-id file)}))))))
(defn- update-file* (defn- update-file*
[{:keys [::db/conn ::wrk/executor] :as cfg} "Internal function, part of the update-file process, that encapsulates
{:keys [profile-id file changes session-id ::created-at skip-validate] :as params}] the changes application offload to a separated thread and emit all
(let [;; Process the file data on separated thread for avoid to do corresponding notifications.
;; the CPU intensive operation on vthread.
file (px/invoke! executor (partial update-file-data cfg file changes skip-validate))
features (db/create-array conn "text" (:features file))]
(db/insert! conn :file-change Follow the inner implementation to `update-file-data!` function.
{:id (uuid/next)
:session-id session-id Only intended for internal use on this module."
:profile-id profile-id [{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
:created-at created-at {:keys [profile-id file features changes session-id skip-validate] :as params}]
:file-id (:id file)
:revn (:revn file) (let [;; Retrieve the file data
:features (db/create-array conn "text" (:features file)) file (feat.fdata/resolve-file-data cfg file)
:data (when (take-snapshot? file)
(:data file)) file (assoc file :features
:changes (blob/encode changes)} (-> features
(set/difference cfeat/frontend-only-features)
(set/union (:features file))))
;; Process the file data on separated thread for avoid to do
;; the CPU intensive operation on vthread.
file (px/invoke! executor
(fn []
(binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))))]
(when (feat.fdata/offloaded? file)
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
(some->> (:data-ref-id file) (sto/touch-object! storage))))
;; TODO: move this to asynchronous task
(when (::snapshot-data file)
(delete-old-snapshots! cfg file))
(persist-file! cfg file)
(let [params (assoc params :file file)
response {:revn (:revn file)
:lagged (get-lagged-changes conn params)}
features (db/create-array conn "text" (:features file))]
;; Insert change (xlog)
(db/insert! conn :file-change
{:id (uuid/next)
:session-id session-id
:profile-id profile-id
:created-at timestamp
:file-id (:id file)
:revn (:revn file)
:version (:version file)
:features features
:label (::snapshot-label file)
:data (::snapshot-data file)
:changes (blob/encode changes)}
{::db/return-keys false})
;; Send asynchronous notifications
(send-notifications! cfg params)
(vary-meta response assoc ::audit/replace-props
{:id (:id file)
:name (:name file)
:features (:features file)
:project-id (:project-id file)
:team-id (:team-id file)}))))
(defn update-file!
"A public api that allows apply a transformation to a file with all context setup."
[cfg file-id update-fn & args]
(let [file (get-file cfg file-id)
file (apply update-file-data! cfg file update-fn args)]
(persist-file! cfg file)))
(def ^:private sql:get-file
"SELECT f.*, p.team_id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
WHERE f.id = ?
AND (f.deleted_at IS NULL OR
f.deleted_at > now())
FOR KEY SHARE")
(defn get-file
"Get not-decoded file, only decodes the features set."
[conn id]
(let [file (db/exec-one! conn [sql:get-file id])]
(when-not file
(ex/raise :type :not-found
:code :object-not-found
:hint (format "file with id '%s' does not exists" id)))
(update file :features db/decode-pgarray #{})))
(defn persist-file!
"Function responsible of persisting already encoded file. Should be
used together with `get-file` and `update-file-data!`.
It also updates the project modified-at attr."
[{:keys [::db/conn ::timestamp]} file]
(let [features (db/create-array conn "text" (:features file))
;; The timestamp can be nil because this function is also
;; intended to be used outside of this module
modified-at (or timestamp (dt/now))]
(db/update! conn :project
{:modified-at modified-at}
{:id (:project-id file)}
{::db/return-keys false}) {::db/return-keys false})
(db/update! conn :file (db/update! conn :file
@@ -250,20 +299,96 @@
:version (:version file) :version (:version file)
:features features :features features
:data-backend nil :data-backend nil
:modified-at created-at :data-ref-id nil
:modified-at modified-at
:has-media-trimmed false} :has-media-trimmed false}
{:id (:id file)}) {:id (:id file)}
{::db/return-keys false})))
(db/update! conn :project (defn- update-file-data!
{:modified-at created-at} "Perform a file data transformation in with all update context setup.
{:id (:project-id file)})
(let [params (assoc params :file file)] This function expected not-decoded file and transformation function. Returns
;; Send asynchronous notifications an encoded file.
(send-notifications! cfg params)
{:revn (:revn file) This function is not responsible of saving the file. It only saves
:lagged (get-lagged-changes conn params)}))) fdata/pointer-map modified fragments."
[cfg {:keys [id] :as file} update-fn & args]
(binding [pmap/*tracked* (pmap/create-tracked)
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(let [file (update file :data (fn [data]
(-> data
(blob/decode)
(assoc :id (:id file)))))
;; For avoid unnecesary overhead of creating multiple pointers
;; and handly internally with objects map in their worst
;; case (when probably all shapes and all pointers will be
;; readed in any case), we just realize/resolve them before
;; applying the migration to the file
file (if (fmg/need-migration? file)
(-> file
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file))
file)
file (apply update-fn cfg file args)
;; TODO: reuse operations if file is migrated
;; TODO: move encoding to a separated thread
file (if (take-snapshot? file)
(let [tpoint (dt/tpoint)
snapshot (-> (:data file)
(feat.fdata/process-pointers deref)
(feat.fdata/process-objects (partial into {}))
(blob/encode))
elapsed (tpoint)
label (str "internal/snapshot/" (:revn file))]
(l/trc :hint "take snapshot"
:file-id (str (:id file))
:revn (:revn file)
:label label
:elapsed (dt/format-duration elapsed))
(-> file
(assoc ::snapshot-data snapshot)
(assoc ::snapshot-label label)))
file)
file (cond-> file
(contains? cfeat/*current* "fdata/objects-map")
(feat.fdata/enable-objects-map)
(contains? cfeat/*current* "fdata/pointer-map")
(feat.fdata/enable-pointer-map)
:always
(update :data blob/encode))]
(feat.fdata/persist-pointers! cfg id)
file)))
(defn- get-file-libraries
"A helper for preload file libraries, mainly used for perform file
semantical and structural validation"
[{:keys [::db/conn] :as cfg} file]
(->> (files/get-file-libraries conn (:id file))
(into [file] (map (fn [{:keys [id]}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* nil]
;; We do not resolve the objects maps here
;; because there is a lower probability that all
;; shapes needed to be loded into memory, so we
;; leeave it on lazy status
(-> (files/get-file cfg id :migrate? false)
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file))))))
(d/index-by :id)))
(defn- soft-validate-file-schema! (defn- soft-validate-file-schema!
[file] [file]
@@ -280,51 +405,20 @@
(l/error :hint "file validation error" (l/error :hint "file validation error"
:cause cause)))) :cause cause))))
(defn- update-file-data (defn- process-changes-and-validate
[{:keys [::db/conn] :as cfg} file changes skip-validate] [cfg file changes skip-validate]
(let [file (update file :data (fn [data] (let [;; WARNING: this ruins performance; maybe we need to find
(-> data
(blob/decode)
(assoc :id (:id file)))))
;; For avoid unnecesary overhead of creating multiple pointers
;; and handly internally with objects map in their worst
;; case (when probably all shapes and all pointers will be
;; readed in any case), we just realize/resolve them before
;; applying the migration to the file
file (if (fmg/need-migration? file)
(-> file
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file))
file)
;; WARNING: this ruins performance; maybe we need to find
;; some other way to do general validation ;; some other way to do general validation
libs (when (and (or (contains? cf/flags :file-validation) libs (when (and (or (contains? cf/flags :file-validation)
(contains? cf/flags :soft-file-validation)) (contains? cf/flags :soft-file-validation))
(not skip-validate)) (not skip-validate))
(->> (files/get-file-libraries conn (:id file)) (get-file-libraries cfg file))
(into [file] (map (fn [{:keys [id]}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* nil]
;; We do not resolve the objects maps here
;; because there is a lower probability that all
;; shapes needed to be loded into memory, so we
;; leeave it on lazy status
(-> (files/get-file cfg id :migrate? false)
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file))))))
(d/index-by :id)))
file (-> (files/check-version! file) file (-> (files/check-version! file)
(update :revn inc) (update :revn inc)
(update :data cpc/process-changes changes) (update :data cpc/process-changes changes)
(update :data d/without-nils))] (update :data d/without-nils))]
(binding [pmap/*tracked* nil] (binding [pmap/*tracked* nil]
(when (contains? cf/flags :soft-file-validation) (when (contains? cf/flags :soft-file-validation)
(soft-validate-file! file libs)) (soft-validate-file! file libs))
@@ -340,29 +434,49 @@
(not skip-validate)) (not skip-validate))
(val/validate-file-schema! file))) (val/validate-file-schema! file)))
(cond-> file file))
(contains? cfeat/*current* "fdata/objects-map")
(feat.fdata/enable-objects-map)
(contains? cfeat/*current* "fdata/pointer-map")
(feat.fdata/enable-pointer-map)
:always
(update :data blob/encode))))
(defn- take-snapshot? (defn- take-snapshot?
"Defines the rule when file `data` snapshot should be saved." "Defines the rule when file `data` snapshot should be saved."
[{:keys [revn modified-at] :as file}] [{:keys [revn modified-at] :as file}]
(let [freq (or (cf/get :file-change-snapshot-every) 20) (when (contains? cf/flags :auto-file-snapshot)
timeout (or (cf/get :file-change-snapshot-timeout) (let [freq (or (cf/get :auto-file-snapshot-every) 20)
(dt/duration {:hours 1}))] timeout (or (cf/get :auto-file-snapshot-timeout)
(or (= 1 freq) (dt/duration {:hours 1}))]
(zero? (mod revn freq))
(> (inst-ms (dt/diff modified-at (dt/now)))
(inst-ms timeout)))))
(def ^:private (or (= 1 freq)
sql:lagged-changes (zero? (mod revn freq))
(> (inst-ms (dt/diff modified-at (dt/now)))
(inst-ms timeout))))))
;; Get the latest available snapshots without exceeding the total
;; snapshot limit.
(def ^:private sql:get-latest-snapshots
"SELECT fch.id, fch.created_at
FROM file_change AS fch
WHERE fch.file_id = ?
AND fch.label LIKE 'internal/%'
ORDER BY fch.created_at DESC
LIMIT ?")
;; Mark all snapshots that are outside the allowed total threshold
;; available for the GC.
(def ^:private sql:delete-snapshots
"UPDATE file_change
SET label = NULL
WHERE file_id = ?
AND label LIKE 'internal/%'
AND created_at < ?")
(defn- delete-old-snapshots!
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
(when-let [snapshots (not-empty (db/exec! conn [sql:get-latest-snapshots id
(cf/get :auto-file-snapshot-total 10)]))]
(let [last-date (-> snapshots peek :created-at)
result (db/exec-one! conn [sql:delete-snapshots id last-date])]
(l/trc :hint "delete old snapshots" :file-id (str id) :total (db/get-update-count result)))))
(def ^:private sql:lagged-changes
"select s.id, s.revn, s.file_id, "select s.id, s.revn, s.file_id,
s.session_id, s.changes s.session_id, s.changes
from file_change as s from file_change as s
+12 -21
View File
@@ -86,6 +86,9 @@
[:font-weight [::sm/one-of {:format "number"} valid-weight]] [:font-weight [::sm/one-of {:format "number"} valid-weight]]
[:font-style [::sm/one-of {:format "string"} valid-style]]]) [: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 (sv/defmethod ::create-font-variant
{::doc/added "1.18" {::doc/added "1.18"
::climit/id [[:process-font/by-profile ::rpc/profile-id] ::climit/id [[:process-font/by-profile ::rpc/profile-id]
@@ -95,12 +98,11 @@
[cfg {:keys [::rpc/profile-id team-id] :as params}] [cfg {:keys [::rpc/profile-id team-id] :as params}]
(db/tx-run! cfg (db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}] (fn [{:keys [::db/conn] :as cfg}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (teams/check-edition-permissions! conn profile-id team-id)
(teams/check-edition-permissions! conn profile-id team-id) (quotes/check! cfg {::quotes/id ::quotes/font-variants-per-team
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team ::quotes/profile-id profile-id
::quotes/profile-id profile-id ::quotes/team-id team-id})
::quotes/team-id team-id}) (create-font-variant cfg (assoc params :profile-id profile-id)))))
(create-font-variant cfg (assoc params :profile-id profile-id))))))
(defn create-font-variant (defn create-font-variant
[{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}] [{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}]
@@ -203,14 +205,13 @@
::sm/params schema:delete-font} ::sm/params schema:delete-font}
[cfg {:keys [::rpc/profile-id id team-id]}] [cfg {:keys [::rpc/profile-id id team-id]}]
(db/tx-run! cfg (db/tx-run! cfg
(fn [{:keys [::db/conn ::sto/storage] :as cfg}] (fn [{:keys [::db/conn] :as cfg}]
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! conn profile-id team-id)
(let [fonts (db/query conn :team-font-variant (let [fonts (db/query conn :team-font-variant
{:team-id team-id {:team-id team-id
:font-id id :font-id id
:deleted-at nil} :deleted-at nil}
{::sql/for-update true}) {::sql/for-update true})
storage (media/configure-assets-storage storage conn)
tnow (dt/now)] tnow (dt/now)]
(when-not (seq fonts) (when-not (seq fonts)
@@ -220,11 +221,7 @@
(doseq [font fonts] (doseq [font fonts]
(db/update! conn :team-font-variant (db/update! conn :team-font-variant
{:deleted-at tnow} {:deleted-at tnow}
{:id (:id font)}) {:id (:id font)}))
(some->> (:woff1-file-id font) (sto/touch-object! storage))
(some->> (:woff2-file-id font) (sto/touch-object! storage))
(some->> (:ttf-file-id font) (sto/touch-object! storage))
(some->> (:otf-file-id font) (sto/touch-object! storage)))
(rph/with-meta (rph/wrap) (rph/with-meta (rph/wrap)
{::audit/props {:id id {::audit/props {:id id
@@ -245,22 +242,16 @@
::sm/params schema:delete-font-variant} ::sm/params schema:delete-font-variant}
[cfg {:keys [::rpc/profile-id id team-id]}] [cfg {:keys [::rpc/profile-id id team-id]}]
(db/tx-run! cfg (db/tx-run! cfg
(fn [{:keys [::db/conn ::sto/storage] :as cfg}] (fn [{:keys [::db/conn] :as cfg}]
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! conn profile-id team-id)
(let [variant (db/get conn :team-font-variant (let [variant (db/get conn :team-font-variant
{:id id :team-id team-id} {:id id :team-id team-id}
{::sql/for-update true}) {::sql/for-update true})]
storage (media/configure-assets-storage storage conn)]
(db/update! conn :team-font-variant (db/update! conn :team-font-variant
{:deleted-at (dt/now)} {:deleted-at (dt/now)}
{:id (:id variant)}) {:id (:id variant)})
(some->> (:woff1-file-id variant) (sto/touch-object! storage))
(some->> (:woff2-file-id variant) (sto/touch-object! storage))
(some->> (:ttf-file-id variant) (sto/touch-object! storage))
(some->> (:otf-file-id variant) (sto/touch-object! storage))
(rph/with-meta (rph/wrap) (rph/with-meta (rph/wrap)
{::audit/props {:font-family (:font-family variant) {::audit/props {:font-family (:font-family variant)
:font-id (:font-id variant)}}))))) :font-id (:font-id variant)}})))))
+9 -11
View File
@@ -8,7 +8,7 @@
(:require (:require
[app.auth.ldap :as ldap] [app.auth.ldap :as ldap]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.schema :as sm]
[app.db :as db] [app.db :as db]
[app.http.session :as session] [app.http.session :as session]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
@@ -19,27 +19,25 @@
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]))
[clojure.spec.alpha :as s]))
;; --- COMMAND: login-with-ldap ;; --- COMMAND: login-with-ldap
(declare login-or-register) (declare login-or-register)
(s/def ::email ::us/email) (def schema:login-with-ldap
(s/def ::password ::us/string) [:map {:title "login-with-ldap"}
(s/def ::invitation-token ::us/string) [:email ::sm/email]
[:password auth/schema:password]
(s/def ::login-with-ldap [:invitation-token {:optional true} auth/schema:token]])
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::login-with-ldap (sv/defmethod ::login-with-ldap
"Performs the authentication using LDAP backend. Only works if LDAP "Performs the authentication using LDAP backend. Only works if LDAP
is properly configured and enabled with `login-with-ldap` flag." is properly configured and enabled with `login-with-ldap` flag."
{::rpc/auth false {::rpc/auth false
::doc/added "1.15" ::doc/added "1.15"
::doc/module :auth} ::doc/module :auth
::sm/params schema:login-with-ldap}
[{:keys [::setup/props ::ldap/provider] :as cfg} params] [{:keys [::setup/props ::ldap/provider] :as cfg} params]
(when-not provider (when-not provider
(ex/raise :type :restriction (ex/raise :type :restriction
+20 -23
View File
@@ -88,10 +88,9 @@
(def ^:private (def ^:private
schema:duplicate-file schema:duplicate-file
(sm/define [:map {:title "duplicate-file"}
[:map {:title "duplicate-file"} [:file-id ::sm/uuid]
[:file-id ::sm/uuid] [:name {:optional true} [:string {:max 250}]]])
[:name {:optional true} :string]]))
(sv/defmethod ::duplicate-file (sv/defmethod ::duplicate-file
"Duplicate a single file in the same team." "Duplicate a single file in the same team."
@@ -150,10 +149,9 @@
(def ^:private (def ^:private
schema:duplicate-project schema:duplicate-project
(sm/define [:map {:title "duplicate-project"}
[:map {:title "duplicate-project"} [:project-id ::sm/uuid]
[:project-id ::sm/uuid] [:name {:optional true} [:string {:max 250}]]])
[:name {:optional true} :string]]))
(sv/defmethod ::duplicate-project (sv/defmethod ::duplicate-project
"Duplicate an entire project with all the files" "Duplicate an entire project with all the files"
@@ -327,10 +325,9 @@
(def ^:private (def ^:private
schema:move-files schema:move-files
(sm/define [:map {:title "move-files"}
[:map {:title "move-files"} [:ids ::sm/set-of-uuid]
[:ids ::sm/set-of-uuid] [:project-id ::sm/uuid]])
[:project-id ::sm/uuid]]))
(sv/defmethod ::move-files (sv/defmethod ::move-files
"Move a set of files from one project to other." "Move a set of files from one project to other."
@@ -382,10 +379,9 @@
(def ^:private (def ^:private
schema:move-project schema:move-project
(sm/define [:map {:title "move-project"}
[:map {:title "move-project"} [:team-id ::sm/uuid]
[:team-id ::sm/uuid] [:project-id ::sm/uuid]])
[:project-id ::sm/uuid]]))
(sv/defmethod ::move-project (sv/defmethod ::move-project
"Move projects between teams" "Move projects between teams"
@@ -397,8 +393,8 @@
;; --- COMMAND: Clone Template ;; --- COMMAND: Clone Template
(defn- clone-template (defn clone-template
[cfg {:keys [project-id ::rpc/profile-id] :as params} template] [cfg {:keys [project-id profile-id] :as params} template]
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}] (db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
;; NOTE: the importation process performs some operations that ;; NOTE: the importation process performs some operations that
;; are not very friendly with virtual threads, and for avoid ;; are not very friendly with virtual threads, and for avoid
@@ -417,6 +413,7 @@
(doseq [file-id result] (doseq [file-id result]
(let [props (assoc props :id file-id) (let [props (assoc props :id file-id)
event (-> (audit/event-from-rpc-params params) event (-> (audit/event-from-rpc-params params)
(assoc ::audit/profile-id profile-id)
(assoc ::audit/name "create-file") (assoc ::audit/name "create-file")
(assoc ::audit/props props))] (assoc ::audit/props props))]
(audit/submit! cfg event)))) (audit/submit! cfg event))))
@@ -425,10 +422,9 @@
(def ^:private (def ^:private
schema:clone-template schema:clone-template
(sm/define [:map {:title "clone-template"}
[:map {:title "clone-template"} [:project-id ::sm/uuid]
[:project-id ::sm/uuid] [:template-id ::sm/word-string]])
[:template-id ::sm/word-string]]))
(sv/defmethod ::clone-template (sv/defmethod ::clone-template
"Clone into the specified project the template by its id." "Clone into the specified project the template by its id."
@@ -439,7 +435,8 @@
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]}) (let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
_ (teams/check-edition-permissions! pool profile-id (:team-id project)) _ (teams/check-edition-permissions! pool profile-id (:team-id project))
template (tmpl/get-template-stream cfg template-id)] template (tmpl/get-template-stream cfg template-id)
params (assoc params :profile-id profile-id)]
(when-not template (when-not template
(ex/raise :type :not-found (ex/raise :type :not-found
+38 -40
View File
@@ -9,7 +9,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.media :as cm] [app.common.media :as cm]
[app.common.spec :as us] [app.common.schema :as sm]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
@@ -25,7 +25,6 @@
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as-alias wrk] [app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.io :as io] [datoteka.io :as io]
[promesa.exec :as px])) [promesa.exec :as px]))
@@ -39,43 +38,37 @@
:quality 85 :quality 85
:format :jpeg}) :format :jpeg})
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
;; --- Create File Media object (upload) ;; --- Create File Media object (upload)
(declare create-file-media-object) (declare create-file-media-object)
(s/def ::content ::media/upload) (def ^:private schema:upload-file-media-object
(s/def ::is-local ::us/boolean) [:map {:title "upload-file-media-object"}
[:id {:optional true} ::sm/uuid]
(s/def ::upload-file-media-object [:file-id ::sm/uuid]
(s/keys :req [::rpc/profile-id] [:is-local ::sm/boolean]
:req-un [::file-id ::is-local ::name ::content] [:name [:string {:max 250}]]
:opt-un [::id])) [:content ::media/upload]])
(sv/defmethod ::upload-file-media-object (sv/defmethod ::upload-file-media-object
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:upload-file-media-object
::climit/id [[:process-image/by-profile ::rpc/profile-id] ::climit/id [[:process-image/by-profile ::rpc/profile-id]
[:process-image/global]]} [:process-image/global]]}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id)
(media/validate-media-type! content)
(media/validate-media-size! content)
(files/check-edition-permissions! pool profile-id file-id) (db/run! cfg (fn [cfg]
(media/validate-media-type! content) (let [object (create-file-media-object cfg params)
(media/validate-media-size! content) props {:name (:name params)
:file-id file-id
(db/run! cfg (fn [cfg] :is-local (:is-local params)
(let [object (create-file-media-object cfg params) :size (:size content)
props {:name (:name params) :mtype (:mtype content)}]
:file-id file-id (with-meta object
:is-local (:is-local params) {::audit/replace-props props})))))
:size (:size content)
:mtype (:mtype content)}]
(with-meta object
{::audit/replace-props props}))))))
(defn- big-enough-for-thumbnail? (defn- big-enough-for-thumbnail?
"Checks if the provided image info is big enough for "Checks if the provided image info is big enough for
@@ -176,18 +169,20 @@
(declare ^:private create-file-media-object-from-url) (declare ^:private create-file-media-object-from-url)
(s/def ::create-file-media-object-from-url (def ^:private schema:create-file-media-object-from-url
(s/keys :req [::rpc/profile-id] [:map {:title "create-file-media-object-from-url"}
:req-un [::file-id ::is-local ::url] [:file-id ::sm/uuid]
:opt-un [::id ::name])) [:is-local ::sm/boolean]
[:url ::sm/uri]
[:id {:optional true} ::sm/uuid]
[:name {:optional true} [:string {:max 250}]]])
(sv/defmethod ::create-file-media-object-from-url (sv/defmethod ::create-file-media-object-from-url
{::doc/added "1.17" {::doc/added "1.17"
::doc/deprecated "1.19"} ::sm/params schema:create-file-media-object-from-url}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id)
(files/check-edition-permissions! pool profile-id file-id) (create-file-media-object-from-url cfg (assoc params :profile-id profile-id)))
(create-file-media-object-from-url cfg (assoc params :profile-id profile-id))))
(defn download-image (defn download-image
[{:keys [::http/client]} uri] [{:keys [::http/client]} uri]
@@ -255,12 +250,15 @@
(declare clone-file-media-object) (declare clone-file-media-object)
(s/def ::clone-file-media-object (def ^:private schema:clone-file-media-object
(s/keys :req [::rpc/profile-id] [:map {:title "clone-file-media-object"}
:req-un [::file-id ::is-local ::id])) [:file-id ::sm/uuid]
[:is-local ::sm/boolean]
[:id ::sm/uuid]])
(sv/defmethod ::clone-file-media-object (sv/defmethod ::clone-file-media-object
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:clone-file-media-object}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)
+65 -58
View File
@@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.plugins :refer [schema:plugin-registry]]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
@@ -40,6 +41,33 @@
(declare strip-private-attrs) (declare strip-private-attrs)
(declare verify-password) (declare verify-password)
(def schema:props
[:map {:title "ProfileProps"}
[:plugins {:optional true} schema:plugin-registry]
[:newsletter-updates {:optional true} ::sm/boolean]
[:newsletter-news {:optional true} ::sm/boolean]
[:onboarding-team-id {:optional true} ::sm/uuid]
[:onboarding-viewed {:optional true} ::sm/boolean]
[:v2-info-shown {:optional true} ::sm/boolean]
[:welcome-file-id {:optional true} [:maybe ::sm/boolean]]
[:release-notes-viewed {:optional true}
[::sm/text {:max 100}]]])
(def schema:profile
[:map {:title "Profile"}
[:id ::sm/uuid]
[:fullname [::sm/word-string {:max 250}]]
[:email ::sm/email]
[:is-active {:optional true} ::sm/boolean]
[:is-blocked {:optional true} ::sm/boolean]
[:is-demo {:optional true} ::sm/boolean]
[:is-muted {:optional true} ::sm/boolean]
[:created-at {:optional true} ::sm/inst]
[:modified-at {:optional true} ::sm/inst]
[:default-project-id {:optional true} ::sm/uuid]
[:default-team-id {:optional true} ::sm/uuid]
[:props {:optional true} schema:props]])
(defn clean-email (defn clean-email
"Clean and normalizes email address string" "Clean and normalizes email address string"
[email] [email]
@@ -53,24 +81,6 @@
email)] email)]
email)) email))
(def ^:private
schema:profile
(sm/define
[:map {:title "Profile"}
[:id ::sm/uuid]
[:fullname [::sm/word-string {:max 250}]]
[:email ::sm/email]
[:is-active {:optional true} :boolean]
[:is-blocked {:optional true} :boolean]
[:is-demo {:optional true} :boolean]
[:is-muted {:optional true} :boolean]
[:created-at {:optional true} ::sm/inst]
[:modified-at {:optional true} ::sm/inst]
[:default-project-id {:optional true} ::sm/uuid]
[:default-team-id {:optional true} ::sm/uuid]
[:props {:optional true}
[:map-of {:title "ProfileProps"} :keyword :any]]]))
;; --- QUERY: Get profile (own) ;; --- QUERY: Get profile (own)
(sv/defmethod ::get-profile (sv/defmethod ::get-profile
@@ -99,11 +109,10 @@
(def ^:private (def ^:private
schema:update-profile schema:update-profile
(sm/define [:map {:title "update-profile"}
[:map {:title "update-profile"} [:fullname [::sm/word-string {:max 250}]]
[:fullname [::sm/word-string {:max 250}]] [:lang {:optional true} [:string {:max 8}]]
[:lang {:optional true} [:string {:max 8}]] [:theme {:optional true} [:string {:max 250}]]])
[:theme {:optional true} [:string {:max 250}]]]))
(sv/defmethod ::update-profile (sv/defmethod ::update-profile
{::doc/added "1.0" {::doc/added "1.0"
@@ -144,11 +153,10 @@
(def ^:private (def ^:private
schema:update-profile-password schema:update-profile-password
(sm/define [:map {:title "update-profile-password"}
[:map {:title "update-profile-password"} [:password [::sm/word-string {:max 500}]]
[:password [::sm/word-string {:max 500}]] ;; Social registered users don't have old-password
;; Social registered users don't have old-password [:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]])
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]]))
(sv/defmethod ::update-profile-password (sv/defmethod ::update-profile-password
{::doc/added "1.0" {::doc/added "1.0"
@@ -199,9 +207,8 @@
(def ^:private (def ^:private
schema:update-profile-photo schema:update-profile-photo
(sm/define [:map {:title "update-profile-photo"}
[:map {:title "update-profile-photo"} [:file ::media/upload]])
[:file ::media/upload]]))
(sv/defmethod ::update-profile-photo (sv/defmethod ::update-profile-photo
{:doc/added "1.1" {:doc/added "1.1"
@@ -210,8 +217,7 @@
[cfg {:keys [::rpc/profile-id file] :as params}] [cfg {:keys [::rpc/profile-id file] :as params}]
;; Validate incoming mime type ;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (update-profile-photo cfg (assoc params :profile-id profile-id)))
(update-profile-photo cfg (assoc params :profile-id profile-id))))
(defn update-profile-photo (defn update-profile-photo
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}] [{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}]
@@ -269,9 +275,8 @@
(def ^:private (def ^:private
schema:request-email-change schema:request-email-change
(sm/define [:map {:title "request-email-change"}
[:map {:title "request-email-change"} [:email ::sm/email]])
[:email ::sm/email]]))
(sv/defmethod ::request-email-change (sv/defmethod ::request-email-change
{::doc/added "1.0" {::doc/added "1.0"
@@ -352,36 +357,38 @@
:extra-data ptoken}) :extra-data ptoken})
nil)) nil))
;; --- MUTATION: Update Profile Props ;; --- MUTATION: Update Profile Props
(def ^:private (def ^:private
schema:update-profile-props schema:update-profile-props
(sm/define [:map {:title "update-profile-props"}
[:map {:title "update-profile-props"} [:props schema:props]])
[:props [:map-of :keyword :any]]]))
(defn update-profile-props
[{:keys [::db/conn] :as cfg} profile-id props]
(let [profile (get-profile conn profile-id ::sql/for-update true)
props (reduce-kv (fn [props k v]
;; We don't accept namespaced keys
(if (simple-ident? k)
(if (nil? v)
(dissoc props k)
(assoc props k v))
props))
(:props profile)
props)]
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id})
(filter-props props)))
(sv/defmethod ::update-profile-props (sv/defmethod ::update-profile-props
{::doc/added "1.0" {::doc/added "1.0"
::sm/params schema:update-profile-props} ::sm/params schema:update-profile-props}
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}] [cfg {:keys [::rpc/profile-id props]}]
(db/with-atomic [conn pool] (db/tx-run! cfg (fn [cfg]
(let [profile (get-profile conn profile-id ::sql/for-update true) (update-profile-props cfg profile-id props))))
props (reduce-kv (fn [props k v]
;; We don't accept namespaced keys
(if (simple-ident? k)
(if (nil? v)
(dissoc props k)
(assoc props k v))
props))
(:props profile)
props)]
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id})
(filter-props props))))
;; --- MUTATION: Delete Profile ;; --- MUTATION: Delete Profile
+57 -49
View File
@@ -8,7 +8,7 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.schema :as sm]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
@@ -21,11 +21,7 @@
[app.rpc.quotes :as quotes] [app.rpc.quotes :as quotes]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]))
[clojure.spec.alpha :as s]))
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
;; --- Check Project Permissions ;; --- Check Project Permissions
@@ -75,13 +71,13 @@
(declare get-projects) (declare get-projects)
(s/def ::team-id ::us/uuid) (def ^:private schema:get-projects
(s/def ::get-projects [:map {:title "get-projects"}
(s/keys :req [::rpc/profile-id] [:team-id ::sm/uuid]])
:req-un [::team-id]))
(sv/defmethod ::get-projects (sv/defmethod ::get-projects
{::doc/added "1.18"} {::doc/added "1.18"
::sm/params schema:get-projects}
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
@@ -112,11 +108,12 @@
(declare get-all-projects) (declare get-all-projects)
(s/def ::get-all-projects (def ^:private schema:get-all-projects
(s/keys :req [::rpc/profile-id])) [:map {:title "get-all-projects"}])
(sv/defmethod ::get-all-projects (sv/defmethod ::get-all-projects
{::doc/added "1.18"} {::doc/added "1.18"
::sm/params schema:get-all-projects}
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(get-all-projects conn profile-id))) (get-all-projects conn profile-id)))
@@ -154,12 +151,13 @@
;; --- QUERY: Get project ;; --- QUERY: Get project
(s/def ::get-project (def ^:private schema:get-project
(s/keys :req [::rpc/profile-id] [:map {:title "get-project"}
:req-un [::id])) [:id ::sm/uuid]])
(sv/defmethod ::get-project (sv/defmethod ::get-project
{::doc/added "1.18"} {::doc/added "1.18"
::sm/params schema:get-project}
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(let [project (db/get-by-id conn :project id)] (let [project (db/get-by-id conn :project id)]
@@ -170,31 +168,36 @@
;; --- MUTATION: Create Project ;; --- MUTATION: Create Project
(s/def ::create-project (defn- create-project
(s/keys :req [::rpc/profile-id] [{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}]
:req-un [::team-id ::name] (let [project (teams/create-project conn params)]
:opt-un [::id])) (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]
[:name [:string {:max 250 :min 1}]]
[:id {:optional true} ::sm/uuid]])
(sv/defmethod ::create-project (sv/defmethod ::create-project
{::doc/added "1.18" {::doc/added "1.18"
::webhooks/event? true} ::webhooks/event? true
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] ::sm/params schema:create-project}
(db/with-atomic [conn pool] [cfg {:keys [::rpc/profile-id team-id] :as params}]
(teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(let [params (assoc params :profile-id profile-id) (teams/check-edition-permissions! cfg profile-id team-id)
project (teams/create-project conn params)] (quotes/check! cfg {::quotes/id ::quotes/projects-per-team
(teams/create-project-role conn profile-id (:id project) :owner) ::quotes/profile-id profile-id
(db/insert! conn :team-project-profile-rel ::quotes/team-id team-id})
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned false})
(assoc project :is-pinned false))))
(let [params (assoc params :profile-id profile-id)]
(db/tx-run! cfg create-project params)))
;; --- MUTATION: Toggle Project Pin ;; --- MUTATION: Toggle Project Pin
@@ -205,14 +208,15 @@
on conflict (team_id, project_id, profile_id) on conflict (team_id, project_id, profile_id)
do update set is_pinned=?") do update set is_pinned=?")
(s/def ::is-pinned ::us/boolean) (def ^:private schema:update-project-pin
(s/def ::project-id ::us/uuid) [:map {:title "update-project-pin"}
(s/def ::update-project-pin [:team-id ::sm/uuid]
(s/keys :req [::rpc/profile-id] [:is-pinned ::sm/boolean]
:req-un [::id ::team-id ::is-pinned])) [:id ::sm/uuid]])
(sv/defmethod ::update-project-pin (sv/defmethod ::update-project-pin
{::doc/added "1.18" {::doc/added "1.18"
::sm/params schema:update-project-pin
::webhooks/batch-timeout (dt/duration "5s") ::webhooks/batch-timeout (dt/duration "5s")
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id) ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
::webhooks/event? true} ::webhooks/event? true}
@@ -226,12 +230,14 @@
(declare rename-project) (declare rename-project)
(s/def ::rename-project (def ^:private schema:rename-project
(s/keys :req [::rpc/profile-id] [:map {:title "rename-project"}
:req-un [::name ::id])) [:name [:string {:max 250 :min 1}]]
[:id ::sm/uuid]])
(sv/defmethod ::rename-project (sv/defmethod ::rename-project
{::doc/added "1.18" {::doc/added "1.18"
::sm/params schema:rename-project
::webhooks/event? true} ::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
@@ -266,12 +272,14 @@
project)) project))
(s/def ::delete-project
(s/keys :req [::rpc/profile-id] (def ^:private schema:delete-project
:req-un [::id])) [:map {:title "delete-project"}
[:id ::sm/uuid]])
(sv/defmethod ::delete-project (sv/defmethod ::delete-project
{::doc/added "1.18" {::doc/added "1.18"
::sm/params schema:delete-project
::webhooks/event? true} ::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
+8 -11
View File
@@ -6,13 +6,12 @@
(ns app.rpc.commands.search (ns app.rpc.commands.search
(:require (:require
[app.common.spec :as us] [app.common.schema :as sm]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :refer [resolve-public-uri]] [app.rpc.commands.files :refer [resolve-public-uri]]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]))
[clojure.spec.alpha :as s]))
(def ^:private sql:search-files (def ^:private sql:search-files
"with projects as ( "with projects as (
@@ -65,16 +64,14 @@
(assoc :thumbnail-uri (resolve-public-uri media-id))) (assoc :thumbnail-uri (resolve-public-uri media-id)))
(dissoc row :media-id)))))) (dissoc row :media-id))))))
(s/def ::team-id ::us/uuid) (def ^:private schema:search-files
(s/def ::search-files ::us/string) [:map {:title "search-files"}
[:team-id ::sm/uuid]
(s/def ::search-files [:search-term {:optional true} :string]])
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]
:opt-un [::search-term]))
(sv/defmethod ::search-files (sv/defmethod ::search-files
{::doc/added "1.17" {::doc/added "1.17"
::doc/module :files} ::doc/module :files
::sm/params schema:search-files}
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id search-term]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id search-term]}]
(some->> search-term (search-files pool profile-id team-id))) (some->> search-term (search-files pool profile-id team-id)))
+394 -125
View File
@@ -15,6 +15,7 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql]
[app.email :as eml] [app.email :as eml]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.main :as-alias main] [app.main :as-alias main]
@@ -28,6 +29,7 @@
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.storage :as sto] [app.storage :as sto]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.blob :as blob]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
@@ -80,6 +82,35 @@
(cond-> row (cond-> row
(some? features) (assoc :features (db/decode-pgarray features #{})))) (some? features) (assoc :features (db/decode-pgarray features #{}))))
(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))]
(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-email-bounce
"Check if the email is part of the global complain report"
[conn email show?]
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email (if show? email "private")
:hint "this email has been repeatedly reported as bounce")))
(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)
(ex/raise :type :restriction
:code :email-has-complaints
:email (if show? email "private")
:hint "this email has been repeatedly reported as spam")))
;; --- Query: Teams ;; --- Query: Teams
(declare get-teams) (declare get-teams)
@@ -194,16 +225,16 @@
;; --- Query: Team Members ;; --- Query: Team Members
(def sql:team-members (def sql:team-members
"select tp.*, "SELECT tp.*,
p.id, p.id,
p.email, p.email,
p.fullname as name, p.fullname AS name,
p.fullname as fullname, p.fullname AS fullname,
p.photo_id, p.photo_id,
p.is_active p.is_active
from team_profile_rel as tp FROM team_profile_rel AS tp
join profile as p on (p.id = tp.profile_id) JOIN profile AS p ON (p.id = tp.profile_id)
where tp.team_id = ?") WHERE tp.team_id = ?")
(defn get-team-members (defn get-team-members
[conn team-id] [conn team-id]
@@ -333,6 +364,24 @@
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(get-team-invitations conn team-id))) (get-team-invitations conn team-id)))
;; --- COMMAND QUERY: get-team-info
(defn- get-team-info
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
(db/get* conn :team
{:id id}
{::sql/columns [:id :is-default]}))
(sv/defmethod ::get-team-info
"Retrieve minimal team info by its ID."
{::rpc/auth false
::doc/added "2.2.0"
::sm/params schema:get-team}
[cfg params]
(db/tx-run! cfg get-team-info params))
;; --- Mutation: Create Team ;; --- Mutation: Create Team
(declare create-team) (declare create-team)
@@ -352,17 +401,19 @@
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:create-team} ::sm/params schema:create-team}
[cfg {:keys [::rpc/profile-id] :as params}] [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) (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
(cfeat/check-client-features! (:features params))) ::quotes/profile-id profile-id})
team (create-team cfg (assoc params
:profile-id profile-id (let [features (-> (cfeat/get-enabled-features cf/flags)
:features features))] (cfeat/check-client-features! (:features params)))
(with-meta team params (-> params
{::audit/props {:id (:id team)}}))))) (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 (defn create-team
"This is a complete team creation process, it creates the team "This is a complete team creation process, it creates the team
@@ -674,8 +725,7 @@
[cfg {:keys [::rpc/profile-id file] :as params}] [cfg {:keys [::rpc/profile-id file] :as params}]
;; Validate incoming mime type ;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (update-team-photo cfg (assoc params :profile-id profile-id)))
(update-team-photo cfg (assoc params :profile-id profile-id))))
(defn update-team-photo (defn update-team-photo
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}] [{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}]
@@ -717,36 +767,51 @@
:member-id member-id})) :member-id member-id}))
(defn- create-profile-identity-token (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) (tokens/generate (::setup/props cfg)
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile) :profile-id profile-id
:exp (dt/in-future {:days 30})})) :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 (defn- create-invitation
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}] [{: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) (let [email (profile/clean-email email)
member (profile/get-profile-by-email conn email)] member (profile/get-profile-by-email conn email)]
(when (and member (not (eml/allow-send-emails? conn member))) (check-profile-muted conn member)
(ex/raise :type :validation (check-email-bounce conn email true)
:code :member-is-muted (check-email-spam conn email true)
:email email
:hint "the profile has reported repeatedly as spam or has bounces"))
;; Secondly check if the invited member email is part of the global bounce report.
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email email
:hint "the email you invite has been repeatedly reported as bounce"))
;; Secondly check if the invited member email is part of the global complain report.
(when (eml/has-complaint-reports? conn email)
(ex/raise :type :restriction
:code :email-has-complaints
:email email
:hint "the email you invite has been repeatedly reported as spam"))
;; When we have email verification disabled and invitation user is ;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the ;; already present in the database, we proceed to add it to the
@@ -780,7 +845,8 @@
(name role) expire (name role) expire
(name role) expire]) (name role) expire])
updated? (not= id (:id invitation)) updated? (not= id (:id invitation))
tprops {:profile-id (:id profile) profile-id (:id profile)
tprops {:profile-id profile-id
:invitation-id (:id invitation) :invitation-id (:id invitation)
:valid-until expire :valid-until expire
:team-id (:id team) :team-id (:id team)
@@ -788,12 +854,11 @@
:member-id (:id member) :member-id (:id member)
:role role} :role role}
itoken (create-invitation-token cfg tprops) 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) (when (contains? cf/flags :log-invitation-tokens)
(l/info :hint "invitation token" :token itoken)) (l/info :hint "invitation token" :token itoken))
(let [props (-> (dissoc tprops :profile-id) (let [props (-> (dissoc tprops :profile-id)
(audit/clean-props)) (audit/clean-props))
evname (if updated? evname (if updated?
@@ -815,122 +880,199 @@
itoken)))) itoken))))
(defn- add-user-to-team
[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))]
;; Do not allow blocked users to join teams.
(when (:is-blocked member)
(ex/raise :type :restriction
:code :profile-blocked))
(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})
;; Delete any request
(db/delete! conn :team-access-request
{:team-id team-id :requester-id (:id member)})
;; Delete any invitation
(db/delete! conn :team-invitation
{:team-id team-id :email-to (:email member)})
(eml/send! {::eml/conn conn
::eml/factory eml/join-team
:public-uri (cf/get :public-uri)
:to email
:invited-by (:fullname profile)
:team (:name team)
:team-id (:id team)})))
(def sql:valid-requests-email
"SELECT p.email
FROM team_access_request AS tr
JOIN profile AS p ON (tr.requester_id = p.id)
WHERE tr.team_id = ?
AND tr.auto_join_until > now()")
(defn- get-valid-requests-email
[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 (def ^:private schema:create-team-invitations
[:map {:title "create-team-invitations"} [:map {:title "create-team-invitations"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:role schema:role] [:role schema:role]
[:emails ::sm/set-of-emails]]) [: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 (sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to "A rpc call that allow to send a single or multiple invitations to
join the team." join the team."
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:create-team-invitations} ::sm/params schema:create-team-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}] [cfg {:keys [::rpc/profile-id team-id emails] :as params}]
(db/with-atomic [conn pool] (let [perms (get-permissions cfg profile-id team-id)
(let [perms (get-permissions conn profile-id team-id) profile (db/get-by-id cfg :profile profile-id)
profile (db/get-by-id conn :profile profile-id) emails (into #{} (map profile/clean-email) emails)]
team (db/get-by-id conn :team team-id)
emails (into #{} (map profile/clean-email) emails)]
(run! (partial quotes/check-quote! conn) (when-not (:is-admin perms)
(list {::quotes/id ::quotes/invitations-per-team (ex/raise :type :validation
::quotes/profile-id profile-id :code :insufficient-permissions))
::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) (when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation (ex/raise :type :validation
:code :insufficient-permissions)) :code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
;; First check if the current profile is allowed to send emails. (-> cfg
(when-not (eml/allow-send-emails? conn profile) (assoc ::quotes/profile-id profile-id)
(ex/raise :type :validation (assoc ::quotes/team-id team-id)
:code :profile-is-muted (assoc ::quotes/incr (count emails))
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) (quotes/check! {::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
(let [cfg (assoc cfg ::db/conn conn) ;; Check if the current profile is allowed to send emails
members (->> (db/exec! conn [sql:team-members team-id]) (check-profile-muted cfg profile)
(into #{} (map :email)))
invitations (into #{} (let [team (db/get-by-id cfg :team team-id)
(comp ;; NOTE: Is important pass RPC method params down to the
;; We don't re-send inviation to already existing members ;; `create-team-invitations` because it uses the implicit
(remove (partial contains? members)) ;; RPC properties from params for fill necessary data on
(map (fn [email] ;; emiting an entry to the audit-log
(-> params invitations (db/tx-run! cfg create-team-invitations
(assoc :email email) (-> params
(assoc :team team) (assoc :profile profile)
(assoc :profile profile) (assoc :team team)
(assoc :role role)))) (assoc :emails emails)))]
(keep (partial create-invitation cfg)))
emails)] (with-meta {:total (count invitations)
(with-meta {:total (count invitations) :invitations invitations}
:invitations invitations} {::audit/props {:invitations (count invitations)}}))))
{::audit/props {:invitations (count invitations)}})))))
;; --- Mutation: Create Team & Invite Members ;; --- Mutation: Create Team & Invite Members
(def ^:private schema:create-team-with-invitations (def ^:private schema:create-team-with-invitations
[:map {:title "create-team-with-invitations"} [:map {:title "create-team-with-invitations"}
[:name :string] [:name [:string {:max 250}]]
[:features {:optional true} ::cfeat/features] [:features {:optional true} ::cfeat/features]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
[:emails ::sm/set-of-emails] [:emails [::sm/set ::sm/email]]
[:role schema:role]]) [:role schema:role]])
(sv/defmethod ::create-team-with-invitations (sv/defmethod ::create-team-with-invitations
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:create-team-with-invitations} ::sm/params schema:create-team-with-invitations
[cfg {:keys [::rpc/profile-id emails role name] :as params}] ::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 params (-> params
(fn [{:keys [::db/conn] :as cfg}] (assoc :profile-id profile-id)
(let [features (-> (cfeat/get-enabled-features cf/flags) (assoc :features features))
(cfeat/check-client-features! (:features params)))
params (-> params team (create-team cfg params)
(assoc :profile-id profile-id) emails (into #{} (map profile/clean-email) emails)]
(assoc :features features))
cfg (assoc cfg ::db/conn conn) (-> cfg
team (create-team cfg params) (assoc ::quotes/profile-id profile-id)
profile (db/get-by-id conn :profile profile-id) (assoc ::quotes/team-id (:id team))
emails (into #{} (map profile/clean-email) emails)] (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} (when (> (count emails) max-invitations-by-request-threshold)
event (-> (audit/event-from-rpc-params params) (ex/raise :type :validation
(assoc ::audit/name "create-team") :code :max-invitations-by-request
(assoc ::audit/props props))] :hint "the maximum of invitation on single request is reached"
(audit/submit! cfg event)) :threshold max-invitations-by-request-threshold))
;; Create invitations for all provided emails. (let [props {:name name :features features}
(->> emails event (-> (audit/event-from-rpc-params params)
(map (fn [email] (assoc ::audit/name "create-team")
(-> params (assoc ::audit/props props))]
(assoc :team team) (audit/submit! cfg event))
(assoc :profile profile)
(assoc :email email)
(assoc :role role))))
(run! (partial create-invitation cfg)))
(run! (partial quotes/check-quote! conn) ;; Create invitations for all provided emails.
(list {::quotes/id ::quotes/teams-per-profile (let [profile (db/get-by-id conn :profile profile-id)
::quotes/profile-id profile-id} params (-> params
{::quotes/id ::quotes/invitations-per-team (assoc :team team)
::quotes/profile-id profile-id (assoc :profile profile)
::quotes/team-id (:id team) (assoc :role role))
::quotes/incr (count emails)} invitations (->> emails
{::quotes/id ::quotes/profiles-per-team (map (fn [email] (assoc params :email email)))
::quotes/profile-id profile-id (map (partial create-invitation cfg)))]
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(vary-meta team assoc ::audit/props {:invitations (count emails)}))))) (vary-meta team assoc ::audit/props {:invitations (count invitations)}))))
;; --- Query: get-team-invitation-token ;; --- Query: get-team-invitation-token
@@ -1007,3 +1149,130 @@
:email-to (profile/clean-email email)} :email-to (profile/clean-email email)}
{::db/return-keys true})] {::db/return-keys true})]
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}}))))) (rph/wrap nil {::audit/props {:invitation-id (:id invitation)}})))))
;; --- Mutation: Request Team Invitation
(def sql:upsert-team-access-request
"INSERT INTO team_access_request (id, team_id, requester_id, valid_until, auto_join_until)
VALUES (?, ?, ?, ?, ?)
ON conflict(id)
DO UPDATE SET valid_until = ?, auto_join_until = ?, updated_at = now()
RETURNING *")
(def sql:team-access-request
"SELECT id, (valid_until < now()) AS expired
FROM team_access_request
WHERE team_id = ?
AND requester_id = ?")
(def sql:team-owner
"SELECT profile_id
FROM team_profile_rel
WHERE team_id = ?
AND is_owner = true")
(defn- create-team-access-request
[{:keys [::db/conn] :as cfg} {:keys [team requester team-owner file is-viewer] :as params}]
(let [old-request (->> (db/exec-one! conn [sql:team-access-request (:id team) (:id requester)])
(decode-row))]
(when (false? (:expired old-request))
(ex/raise :type :validation
:code :request-already-sent
:hint "you have already made a request to join this team less than 24 hours ago"))
(let [id (or (:id old-request) (uuid/next))
valid_until (dt/in-future "24h")
auto_join_until (dt/in-future "168h") ;; 7 days
request (db/exec-one! conn [sql:upsert-team-access-request
id (:id team) (:id requester) valid_until auto_join_until
valid_until auto_join_until])
factory (cond
(and (some? file) (:is-default team) is-viewer)
eml/request-file-access-yourpenpot-view
(and (some? file) (:is-default team))
eml/request-file-access-yourpenpot
(some? file)
eml/request-file-access
:else
eml/request-team-access)
page-id (when (some? file)
(-> file :data :pages first))]
;; TODO needs audit?
(eml/send! {::eml/conn conn
::eml/factory factory
:public-uri (cf/get :public-uri)
:to (:email team-owner)
:requested-by (:fullname requester)
:requested-by-email (:email requester)
:team-name (:name team)
:team-id (:id team)
:file-name (:name file)
:file-id (:id file)
:page-id page-id})
request)))
(def ^:private schema:create-team-access-request
[:and
[:map {:title "create-team-access-request"}
[:file-id {:optional true} ::sm/uuid]
[:team-id {:optional true} ::sm/uuid]
[:is-viewer {:optional true} ::sm/boolean]]
[:fn (fn [params]
(or (contains? params :file-id)
(contains? params :team-id)))]])
(sv/defmethod ::create-team-access-request
"A rpc call that allow to request for an invitations to join the team."
{::doc/added "2.2.0"
::sm/params schema:create-team-access-request}
[cfg {:keys [::rpc/profile-id file-id team-id is-viewer] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [requester (db/get-by-id conn :profile profile-id)
team-id (if (some? team-id)
team-id
(:id (get-team-for-file conn file-id)))
team (db/get-by-id conn :team team-id)
owner-id (->> (db/exec! conn [sql:team-owner (:id team)])
(map decode-row)
(first)
:profile-id)
team-owner (db/get-by-id conn :profile owner-id)
file (when (some? file-id)
(db/get* conn :file
{:id file-id}
{::sql/columns [:id :name :data]}))
file (when (some? file)
(assoc file :data (blob/decode (:data file))))]
;;TODO needs quotes?
(when (or (nil? requester) (nil? team) (nil? team-owner) (and (some? file-id) (nil? file)))
(ex/raise :type :validation
:code :invalid-parameters))
;; Check that the requester is not muted
(check-profile-muted conn requester)
;; Check that the owner is not marked as bounce nor spam
(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})]
(when request
(with-meta {:request request}
{::audit/props {:request 1}})))))))
+39 -46
View File
@@ -7,7 +7,7 @@
(ns app.rpc.commands.verify-token (ns app.rpc.commands.verify-token
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.schema :as sm]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
@@ -24,29 +24,25 @@
[app.tokens :as tokens] [app.tokens :as tokens]
[app.tokens.spec.team-invitation :as-alias spec.team-invitation] [app.tokens.spec.team-invitation :as-alias spec.team-invitation]
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s])) [app.util.time :as dt]))
(s/def ::iss keyword?)
(s/def ::exp ::us/inst)
(defmulti process-token (fn [_ _ claims] (:iss claims))) (defmulti process-token (fn [_ _ claims] (:iss claims)))
(s/def ::verify-token (def ^:private schema:verify-token
(s/keys :req-un [::token] [:map {:title "verify-token"}
:opt [::rpc/profile-id])) [:token [:string {:max 5000}]]])
(sv/defmethod ::verify-token (sv/defmethod ::verify-token
{::rpc/auth false {::rpc/auth false
::doc/added "1.15" ::doc/added "1.15"
::doc/module :auth} ::doc/module :auth
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}] ::sm/params schema:verify-token}
(db/with-atomic [conn pool] [cfg {:keys [token] :as params}]
(let [claims (tokens/verify (::setup/props cfg) {:token token}) (let [claims (tokens/verify (::setup/props cfg) {:token token})]
cfg (assoc cfg :conn conn)] (db/tx-run! cfg process-token params claims)))
(process-token cfg params claims))))
(defmethod process-token :change-email (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)] (let [email (profile/clean-email email)]
(when (profile/get-profile-by-email conn email) (when (profile/get-profile-by-email conn email)
(ex/raise :type :validation (ex/raise :type :validation
@@ -62,7 +58,7 @@
::audit/profile-id profile-id}))) ::audit/profile-id profile-id})))
(defmethod process-token :verify-email (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) (let [profile (profile/get-profile conn profile-id)
claims (assoc claims :profile profile)] claims (assoc claims :profile profile)]
@@ -83,22 +79,14 @@
::audit/profile-id (:id profile)})))) ::audit/profile-id (:id profile)}))))
(defmethod process-token :auth (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 {::sql/for-update true}) (let [profile (profile/get-profile conn profile-id)]
props (merge (:props profile) (assoc claims :profile profile)))
(:props claims))]
(when (not= props (:props profile))
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id}))
(let [profile (assoc profile :props props)]
(assoc claims :profile profile))))
;; --- Team Invitation ;; --- Team Invitation
(defn- accept-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 (let [;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role) role (or (some-> invitation :role keyword) role)
params (merge params (merge
@@ -111,10 +99,9 @@
(ex/raise :type :restriction (ex/raise :type :restriction
:code :profile-blocked)) :code :profile-blocked))
(quotes/check-quote! conn (quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
{::quotes/id ::quotes/profiles-per-team ::quotes/profile-id (:id member)
::quotes/profile-id (:id member) ::quotes/team-id team-id})
::quotes/team-id team-id})
;; Insert the invited member to the team ;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
@@ -130,28 +117,34 @@
(db/delete! conn :team-invitation (db/delete! conn :team-invitation
{:team-id team-id :email-to member-email}) {:team-id team-id :email-to member-email})
;; Delete any request
(db/delete! conn :team-access-request
{:team-id team-id :requester-id (:id member)})
(assoc member :is-active true))) (assoc member :is-active true)))
(s/def ::spec.team-invitation/profile-id ::us/uuid) (def schema:team-invitation-claims
(s/def ::spec.team-invitation/role ::us/keyword) [:map {:title "TeamInvitationClaims"}
(s/def ::spec.team-invitation/team-id ::us/uuid) [:iss :keyword]
(s/def ::spec.team-invitation/member-email ::us/email) [:exp ::dt/instant]
(s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid)) [:profile-id ::sm/uuid]
[:role teams/schema:role]
[:team-id ::sm/uuid]
[:member-email ::sm/email]
[:member-id {:optional true} ::sm/uuid]])
(s/def ::team-invitation-claims (def valid-team-invitation-claims?
(s/keys :req-un [::iss ::exp (sm/lazy-validator schema:team-invitation-claims))
::spec.team-invitation/profile-id
::spec.team-invitation/role
::spec.team-invitation/team-id
::spec.team-invitation/member-email]
:opt-un [::spec.team-invitation/member-id]))
(defmethod process-token :team-invitation (defmethod process-token :team-invitation
[{:keys [conn] :as cfg} [{:keys [::db/conn] :as cfg}
{:keys [::rpc/profile-id token] :as params} {:keys [::rpc/profile-id token] :as params}
{:keys [member-id team-id member-email] :as claims}] {:keys [member-id team-id member-email] :as claims}]
(us/verify! ::team-invitation-claims claims) (when-not (valid-team-invitation-claims? claims)
(ex/raise :type :validation
:code :invalid-invitation-token
:hint "invitation token contains unexpected data"))
(let [invitation (db/get* conn :team-invitation (let [invitation (db/get* conn :team-invitation
{:team-id team-id :email-to member-email}) {:team-id team-id :email-to member-email})
+1 -1
View File
@@ -98,7 +98,7 @@
(assoc ::perms perms) (assoc ::perms perms)
(assoc :profile-id profile-id))] (assoc :profile-id profile-id))]
;; When we have neither profile nor share, we just return a not ;; When we have neither profile nor share, we just return a not
;; found response to the user. ;; found response to the user.
(when-not perms (when-not perms
(ex/raise :type :not-found (ex/raise :type :not-found
+33 -27
View File
@@ -8,7 +8,7 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.schema :as sm]
[app.common.uri :as u] [app.common.uri :as u]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
@@ -19,7 +19,6 @@
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(defn decode-row (defn decode-row
@@ -29,18 +28,6 @@
;; --- Mutation: Create Webhook ;; --- Mutation: Create Webhook
(s/def ::team-id ::us/uuid)
(s/def ::uri ::us/uri)
(s/def ::is-active ::us/boolean)
(s/def ::mtype
#{"application/json"
"application/transit+json"})
(s/def ::create-webhook
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::uri ::mtype]
:opt-un [::is-active]))
;; NOTE: for now the quote is hardcoded but this need to be solved in ;; NOTE: for now the quote is hardcoded but this need to be solved in
;; a more universal way for handling properly object quotes ;; a more universal way for handling properly object quotes
(def max-hooks-for-team 8) (def max-hooks-for-team 8)
@@ -99,31 +86,49 @@
{::db/return-keys true}) {::db/return-keys true})
(decode-row))) (decode-row)))
(def valid-mtypes
#{"application/json"
"application/transit+json"})
(def ^:private schema:create-webhook
[:map {:title "create-webhook"}
[:team-id ::sm/uuid]
[:uri ::sm/uri]
[:mtype [::sm/one-of {:format "string"} valid-mtypes]]])
(sv/defmethod ::create-webhook (sv/defmethod ::create-webhook
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:create-webhook}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(check-edition-permissions! pool profile-id team-id) (check-edition-permissions! pool profile-id team-id)
(validate-quotes! cfg params) (validate-quotes! cfg params)
(validate-webhook! cfg nil params) (validate-webhook! cfg nil params)
(insert-webhook! cfg params)) (insert-webhook! cfg params))
(s/def ::update-webhook (def ^:private schema:update-webhook
(s/keys :req-un [::id ::uri ::mtype ::is-active])) [:map {:title "update-webhook"}
[:id ::sm/uuid]
[:uri ::sm/uri]
[:mtype [::sm/one-of {:format "string"} valid-mtypes]]
[:is-active ::sm/boolean]])
(sv/defmethod ::update-webhook (sv/defmethod ::update-webhook
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:update-webhook}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))] (let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
(check-edition-permissions! pool profile-id (:team-id whook)) (check-edition-permissions! pool profile-id (:team-id whook))
(validate-webhook! cfg whook params) (validate-webhook! cfg whook params)
(update-webhook! cfg whook params))) (update-webhook! cfg whook params)))
(s/def ::delete-webhook (def ^:private schema:delete-webhook
(s/keys :req [::rpc/profile-id] [:map {:title "delete-webhook"}
:req-un [::id])) [:id ::sm/uuid]])
(sv/defmethod ::delete-webhook (sv/defmethod ::delete-webhook
{::doc/added "1.17"} {::doc/added "1.17"
::sm/params schema:delete-webhook}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [whook (-> (db/get conn :webhook {:id id}) decode-row)] (let [whook (-> (db/get conn :webhook {:id id}) decode-row)]
@@ -133,16 +138,17 @@
;; --- Query: Webhooks ;; --- Query: Webhooks
(s/def ::team-id ::us/uuid)
(s/def ::get-webhooks
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(def sql:get-webhooks (def sql:get-webhooks
"select id, uri, mtype, is_active, error_code, error_count "select id, uri, mtype, is_active, error_code, error_count
from webhook where team_id = ? order by uri") from webhook where team_id = ? order by uri")
(def ^:private schema:get-webhooks
[:map {:title "get-webhooks"}
[:team-id ::sm/uuid]])
(sv/defmethod ::get-webhooks (sv/defmethod ::get-webhooks
{::doc/added "1.17"
::sm/params schema:get-webhooks}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
+61 -60
View File
@@ -26,7 +26,6 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[malli.transform :as mt]
[pretty-spec.core :as ps] [pretty-spec.core :as ps]
[ring.response :as-alias rres])) [ring.response :as-alias rres]))
@@ -98,77 +97,79 @@
;; OPENAPI / SWAGGER (v3.1) ;; OPENAPI / SWAGGER (v3.1)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def output-transformer
(mt/transformer
sm/default-transformer
(mt/key-transformer {:encode str/camel
:decode (comp keyword str/kebab)})))
(defn prepare-openapi-context (defn prepare-openapi-context
[methods] [methods]
(letfn [(gen-response-doc [tsx schema] (let [definitions (atom {})
(let [schema (sm/schema schema) options {:registry sr/default-registry
example (sm/generate schema) ::oapi/definitions-path "#/components/schemas/"
example (sm/encode schema example output-transformer)] ::oapi/definitions definitions}
{:default
{:description "A default response"
:content
{"application/json"
{:schema tsx
:example example}}}}))
(gen-params-doc [tsx schema] output-transformer
(let [example (sm/generate schema) (sm/json-transformer)
example (sm/encode schema example output-transformer)]
{:required true
:content
{"application/json"
{:schema tsx
:example example}}}))
(gen-method-doc [options mdata] gen-response-doc
(let [pschema (::sm/params mdata) (fn [tsx schema]
rschema (::sm/result mdata) (let [schema (sm/schema schema)
example (sm/generate schema)
example (sm/encode schema example output-transformer)]
{:default
{:description "A default response"
:content
{"application/json"
{:schema tsx
:example example}}}}))
sparams (-> pschema (oapi/transform options) (gen-params-doc pschema)) gen-params-doc
sresp (some-> rschema (oapi/transform options) (gen-response-doc rschema)) (fn [tsx schema]
(let [example (sm/generate schema)
example (sm/encode schema example output-transformer)]
{:required true
:content
{"application/json"
{:schema tsx
:example example}}}))
rpost {:description (::sv/docstring mdata) gen-method-doc
:deprecated (::deprecated mdata false) (fn [mdata]
:requestBody sparams} (let [pschema (::sm/params mdata)
rschema (::sm/result mdata)
rpost (cond-> rpost sparams (-> pschema (oapi/transform options) (gen-params-doc pschema))
(some? sresp) sresp (some-> rschema (oapi/transform options) (gen-response-doc rschema))
(assoc :responses sresp))]
{:name (-> mdata ::sv/name d/name) rpost {:description (::sv/docstring mdata)
:module (-> (:ns mdata) (str/split ".") last) :deprecated (::deprecated mdata false)
:repr {:post rpost}}))] :requestBody sparams}
(let [definitions (atom {}) rpost (cond-> rpost
options {:registry sr/default-registry (some? sresp)
::oapi/definitions-path "#/components/schemas/" (assoc :responses sresp))]
::oapi/definitions definitions}
paths (binding [oapi/*definitions* definitions] {:name (-> mdata ::sv/name d/name)
(->> methods :module (-> (:ns mdata) (str/split ".") last)
(map (comp first val)) :repr {:post rpost}}))
(filter ::sm/params)
(map (partial gen-method-doc options)) paths
(sort-by (juxt :module :name)) (binding [oapi/*definitions* definitions]
(map (fn [doc] (->> methods
[(str/ffmt "/command/%" (:name doc)) (:repr doc)])) (map (comp first val))
(into {})))] (filter ::sm/params)
{:openapi "3.0.0" (map gen-method-doc)
:info {:version (:main cf/version)} (sort-by (juxt :module :name))
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri)) (map (fn [doc]
[(str/ffmt "/command/%" (:name doc)) (:repr doc)]))
(into {})))]
{:openapi "3.0.0"
:info {:version (:main cf/version)}
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
;; :description "penpot backend" ;; :description "penpot backend"
}] }]
:security :security
{:api_key []} {:api_key []}
:paths paths :paths paths
:components {:schemas @definitions}}))) :components {:schemas @definitions}}))
(defn openapi-json-handler (defn openapi-json-handler
[context] [context]
+6 -6
View File
@@ -12,14 +12,14 @@
[app.common.spec :as us] [app.common.spec :as us]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
(sm/def! ::permissions (sm/register! ::permissions
[:map {:title "Permissions"} [:map {:title "Permissions"}
[:type {:gen/elements [:membership :share-link]} :keyword] [:type {:gen/elements [:membership :share-link]} :keyword]
[:is-owner :boolean] [:is-owner ::sm/boolean]
[:is-admin :boolean] [:is-admin ::sm/boolean]
[:can-edit :boolean] [:can-edit ::sm/boolean]
[:can-read :boolean] [:can-read ::sm/boolean]
[:is-logged :boolean]]) [:is-logged ::sm/boolean]])
(s/def ::role #{:admin :owner :editor :viewer}) (s/def ::role #{:admin :owner :editor :viewer})
+181 -123
View File
@@ -7,16 +7,13 @@
(ns app.rpc.quotes (ns app.rpc.quotes
"Penpot resource usage quotes." "Penpot resource usage quotes."
(:require (:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(defmulti check-quote ::id) (defmulti check-quote ::id)
@@ -26,14 +23,16 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:quote (def ^:private schema:quote
(sm/define [:map {:title "Quote"}
[:map {:title "Quote"} [::team-id {:optional true} ::sm/uuid]
[::team-id {:optional true} ::sm/uuid] [::project-id {:optional true} ::sm/uuid]
[::project-id {:optional true} ::sm/uuid] [::file-id {:optional true} ::sm/uuid]
[::file-id {:optional true} ::sm/uuid] [::incr {:optional true} [::sm/int {:min 0}]]
[::incr {:optional true} [:int {:min 0}]] [::id :keyword]
[::id :keyword] [::profile-id ::sm/uuid]])
[::profile-id ::sm/uuid]]))
(def valid-quote?
(sm/lazy-validator schema:quote))
(def ^:private enabled (volatile! true)) (def ^:private enabled (volatile! true))
@@ -47,20 +46,31 @@
[] []
(vswap! enabled (constantly false))) (vswap! enabled (constantly false)))
(defn check-quote! (defn- check
[ds quote] [cfg quote]
(dm/assert! (let [quote (merge cfg quote)
"expected valid quote map" id (::id quote)]
(sm/validate schema:quote quote))
(when (contains? cf/flags :quotes) (when-not (valid-quote? quote)
(when @enabled (ex/raise :type :internal
;; This approach add flexibility on how and where the :code :invalid-quote-definition
;; check-quote! can be called (in or out of transaction) :hint "found invalid data for quote schema"
(db/run! ds (fn [cfg] :quote (name id)))
(-> (merge cfg quote)
(assoc ::target (name (::id quote))) (-> (assoc quote ::target (name id))
(check-quote))))))) (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! (defn- send-notification!
[{:keys [::db/conn] :as params}] [{:keys [::db/conn] :as params}]
@@ -101,7 +111,7 @@
(map :quote) (map :quote)
(reduce max (- Integer/MAX_VALUE))) (reduce max (- Integer/MAX_VALUE)))
quote (if (pos? quote) quote default) 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) (when (> (+ total incr) quote)
(if (contains? cf/flags :soft-quotes) (if (contains? cf/flags :soft-quotes)
@@ -113,72 +123,81 @@
:count total))))) :count total)))))
(def ^:private sql:get-quotes-1 (def ^:private sql:get-quotes-1
"select id, quote from usage_quote "SELECT id, quote
where target = ? FROM usage_quote
and profile_id = ? WHERE target = ?
and team_id is null AND profile_id = ?
and project_id is null AND team_id IS NULL
and file_id is null;") AND project_id IS NULL
AND file_id IS NULL;")
(def ^:private sql:get-quotes-2 (def ^:private sql:get-quotes-2
"select id, quote from usage_quote "SELECT id, quote
where target = ? FROM usage_quote
and ((team_id = ? and (profile_id = ? or profile_id is null)) or WHERE target = ?
(profile_id = ? and team_id is null and project_id is null and file_id is null));") 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 (def ^:private sql:get-quotes-3
"select id, quote from usage_quote "SELECT id, quote
where target = ? FROM usage_quote
and ((project_id = ? and (profile_id = ? or profile_id is null)) or WHERE target = ?
(team_id = ? and (profile_id = ? or profile_id is null)) or AND ((project_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));") (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 (def ^:private sql:get-quotes-4
"select id, quote from usage_quote "SELECT id, quote
where target = ? FROM usage_quote
and ((file_id = ? and (profile_id = ? or profile_id is null)) or WHERE target = ?
(project_id = ? and (profile_id = ? or profile_id is null)) or AND ((file_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(team_id = ? and (profile_id = ? or profile_id is null)) or (project_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));") (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 ;; QUOTE: TEAMS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-teams-per-profile (def ^:private schema:teams-per-profile
"select count(*) as total [:map [::profile-id ::sm/uuid]])
from team_profile_rel
where profile_id = ?")
(s/def ::profile-id ::us/uuid) (def ^:private valid-teams-per-profile-quote?
(s/def ::teams-per-profile (sm/lazy-validator schema:teams-per-profile))
(s/keys :req [::profile-id ::target]))
(def ^:private sql:get-teams-per-profile
"SELECT count(*) AS total
FROM team_profile_rel
WHERE profile_id = ?")
(defmethod check-quote ::teams-per-profile (defmethod check-quote ::teams-per-profile
[{:keys [::profile-id ::target] :as quote}] [{:keys [::profile-id ::target] :as quote}]
(us/assert! ::teams-per-profile quote) (assert (valid-teams-per-profile-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id]) (assoc ::quote-sql [sql:get-quotes-1 target profile-id])
(assoc ::count-sql [sql:get-teams-per-profile profile-id]) (assoc ::count-sql [sql:get-teams-per-profile profile-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: ACCESS-TOKENS-PER-PROFILE ;; QUOTE: ACCESS-TOKENS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-access-tokens-per-profile (def ^:private schema:access-tokens-per-profile
"select count(*) as total [:map [::profile-id ::sm/uuid]])
from access_token
where profile_id = ?")
(s/def ::access-tokens-per-profile (def ^:private valid-access-tokens-per-profile-quote?
(s/keys :req [::profile-id ::target])) (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 (defmethod check-quote ::access-tokens-per-profile
[{:keys [::profile-id ::target] :as quote}] [{: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 (-> quote
(assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id]) (assoc ::quote-sql [sql:get-quotes-1 target profile-id])
@@ -189,40 +208,51 @@
;; QUOTE: PROJECTS-PER-TEAM ;; QUOTE: PROJECTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-projects-per-team (def ^:private schema:projects-per-team
"select count(*) as total [:map
from project as p [::profile-id ::sm/uuid]
where p.team_id = ? [::team-id ::sm/uuid]])
and p.deleted_at is null")
(s/def ::team-id ::us/uuid) (def ^:private valid-projects-per-team-quote?
(s/def ::projects-per-team (sm/lazy-validator schema:projects-per-team))
(s/keys :req [::profile-id ::team-id ::target]))
(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 (defmethod check-quote ::projects-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}] [{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-projects-per-team-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE)) (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 ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-projects-per-team team-id]) (assoc ::count-sql [sql:get-projects-per-team team-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FONT-VARIANTS-PER-TEAM ;; QUOTE: FONT-VARIANTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-font-variants-per-team (def ^:private schema:font-variants-per-team
"select count(*) as total [:map
from team_font_variant as v [::profile-id ::sm/uuid]
where v.team_id = ?") [::team-id ::sm/uuid]])
(s/def ::font-variants-per-team (def ^:private valid-font-variant-per-team-quote?
(s/keys :req [::profile-id ::team-id ::target])) (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 (defmethod check-quote ::font-variants-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}] [{: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 (-> quote
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id]) (assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
@@ -234,70 +264,86 @@
;; QUOTE: INVITATIONS-PER-TEAM ;; QUOTE: INVITATIONS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-invitations-per-team (def ^:private schema:invitations-per-team
"select count(*) as total [:map
from team_invitation [::profile-id ::sm/uuid]
where team_id = ?") [::team-id ::sm/uuid]])
(s/def ::invitations-per-team (def ^:private valid-invitations-per-team-quote?
(s/keys :req [::profile-id ::team-id ::target])) (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 (defmethod check-quote ::invitations-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}] [{: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 (-> quote
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE)) (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 ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-invitations-per-team team-id]) (assoc ::count-sql [sql:get-invitations-per-team team-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: PROFILES-PER-TEAM ;; 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 (def ^:private sql:get-profiles-per-team
"select (select count(*) "SELECT (SELECT count(*)
from team_profile_rel FROM team_profile_rel
where team_id = ?) + WHERE team_id = ?) +
(select count(*) (SELECT count(*)
from team_invitation FROM team_invitation
where team_id = ? WHERE team_id = ?
and valid_until > now()) as total;") AND valid_until > now()) AS total;")
;; NOTE: the total number of profiles is determined by the number of ;; NOTE: the total number of profiles is determined by the number of
;; effective members plus ongoing valid invitations. ;; 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 (defmethod check-quote ::profiles-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}] [{: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 (-> quote
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE)) (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 ::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]) (assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FILES-PER-PROJECT ;; QUOTE: FILES-PER-PROJECT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-files-per-project (def ^:private schema:files-per-project
"select count(*) as total [:map
from file as f [::profile-id ::sm/uuid]
where f.project_id = ? [::project-id ::sm/uuid]
and f.deleted_at is null") [::team-id ::sm/uuid]])
(s/def ::project-id ::us/uuid) (def ^:private valid-files-per-project-quote?
(s/def ::files-per-project (sm/lazy-validator schema:files-per-project))
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(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 (defmethod check-quote ::files-per-project
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}] [{: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 (-> quote
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id]) (assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
@@ -308,17 +354,24 @@
;; QUOTE: COMMENT-THREADS-PER-FILE ;; QUOTE: COMMENT-THREADS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comment-threads-per-file (def ^:private schema:comment-threads-per-file
"select count(*) as total [:map
from comment_thread as ct [::profile-id ::sm/uuid]
where ct.file_id = ?") [::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::comment-threads-per-file (def ^:private valid-comment-threads-per-file-quote?
(s/keys :req [::profile-id ::project-id ::team-id ::target])) (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 (defmethod check-quote ::comment-threads-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}] [{: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 (-> quote
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE)) (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 (assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
@@ -326,23 +379,28 @@
(assoc ::count-sql [sql:get-comment-threads-per-file file-id]) (assoc ::count-sql [sql:get-comment-threads-per-file file-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: COMMENTS-PER-FILE ;; QUOTE: COMMENTS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comments-per-file (def ^:private schema:comments-per-file
"select count(*) as total [:map
from comment as c [::profile-id ::sm/uuid]
join comment_thread as ct on (ct.id = c.thread_id) [::project-id ::sm/uuid]
where ct.file_id = ?") [::team-id ::sm/uuid]])
(s/def ::comments-per-file (def ^:private valid-comments-per-file-quote?
(s/keys :req [::profile-id ::project-id ::team-id ::target])) (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 (defmethod check-quote ::comments-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}] [{: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 (-> quote
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE)) (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 (assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
+18 -19
View File
@@ -10,6 +10,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.http.client :as http] [app.http.client :as http]
@@ -19,28 +20,26 @@
[datoteka.fs :as fs] [datoteka.fs :as fs]
[integrant.core :as ig])) [integrant.core :as ig]))
(def ^:private (def ^:private schema:template
schema:template [:map {:title "Template"}
(sm/define [:id ::sm/word-string]
[:map {:title "Template"} [:name ::sm/word-string]
[:id ::sm/word-string] [:file-uri ::sm/word-string]])
[:name ::sm/word-string]
[:file-uri ::sm/word-string]]))
(def ^:private (def ^:private schema:templates
schema:templates [:vector schema:template])
(sm/define
[:vector schema:template])) (def check-templates!
(sm/check-fn schema:templates
:code :invalid-templates
:hint "invalid templates"))
(defmethod ig/init-key ::setup/templates (defmethod ig/init-key ::setup/templates
[_ _] [_ _]
(let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string) (let [templates (-> "app/onboarding.edn" io/resource slurp edn/read-string)
templates (check-templates! templates)
dest (fs/join fs/*cwd* "builtin-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] (doseq [{:keys [id path] :as template} templates]
(let [path (or path (fs/join dest id))] (let [path (or path (fs/join dest id))]
(if (fs/exists? path) (if (fs/exists? path)
@@ -60,9 +59,9 @@
(let [resp (http/req! cfg (let [resp (http/req! cfg
{:method :get :uri (:file-uri template)} {:method :get :uri (:file-uri template)}
{:response-type :input-stream :sync? true})] {:response-type :input-stream :sync? true})]
(when-not (= 200 (:status resp))
(dm/verify! (ex/raise :type :internal
"unexpected response found on fetching template" :code :unexpected-status-code
(= 200 (:status resp))) :hint (str "unable to download template, recevied status " (:status resp))))
(io/input-stream (:body resp))))))) (io/input-stream (:body resp)))))))
+64
View File
@@ -0,0 +1,64 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.setup.welcome-file
(:require
[app.common.logging :as l]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.files-update :as fupdate]
[app.rpc.commands.management :as management]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.setup :as-alias setup]
[app.setup.templates :as tmpl]
[app.worker :as-alias wrk]))
(def ^:private page-id #uuid "2c6952ee-d00e-8160-8004-d2250b7210cb")
(def ^:private shape-id #uuid "765e9f82-c44e-802e-8004-d72a10b7b445")
(def ^:private update-path
[:data :pages-index page-id :objects shape-id
:content :children 0 :children 0 :children 0])
(def ^:private sql:mark-file-object-thumbnails-deleted
"UPDATE file_tagged_object_thumbnail
SET deleted_at = now()
WHERE file_id = ?")
(def ^:private sql:mark-file-thumbnail-deleted
"UPDATE file_thumbnail
SET deleted_at = now()
WHERE file_id = ?")
(defn- update-welcome-shape
[_ file name]
(let [text (str "Welcome to Penpot, " name "!")]
(-> file
(update-in update-path assoc :text text)
(update-in [:data :pages-index page-id :objects shape-id] assoc :name "Welcome to Penpot!")
(update-in [:data :pages-index page-id :objects shape-id] dissoc :position-data))))
(defn create-welcome-file
[cfg {:keys [id fullname] :as profile}]
(try
(let [cfg (dissoc cfg ::db/conn)
params {:profile-id (:id profile)
:project-id (:default-project-id profile)}
template-stream (tmpl/get-template-stream cfg "welcome")
file-id (-> (management/clone-template cfg params template-stream)
first)]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(fupdate/update-file! cfg file-id update-welcome-shape fullname)
(profile/update-profile-props cfg id {:welcome-file-id file-id})
(db/exec-one! conn [sql:mark-file-object-thumbnails-deleted file-id])
(db/exec-one! conn [sql:mark-file-thumbnail-deleted file-id]))))
(catch Throwable cause
(l/error :hint "unexpected error on create welcome file " :cause cause))))
+1
View File
@@ -75,6 +75,7 @@
:created-at (:created-at file) :created-at (:created-at file)
:modified-at (:modified-at file) :modified-at (:modified-at file)
:data-backend nil :data-backend nil
:data-ref-id nil
:has-media-trimmed false} :has-media-trimmed false}
{:id (:id file)}))) {:id (:id file)})))
+15 -10
View File
@@ -155,9 +155,10 @@
(defn enable-team-feature! (defn enable-team-feature!
[team-id feature] [team-id feature]
(dm/verify! (when-not (contains? cfeat/supported-features feature)
"feature should be supported" (ex/raise :type :assertion
(contains? cfeat/supported-features feature)) :code :feature-not-supported
:hint (str "feature '" feature "' not supported")))
(let [team-id (h/parse-uuid team-id)] (let [team-id (h/parse-uuid team-id)]
(db/tx-run! main/system (db/tx-run! main/system
@@ -173,9 +174,11 @@
(defn disable-team-feature! (defn disable-team-feature!
[team-id feature] [team-id feature]
(dm/verify!
"feature should be supported" (when-not (contains? cfeat/supported-features feature)
(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)] (let [team-id (h/parse-uuid team-id)]
(db/tx-run! main/system (db/tx-run! main/system
@@ -203,9 +206,11 @@
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level] [{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
:or {code :generic level :info} :or {code :generic level :info}
:as params}] :as params}]
(dm/verify!
["invalid level %" level] (when-not (contains? #{:success :error :info :warning} level)
(contains? #{:success :error :info :warning} level)) (ex/raise :type :assertion
:code :incorrect-level
:hint (str "level '" level "' not supported")))
(letfn [(send [dest] (letfn [(send [dest]
(l/inf :hint "sending notification" :dest (str dest)) (l/inf :hint "sending notification" :dest (str dest))
@@ -754,7 +759,7 @@
{:deleted deleted :total total})))] {:deleted deleted :total total})))]
(let [path (fs/path path) (let [path (fs/path path)
deleted-at (dt/minus (dt/now) cf/deletion-delay)] deleted-at (dt/minus (dt/now) (cf/get-deletion-delay))]
(when-not (fs/exists? path) (when-not (fs/exists? path)
(throw (ex-info "path does not exists" {:path path}))) (throw (ex-info "path does not exists" {:path path})))
+75 -27
View File
@@ -6,28 +6,39 @@
(ns app.storage (ns app.storage
"Objects storage abstraction layer." "Objects storage abstraction layer."
(:refer-clojure :exclude [resolve])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db] [app.db :as db]
[app.storage.fs :as sfs] [app.storage.fs :as sfs]
[app.storage.impl :as impl] [app.storage.impl :as impl]
[app.storage.s3 :as ss3] [app.storage.s3 :as ss3]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.fs :as fs] [datoteka.fs :as fs]
[integrant.core :as ig] [integrant.core :as ig])
[promesa.core :as p])
(:import (:import
java.io.InputStream)) java.io.InputStream))
(defn get-legacy-backend
[]
(let [name (cf/get :assets-storage-backend)]
(case name
:assets-fs :fs
:assets-s3 :s3
nil)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Storage Module State ;; Storage Module State
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::id #{:assets-fs :assets-s3 :s3 :fs}) (s/def ::id #{:assets-fs :assets-s3 :fs :s3})
(s/def ::s3 ::ss3/backend) (s/def ::s3 ::ss3/backend)
(s/def ::fs ::sfs/backend) (s/def ::fs ::sfs/backend)
(s/def ::type #{:fs :s3}) (s/def ::type #{:fs :s3})
@@ -43,13 +54,23 @@
(defmethod ig/init-key ::storage (defmethod ig/init-key ::storage
[_ {:keys [::backends ::db/pool] :as cfg}] [_ {:keys [::backends ::db/pool] :as cfg}]
(-> (d/without-nils cfg) (let [backend (or (get-legacy-backend)
(assoc ::backends (d/without-nils backends)) (cf/get :objects-storage-backend)
(assoc ::db/pool-or-conn pool))) :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 ::backend keyword?)
(s/def ::storage (s/def ::storage
(s/keys :req [::backends ::db/pool ::db/pool-or-conn] (s/keys :req [::backends ::db/pool ::db/connectable]
:opt [::backend])) :opt [::backend]))
(s/def ::storage-with-backend (s/def ::storage-with-backend
@@ -61,23 +82,26 @@
(defn get-metadata (defn get-metadata
[params] [params]
(into {} (reduce-kv (fn [res k _]
(remove (fn [[k _]] (qualified-keyword? k))) (if (qualified-keyword? k)
params)) (dissoc res k)
res))
params
params))
(defn- get-database-object-by-hash (defn- get-database-object-by-hash
[pool-or-conn backend bucket hash] [connectable backend bucket hash]
(let [sql (str "select * from storage_object " (let [sql (str "select * from storage_object "
" where (metadata->>'~:hash') = ? " " where (metadata->>'~:hash') = ? "
" and (metadata->>'~:bucket') = ? " " and (metadata->>'~:bucket') = ? "
" and backend = ?" " and backend = ?"
" and deleted_at is null" " and deleted_at is null"
" limit 1")] " limit 1")]
(some-> (db/exec-one! pool-or-conn [sql hash bucket (name backend)]) (some-> (db/exec-one! connectable [sql hash bucket (name backend)])
(update :metadata db/decode-transit-pgobject)))) (update :metadata db/decode-transit-pgobject))))
(defn- create-database-object (defn- create-database-object
[{:keys [::backend ::db/pool-or-conn]} {:keys [::content ::expired-at ::touched-at] :as params}] [{:keys [::backend ::db/connectable]} {:keys [::content ::expired-at ::touched-at ::touch] :as params}]
(let [id (or (:id params) (uuid/random)) (let [id (or (:id params) (uuid/random))
mdata (cond-> (get-metadata params) mdata (cond-> (get-metadata params)
(satisfies? impl/IContentHash content) (satisfies? impl/IContentHash content)
@@ -86,7 +110,9 @@
:always :always
(dissoc :id)) (dissoc :id))
;; FIXME: touch object on deduplicated put operation ?? touched-at (if touch
(or touched-at (dt/now))
touched-at)
;; NOTE: for now we don't reuse the deleted objects, but in ;; NOTE: for now we don't reuse the deleted objects, but in
;; futute we can consider reusing deleted objects if we ;; futute we can consider reusing deleted objects if we
@@ -95,10 +121,20 @@
result (when (and (::deduplicate? params) result (when (and (::deduplicate? params)
(:hash mdata) (:hash mdata)
(:bucket mdata)) (:bucket mdata))
(get-database-object-by-hash pool-or-conn backend (:bucket mdata) (:hash mdata))) (let [result (get-database-object-by-hash connectable backend
(:bucket mdata)
(:hash mdata))]
(if touch
(do
(db/update! connectable :storage-object
{:touched-at touched-at}
{:id (:id result)}
{::db/return-keys false})
(assoc result :touced-at touched-at))
result)))
result (or result result (or result
(-> (db/insert! pool-or-conn :storage-object (-> (db/insert! connectable :storage-object
{:id id {:id id
:size (impl/get-size content) :size (impl/get-size content)
:backend (name backend) :backend (name backend)
@@ -154,9 +190,9 @@
(dm/export impl/object?) (dm/export impl/object?)
(defn get-object (defn get-object
[{:keys [::db/pool-or-conn] :as storage} id] [{:keys [::db/connectable] :as storage} id]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(retrieve-database-object pool-or-conn id)) (retrieve-database-object connectable id))
(defn put-object! (defn put-object!
"Creates a new object with the provided content." "Creates a new object with the provided content."
@@ -172,10 +208,10 @@
(defn touch-object! (defn touch-object!
"Mark object as touched." "Mark object as touched."
[{:keys [::db/pool-or-conn] :as storage} object-or-id] [{:keys [::db/connectable] :as storage} object-or-id]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)] (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)]
(-> (db/update! pool-or-conn :storage-object (-> (db/update! connectable :storage-object
{:touched-at (dt/now)} {:touched-at (dt/now)}
{:id id}) {:id id})
(db/get-update-count) (db/get-update-count)
@@ -195,11 +231,10 @@
"Returns a byte array of object content." "Returns a byte array of object content."
[storage object] [storage object]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(if (or (nil? (:expired-at object)) (when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now))) (dt/is-after? (:expired-at object) (dt/now)))
(-> (impl/resolve-backend storage (:backend object)) (-> (impl/resolve-backend storage (:backend object))
(impl/get-object-bytes object)) (impl/get-object-bytes object))))
(p/resolved nil)))
(defn get-object-url (defn get-object-url
([storage object] ([storage object]
@@ -223,13 +258,26 @@
(-> (impl/get-object-url backend object nil) file-url->path)))) (-> (impl/get-object-url backend object nil) file-url->path))))
(defn del-object! (defn del-object!
[{:keys [::db/pool-or-conn] :as storage} object-or-id] [{:keys [::db/connectable] :as storage} object-or-id]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id) (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
res (db/update! pool-or-conn :storage-object res (db/update! connectable :storage-object
{:deleted-at (dt/now)} {:deleted-at (dt/now)}
{:id id})] {:id id})]
(pos? (db/get-update-count res)))) (pos? (db/get-update-count res))))
(dm/export impl/resolve-backend)
(dm/export impl/calculate-hash) (dm/export impl/calculate-hash)
(defn configure
[storage connectable]
(assoc storage ::db/connectable connectable))
(defn resolve
"Resolves the storage instance with preconfigured backend. You can
specify to reuse the database connection from provided
cfg/system (default false)."
[cfg & {:as opts}]
(let [storage (::storage cfg)]
(if (::db/reuse-conn opts false)
(configure storage (db/get-connectable cfg))
storage)))
Loaded 100 of 1401 files, more files were not shown because too many files have changed in this diff. Show more