Compare commits

...

144 Commits

Author SHA1 Message Date
Alejandro Alonso
8affefbbab 📎 Update changelog 2025-03-18 10:13:12 +01:00
jdo-odoo
0225919a45 📚 Fix typos in shortcut and insert image section
Signed-off-by: jdo-odoo <108932862+jdo-odoo@users.noreply.github.com>
2025-03-18 10:09:58 +01:00
luisδμ
5155cf2b23 🐛 Fix clicking on a comment at the viewer's sidebar is not opening threads (#6083) 2025-03-17 12:25:23 +01:00
Andrey Antukh
7403f60366 Merge pull request #6076 from penpot/alotor-fix-problem-inspect
🐛 Fix problem with readonly and inspect
2025-03-14 15:50:01 +01:00
Alejandro
a8c34ccc1a Merge pull request #6070 from penpot/alotor-bugfix-grid-layout
🐛 Fix problem with grid component propagation
2025-03-14 12:39:54 +01:00
alonso.torres
8c501db2fa 🐛 Fix problem with readonly and inspect 2025-03-14 12:24:34 +01:00
alonso.torres
d2fbb9dfa7 🐛 Fix problem with grid component propagation 2025-03-14 11:50:55 +01:00
Andrey Antukh
05d6d2fcd4 🐛 Fix several corner cases that causes race conditions on workspace and dashboard loading
* 🐛 Fix several race conditions on workspace and dashboard code

It also fixes a corner case that happens when penpot workspace
is loaded in a background tab on firefox.

* 🐛 Add missing team-id prop to several file returning endpoints
2025-03-14 09:55:41 +01:00
Andrey Antukh
61800d8945 Merge pull request #6074 from penpot/dfelinto-export-webp
🎉 Add support for WEBP format for shape export
2025-03-13 16:24:55 +01:00
Dalai Felinto
f450c9dbe3 🎉 Add support for WEBP format on shape export
It is very convenient to be able to export WEBP right from penpot.
Otherwise users have to first download to PNG then convert it locally.

---

Playwright only supports JPEG and PNG. So in order to support WEBP I had
to first generate a PNG and then convert it afterwards.

Signed-off-by: Dalai Felinto <dalai@blender.org>
2025-03-13 16:15:30 +01:00
Andrey Antukh
e3b3fa3342 📎 Update changelog 2025-03-13 09:32:19 +01:00
Alejandro
fe04f3e45d Merge pull request #6065 from penpot/superalex-fix-sync-issues-components
🐛 Fix sync issues components
2025-03-12 12:27:25 +01:00
Alejandro Alonso
363c1d5b56 🐛 Fix sync libraries of components 2025-03-12 12:15:00 +01:00
Yamila Moreno
3ee3df9b24 Merge pull request #6019 from penpot/yms-improve-troubleshooting-toc
📚 Improve TOC in Troubleshooting section
2025-03-11 16:30:03 +01:00
Yamila Moreno
332657bd1b 📎 Add minor improvements 2025-03-11 16:21:16 +01:00
Yamila Moreno
953f770fdd 📚 Improve TOC for self-hosting guide 2025-03-11 12:45:30 +01:00
Yamila Moreno
c83b9ea305 📚 Improve TOC in Troubleshooting section 2025-03-11 12:45:30 +01:00
Alejandro
3007aa19a2 Merge pull request #6046 from penpot/eva-fix-storybook-icons-list
🐛 Fix storybook icons list scroll
2025-03-11 09:59:42 +01:00
Eva Marco
e20adda766 🐛 Fix storybook icons list scroll 2025-03-11 09:43:35 +01:00
Alejandro
3d9fda7a21 Merge pull request #6025 from penpot/eva-fix-storybook-scroll
🐛 Fix scroll on storybook doc files
2025-03-07 07:52:30 +01:00
Alejandro
7a5dea5cfe Merge pull request #6023 from penpot/marina-consolidate-empty-workspace-board-tool
🎉 Consolidate when workspace empty, board tool selected
2025-03-07 07:49:27 +01:00
alonso.torres
b47df2c230 🐛 Fix problem with components and grid layout 2025-03-06 15:43:55 +01:00
Marina López
b8b3cc641a 🎉 Consolidate when workspace empty, board tool selected 2025-03-06 13:31:12 +01:00
Eva Marco
09ff7372da 🐛 Fix scroll on storybook doc filesç 2025-03-06 12:29:48 +01:00
Alejandro
f45fa95935 Merge pull request #6017 from penpot/alotor-bugfix
🐛 Fix problem with selection colors
2025-03-06 07:35:26 +01:00
alonso.torres
ce02cbc3f1 🐛 Fix problem with selection colors 2025-03-05 14:34:08 +01:00
Alejandro Alonso
b386403fa8 🐛 Fix multiple nav events when open workspace 2025-03-05 12:09:05 +01:00
Alejandro Alonso
0a6e884584 🐛 Remove unnecesary console.log 2025-03-05 12:09:05 +01:00
Alejandro
06f6a49bce Merge pull request #6008 from penpot/superalex-fix-multiple-nav-events-when-open-workspace
🐛 Fix multiple nav events when open workspace
2025-03-05 10:57:51 +01:00
Alejandro Alonso
afd309c62b 🐛 Fix multiple nav events when open workspace 2025-03-05 10:50:47 +01:00
Alejandro Alonso
214a89e20d 📎 Update CHANGES.md file 2025-03-03 07:13:23 +01:00
Yamila Moreno
e64cf9f283 Merge pull request #5908 from penpot/yms-proxy-documentation
📚 Document how to use a proxy
2025-02-28 17:18:46 +01:00
Marina López
3a34c51e43 Add pricing page event 2025-02-28 13:04:15 +01:00
Yamila Moreno
0ff9c44246 🐳 Improve nginx resolvers (#5967) 2025-02-28 09:02:40 +01:00
Yamila Moreno
5bfab454f5 📚 Document how to use a proxy - caddy 2025-02-28 08:43:40 +01:00
Yamila Moreno
5ebde405ea 📚 Document how to use a proxy - nginx 2025-02-28 08:43:40 +01:00
Alejandro
531b002a5c Merge pull request #5976 from penpot/hotfix-texts
🐛 Fix problems with empty position-data
2025-02-27 15:33:25 +01:00
alonso.torres
3eae3178a2 🐛 Fix problems with empty position-data 2025-02-27 14:31:49 +01:00
luisδμ
2cf3e37b7a 🐛 Fix comment update fails in viewer (#5958)
* 🐛 Fix comment update fails in viewer

* 🐛 Reload team members in workspace but not in viewer
2025-02-26 14:29:51 +01:00
luisδμ
e0b9751b16 Merge pull request #5947 from penpot/luis-fix-design-panel-draft-comment-open
🐛 Fix design panel does not reappear if comment draft is open
2025-02-26 11:33:06 +01:00
Alejandro
ccea9b1564 Merge pull request #5962 from penpot/elenatorro-9339-fix-typo-libraries-es
🐛 Fix typo at libraries modal
2025-02-26 09:32:23 +01:00
luisδμ
5fcf889d3c Merge pull request #5960 from penpot/luis-fix-avoid-post-blank-comment
🐛 Fix avoid enabling post button if blank comment
2025-02-26 09:28:59 +01:00
luisδμ
7247db14b2 Merge pull request #5957 from penpot/luis-fix-mention-users-in-viewer
🐛 Fix mention users in viewer
2025-02-26 09:28:29 +01:00
elenatorro
658e5dce22 🐛 Fix ES typo in libraries modal 2025-02-26 09:22:45 +01:00
Elena Torró
f27cbfa0ec Merge pull request #5953 from penpot/marina-fix-typo-libraries
🐛 Fix typo at libraries modal
2025-02-26 09:12:21 +01:00
Luis de Dios
5754c393b9 🐛 Fix avoid enabling post button if blank comment 2025-02-25 18:09:22 +01:00
Luis de Dios
c618efc29e 🐛 Fix mention users in viewer 2025-02-25 15:43:29 +01:00
Marina López
3685f7b32b 🐛 Fix typo at libraries modal 2025-02-25 13:41:26 +01:00
andrés gonzález
06b5304926 📚 New section for Your account (#5941)
* 📚 New section for Your account

* Update docs/user-guide/the-interface/index.njk

Co-authored-by: Madalena Melo <madalena.melo@kaleidos.net>

* Update docs/user-guide/the-interface/index.njk

Co-authored-by: Madalena Melo <madalena.melo@kaleidos.net>

* Update docs/user-guide/the-interface/index.njk

Co-authored-by: Madalena Melo <madalena.melo@kaleidos.net>

---------

Co-authored-by: Madalena Melo <madalena.melo@kaleidos.net>
2025-02-25 13:05:49 +01:00
Marina López
8f06fa1026 🎉 Add AB test descriptive board tooltip (#5936)
* 🎉 Add AB test descriptive board tooltip

* 📎 Add changes from feedback
2025-02-25 12:50:16 +01:00
Andrey Antukh
a549d783ba Downgrade s3 sdk for compatibility with minio (#5946)
* 📚 Add `⬇️` emoji to the contributing guide

For cases where we need to downgrade dependencies

*  Downgrade S3 SDK version

Mainly for minio and other S3-compatible services
2025-02-25 12:49:33 +01:00
Luis de Dios
91efcd17a2 🐛 Fix design panel does not reappear if comment draft is open 2025-02-25 10:30:11 +01:00
Andrey Antukh
6c1e8c3fe8 ⬆️ Update deps (fix compat issues with minio) 2025-02-25 09:39:13 +01:00
luisδμ
7f9a9ad774 🐛 Fix visual glitches in the comment dropdown at the dashboard (#5938) 2025-02-25 09:17:42 +01:00
luisδμ
2219d91e4d 🐛 Fix all comments appear in every page (#5943) 2025-02-25 09:16:46 +01:00
Yamila Moreno
fac2314d62 🔧 Relax secure cookies requirement when non-https public uri is set (#5939) 2025-02-25 09:10:53 +01:00
Yamila Moreno
aac61ff229 📚 Document how to troubleshoot Penpot (#5937) 2025-02-24 12:07:47 +01:00
Andrey Antukh
15d09eb0d4 🐛 Fix incorrect data id assignation on creating a snapshot (#5934)
* 📎 Set proper name to relink-refs mechanism function

* 🐛 Fix incorrect id assignation on snapshot file resolution

* ♻️ Use uniform api for file retrieval on file snapshot code
2025-02-24 11:05:16 +01:00
Alejandro
786383c25d Merge pull request #5935 from penpot/niwinz-viewer-bugfix
🐛 Fix incorrect data returned on viewer subapp bundle
2025-02-24 10:41:17 +01:00
elhombretecla
662c3c64a9 Fix 2.5 slides wording (#5925) 2025-02-24 09:39:43 +01:00
Juanfran
9084c184e7 🐛 Filter out recent fonts from search results (#5927) 2025-02-24 09:12:12 +01:00
Andrey Antukh
ae718c3328 🐛 Fix incorrect data returned on viewer subapp bundle 2025-02-24 09:03:57 +01:00
Marina López
702bd41047 🐛 Fix error when getting file from libs 2025-02-24 08:26:27 +01:00
Aitor Moreno
9896275fa8 Merge pull request #5932 from penpot/niwinz-objects-gc-bugfix
 Remove automatic cascade on file_change table fk constraint
2025-02-21 14:32:24 +01:00
Andrey Antukh
d2c800fc0f 🐛 Add missing handling of file_change on delete-object task 2025-02-21 14:24:18 +01:00
Andrey Antukh
893f19fa5e Remove automatic cascade on file_change table fk constraint 2025-02-21 14:24:07 +01:00
Andrey Antukh
624750ad16 🐛 Fix incorrect order of execution of internal procs on objects-gc (#5929)
*  Improve efficiency on objects-gc tasks

Replacing db/cursor with db/plan

* 🐛 Use correct order of subprocs on objects-gc

Mainly affects the file deletion

* 📎 Increase chunk-size to 100 on objects-gc
2025-02-21 13:01:20 +01:00
Andrey Antukh
24cb1728b0 🐛 Fix context menu event handling issues (#5917)
* 💄 Change call convention for dashboard grid component

* 🎉 Add helper component for easy portal to document

* 🐛 Fix context menu event handling issues

With this commit, the behavior of context menu and scroll is changed
to: close menu on scroll instead of disabling all pointer events while
menu is open. The previous behavior causes a second event of context
menu open a native browser context menu instead of penpot menu.
2025-02-21 07:57:56 +01:00
Eva Marco
dda9f62504 🐛 Fix menu shadow color 2025-02-20 17:23:28 +01:00
Andrey Antukh
479f39338b 🐛 Don't send invitation email to profiles that explicitly disallow that
* 🐛 Don't send invitation email to profiles that explicitly disallow that

* 📎 Add changes post feedback

* 📎 Fix typo on changelog
2025-02-20 09:20:28 +01:00
Alejandro
befa5f4c7f Merge pull request #5906 from penpot/niwinz-libraries-fix
🐛 Fix incorrect libraries filtering on workspace
2025-02-20 07:40:24 +01:00
Andrey Antukh
6e92e3b765 🐛 Fix inconsistency on naming
This also a fix of passing incorrect prop :shared-libs
to a component that already expectes :libraries.

It also removes unnecesary use of refs/libraries ref
2025-02-20 07:32:11 +01:00
Andrey Antukh
0e73de17ec 🐛 Fix incorrect libraries filtering on workspace 2025-02-20 07:32:10 +01:00
Alejandro
2dcf692853 Merge pull request #5911 from penpot/niwinz-notifications-post-save-refresh
🐛 Fix incorrect notification assignation after update operation
2025-02-20 07:06:49 +01:00
Alejandro
66f2e0aa5e Merge pull request #5912 from penpot/niwinz-binfile-v3-fixes
🐛 Add proper feature handling for binfile imports
2025-02-20 07:03:59 +01:00
Andrey Antukh
dd6ae81e83 🐛 Add correct feature handling on dbg binfile import 2025-02-19 22:47:55 +01:00
Andrey Antukh
cb8e31e7f8 🐛 Add correct handling of features on clone-template 2025-02-19 22:47:54 +01:00
Andrey Antukh
ca9b5b1b8a 📎 Use standard asserts on binfile common ns 2025-02-19 22:47:54 +01:00
Andrey Antukh
a391d71b60 🐛 Add correct feature handling on binfile import 2025-02-19 22:47:40 +01:00
Andrey Antukh
7d0c19fcc7 🐛 Add correct feature check on manifest reading
Instead on the file save operation so we can raise
exception if something does not match without processing
the whole file
2025-02-19 22:45:08 +01:00
Andrey Antukh
e4ee585704 🐛 Fix incorrect notification assignation after update operation 2025-02-19 17:21:52 +01:00
Marina López
5f61254a75 🐛 Fix library button condition and copy (#5889)
* 🐛 Fix library button condition and copy

* 📎 Add changes from feedback

* 📎 Add changes from feedback
2025-02-19 16:52:35 +01:00
luisδμ
0784d6b62a 🐛 Fix reposition comment bubbles under viewer role (#5905) 2025-02-19 16:47:53 +01:00
Alejandro
7a7fa44f6b 🐛 Fix click prototype flow (#5896) 2025-02-19 16:00:17 +01:00
Yamila Moreno
4b5d304a40 📚 Improve technical guide
* 📚 Improve technical guide

* 📚 Improve technical guide

* 📚 Improve technical guide

* 📚 Improve technical guide

* 📚 Improve technical guide

* 📚 Improve technical guide

* 📚 Improve technical guide

* 📚 Improve technical guide

* 📚 Improve technical guide

* 📚 Improve technical guide
2025-02-19 14:35:40 +01:00
alonso.torres
e7b9ae6415 🐛 Remove fit content shortcut 2025-02-19 13:02:53 +01:00
luisδμ
4ac52c138c Merge pull request #5895 from penpot/niwinz-bugfix-comments
🐛 Fix unexpected exception on clicking empty area on creating comment
2025-02-19 11:29:21 +01:00
Andrey Antukh
4744085426 🐛 Fix incorrect handling request access with deleted profiles
* 📎 Add minor improvements to team tests

* 🐛 Fix incorrect handling request access with deleted profiles

* 🐛 Fix redirect loop on empty route

Happens when the current profile is deleted from team

* 🐛 Fix urls on request access emails

* 📎 Revert url changes on emails
2025-02-19 11:04:19 +01:00
Alejandro
19bae05f41 Merge pull request #5884 from penpot/niwinz-bugfix-4
🐛 Fix update-libraries dialog disappear when clicking outside
2025-02-19 07:53:35 +01:00
Alejandro
02f78d80d7 Merge pull request #5883 from penpot/niwinz-bugfix-3
🐛 Fix incorrect navigation on show-main-component menu option
2025-02-19 07:42:02 +01:00
Alejandro
51202df105 Merge pull request #5885 from penpot/niwinz-bugfix-5
🐛 Fix regression on request-access dialog
2025-02-19 07:15:30 +01:00
Andrey Antukh
cd1eefb214 Add safety checks for node on comment-input* component 2025-02-18 18:33:02 +01:00
Andrey Antukh
869a412c74 🐛 Fix unexpected exception on clicking empty area on creating comment 2025-02-18 18:19:08 +01:00
luisδμ
d019afe667 🐛 Fix incorrect number of replies in comments (#5893) 2025-02-18 17:25:43 +01:00
Andrey Antukh
c41aa56a60 Merge pull request #5869 from penpot/marina-empty-workspace-create-board-tool-default
🎉 Add AB test for empty workspace set board tool by default
2025-02-18 17:24:52 +01:00
Andrey Antukh
7d840722c4 Add abstraction for page emptiness checking 2025-02-18 16:59:00 +01:00
Marina López
272bbdd54a 🎉 Add AB test for empty workspace set board tool by default 2025-02-18 16:57:59 +01:00
Andrey Antukh
fe3fec7a50 🐛 Fix workspace hot reload race condtion
This reverts commit 8139ee3ef9.
2025-02-18 16:49:50 +01:00
Andrey Antukh
63524dce8d 🐛 Fix regression on request-access dialog 2025-02-18 12:37:04 +01:00
Yamila Moreno
807b8d82e3 🔧 Improve flags documentation (#5863)
* 📎 Fix typo

* 🔧 Enable certain flags by default

* 🔧 Compile all flags in a single source of truth

* 📎 Move all default flags to common

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-02-18 12:36:16 +01:00
Andrey Antukh
3f45863823 🐛 Fix update-libraries dialog disappear when clicking outside 2025-02-18 12:04:10 +01:00
Andrey Antukh
f9f5f0af7d ♻️ Refactor code style on actionable ds component 2025-02-18 12:04:09 +01:00
Andrey Antukh
f98dbef228 Simplify code and use lookup helpers 2025-02-18 12:04:09 +01:00
Andrey Antukh
713d6a31df Add better way to delay a stream for library notification 2025-02-18 12:04:09 +01:00
Andrey Antukh
77f906ae37 Improve notification show params validation assertion 2025-02-18 12:04:09 +01:00
María Valderrama
6a5538bb15 🐛 Fix unreachable Save color style button (#5879)
* 🐛 Fix unreachable Save color style button

* 📎 Fix unreachable Save color style button code review
2025-02-18 11:32:00 +01:00
Andrey Antukh
0ce99968b3 🐛 Fix incorrect navigation on show-main-component menu option 2025-02-18 10:53:32 +01:00
Alonso Torres
0900b7a572 🐛 Fix problem with grid layout duplicate positioning (#5877) 2025-02-18 10:17:42 +01:00
Andrey Antukh
3412a0a18a Merge pull request #5868 from penpot/niwinz-terms-link-fix
🐛 Set correct default for terms link on fonts hero
2025-02-18 09:55:52 +01:00
Eva Marco
5e3b47e455 🎉 Add integration test for bug (#5875) 2025-02-17 16:29:28 +01:00
Andrey Antukh
83423a9509 Merge pull request #5864 from penpot/eva-fix-colorpicker-dnd
🐛 Fix add recent color while drag and drop
2025-02-17 15:22:49 +01:00
Alonso Torres
ccabaf4552 🐛 Fix style problem with update lib notifications (#5871) 2025-02-17 13:31:29 +01:00
Alonso Torres
ad15ac6c1e 🐛 Fix several problems with navigation in viewer (#5872) 2025-02-17 13:24:15 +01:00
luisδμ
a9340709c8 🐛 Fix open comment in workspace from dashboard notification (#5865) 2025-02-17 12:29:19 +01:00
Alonso Torres
faa3451da9 🐛 Fix problem with board name input style (#5870) 2025-02-17 12:26:04 +01:00
Alonso Torres
0aa95ea058 🐛 Fix problem with copy/paste props (#5867) 2025-02-17 12:25:42 +01:00
Andrey Antukh
66182152cb 🐛 Set correct default for terms link 2025-02-17 11:51:21 +01:00
Andrey Antukh
b9629b7be6 🔥 Remove unused default flags on frontend 2025-02-17 11:23:46 +01:00
Eva Marco
6c9875e4f9 🐛 Fix add recent color while drag and drop 2025-02-17 10:20:40 +01:00
Andrey Antukh
f90c63b5f0 Merge pull request #5834 from penpot/juan-slides-2.5
🎉 Add slides for 2.5 release
2025-02-14 15:52:57 +01:00
Elhombretecla
680e611266 🎉 Add slides for 2.5 version 2025-02-14 15:41:27 +01:00
Belén Albeza
cad7d75590 🐛 Fix libraries context menu (#5854)
*  Add integration test for Bug #10421

* 🐛 Fix dashboard library item menu

*  Fixup integration test
2025-02-14 14:34:54 +01:00
luisδμ
8c81d48858 Merge pull request #5844 from penpot/luis-refactor-zero-width-space
♻️ Use constant for zero width space
2025-02-14 14:06:28 +01:00
Eva Marco
a7ed5228d3 🐛 Fix lost translation strings (#5846)
* 🐛 Fix lost translation strings

* 🐛 Fix form error management internal issues and inconsistencies

* 📎 Add better validation conditons for ::sm/text schema

* 🐛 Add better touched detection mechanism for input and textarea

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-02-14 12:51:14 +01:00
Eva Marco
6bb7fa26f4 🐛 Fix rename blocked boards (#5845) 2025-02-14 11:47:16 +01:00
Andrey Antukh
8b6a9b373d Improve efficiency and logging on process-files! srepl helper 2025-02-14 10:02:34 +01:00
Andrey Antukh
8139ee3ef9 Revert "🐛 Fix workspace hot reload race condtion (#5851)"
This reverts commit 5d56d28cb6.
2025-02-14 10:02:05 +01:00
Marina López
af93325fd9 🎉 Consolidate suggested libraries and add library button (#5828) 2025-02-13 20:06:28 +01:00
Pablo Alba
d836cc66da 🐛 Fix unable to drag & drop assets into/outside component groups (#5849) 2025-02-13 17:30:33 +01:00
Andrey Antukh
5d56d28cb6 🐛 Fix workspace hot reload race condtion (#5851)
Mainly ensure that all required paramers for workspace
file and page bootstrap are always available from parameters
and not taken from context
2025-02-13 17:04:34 +01:00
Eva Marco
46d2359107 🐛 Fix empty translation strings (#5847) 2025-02-13 15:48:44 +01:00
Alejandro
f8820695cc 🐛 Fix incorrect numbering files when new team (#5835) 2025-02-13 11:55:59 +01:00
andrés gonzález
2d1d1fee1c 📚 Info about gradients (#5843) 2025-02-13 10:42:56 +01:00
Luis de Dios
4c6f086f82 ♻️ Use constant for zero width space 2025-02-13 09:17:09 +01:00
Alonso Torres
688b9f2194 🐛 Fix focus to main component (#5842) 2025-02-12 17:16:14 +01:00
luisδμ
8992eb98ec Merge pull request #5841 from penpot/luis-fix-comments-mentions-keyboard
🐛 Fix keyboard interactions with mentions
2025-02-12 17:03:44 +01:00
Luis de Dios
638a8a8d3f 🐛 Fix keyboard interactions with mentions 2025-02-12 16:37:51 +01:00
Belén Albeza
fb6cd3d9d4 🐛 Fix "Publish empty library" modal appearing for non-empty libraries (#5838)
* 🐛 Fix 'Publish empty library' modal appearing for non-empty libraries

*  Add integration test for bug 10113
2025-02-12 15:36:01 +01:00
Alonso Torres
fb0e22c16b 🐛 Fix problem with team permissions redirection (#5839) 2025-02-12 15:35:28 +01:00
Alejandro
6b26adb187 🐛 Fix team doesn't disappear after deletion (#5832) 2025-02-12 14:36:46 +01:00
Alonso Torres
8fe1271690 🐛 Fix problem opening url when page-id didn't exist (#5833)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-02-12 14:36:05 +01:00
Alonso Torres
ceb90cd9e0 🐛 Fix problem with dashboard multiple selection (#5836) 2025-02-12 14:34:05 +01:00
Alonso Torres
51f924a5e1 🐛 Fix problem with onboarding team invite (#5829)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-02-12 13:09:37 +01:00
Alonso Torres
fb24a37e83 🐛 Fix problem with grid layout crashing (#5831) 2025-02-12 13:07:41 +01:00
235 changed files with 4032 additions and 2414 deletions

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check Commit Type
uses: gsactions/commit-message-checker@v2
with:
pattern: '^:(lipstick|globe_with_meridians|wrench|books|arrow_up|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):\s[A-Z].*[^.]$'
pattern: '^:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):\s[A-Z].*[^.]$'
flags: 'gm'
error: 'Commit should match CONTRIBUTING.md guideline'
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request

View File

@@ -1,8 +1,46 @@
# CHANGELOG
## 2.5.0 (Unreleased)
## 2.5.4
### :rocket: Epics and highlights
### :sparkles: New features
- Add support for WEBP format on shape export [Github #6053](https://github.com/penpot/penpot/pull/6053) and [Github #6074](https://github.com/penpot/penpot/pull/6074)
### :bug: Bugs fixed
- Fix feature loading on workspace when opening a file in a background
tab [Taiga #10377](https://tree.taiga.io/project/penpot/issue/10377)
- Fix minor inconsistencies on RPC `get-file-libraries` and `get-file`
methods (add missing team-id prop)
- Fix problem with viewer role and inspect mode [Taiga #9751](https://tree.taiga.io/project/penpot/issue/9751)
- Fix error when clicking on a comment at the viewer's sidebar [Taiga #10465](https://tree.taiga.io/project/penpot/issue/10465)
## 2.5.3
### :bug: Bugs fixed
- Component sync issues with multiple tabs [Taiga #10471](https://tree.taiga.io/project/penpot/issue/10471)
## 2.5.2
### :sparkles: New features
- When the workspace is empty, set default the board creation tool [Taiga #9425](https://tree.taiga.io/project/penpot/us/9425)
### :bug: Bugs fixed
- Fix scroll on storybook docs [taiga #9962](https://tree.taiga.io/project/penpot/issue/9962)
- Navigate tracking event firing multiple times [Taiga #10415](https://tree.taiga.io/project/penpot/issue/10415)
- Fix problem with selection colors [Taiga #10376](https://tree.taiga.io/project/penpot/issue/10376)
- Fix scroll on storybook icons list [taiga #9962](https://tree.taiga.io/project/penpot/issue/9962)
## 2.5.1
### :sparkles: New features
- Improve Nginx entryponit to get the resolvers dinamically by default
## 2.5.0
### :boom: Breaking changes & Deprecations
@@ -32,9 +70,6 @@ If you have a big database and many cores available, you can reduce the time of
all files by increasing paralelizacion changing the `max-jobs` value from 1 to N (where N
is a number of cores)
### :heart: Community contributions (Thank you!)
### :sparkles: New features
- [GRADIENTS] New gradients UI with multi-stop support. [Taiga #3418](https://tree.taiga.io/project/penpot/epic/3418)
@@ -50,8 +85,11 @@ is a number of cores)
- [COMMENTS] Notifications in Backend, Profile Section, and Mention Email Notification [Taiga #9233](https://tree.taiga.io/project/penpot/us/9233)
### :bug: Bugs fixed
- Fix missing state refresh on notifications update [Taiga #10253](https://tree.taiga.io/project/penpot/issue/10253)
- Fix icon visualization on select component [Taiga #8889](https://tree.taiga.io/project/penpot/issue/8889)
- Fix typo on integration tests docs [Taiga #10112](https://tree.taiga.io/project/penpot/issue/10112)
- Fix menu shadow color [Taiga #10102](https://tree.taiga.io/project/penpot/issue/10102)
- Fix problem with alt key measures being stuck [Taiga #9348](https://tree.taiga.io/project/penpot/issue/9348)
- Fix error when reseting stroke cap
- Fix problem with strokes not refreshing in Safari [Taiga #9040](https://tree.taiga.io/project/penpot/issue/9040)
@@ -62,6 +100,13 @@ is a number of cores)
- Added upload svg with images method [#5489](https://github.com/penpot/penpot/issues/5489)
- Fix problem with root frame parent reference [Taiga #9437](https://tree.taiga.io/project/penpot/issue/9437)
- Fix change flex direction using plugins API [Taiga #9407](https://tree.taiga.io/project/penpot/issue/9407)
- Fix problem opening url when page-id didn't exist [Taiga #10157](https://tree.taiga.io/project/penpot/issue/10157)
- Fix problem with onboarding to a team [Taiga #10143](https://tree.taiga.io/project/penpot/issue/10143)
- Fix problem with grid layout crashing [Taiga #10127](https://tree.taiga.io/project/penpot/issue/10127)
- Fix rename locked boards [Taiga #10174](https://tree.taiga.io/project/penpot/issue/10174)
- Fix update-libraries dialog disappear when clicking outside [Taiga #10238](https://tree.taiga.io/project/penpot/issue/10238)
- Fix incorrect handling of team access requests with deleted/recreated users
- Fix incorect handling of profile settings related to invitation notifications [Taiga #10252](https://tree.taiga.io/project/penpot/issue/10252)
## 2.4.3

View File

@@ -84,6 +84,7 @@ Where type is:
- :whale: `:whale:` a commit for docker related stuff
- :paperclip: `:paperclip:` a commit with other not relevant changes
- :arrow_up: `:arrow_up:` a commit with dependencies updates
- :arrow_down: `:arrow_down:` a commit with dependencies downgrades
- :fire: `:fire:` a commit that removes files or code
More info:

View File

@@ -35,7 +35,7 @@
org.postgresql/postgresql {:mvn/version "42.7.5"}
org.xerial/sqlite-jdbc {:mvn/version "3.48.0.0"}
com.zaxxer/HikariCP {:mvn/version "6.0.0"}
com.zaxxer/HikariCP {:mvn/version "6.2.1"}
io.whitfin/siphash {:mvn/version "2.0.0"}
@@ -59,8 +59,7 @@
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.30.7"}
}
software.amazon.awssdk/s3 {:mvn/version "2.28.26"}}
:paths ["src" "resources" "target/classes"]
:aliases

View File

@@ -207,7 +207,7 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true"
<a href="{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;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:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
</td>
@@ -251,4 +251,4 @@
</div>
</body>
</html>
</html>

View File

@@ -6,7 +6,7 @@ Since this file is in your Penpot team, you can provide access by sending a view
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
{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true

View File

@@ -230,9 +230,9 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;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:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
<a href="{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;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:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
</td>
</tr>
</table>
@@ -274,4 +274,4 @@
</div>
</body>
</html>
</html>

View File

@@ -19,7 +19,7 @@ Alternatively, you can create and share a view-only link to the file. This will
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
{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true

View File

@@ -214,7 +214,7 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape }}"
<a href="{{ public-uri }}/#/dashboard/members?team-id={{team-id}}&invite-email={{requested-by-email|urlescape }}"
style="display:inline-block;background:#6911d4;color:#FFFFFF;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:8px;"
target="_blank"> GIVE ACCESS TO “{{team-name|abbreviate:25}}” TEAM </a>
</td>
@@ -247,9 +247,9 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;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:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
<a href="{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;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:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
</td>
</tr>
</table>
@@ -292,4 +292,4 @@
</div>
</body>
</html>
</html>

View File

@@ -13,7 +13,7 @@ This will automatically include {{requested-by|abbreviate:25}} in the team, so t
Click the link below to provide team access:
{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}
{{ public-uri }}/#/dashboard/members?team-id{{team-id}}&invite-email={{requested-by-email|urlescape}}
@@ -23,8 +23,7 @@ Alternatively, you can create and share a view-only link to the file. This will
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
{{ public-uri }}/#/view?file-id={{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.

View File

@@ -205,7 +205,7 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}"
<a href="{{ public-uri }}/#/dashboard/members?team-id={{team-id}}&invite-email={{requested-by-email|urlescape}}"
style="display:inline-block;background:#6911d4;color:#FFFFFF;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:8px;"
target="_blank"> GIVE ACCESS TO “{{team-name|abbreviate:25}}” TEAM </a>
</td>
@@ -249,4 +249,4 @@
</div>
</body>
</html>
</html>

View File

@@ -4,7 +4,7 @@ Hello!
To provide access, please click the link below:
{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}
{{ public-uri }}/#/dashboard/members?team-id={{team-id}}&invite-email={{requested-by-email|urlescape}}
If you do not wish to grant access at this time, you can simply disregard this email.

View File

@@ -9,7 +9,6 @@
binfile format implementations and management rpc methods."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.files.helpers :as cfh]
@@ -21,7 +20,6 @@
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.features.components-v2 :as feat.compv2]
[app.features.fdata :as feat.fdata]
[app.features.file-migrations :as feat.fmigr]
[app.loggers.audit :as-alias audit]
@@ -219,10 +217,8 @@
"Given a set of file-id's, return all matching relations with the libraries"
[cfg ids]
(dm/assert!
"expected a set of uuids"
(and (set? ids)
(every? uuid? ids)))
(assert (set? ids) "expected a set of uuids")
(assert (every? uuid? ids) "expected a set of uuids")
(db/run! cfg (fn [{:keys [::db/conn]}]
(let [ids (db/create-array conn "uuid" ids)
@@ -310,7 +306,7 @@
update-shapes
(fn [data {:keys [page-id shape-id]}]
(d/update-in-when data [:pages-index page-id :objects shape-id] cfh/relink-media-refs lookup-index))
(d/update-in-when data [:pages-index page-id :objects shape-id] cfh/relink-refs lookup-index))
file
(update file :data #(reduce update-shapes % media-refs))]
@@ -378,7 +374,7 @@
replace the old :component-file reference with the new
ones, using the provided file-index."
[data]
(cfh/relink-media-refs data lookup-index))
(cfh/relink-refs data lookup-index))
(defn- relink-media
"A function responsible of process the :media attr of file data and
@@ -503,9 +499,7 @@
specific, should not be used outside of binfile domain"
[{:keys [::timestamp] :as cfg} file & {:as opts}]
(dm/assert!
"expected valid timestamp"
(dt/instant? timestamp))
(assert (dt/instant? timestamp) "expected valid timestamp")
(let [file (-> file
(assoc :created-at timestamp)
@@ -513,12 +507,11 @@
(assoc :ignore-sync-until (dt/plus timestamp (dt/duration {:seconds 5})))
(update :features
(fn [features]
(let [features (cfeat/check-supported-features! features)]
(-> (::features cfg #{})
(set/union features)
;; We never want to store
;; frontend-only features on file
(set/difference cfeat/frontend-only-features))))))]
(-> (::features cfg #{})
(set/union features)
;; We never want to store
;; frontend-only features on file
(set/difference cfeat/frontend-only-features)))))]
(when (contains? cf/flags :file-schema-validation)
(fval/validate-file-schema! file))
@@ -529,34 +522,3 @@
(l/error :hint "file schema validation error" :cause result))))
(insert-file! cfg file opts)))
(defn register-pending-migrations!
"All features that are enabled and requires explicit migration are
added to the state for a posterior migration step."
[cfg {:keys [id features] :as file}]
(doseq [feature (-> (::features cfg)
(set/difference cfeat/no-migration-features)
(set/difference cfeat/backend-only-features)
(set/difference features))]
(vswap! *state* update :pending-to-migrate (fnil conj []) [feature id]))
file)
(defn apply-pending-migrations!
"Apply alredy registered pending migrations to files"
[cfg]
(doseq [[feature file-id] (-> *state* deref :pending-to-migrate)]
(case feature
"components/v2"
(feat.compv2/migrate-file! cfg file-id
:validate? (::validate cfg true)
:skip-on-graphic-error? true)
"fdata/shape-data-type"
nil
(ex/raise :type :internal
:code :no-migration-defined
:hint (str/ffmt "no migation for feature '%' on file importation" feature)
:feature feature))))

View File

@@ -0,0 +1,45 @@
;; 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.binfile.migrations
"A binfile related migrations handling"
(:require
[app.binfile.common :as bfc]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.features.components-v2 :as feat.compv2]
[clojure.set :as set]
[cuerdas.core :as str]))
(defn register-pending-migrations!
"All features that are enabled and requires explicit migration are
added to the state for a posterior migration step."
[cfg {:keys [id features] :as file}]
(doseq [feature (-> (::features cfg)
(set/difference cfeat/no-migration-features)
(set/difference cfeat/backend-only-features)
(set/difference features))]
(vswap! bfc/*state* update :pending-to-migrate (fnil conj []) [feature id]))
file)
(defn apply-pending-migrations!
"Apply alredy registered pending migrations to files"
[cfg]
(doseq [[feature file-id] (-> bfc/*state* deref :pending-to-migrate)]
(case feature
"components/v2"
(feat.compv2/migrate-file! cfg file-id
:validate? (::validate cfg true)
:skip-on-graphic-error? true)
"fdata/shape-data-type"
nil
(ex/raise :type :internal
:code :no-migration-defined
:hint (str/ffmt "no migation for feature '%' on file importation" feature)
:feature feature))))

View File

@@ -9,6 +9,7 @@
(:refer-clojure :exclude [assert])
(:require
[app.binfile.common :as bfc]
[app.binfile.migrations :as bfm]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
@@ -473,7 +474,7 @@
(read-section options))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects])
(bfc/apply-pending-migrations! cfg)
(bfm/apply-pending-migrations! cfg)
;; Knowing that the ids of the created files are in index,
;; just lookup them and return it as a set

View File

@@ -9,6 +9,7 @@
(:refer-clojure :exclude [read])
(:require
[app.binfile.common :as bfc]
[app.binfile.migrations :as bfm]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
@@ -735,7 +736,7 @@
(bfc/process-file))]
(bfc/register-pending-migrations! cfg file)
(bfm/register-pending-migrations! cfg file)
(bfc/save-file! cfg file ::db/return-keys false)
file-id')))
@@ -875,14 +876,17 @@
:manifest manifest))
;; Check if all files referenced on manifest are present
(doseq [{file-id :id} (:files manifest)]
(doseq [{file-id :id features :features} (:files manifest)]
(let [path (str "files/" file-id ".json")]
(when-not (get-zip-entry input path)
(ex/raise :type :validation
:code :invalid-binfile-v3
:hint "some files referenced on manifest not found"
:path path
:file-id file-id))))
:file-id file-id))
(cfeat/check-supported-features! features)))
(events/tap :progress {:section :manifest})
@@ -912,7 +916,7 @@
(import-file-media cfg)
(import-file-thumbnails cfg)
(bfc/apply-pending-migrations! cfg)
(bfm/apply-pending-migrations! cfg)
ids)))))))

View File

@@ -12,6 +12,7 @@
[app.common.exceptions :as ex]
[app.common.flags :as flags]
[app.common.schema :as sm]
[app.common.uri :as u]
[app.common.version :as v]
[app.util.overrides]
[app.util.time :as dt]
@@ -228,19 +229,16 @@
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]
[:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
(def default-flags
[:enable-backend-api-doc
:enable-backend-openapi-doc
:enable-backend-worker
:enable-secure-session-cookies
:enable-email-verification
:enable-v2-migration])
(defn- parse-flags
[config]
(flags/parse flags/default
default-flags
(:flags config)))
(let [public-uri (c/get config :public-uri)
public-uri (some-> public-uri (u/uri))
extra-flags (if (and public-uri
(= (:scheme public-uri) "http")
(not= (:host public-uri) "localhost"))
#{:disable-secure-session-cookies}
#{})]
(flags/parse flags/default extra-flags (:flags config))))
(defn read-env
[prefix]

View File

@@ -12,6 +12,7 @@
[app.binfile.v3 :as bf.v3]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.uuid :as uuid]
@@ -21,6 +22,7 @@
[app.rpc.commands.auth :as auth]
[app.rpc.commands.files-create :refer [create-file]]
[app.rpc.commands.profile :as profile]
[app.rpc.commands.teams :as teams]
[app.setup :as-alias setup]
[app.srepl.helpers :as srepl]
[app.storage :as-alias sto]
@@ -317,7 +319,10 @@
:hint "missing upload file"))
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)]
project-id (:default-project-id profile)
team (teams/get-team pool
:profile-id profile-id
:project-id project-id)]
(when-not project-id
(ex/raise :type :validation
@@ -329,7 +334,8 @@
cfg (assoc cfg
::bfc/profile-id profile-id
::bfc/project-id project-id
::bfc/input path)]
::bfc/input path
::bfc/features (cfeat/get-team-enabled-features cf/flags team))]
(if (= format :binfile-v3)
(bf.v3/import-files! cfg)

View File

@@ -435,7 +435,10 @@
:fn (mg/resource "app/migrations/sql/0137-add-file-migration-table.sql")}
{:name "0138-mod-file-data-fragment-table.sql"
:fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")}])
:fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")}
{:name "0139-mod-file-change-table.sql"
:fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@@ -0,0 +1,5 @@
ALTER TABLE file_change
DROP CONSTRAINT file_change_file_id_fkey,
DROP CONSTRAINT file_change_profile_id_fkey,
ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE,
ADD FOREIGN KEY (profile_id) REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE;

View File

@@ -10,8 +10,10 @@
[app.binfile.common :as bfc]
[app.binfile.v1 :as bf.v1]
[app.binfile.v3 :as bf.v3]
[app.common.features :as cfeat]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.config :as cf]
[app.db :as db]
[app.http.sse :as sse]
[app.loggers.audit :as-alias audit]
@@ -20,6 +22,7 @@
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.tasks.file-gc]
[app.util.services :as sv]
@@ -91,41 +94,30 @@
;; --- Command: import-binfile
(defn- import-binfile-v1
[{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}]
(let [cfg (-> cfg
(assoc ::bfc/project-id project-id)
(assoc ::bfc/profile-id profile-id)
(assoc ::bfc/name name)
(assoc ::bfc/input (:path file)))]
;; NOTE: the importation process performs some operations that are
;; not very friendly with virtual threads, and for avoid
;; unexpected blocking of other concurrent operations we dispatch
;; that operation to a dedicated executor.
(px/invoke! executor (partial bf.v1/import-files! cfg))))
(defn- import-binfile-v3
[{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}]
(let [cfg (-> cfg
(assoc ::bfc/project-id project-id)
(assoc ::bfc/profile-id profile-id)
(assoc ::bfc/name name)
(assoc ::bfc/input (:path file)))]
;; NOTE: the importation process performs some operations that are
;; not very friendly with virtual threads, and for avoid
;; unexpected blocking of other concurrent operations we dispatch
;; that operation to a dedicated executor.
(px/invoke! executor (partial bf.v3/import-files! cfg))))
(defn- import-binfile
[{:keys [::db/pool] :as cfg} {:keys [project-id version] :as params}]
(let [result (case (int version)
1 (import-binfile-v1 cfg params)
3 (import-binfile-v3 cfg params))]
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id project-id version name file]}]
(let [team (teams/get-team pool
:profile-id profile-id
:project-id project-id)
cfg (-> cfg
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
(assoc ::bfc/project-id project-id)
(assoc ::bfc/profile-id profile-id)
(assoc ::bfc/name name)
(assoc ::bfc/input (:path file)))
;; NOTE: the importation process performs some operations that are
;; not very friendly with virtual threads, and for avoid
;; unexpected blocking of other concurrent operations we dispatch
;; that operation to a dedicated executor.
result (case (int version)
1 (px/invoke! executor (partial bf.v1/import-files! cfg))
3 (px/invoke! executor (partial bf.v3/import-files! cfg)))]
(db/update! pool :project
{:modified-at (dt/now)}
{:id project-id})
result))
(def ^:private schema:import-binfile

View File

@@ -323,6 +323,7 @@
file (-> (get-file cfg id :project-id project-id)
(assoc :permissions perms)
(assoc :team-id (:id team))
(check-version!))]
(-> (cfeat/get-team-enabled-features cf/flags team)
@@ -384,8 +385,10 @@
f.revn,
f.vern,
f.is_shared,
ft.media_id AS thumbnail_id
ft.media_id AS thumbnail_id,
p.team_id
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id
and ft.revn = f.revn
and ft.deleted_at is null)
@@ -539,7 +542,8 @@
f.modified_at,
f.name,
f.is_shared,
ft.media_id
ft.media_id,
p.team_id
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn and ft.deleted_at is null)
@@ -549,7 +553,6 @@
and p.team_id = ?
order by f.modified_at desc")
(defn- get-library-summary
[cfg {:keys [id data] :as file}]
(letfn [(assets-sample [assets limit]
@@ -611,6 +614,7 @@
SELECT l.id,
l.features,
l.project_id,
p.team_id,
l.created_at,
l.modified_at,
l.deleted_at,
@@ -620,6 +624,7 @@
l.synced_at,
l.is_shared
FROM libs AS l
INNER JOIN project AS p ON (p.id = l.project_id)
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
(defn get-file-libraries
@@ -686,7 +691,8 @@
f.name,
f.is_shared,
ft.media_id AS thumbnail_id,
row_number() over w as row_num
row_number() over w as row_num,
p.team_id
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.files-snapshot
(:require
[app.binfile.common :as bfc]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
@@ -22,7 +23,6 @@
[app.rpc.quotes :as quotes]
[app.storage :as sto]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[cuerdas.core :as str]))
@@ -58,26 +58,6 @@
(files/check-read-permissions! conn profile-id file-id)
(get-file-snapshots conn file-id))))
(def ^:private sql:get-file
"SELECT f.*,
p.id AS project_id,
p.team_id AS team_id
FROM file AS f
INNER JOIN project AS p ON (p.id = f.project_id)
WHERE f.id = ?")
(defn- get-file
[cfg file-id]
(let [file (->> (db/exec-one! cfg [sql:get-file 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 assoc ::id file-id)
(update :data blob/encode)))))
(defn- generate-snapshot-label
[]
(let [ts (-> (dt/now)
@@ -87,49 +67,53 @@
(str "snapshot-" ts)))
(defn create-file-snapshot!
[cfg profile-id file-id label]
(let [file (get-file cfg file-id)
[cfg file & {:keys [label created-by deleted-at profile-id]
:or {deleted-at :default
created-by :system}}]
(assert (#{:system :user :admin} created-by)
"expected valid keyword for created-by")
(let [conn
(db/get-connection cfg)
;; NOTE: final user never can provide label as `:system`
;; keyword because the validator implies label always as
;; string; keyword is used for signal a special case
created-by
(if (= label :system)
"system"
"user")
(name created-by)
deleted-at
(if (= label :system)
(cond
(= deleted-at :default)
(dt/plus (dt/now) (cf/get-deletion-delay))
(dt/instant? deleted-at)
deleted-at
:else
nil)
label
(if (= label :system)
(str "internal/snapshot/" (:revn file))
(or label (generate-snapshot-label)))
(or label (generate-snapshot-label))
snapshot-id
(uuid/next)]
(uuid/next)
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/project-id (:project-id file))
(assoc ::quotes/team-id (:team-id file))
(assoc ::quotes/file-id (:id file))
(quotes/check! {::quotes/id ::quotes/snapshots-per-file}
{::quotes/id ::quotes/snapshots-per-team}))
data
(blob/encode (:data file))
features
(db/encode-pgarray (:features file) conn "text")]
(l/debug :hint "creating file snapshot"
:file-id (str file-id)
:file-id (str (:id file))
:id (str snapshot-id)
:label label)
(db/insert! cfg :file-change
{:id snapshot-id
:revn (:revn file)
:data (:data file)
:data data
:version (:version file)
:features (:features file)
:features features
:profile-id profile-id
:file-id (:id file)
:label label
@@ -146,12 +130,25 @@
(sv/defmethod ::create-file-snapshot
{::doc/added "1.20"
::sm/params schema:create-file-snapshot}
[cfg {:keys [::rpc/profile-id file-id label]}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
(create-file-snapshot! cfg profile-id file-id label))))
::sm/params schema:create-file-snapshot
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id label]}]
(files/check-edition-permissions! conn profile-id file-id)
(let [file (bfc/get-file cfg file-id)
project (db/get-by-id cfg :project (:project-id file))]
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/project-id (:project-id file))
(assoc ::quotes/team-id (:team-id project))
(assoc ::quotes/file-id (:id file))
(quotes/check! {::quotes/id ::quotes/snapshots-per-file}
{::quotes/id ::quotes/snapshots-per-team}))
(create-file-snapshot! cfg file
{:label label
:profile-id profile-id
:created-by :user})))
(defn restore-file-snapshot!
[{:keys [::db/conn ::mbus/msgbus] :as cfg} file-id snapshot-id]
@@ -237,8 +234,11 @@
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
(create-file-snapshot! cfg profile-id file-id :system)
(restore-file-snapshot! cfg file-id id))))
(let [file (bfc/get-file cfg file-id)]
(create-file-snapshot! cfg file
{:profile-id profile-id
:created-by :system})
(restore-file-snapshot! cfg file-id id)))))
(def ^:private schema:update-file-snapshot
[:map {:title "update-file-snapshot"}

View File

@@ -406,12 +406,16 @@
:prefix "penpot.template."
:suffix ""
:min-age "30m")
format (bfc/parse-file-format template)
format (bfc/parse-file-format template)
team (teams/get-team conn
:profile-id profile-id
:project-id project-id)
cfg (-> cfg
(assoc ::bfc/project-id project-id)
(assoc ::bfc/profile-id profile-id)
(assoc ::bfc/input template))
(assoc ::bfc/input template)
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
result (if (= format :binfile-v3)
(px/invoke! executor (partial bf.v3/import-files! cfg))

View File

@@ -58,7 +58,8 @@
[:welcome-file-id {:optional true} [:maybe ::sm/boolean]]
[:release-notes-viewed {:optional true}
[::sm/text {:max 100}]]
[:notifications {:optional true} schema:props-notifications]])
[:notifications {:optional true} schema:props-notifications]
[:workspace-visited {:optional true} ::sm/boolean]])
(def schema:profile
[:map {:title "Profile"}

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.teams-invitations
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
@@ -15,7 +16,6 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.email :as eml]
[app.loggers.audit :as audit]
[app.main :as-alias main]
@@ -34,7 +34,6 @@
;; --- Mutation: Create Team Invitation
(def sql:upsert-team-invitation
"insert into team_invitation(id, team_id, email_to, created_by, role, valid_until)
values (?, ?, ?, ?, ?, ?)
@@ -79,27 +78,23 @@
[:role ::types.team/role]
[:email ::sm/email]])
(def ^:private check-create-invitation-params!
(def ^:private check-create-invitation-params
(sm/check-fn schema:create-invitation))
(defn- allow-invitation-emails?
[member]
(let [notifications (dm/get-in member [:props :notifications])]
(not= :none (:email-invites notifications))))
(defn- create-invitation
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
(dm/assert!
"expected valid connection on cfg parameter"
(db/connection? conn))
(dm/assert!
"expected valid params for `create-invitation` fn"
(check-create-invitation-params! params))
(assert (db/connection? conn) "expected valid connection on cfg parameter")
(assert (check-create-invitation-params params))
(let [email (profile/clean-email email)
member (profile/get-profile-by-email conn email)]
(teams/check-profile-muted conn member)
(teams/check-email-bounce conn email true)
(teams/check-email-spam conn email true)
;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the
;; team as-is, without email roundtrip.
@@ -125,62 +120,65 @@
nil)
(let [id (uuid/next)
expire (dt/in-future "168h") ;; 7 days
invitation (db/exec-one! conn [sql:upsert-team-invitation id
(:id team) (str/lower email)
(:id profile)
(name role) expire
(name role) expire])
updated? (not= id (:id invitation))
profile-id (:id profile)
tprops {:profile-id profile-id
:invitation-id (:id invitation)
:valid-until expire
:team-id (:id team)
:member-email (:email-to invitation)
:member-id (:id member)
:role role}
itoken (create-invitation-token cfg tprops)
ptoken (create-profile-identity-token cfg profile-id)]
(do
(some->> member (teams/check-profile-muted conn))
(teams/check-email-bounce conn email true)
(teams/check-email-spam conn email true)
(when (contains? cf/flags :log-invitation-tokens)
(l/info :hint "invitation token" :token itoken))
(let [id (uuid/next)
expire (dt/in-future "168h") ;; 7 days
invitation (db/exec-one! conn [sql:upsert-team-invitation id
(:id team) (str/lower email)
(:id profile)
(name role) expire
(name role) expire])
updated? (not= id (:id invitation))
profile-id (:id profile)
tprops {:profile-id profile-id
:invitation-id (:id invitation)
:valid-until expire
:team-id (:id team)
:member-email (:email-to invitation)
:member-id (:id member)
:role role}
itoken (create-invitation-token cfg tprops)
ptoken (create-profile-identity-token cfg profile-id)]
(let [props (-> (dissoc tprops :profile-id)
(audit/clean-props))
evname (if updated?
"update-team-invitation"
"create-team-invitation")
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name evname)
(assoc ::audit/props props))]
(audit/submit! cfg event))
(when (contains? cf/flags :log-invitation-tokens)
(l/info :hint "invitation token" :token itoken))
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
:public-uri (cf/get :public-uri)
:to email
:invited-by (:fullname profile)
:team (:name team)
:token itoken
:extra-data ptoken})
(let [props (-> (dissoc tprops :profile-id)
(audit/clean-props))
evname (if updated?
"update-team-invitation"
"create-team-invitation")
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name evname)
(assoc ::audit/props props))]
(audit/submit! cfg event))
itoken))))
(when (allow-invitation-emails? member)
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
:public-uri (cf/get :public-uri)
:to email
:invited-by (:fullname profile)
:team (:name team)
:token itoken
:extra-data ptoken}))
(defn- add-user-to-team
[conn profile team role email]
itoken)))))
(defn- add-member-to-team
[conn profile team role member]
(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)}
(get types.team/permissions-for-role role))]
;; Do not allow blocked users to join teams.
;; Do not allow blocked users to join teams.
(when (:is-blocked member)
(ex/raise :type :restriction
:code :profile-blocked))
@@ -205,29 +203,33 @@
(eml/send! {::eml/conn conn
::eml/factory eml/join-team
:public-uri (cf/get :public-uri)
:to email
:to (:email member)
:invited-by (:fullname profile)
:team (:name team)
:team-id (:id team)})))
(def sql:valid-requests-email
"SELECT p.email
(def ^:private sql:valid-access-request-profiles
"SELECT p.id, p.email, p.is_blocked
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()")
AND tr.auto_join_until > now()
AND (p.deleted_at IS NULL OR
p.deleted_at > now())")
(defn- get-valid-requests-email
(defn- get-valid-access-request-profiles
[conn team-id]
(db/exec! conn [sql:valid-requests-email team-id]))
(db/exec! conn [sql:valid-access-request-profiles team-id]))
(def ^:private xf:map-email
(map :email))
(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)))
(let [emails (set emails)
join-requests (->> (get-valid-access-request-profiles conn (:id team))
(d/index-by :email))
team-members (into #{} xf:map-email
(teams/get-team-members conn (:id team)))
@@ -245,8 +247,10 @@
;; 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)))
(->> join-requests
(filter #(contains? emails (key %)))
(map val)
(run! (partial add-member-to-team conn profile team role)))
invitations))
@@ -572,5 +576,3 @@
(with-meta {:request request}
{::audit/props {:request 1}}))))

View File

@@ -16,7 +16,8 @@
[app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]))
[app.util.services :as sv]
[cuerdas.core :as str]))
;; --- QUERY: View Only Bundle
@@ -26,6 +27,27 @@
(update :pages (fn [pages] (filterv #(contains? allowed %) pages)))
(update :pages-index select-keys allowed)))
(defn obfuscate-email
[email]
(let [[name domain]
(str/split email "@" 2)
[_ rest]
(str/split domain "." 2)
name
(if (> (count name) 3)
(str (subs name 0 1) (apply str (take (dec (count name)) (repeat "*"))))
"****")]
(str name "@****." rest)))
(defn anonymize-member
[member]
(-> (select-keys member [:id :email :name :fullname :photo-id])
(update :email obfuscate-email)
(assoc :can-read true)))
(defn- get-view-only-bundle
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}]
(let [file (files/get-file cfg file-id)
@@ -37,7 +59,10 @@
team (-> (db/get conn :team {:id (:team-id project)})
(teams/decode-row))
members (teams/get-team-members conn (:team-id project))
members (cond->> (teams/get-team-members conn (:team-id project))
(= :share-link (:type perms))
(mapv anonymize-member))
member-ids (into #{} (map :id) members)
perms (assoc perms :in-team (contains? member-ids profile-id))

View File

@@ -15,7 +15,8 @@
[app.features.components-v2 :as feat.comp-v2]
[app.main :as main]
[app.rpc.commands.files :as files]
[app.rpc.commands.files-snapshot :as fsnap]))
[app.rpc.commands.files-snapshot :as fsnap]
[app.util.time :as dt]))
(def ^:dynamic *system* nil)
@@ -96,8 +97,11 @@
(let [conn (db/get-connection system)]
(->> (feat.comp-v2/get-and-lock-team-files conn team-id)
(reduce (fn [result file-id]
(fsnap/create-file-snapshot! system nil file-id label)
(inc result))
(let [file (fsnap/get-file-snapshots system file-id)]
(fsnap/create-file-snapshot! system file
{:label label
:created-by :admin})
(inc result)))
0))))
(defn restore-team-snapshot!
@@ -143,7 +147,10 @@
(cfv/validate-file-schema! file'))
(when (string? label)
(fsnap/create-file-snapshot! system nil file-id label))
(fsnap/create-file-snapshot! system file
{:label label
:deleted-at (dt/in-future {:days 30})
:created-by :admin}))
(let [file' (update file' :revn inc)]
(bfc/update-file! system file')

View File

@@ -431,25 +431,40 @@
process-file
(fn [file-id idx tpoint]
(try
(l/trc :hint "process:file:start" :file-id (str file-id) :index idx)
(let [system (assoc main/system ::db/rollback rollback?)]
(db/tx-run! system (fn [system]
(binding [h/*system* system]
(h/process-file! system file-id update-fn opts)))))
(catch Throwable cause
(l/wrn :hint "unexpected error on processing file (skiping)"
(let [thread-id (px/get-thread-id)]
(try
(l/trc :hint "process:file:start"
:tid thread-id
:file-id (str file-id)
:index idx
:cause cause))
(finally
(ps/release! sjobs)
(let [elapsed (dt/format-duration (tpoint))]
(l/trc :hint "process:file:end"
:index idx)
(let [system (assoc main/system ::db/rollback rollback?)]
(db/tx-run! system (fn [system]
(binding [h/*system* system]
(h/process-file! system file-id update-fn opts)))))
(catch Throwable cause
(l/wrn :hint "unexpected error on processing file (skiping)"
:tid thread-id
:file-id (str file-id)
:index idx
:elapsed elapsed)))))
:cause cause))
(finally
(when-let [pause (:pause opts)]
(Thread/sleep (int pause)))
(ps/release! sjobs)
(let [elapsed (dt/format-duration (tpoint))]
(l/trc :hint "process:file:end"
:tid thread-id
:file-id (str file-id)
:index idx
:elapsed elapsed))))))
process-file*
(fn [idx file-id]
(ps/acquire! sjobs)
(px/run! executor (partial process-file file-id idx (dt/tpoint)))
(inc idx))
process-files
(fn [{:keys [::db/conn] :as system}]
@@ -457,14 +472,12 @@
(db/exec! conn ["SET idle_in_transaction_session_timeout = 0"])
(try
(reduce (fn [idx file-id]
(ps/acquire! sjobs)
(px/run! executor (partial process-file file-id idx (dt/tpoint)))
(inc idx))
0
(->> (db/cursor conn [query] {:chunk-size 1})
(take max-items)
(map :id)))
(->> (db/plan conn [query])
(transduce (comp
(take max-items)
(map :id))
(completing process-file*)
0))
(finally
;; Close and await tasks
(pu/close! executor))))]

View File

@@ -40,6 +40,11 @@
:file-id id
:cause cause))))
;; Mark file change to be deleted
(db/update! conn :file-change
{:deleted-at deleted-at}
{:file-id id})
;; Mark file media objects to be deleted
(db/update! conn :file-media-object
{:deleted-at deleted-at}

View File

@@ -26,7 +26,7 @@
(defn- delete-profiles!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-profiles min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-profiles min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id photo-id]}]
(l/trc :hint "permanently delete" :rel "profile" :id (str id))
@@ -49,7 +49,7 @@
(defn- delete-teams!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-teams min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-teams min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id photo-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "team"
@@ -77,7 +77,7 @@
(defn- delete-fonts!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-fonts min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-fonts min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id team-id deleted-at] :as font}]
(l/trc :hint "permanently delete"
:rel "team-font-variant"
@@ -109,7 +109,7 @@
(defn- delete-projects!
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
(->> (db/cursor conn [sql:get-projects min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-projects min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id team-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "project"
@@ -135,7 +135,7 @@
(defn- delete-files!
[{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}]
(->> (db/cursor conn [sql:get-files min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-files min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id deleted-at project-id] :as file}]
(l/trc :hint "permanently delete"
:rel "file"
@@ -164,7 +164,7 @@
(defn delete-file-thumbnails!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-thumbnails min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-file-thumbnails min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id revn media-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "file-thumbnail"
@@ -193,7 +193,7 @@
(defn delete-file-object-thumbnails!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-object-thumbnails min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-file-object-thumbnails min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id object-id media-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "file-tagged-object-thumbnail"
@@ -222,7 +222,7 @@
(defn- delete-file-data-fragments!
[{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}]
(->> (db/cursor conn [sql:get-file-data-fragments min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-file-data-fragments min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id id deleted-at data-ref-id]}]
(l/trc :hint "permanently delete"
:rel "file-data-fragment"
@@ -248,7 +248,7 @@
(defn- delete-file-media-objects!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-media-objects min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-file-media-objects min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id file-id deleted-at] :as fmo}]
(l/trc :hint "permanently delete"
:rel "file-media-object"
@@ -275,9 +275,9 @@
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-change!
(defn- delete-file-changes!
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-change min-age chunk-size] {:chunk-size 5})
(->> (db/plan conn [sql:get-file-change min-age chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id file-id deleted-at] :as xlog}]
(l/trc :hint "permanently delete"
:rel "file-change"
@@ -299,11 +299,11 @@
#'delete-file-data-fragments!
#'delete-file-object-thumbnails!
#'delete-file-thumbnails!
#'delete-file-changes!
#'delete-files!
#'delete-projects!
#'delete-fonts!
#'delete-teams!
#'delete-file-change!])
#'delete-teams!])
(defn- execute-proc!
"A generic function that executes the specified proc iterativelly
@@ -326,7 +326,7 @@
[k v]
{k (assoc v
::min-age (cf/get-deletion-delay)
::chunk-size 50)})
::chunk-size 100)})
(defmethod ig/init-key ::handler
[_ cfg]

View File

@@ -97,6 +97,7 @@
(th/db-query :file-change
{:file-id (:id file)}
{:order-by [:created-at]})]
(t/is (= 2 (count rows)))
(t/is (= "user" (:created-by row1)))
(t/is (= "system" (:created-by row2)))))

View File

@@ -37,18 +37,17 @@
:role :editor}]
;; invite external user without complaints
(let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)
(let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)
;; retrieve the value from the database and check its content
invitation (db/exec-one!
th/*pool*
["select count(*) as num from team_invitation where team_id = ? and email_to = ?"
(:team-id data) "foo@bar.com"])]
invitations (th/db-query :team-invitation
{:team-id (:team-id data)
:email-to "foo@bar.com"})]
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock))))
(t/is (= 1 (:num invitation))))
(t/is (= 1 (count invitations))))
;; invite internal user without complaints
(th/reset-mock! mock)
@@ -102,6 +101,105 @@
(t/is (= :validation (:type edata)))
(t/is (= :member-is-muted (:code edata))))))))
(t/deftest create-team-invitations-with-request-access
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})
requester (th/create-profile* 2 {:is-active true :email "requester@example.com"})
team (th/create-team* 1 {:profile-id (:id profile1)})
proj (th/create-project* 1 {:profile-id (:id profile1)
:team-id (:id team)})
file (th/create-file* 1 {:profile-id (:id profile1)
:project-id (:id proj)})]
(let [data {::th/type :create-team-access-request
::rpc/profile-id (:id requester)
:file-id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))
(th/reset-mock! mock)
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:team-id (:id team)
:role :editor
:emails ["requester@example.com"]}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
;; Check that request is properly removed
(let [requests (th/db-query :team-access-request
{:requester-id (:id requester)})]
(t/is (= 0 (count requests))))
(let [rows (th/db-query :team-profile-rel {:team-id (:id team)})]
(t/is (= 2 (count rows))))))))
(t/deftest create-team-invitations-with-request-access-2
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})
requester (th/create-profile* 2 {:is-active true
:email "requester@example.com"})
team (th/create-team* 1 {:profile-id (:id profile1)})
proj (th/create-project* 1 {:profile-id (:id profile1)
:team-id (:id team)})
file (th/create-file* 1 {:profile-id (:id profile1)
:project-id (:id proj)})]
;; Create the first access request
(let [data {::th/type :create-team-access-request
::rpc/profile-id (:id requester)
:file-id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))
(th/reset-mock! mock)
;; Proceed to delete the requester user
(th/db-update! :profile
{:deleted-at (dt/in-past "1h")}
{:id (:id requester)})
;; Create a new profile with the same email
(let [requester' (th/create-profile* 3 {:is-active true :email "requester@example.com"})]
;; Create a request access with new requester
(let [data {::th/type :create-team-access-request
::rpc/profile-id (:id requester')
:file-id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))
(th/reset-mock! mock)
;; Create an invitation for the requester email
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:team-id (:id team)
:role :editor
:emails ["requester@example.com"]}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))
;; Check that request is properly removed
(let [requests (th/db-query :team-access-request
{:requester-id (:id requester')})]
(t/is (= 0 (count requests))))
(let [[r1 r2 :as rows] (th/db-query :team-profile-rel
{:team-id (:id team)}
{:order-by [:created-at]})]
(t/is (= 2 (count rows)))
(t/is (= (:profile-id r1) (:id profile1)))
(t/is (= (:profile-id r2) (:id requester'))))))))
(t/deftest invitation-tokens
(with-mocks [mock {:target 'app.email/send! :return nil}]
@@ -486,14 +584,12 @@
;; request success
(let [out (th/command! data)
;; retrieve the value from the database and check its content
request (db/exec-one!
th/*pool*
["select count(*) as num from team_access_request where team_id = ? and requester_id = ?"
(:id team) (:id requester)])]
requests (th/db-query :team-access-request
{:team-id (:id team)
:requester-id (:id requester)})]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
(t/is (= 1 (:num request))))
(t/is (= 1 (count requests))))
;; request again fails
(th/reset-mock! mock)
@@ -509,10 +605,10 @@
;; request again when is expired success
(th/reset-mock! mock)
(db/exec-one!
th/*pool*
["update team_access_request set valid_until = ? where team_id = ? and requester_id = ?"
(dt/in-past "1h") (:id team) (:id requester)])
(th/db-update! :team-access-request
{:valid-until (dt/in-past "1h")}
{:team-id (:id team)
:requester-id (:id requester)})
(t/is (th/success? (th/command! data)))
(t/is (= 1 (:call-count @mock))))))

View File

@@ -141,7 +141,7 @@
(keep flag->feature))
(defn get-enabled-features
"Get the globally enabled fratures set."
"Get the globally enabled features set."
[flags]
(into default-features xf-flag-to-feature flags))

View File

@@ -154,7 +154,7 @@
(let [data (::file-data (meta changes))]
(dm/get-in data [:pages-index uuid/zero :objects])))
(defn- apply-changes-local
(defn apply-changes-local
[changes]
(dm/assert!
"expected valid changes"

View File

@@ -570,10 +570,9 @@
(into xform:collect-media-refs (vals (:components data)))
(into (keys (:media data)))))
(defn relink-media-refs
"A function responsible to analyze all file data and replace the
old :component-file reference with the new ones, using the provided
file-index."
(defn relink-refs
"A function responsible to analyze the file data or shape for references
and apply lookup-index on it."
[data lookup-index]
(letfn [(process-map-form [form]
(cond-> form
@@ -724,7 +723,7 @@
(defn split-by-last-period
"Splits a string into two parts:
the text before and including the last period,
the text before and including the last period,
and the text after the last period."
[s]
(if-let [last-period (str/last-index-of s ".")]

View File

@@ -7,13 +7,145 @@
(ns app.common.flags
"Flags parsing algorithm."
(:require
[clojure.set :as set]
[cuerdas.core :as str]))
(def login
"Flags related to login features"
#{;; Allows registration with login / password
;; if disabled, it's still possible to register/login with providers
:registration
;; Redundant flag. TODO: remove it
:login
;; enables the section of Access Tokens on profile.
:access-tokens
;; Uses email and password as credentials.
:login-with-password
;; Uses Github authentication as credentials.
:login-with-github
;; Uses GitLab authentication as credentials.
:login-with-gitlab
;; Uses Google/Gmail authentication as credentials.
:login-with-google
;; Uses LDAP authentication as credentials.
:login-with-ldap
;; Uses any generic authentication provider that implements OIDC protocol as credentials.
:login-with-oidc
;; Allows registration with Open ID
:oidc-registration
;; This logs to console the invitation tokens. It's useful in case the SMTP is not configured.
:log-invitation-tokens})
(def email
"Flags related to email features"
#{;; Uses the domains in whitelist as the only allowed domains to register in the application.
;; Used with PENPOT_REGISTRATION_DOMAIN_WHITELIST
:email-whitelist
;; Prevents the domains in blacklist to register in the application.
;; Used with PENPOT_REGISTRATION_DOMAIN_BLACKLIST
:email-blacklist
;; Skips the email verification process. Not recommended for production environments.
:email-verification
;; Only used if SMTP is disabled. Logs the emails into the console.
:log-emails
;; Enable it to configure email settings.
:smtp
;; Enables the debug mode of the SMTP library.
:smtp-debug})
(def varia
"Rest of the flags"
#{:audit-log
:audit-log-archive
:audit-log-gc
:auto-file-snapshot
;; enables the `/api/doc` endpoint that lists all the rpc methods available.
:backend-api-doc
;; TODO: remove it and use only `backend-api-doc` flag
:backend-openapi-doc
;; Disable it to start the RPC without the worker.
:backend-worker
;; Only for development
:component-thumbnails
;; enables the default cors configuration that allows all domains (currently this configuration is only used for development).
:cors
;; Enables the templates dialog on Penpot dashboard.
:dashboard-templates-section
;; disabled by default. When enabled, Penpot create demo users with a 7 days expiration.
:demo-users
;; disabled by default. When enabled, it displays a warning that this is a test instance and data will be deleted periodically.
:demo-warning
;; Activates the schema validation during update file.
:file-schema-validation
;; Reports the schema validation errors internally.
:soft-file-schema-validation
;; Activates the referential integrity validation during update file; related to components-v2.
:file-validation
;; Reports the referential integrity validation errors internally.
:soft-file-validation
;; TODO: deprecate this flag and consolidate the code
:frontend-svgo
;; TODO: deprecate this flag and consolidate the code
:exporter-svgo
;; TODO: deprecate this flag and consolidate the code
:backend-svgo
;; If enabled, it makes the Google Fonts available.
:google-fonts-provider
;; Only for development.
:nrepl-server
;; Interactive repl. Only for development.
:urepl-server
;; Programatic access to the runtime, used in administrative tasks.
;; It's mandatory to enable it to use the `manage.py` script.
:prepl-server
;; Shows the onboarding modals right after registration.
:onboarding
:quotes
:soft-quotes
;; Concurrency limit.
:rpc-climit
;; Rate limit.
:rpc-rlimit
;; Soft rate limit.
:soft-rpc-rlimit
;; Disable it if you want to serve Penpot under a different domain than `http://localhost` without HTTPS.
:secure-session-cookies
;; If `cors` enabled, this is ignored.
:strict-session-cookies
:telemetry
:terms-and-privacy-checkbox
;; Only for developtment.
:tiered-file-data-storage
:transit-readable-response
:user-feedback
;; TODO: remove this flag.
:v2-migration
:webhooks
;; TODO: deprecate this flag and consolidate the code
:export-file-v3
:render-wasm-dpr
:hide-release-modal})
(def all-flags
(set/union email login varia))
(def default
"A common flags that affects both: backend and frontend."
"Flags with default configuration"
[:enable-registration
:enable-login-with-password
:enable-export-file-v3
:enable-login-with-password])
:enable-frontend-svgo
:enable-exporter-svgo
:enable-backend-svgo
:enable-backend-api-doc
:enable-backend-openapi-doc
:enable-backend-worker
:enable-secure-session-cookies
:enable-email-verification
:enable-onboarding
:enable-dashboard-templates-section
:enable-google-fonts-provider
:enable-component-thumbnails])
(defn parse
[& flags]

View File

@@ -212,8 +212,10 @@
(if (= type :column)
[:column :column-span]
[:row :row-span])
from-idx (dec (get cell prop))
to-idx (+ (dec (get cell prop)) (get cell prop-span))
from-idx (-> (dec (get cell prop))
(mth/clamp 0 (dec (count track-list))))
to-idx (-> (+ (dec (get cell prop)) (get cell prop-span))
(mth/clamp 0 (dec (count track-list))))
tracks (subvec track-list from-idx to-idx)]
(some? (->> tracks (d/seek #(= :flex (:type %)))))))
@@ -291,8 +293,10 @@
(fn [allocated cell]
(let [shape-id (first (:shapes cell))
from-idx (dec (get cell prop))
to-idx (+ (dec (get cell prop)) (get cell prop-span))
from-idx (-> (dec (get cell prop))
(mth/clamp 0 (dec (count track-list))))
to-idx (-> (+ (dec (get cell prop)) (get cell prop-span))
(mth/clamp 0 (dec (count track-list))))
indexed-tracks (subvec (d/enumerate track-list) from-idx to-idx)
to-allocate (size-to-allocate type parent (get children-map shape-id) cell bounds objects)
@@ -597,11 +601,10 @@
row (nth row-tracks (dec (:row grid-cell)) nil)
column-start-p (:start-p column)
row-start-p (:start-p row)
start-p (gpt/add origin
(gpt/add
(gpt/to-vec origin column-start-p)
(gpt/to-vec origin row-start-p)))]
(assoc grid-cell :start-p start-p)))))
row-start-p (:start-p row)]
(when (and (some? column-start-p) (some? row-start-p))
(let [start-p (gpt/add origin
(gpt/add
(gpt/to-vec origin column-start-p)
(gpt/to-vec origin row-start-p)))]
(assoc grid-cell :start-p start-p)))))))

View File

@@ -114,61 +114,62 @@
(defn child-position-delta
[parent child child-bounds child-width child-height layout-data cell-data]
(let [cell-bounds (cell-bounds layout-data cell-data)
child-origin (gpo/origin child-bounds)
(if-let [cell-bounds (cell-bounds layout-data cell-data)]
(let [child-origin (gpo/origin child-bounds)
align (:layout-align-items parent)
justify (:layout-justify-items parent)
align-self (:align-self cell-data)
justify-self (:justify-self cell-data)
align (:layout-align-items parent)
justify (:layout-justify-items parent)
align-self (:align-self cell-data)
justify-self (:justify-self cell-data)
align-self (when (and align-self (not= align-self :auto)) align-self)
justify-self (when (and justify-self (not= justify-self :auto)) justify-self)
align-self (when (and align-self (not= align-self :auto)) align-self)
justify-self (when (and justify-self (not= justify-self :auto)) justify-self)
align (or align-self align)
justify (or justify-self justify)
align (or align-self align)
justify (or justify-self justify)
origin-h (gpo/project-point cell-bounds :h child-origin)
origin-v (gpo/project-point cell-bounds :v child-origin)
hv (partial gpo/start-hv cell-bounds)
vv (partial gpo/start-vv cell-bounds)
origin-h (gpo/project-point cell-bounds :h child-origin)
origin-v (gpo/project-point cell-bounds :v child-origin)
hv (partial gpo/start-hv cell-bounds)
vv (partial gpo/start-vv cell-bounds)
[top-m right-m bottom-m left-m] (ctl/child-margins child)
[top-m right-m bottom-m left-m] (ctl/child-margins child)
;; Adjust alignment/justify
[from-h to-h]
(case justify
:end
[(gpt/add origin-h (hv child-width))
(gpt/subtract (nth cell-bounds 1) (hv right-m))]
;; Adjust alignment/justify
[from-h to-h]
(case justify
:end
[(gpt/add origin-h (hv child-width))
(gpt/subtract (nth cell-bounds 1) (hv right-m))]
:center
[(gpt/add origin-h (hv (/ child-width 2)))
(-> (gpo/project-point cell-bounds :h (gpo/center cell-bounds))
(gpt/add (hv (/ left-m 2)))
(gpt/subtract (hv (/ right-m 2))))]
:center
[(gpt/add origin-h (hv (/ child-width 2)))
(-> (gpo/project-point cell-bounds :h (gpo/center cell-bounds))
(gpt/add (hv (/ left-m 2)))
(gpt/subtract (hv (/ right-m 2))))]
[origin-h
(gpt/add (first cell-bounds) (hv left-m))])
[origin-h
(gpt/add (first cell-bounds) (hv left-m))])
[from-v to-v]
(case align
:end
[(gpt/add origin-v (vv child-height))
(gpt/subtract (nth cell-bounds 3) (vv bottom-m))]
[from-v to-v]
(case align
:end
[(gpt/add origin-v (vv child-height))
(gpt/subtract (nth cell-bounds 3) (vv bottom-m))]
:center
[(gpt/add origin-v (vv (/ child-height 2)))
(-> (gpo/project-point cell-bounds :v (gpo/center cell-bounds))
(gpt/add (vv top-m))
(gpt/subtract (vv bottom-m)))]
:center
[(gpt/add origin-v (vv (/ child-height 2)))
(-> (gpo/project-point cell-bounds :v (gpo/center cell-bounds))
(gpt/add (vv top-m))
(gpt/subtract (vv bottom-m)))]
[origin-v
(gpt/add (first cell-bounds) (vv top-m))])]
[origin-v
(gpt/add (first cell-bounds) (vv top-m))])]
(-> (gpt/point)
(gpt/add (gpt/to-vec from-h to-h))
(gpt/add (gpt/to-vec from-v to-v)))))
(-> (gpt/point)
(gpt/add (gpt/to-vec from-h to-h))
(gpt/add (gpt/to-vec from-v to-v))))
(gpt/point 0 0)))
(defn child-modifiers
[parent parent-bounds child child-bounds layout-data cell-data]

View File

@@ -12,7 +12,6 @@
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.grid-layout :as gslg]
[app.common.logging :as log]
[app.common.logic.shapes :as cls]
[app.common.spec :as us]
@@ -226,18 +225,19 @@
changes
(if (ctl/grid-layout? objects (:parent-id first-shape))
(let [target-cell (-> position meta :cell)
[row column]
(if (some? target-cell)
[(:row target-cell) (:column target-cell)]
(gslg/get-drop-cell (:parent-id first-shape) objects position))]
(when (some? target-cell)
[(:row target-cell) (:column target-cell)])]
(-> changes
(pcb/update-shapes
[(:parent-id first-shape)]
(fn [shape objects]
(-> shape
(ctl/assign-cells objects)
(ctl/push-into-cell [(:id first-shape)] row column)
(ctl/assign-cells objects)))
(cond-> (and (some? row) (some? column))
(-> (ctl/push-into-cell [(:id first-shape)] row column)
(ctl/assign-cells objects)))))
{:with-objects? true})
(pcb/reorder-grid-children [(:parent-id first-shape)])))
changes)
@@ -747,42 +747,35 @@
(let [omit-touched? (not reset?)
clear-remote-synced? (and initial-root? reset?)
set-remote-synced? (and (not initial-root?) reset?)
changes (cond-> changes
:always
(update-attrs shape-inst
shape-main
root-inst
root-main
container
omit-touched?)
changes
(cond-> changes
:always
(update-attrs shape-inst
shape-main
root-inst
root-main
container
omit-touched?)
(ctl/flex-layout? shape-main)
(update-flex-child-copy-attrs shape-main
shape-inst
library
component
container
omit-touched?)
(ctl/flex-layout? shape-main)
(update-flex-child-copy-attrs shape-main
shape-inst
library
component
container
omit-touched?)
(ctl/grid-layout? shape-main)
(update-grid-copy-attrs shape-main
shape-inst
library
component
container
omit-touched?)
reset?
(change-touched shape-inst
shape-main
container
{:reset-touched? true})
reset?
(change-touched shape-inst
shape-main
container
{:reset-touched? true})
clear-remote-synced?
(change-remote-synced shape-inst container nil)
clear-remote-synced?
(change-remote-synced shape-inst container nil)
set-remote-synced?
(change-remote-synced shape-inst container true))
set-remote-synced?
(change-remote-synced shape-inst container true))
component-container (find-main-container container shape-inst shape-main library component)
@@ -859,23 +852,36 @@
(d/index-of children-inst child-inst)
(d/index-of children-main child-main)
container
omit-touched?))]
omit-touched?))
(compare-children changes
children-inst
children-main
container
component-container
file
libraries
only-inst
only-main
both
swapped
moved
false
reset?
components-v2))))
changes
(compare-children changes
children-inst
children-main
container
component-container
file
libraries
only-inst
only-main
both
swapped
moved
false
reset?
components-v2)
changes
(cond-> changes
(ctl/grid-layout? shape-inst)
(update-grid-copy-attrs
(:id shape-inst)
shape-main
library
component
omit-touched?))]
changes)))
(defn generate-rename-component
"Generate the changes for rename the component with the given id, in the current file library."
@@ -1710,30 +1716,50 @@
(defn- update-grid-copy-attrs
"Synchronizes the `layout-grid-cells` property from the main shape to the copies"
[changes shape-main shape-copy main-container main-component copy-container omit-touched?]
(let [ids-map
(into {}
(comp
(map #(dm/get-in copy-container [:objects %]))
(keep
(fn [copy-shape]
(let [main-shape (ctf/get-ref-shape main-container main-component copy-shape)]
[(:id main-shape) (:id copy-shape)]))))
(:shapes shape-copy))
[changes shape-copy-id shape-main main-container main-component omit-touched?]
(-> changes
(pcb/apply-changes-local)
(pcb/update-shapes
[shape-copy-id]
(fn [shape-copy objects]
(let [component-page
(ctf/get-component-page main-container main-component)
new-changes
(-> (pcb/empty-changes)
(pcb/with-container copy-container)
(pcb/with-objects (:objects copy-container))
(pcb/update-shapes
[(:id shape-copy)]
(fn [shape-copy]
component-swap-children
(->> shape-main
:shapes
(map #(get (:objects component-page) %))
(filter #(some? (ctk/get-swap-slot %)))
(group-by ctk/get-swap-slot))
ids-map
(into {}
(comp
(map #(get objects %))
(keep
(fn [copy-shape]
(let [main-shape
(if (some? (ctk/get-swap-slot copy-shape))
(first (get component-swap-children (ctk/get-swap-slot copy-shape)))
(ctf/get-ref-shape main-container main-component copy-shape))]
[(:id main-shape) (:id copy-shape)]))))
(:shapes shape-copy))
remove-orphan-cells
(fn [cells {:keys [shapes]}]
(let [child? (set shapes)]
(-> cells
(update-vals
(fn [cell]
(update cell :shapes #(filterv child? %)))))))
;; Take cells from main and remap the shapes to assign it to the copy
(let [copy-cells (:layout-grid-cells shape-copy)
main-cells (-> (ctl/remap-grid-cells shape-main ids-map) :layout-grid-cells)]
(assoc shape-copy :layout-grid-cells (ctl/merge-cells copy-cells main-cells omit-touched?))))
{:ignore-touched true}))]
(pcb/concat-changes changes new-changes)))
copy-cells (-> shape-copy :layout-grid-cells (remove-orphan-cells shape-copy))
main-cells (-> shape-main (ctl/remap-grid-cells ids-map) :layout-grid-cells)]
(-> shape-copy
(assoc :layout-grid-cells
(ctl/merge-cells main-cells copy-cells omit-touched?))
(ctl/assign-cells objects))))
{:ignore-touched true :with-objects? true})))
(defn- update-grid-main-attrs
"Synchronizes the `layout-grid-cells` property from the copy to the main shape"

View File

@@ -963,7 +963,6 @@
{:title "string"
:description "not whitespace string"
:gen/gen (sg/word-string)
:error/code "errors.invalid-text"
:error/fn
(fn [{:keys [value schema]}]
(let [{:keys [max min] :as props} (properties schema)]
@@ -971,16 +970,23 @@
(and (string? value)
(number? max)
(> (count value) max))
["errors.field-max-length" max]
{:code ["errors.field-max-length" max]}
(and (string? value)
(number? min)
(< (count value) min))
["errors.field-min-length" min]
{:code ["errors.field-min-length" min]}
(and (string? value)
(str/empty? value))
{:code "errors.field-missing"}
(and (string? value)
(str/blank? value))
"errors.field-not-all-whitespace")))}})
{:code "errors.field-not-all-whitespace"}
:else
{:code "errors.invalid-text"})))}})
(register!
{:type ::password

View File

@@ -29,7 +29,7 @@
(sm/register! ::component schema:component)
(def check-component!
(def check-component
(sm/check-fn schema:component))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -5,6 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.types.page
(:refer-clojure :exclude [empty?])
(:require
[app.common.data :as d]
[app.common.geom.point :as-alias gpt]
@@ -98,3 +99,8 @@
(defn get-frame-flow
[flows frame-id]
(d/seek #(= (:starting-frame %) frame-id) (vals flows)))
(defn is-empty?
"Check if page is empty or contains shapes"
[page]
(= 1 (count (:objects page))))

View File

@@ -588,51 +588,51 @@
;; - Blur
;; - Border radius
(def ^:private basic-extract-props
[:fills
:strokes
:opacity
#{:fills
:strokes
:opacity
;; Layout Item
:layout-item-margin
:layout-item-margin-type
:layout-item-h-sizing
:layout-item-v-sizing
:layout-item-max-h
:layout-item-min-h
:layout-item-max-w
:layout-item-min-w
:layout-item-absolute
:layout-item-z-index
;; Layout Item
:layout-item-margin
:layout-item-margin-type
:layout-item-h-sizing
:layout-item-v-sizing
:layout-item-max-h
:layout-item-min-h
:layout-item-max-w
:layout-item-min-w
:layout-item-absolute
:layout-item-z-index
;; Constraints
:constraints-h
:constraints-v
;; Constraints
:constraints-h
:constraints-v
:shadow
:blur
:shadow
:blur
;; Radius
:r1
:r2
:r3
:r4])
;; Radius
:r1
:r2
:r3
:r4})
(def ^:private layout-extract-props
[:layout
:layout-flex-dir
:layout-gap-type
:layout-gap
:layout-wrap-type
:layout-align-items
:layout-align-content
:layout-justify-items
:layout-justify-content
:layout-padding-type
:layout-padding
:layout-grid-dir
:layout-grid-rows
:layout-grid-columns
:layout-grid-cells])
#{:layout
:layout-flex-dir
:layout-gap-type
:layout-gap
:layout-wrap-type
:layout-align-items
:layout-align-content
:layout-justify-items
:layout-justify-content
:layout-padding-type
:layout-padding
:layout-grid-dir
:layout-grid-rows
:layout-grid-columns
:layout-grid-cells})
(defn extract-props
"Retrieves an object with the 'pasteable' properties for a shape."
@@ -668,10 +668,13 @@
[props shape]
(d/patch-object props (select-keys shape layout-extract-props)))]
(-> shape
(select-keys basic-extract-props)
(cond-> (cfh/text-shape? shape) (extract-text-props shape))
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape)))))
(let [;; For texts we don't extract the fill
extract-props
(cond-> basic-extract-props (cfh/text-shape? shape) (disj :fills))]
(-> shape
(select-keys extract-props)
(cond-> (cfh/text-shape? shape) (extract-text-props shape))
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape))))))
(defn patch-props
"Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary"

View File

@@ -8,7 +8,7 @@
(:require
[app.common.schema :as sm]))
(def types #{:png :jpeg :svg :pdf})
(def types #{:png :jpeg :webp :svg :pdf})
(def schema:export
[:map {:title "ShapeExport"}

View File

@@ -1307,9 +1307,9 @@
"Push the shapes into the row/column cell and moves the rest"
[parent shape-ids row column]
(let [cells (vec (get-cells parent {:sort? true}))
(let [parent (-> parent (free-cell-shapes shape-ids))
cells (vec (get-cells parent {:sort? true}))
[start-index start-cell] (seek-indexed-cell cells row column)]
(if (some? start-cell)
(let [;; start-index => to-index is the range where the shapes inserted will be added
to-index (min (+ start-index (count shape-ids)) (dec (count cells)))]
@@ -1642,11 +1642,11 @@
"Given target cells update with source cells while trying to keep target as
untouched as possible"
[target-cells source-cells omit-touched?]
(if (not omit-touched?)
source-cells
(letfn [(get-data [cells id]
(dissoc (get cells id) :shapes :row :column :row-span :column-span))]
(if omit-touched?
(letfn [(merge-cells [source-cell target-cell]
(-> source-cell
(d/patch-object
(dissoc target-cell :row :column :row-span :column-span))))]
(let [deleted-cells
(into #{}
(filter #(not (contains? source-cells %)))
@@ -1654,15 +1654,13 @@
touched-cells
(into #{}
(filter #(and
(not (contains? deleted-cells %))
(not= (get-data source-cells %)
(get-data target-cells %))))
(filter #(not (contains? deleted-cells %)))
(keys target-cells))]
(->> touched-cells
(reduce
(fn [cells id]
(-> cells
(d/update-when id d/patch-object (get-data target-cells id))))
source-cells))))))
(d/update-when id merge-cells (get target-cells id))))
source-cells))))
source-cells))

View File

@@ -11,6 +11,7 @@ RUN set -ex; \
ADD ./bundle-frontend/ /var/www/app/
ADD ./files/config.js /var/www/app/js/config.js
ADD ./files/nginx.conf /etc/nginx/nginx.conf.template
ADD ./files/resolvers.conf /etc/nginx/overrides.d/resolvers.conf.template
ADD ./files/nginx-mime.types /etc/nginx/mime.types
ADD ./files/nginx-entrypoint.sh /entrypoint.sh

View File

@@ -21,10 +21,14 @@ update_flags /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060};
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061};
export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-127.0.0.11};
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)";
export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-$PENPOT_DEFAULT_INTERNAL_RESOLVER};
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600}; # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_INTERNAL_RESOLVER,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
< /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
< /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf;
envsubst "\$PENPOT_INTERNAL_RESOLVER" \
< /etc/nginx/overrides.d/resolvers.conf.template > /etc/nginx/overrides.d/resolvers.conf;
exec "$@";

View File

@@ -46,7 +46,6 @@ http {
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
proxy_buffers 32 4k;
resolver $PENPOT_INTERNAL_RESOLVER ipv6=off;
map $http_upgrade $connection_upgrade {
default upgrade;

View File

@@ -0,0 +1 @@
resolver $PENPOT_INTERNAL_RESOLVER ipv6=off valid=10s;

View File

@@ -71,7 +71,9 @@
</main>
<div class="pre-footer">
<a href="https://github.com/penpot/penpot/blob/main/docs/{{ page.inputPath }}">Edit this page on GitHub</a>
<a href="https://github.com/penpot/penpot/blob/main/docs/{{ page.inputPath }}">Edit this page on GitHub</a>
&nbsp;or ask a&nbsp;
<a href="https://github.com/penpot/penpot/issues/new/choose">question</a>.
</div>
<footer class="footer">
<div class="footer-inside">

View File

@@ -12,7 +12,7 @@ templateClass: tmpl-contributing-guide
{{ show_children(child) }}
{%- endif -%}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | safe }}
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}

View File

@@ -12,7 +12,7 @@ templateClass: tmpl-user-guide
{{ show_children(child) }}
{%- endif -%}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | safe }}
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}

BIN
docs/img/dev-tools-1.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
docs/img/dev-tools-2.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/img/penpot-report.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -2,73 +2,107 @@
title: 2. Penpot Configuration
---
# Penpot Configuration #
# Penpot Configuration
This section intends to explain all available configuration options, when you
are self-hosting Penpot or also if you are using the Penpot developer setup.
This section explains the configuration options, both for self-hosting and developer setup.
Penpot is configured using environment variables. All variables start with <code class="language-bash">PENPOT_</code>
prefix.
<p class="advice">
Penpot is configured using environment variables and flags.
</p>
Variables are initialized in the <code class="language-bash">docker-compose.yaml</code> file, as explained in the
Self-hosting guide with [Elestio][1] or [Docker][2].
## How the configuration works
Additionally, if you are using the developer environment, you may override their values in
the startup scripts, as explained in the [Developer Guide][3].
Penpot is configured using environment variables and flags. **Environment variables** start
with <code class="language-bash">PENPOT_</code>. **Flags** use the format
<code class="language-bash"><enable|disable>-<flag-name></code>.
**NOTE**: All the examples that have values represent the **default** values, and the
examples that do not have values are optional, and inactive by default.
## Common ##
This section will list all common configuration between backend and frontend.
There are two types of configuration: options (properties that require some value) and
flags (that just enables or disables something). All flags are set in a single
<code class="language-bash">PENPOT_FLAGS</code> environment variable. The envvar is a list of strings using this
format: <code class="language-bash"><enable|disable>-\<flag-name></code>. For example:
Flags are used to enable/disable a feature or behaviour (registration, feedback),
while environment variables are used to configure the settings (auth, smtp, etc).
Flags and evironment variables are also used together; for example:
```bash
PENPOT_FLAGS: enable-smtp disable-registration disable-email-verification
# This flag enables the use of SMTP email
PENPOT_FLAGS: enable-smtp
# These environment variables configure the specific SMPT service
# Backend
PENPOT_SMTP_HOST: <host>
PENPOT_SMTP_PORT: 587
```
### Registration ###
**Flags** are configured in a single list, no matter they affect the backend, the frontend,
the exporter, or all of them; on the other hand, **environment variables** are configured for
each specific service. For example:
Penpot comes with an option to completely disable the registration process;
for this, use the following variable:
```bash
PENPOT_FLAGS: enable-login-with-google
# Backend
PENPOT_GOOGLE_CLIENT_ID: <client-id>
PENPOT_GOOGLE_CLIENT_SECRET: <client-secret>
```
Check the configuration guide for [Elestio][1] or [Docker][2]. Additionally, if you are using
the developer environment, you may override its values in the startup scripts,
as explained in the [Developer Guide][3].
**NOTE**: All the examples that have value represent the **default** value, and the
examples that do not have value are optional, and inactive or disabled by default.
## Telemetries
Penpot uses anonymous telemetries from the self-hosted instances to improve the platform experience.
Consider sharing these anonymous telemetries enabling the corresponding flag:
```bash
PENPOT_FLAGS: enable-telemetries
```
## Registration and authentication
There are different ways of registration and authentication in Penpot:
- email/password
- Authentication providers like Google, Github or GitLab
- LDAP
You can choose one of them or combine several methods, depending on your needs.
By default, the email/password registration is enabled and the rest are disabled.
### Penpot
This method of registration and authentication is enabled by default. For a production environment,
it should be configured next to the SMTP settings, so there is a proper registration and verification
process.
You may want to restrict the registrations to a closed list of domains,
or exclude a specific list of domains:
```bash
# Backend
# comma separated list of domains
PENPOT_REGISTRATION_DOMAIN_WHITELIST:
# Backend
# or a file with a domain per line
PENPOT_EMAIL_DOMAIN_WHITELIST: path/to/whitelist.txt
PENPOT_EMAIL_DOMAIN_BLACKLIST: path/to/blacklist.txt
```
__Since version 2.1__
Email whitelisting should be explicitly
enabled with <code class="language-bash">enable-email-whitelist</code> flag. For backward compatibility, we
autoenable it when <code class="language-bash">PENPOT_REGISTRATION_DOMAIN_WHITELIST</code> is set with
not-empty content.
Penpot also comes with an option to completely disable the registration process;
for this, use the following flag:
```bash
PENPOT_FLAGS: [...] disable-registration
```
You may also want to restrict the registrations to a closed list of domains:
```bash
# comma separated list of domains (backend only)
PENPOT_REGISTRATION_DOMAIN_WHITELIST:
# OR (backend only)
PENPOT_EMAIL_DOMAIN_WHITELIST: path/to/whitelist.txt
```
**NOTE**: Since version 2.1, email whitelisting should be explicitly
enabled with <code class="language-bash">enable-email-whitelist</code> flag. For backward compatibility, we
autoenable it when <code class="language-bash">PENPOT_REGISTRATION_DOMAIN_WHITELIST</code> is set with
not-empty content.
### Demo users ###
Penpot comes with facilities for fast creation of demo users without the need of a
registration process. The demo users by default have an expiration time of 7 days, and
once expired they are completely deleted with all the generated content. Very useful for
testing or demonstration purposes.
You can enable demo users using the following variable:
```bash
PENPOT_FLAGS: [...] enable-demo-users
```
This option is only recommended for demo instances, not for production environments.
### Authentication Providers
@@ -82,7 +116,6 @@ The callback has the following format:
https://<your_domain>/api/auth/oauth/<oauth_provider>/callback
```
You will need to change <your_domain> and <oauth_provider> according to your setup.
This is how it looks with Gitlab provider:
@@ -90,22 +123,6 @@ This is how it looks with Gitlab provider:
https://<your_domain>/api/auth/oauth/gitlab/callback
```
#### Penpot
Consists on registration and authentication via email / password. It is enabled by default,
but login can be disabled with the following flags:
```bash
PENPOT_FLAGS: [...] disable-login-with-password
```
And the registration also can be disabled with:
```bash
PENPOT_FLAGS: [...] disable-registration
```
#### Google
Allows integrating with Google as OAuth provider:
@@ -145,7 +162,7 @@ PENPOT_GITHUB_CLIENT_SECRET: <client-secret>
#### OpenID Connect
**NOTE:** Since version 1.5.0
__Since version 1.5.0__
Allows integrating with a generic authentication provider that implements the OIDC
protocol (usually used for SSO).
@@ -155,7 +172,7 @@ All the other options are backend only:
```bash
PENPOT_FLAGS: [...] enable-login-with-oidc
## Backend only
# Backend
PENPOT_OIDC_CLIENT_ID: <client-id>
# Mainly used for auto discovery the openid endpoints
@@ -231,7 +248,6 @@ register with another method.
PENPOT_FLAGS: [...] enable-oidc-registration
```
#### Azure Active Directory using OpenID Connect
Allows integrating with Azure Active Directory as authentication provider:
@@ -240,12 +256,12 @@ Allows integrating with Azure Active Directory as authentication provider:
# Backend & Frontend
PENPOT_OIDC_CLIENT_ID: <client-id>
## Backend only
# Backend
PENPOT_OIDC_BASE_URI: https://login.microsoftonline.com/<tenant-id>/v2.0/
PENPOT_OIDC_CLIENT_SECRET: <client-secret>
```
### LDAP ###
### LDAP
Penpot comes with support for *Lightweight Directory Access Protocol* (LDAP). This is the
example configuration we use internally for testing this authentication backend.
@@ -253,7 +269,7 @@ example configuration we use internally for testing this authentication backend.
```bash
PENPOT_FLAGS: [...] enable-login-with-ldap
## Backend only
# Backend
PENPOT_LDAP_HOST: ldap
PENPOT_LDAP_PORT: 10389
PENPOT_LDAP_SSL: false
@@ -268,39 +284,34 @@ PENPOT_LDAP_ATTRS_FULLNAME: cn
PENPOT_LDAP_ATTRS_PHOTO: jpegPhoto
```
If you miss something, please open an issue and we discuss it.
## Penpot URI
## Backend ##
This section enumerates the backend only configuration variables.
### Database
We only support PostgreSQL and we highly recommend >=13 version. If you are using official
docker images this is already solved for you.
Essential database configuration:
You will need to set the <code class="language-bash">PENPOT_PUBLIC_URI</code> environment variable in case you go to serve Penpot to the users;
it should point to public URI where users will access the application:
```bash
# Backend
PENPOT_DATABASE_USERNAME: penpot
PENPOT_DATABASE_PASSWORD: penpot
PENPOT_DATABASE_URI: postgresql://127.0.0.1/penpot
PENPOT_PUBLIC_URI: https://penpot.mycompany.com
# Frontend
PENPOT_PUBLIC_URI: https://penpot.mycompany.com
# Exporter
PENPOT_PUBLIC_URI: https://penpot.mycompany.com
```
The username and password are optional. These settings should be compatible with the ones
in the postgres configuration:
If you're using the official <code class="language-bash">docker-compose.yml</code> you only need to configure the
<code class="language-bash">PENPOT_PUBLIC_URI</code> envvar in the top of the file.
```bash
# Postgres
POSTGRES_DATABASE: penpot
POSTGRES_USER: penpot
POSTGRES_PASSWORD: penpot
```
<p class="advice">
If you plan to serve Penpot under different domain than `localhost` without HTTPS,
you need to disable the `secure` flag on cookies, with the `disable-secure-session-cookies` flag.
This is a configuration NOT recommended for production environments; as some browser APIs do
not work properly under non-https environments, this unsecure configuration
may limit the usage of Penpot; as an example, the clipboard does not work with HTTP.
</p>
### Email (SMTP)
## Email configuration
By default, <code class="language-bash">smpt</code> flag is disabled, the email will be
printed to the console, which means that the emails will be shown in the stdout.
@@ -326,6 +337,7 @@ Enable SMTP:
```bash
PENPOT_FLAGS: [...] enable-smtp
# Backend
PENPOT_SMTP_HOST: <host>
PENPOT_SMTP_PORT: 587
@@ -334,14 +346,108 @@ PENPOT_SMTP_PASSWORD: <password>
PENPOT_SMTP_TLS: true
```
If you are not using SMTP configuration and want to log the emails in the console, you should use the following flag:
```bash
PENPOT_FLAGS: [...] enable-log-emails
```
## Redis
The Redis configuration is very simple, just provide a valid redis URI. Redis is used
mainly for websocket notifications coordination.
```bash
# Backend
PENPOT_REDIS_URI: redis://localhost/0
# Exporter
PENPOT_REDIS_URI: redis://localhost/0
```
If you are using the official docker compose file, this is already configurRed.
## Demo environment
Penpot comes with facilities to create a demo environment so you can test the system quickly.
This is an example of a demo configuration:
```bash
PENPOT_FLAGS: disable-registration enable-demo-users enable-demo-warning
```
**disable-registration** prevents any user from registering in the platform.
**enable-demo-users** creates users with a default expiration time of 7 days, and
once expired they are completely deleted with all the generated content.
From the registration page, there is a link with a `Create demo account` which creates one of these
users and logs in automatically.
**enable-demo-warning** is a modal in the registration and login page saying that the
environment is a testing one and the data may be wiped without notice.
Another way to work in a demo environment is allowing users to register but removing the
verification process:
```bash
PENPOT_FLAGS: disable-email-verification enable-demo-warning
```
## Backend
This section enumerates the backend only configuration variables.
### Secret key
The <code class="language-bash">PENPOT_SECRET_KEY</code> envvar serves a master key from which other keys
for subsystems (eg http sessions, or invitations) are derived.
If you don't use it, all created sessions and invitations will become invalid on container restart
or service restart.
To use it, we recommend using a truly randomly generated 512 bits base64 encoded string here.
You can generate one with:
```bash
python3 -c "import secrets; print(secrets.token_urlsafe(64))"
```
And configure it:
```bash
# Backend
PENPOT_SECRET_KEY: my-super-secure-key
```
### Database
Penpot only supports PostgreSQL and we highly recommend >=13 version. If you are using official
docker images this is already solved for you.
Essential database configuration:
```bash
# Backend
PENPOT_DATABASE_USERNAME: penpot
PENPOT_DATABASE_PASSWORD: penpot
PENPOT_DATABASE_URI: postgresql://127.0.0.1/penpot
```
The username and password are optional. These settings should be compatible with the ones
in the postgres configuration:
```bash
# Postgres
POSTGRES_DATABASE: penpot
POSTGRES_USER: penpot
POSTGRES_PASSWORD: penpot
```
### Storage
Storage refers to storage used for store the user uploaded assets.
Storage refers to storing the user uploaded assets.
Assets storage is implemented using "plugable" backends. Currently there are three
Assets storage is implemented using "plugable" backends. Currently there are two
backends available: <code class="language-bash">fs</code> and <code class="language-bash">s3</code> (for AWS S3).
#### FS Backend (default) ####
#### FS Backend (default)
This is the default backend when you use the official docker images and the default
configuration looks like this:
@@ -360,8 +466,7 @@ configure the nginx yourself.
In case you want understand how it internally works, you can take a look on the [nginx
configuration file][4] used in the docker images.
#### AWS S3 Backend ####
#### AWS S3 Backend
This backend uses AWS S3 bucket for store the user uploaded assets. For use it you should
have an appropriate account on AWS cloud and have the credentials, region and the bucket.
@@ -369,11 +474,9 @@ have an appropriate account on AWS cloud and have the credentials, region and th
This is how configuration looks for S3 backend:
```bash
# AWS Credentials
# Backend
AWS_ACCESS_KEY_ID: <you-access-key-id-here>
AWS_SECRET_ACCESS_KEY: <your-secret-access-key-here>
# Backend configuration
PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
PENPOT_STORAGE_ASSETS_S3_REGION: <aws-region>
PENPOT_STORAGE_ASSETS_S3_BUCKET: <bucket-name>
@@ -382,38 +485,11 @@ PENPOT_STORAGE_ASSETS_S3_BUCKET: <bucket-name>
PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <endpoint-uri>
```
### Redis
The redis configuration is very simple, just provide with a valid redis URI. Redis is used
mainly for websocket notifications coordination.
```bash
# Backend
PENPOT_REDIS_URI: redis://localhost/0
```
If you are using the official docker compose file, this is already configured.
### HTTP
You will need to set the <code class="language-bash">PENPOT_PUBLIC_URI</code> environment
variable in case you go to serve Penpot to the users; it should point to public URI
where users will access the application:
```bash
PENPOT_PUBLIC_URI: http://localhost:9001
```
<p class="advice">
If you plan to serve Penpot under different domain than `localhost` without HTTPS,
you need to disable the `secure` flag on cookies, with the `disable-secure-session-cookies` flag.
This is a configuration NOT recommended for production environments.
These settings are equally useful if you have a Minio storage system.
</p>
Check all the [flags](#other-flags) to fully customize your instance.
## Frontend ##
## Frontend
In comparison with backend, frontend only has a small number of runtime configuration
options, and they are located in the <code class="language-bash">\<dist>/js/config.js</code> file.
@@ -422,10 +498,7 @@ If you are using the official docker images, the best approach to set any config
using environment variables, and the image automatically generates the <code class="language-bash">config.js</code> from
them.
**NOTE**: many frontend related configuration variables are explained in the
[Common](#common) section, this section explains **frontend only** options.
But in case you have a custom setup you probably need setup the following environment
In case you have a custom setup, you probably need to configure the following environment
variables on the frontend container:
To connect the frontend to the exporter and backend, you need to fill out these environment variables.
@@ -438,54 +511,36 @@ PENPOT_EXPORTER_URI: http://your-penpot-exporter:6061
These variables are used for generate correct nginx.conf file on container startup.
### Demo warning ###
If you want to show a warning in the register and login page saying that this is a
demonstration purpose instance (no backups, periodical data wipe, ...), set the following
variable:
```bash
PENPOT_FLAGS: [...] enable-demo-warning
```
## Other flags
There are other flags that are useful for a more customized Penpot experience. This section has the list of the flags meant
for the user:
- <code class="language-bash">enable-cors</code>: Enables the default cors cofiguration that allows all domains
(this configuration is designed only for dev purposes right now)
- <code class="language-bash">enable-backend-api-doc</code>: Enables the <code class="language-bash">/api/doc</code>
endpoint that lists all rpc methods available on backend
- <code class="language-bash">disable-email-verification</code>: Deactivates the email verification process
(only recommended for local or internal setups)
- <code class="language-bash">disable-secure-session-cookies</code>: By default, Penpot uses the
<code class="language-bash">secure</code> flag on cookies, this flag disables it;
it is useful if you plan to serve Penpot under different
domain than <code class="language-bash">localhost</code> without HTTPS
- <code class="language-bash">disable-login-with-password</code>: allows disable password based login form
- <code class="language-bash">disable-registration</code>: disables registration (still enabled for invitations only).
- <code class="language-bash">enable-prepl-server</code>: enables PREPL server, used by manage.py and other additional
tools for communicate internally with Penpot backend
tools to communicate internally with Penpot backend. Check the [CLI section][5] to get more detail.
__Since version 1.13.0__
- <code class="language-bash">enable-log-invitation-tokens</code>: for cases where you don't have email configured, this
will log to console the invitation tokens
- <code class="language-bash">enable-log-emails</code>: if you want to log in console send emails. This only works if smtp
is not configured
will log to console the invitation tokens.
__Since version 2.0.0__
- <code class="language-bash">disable-onboarding-team</code>: for disable onboarding team creation modal
- <code class="language-bash">disable-onboarding-newsletter</code>: for disable onboarding newsletter modal
- <code class="language-bash">disable-onboarding-questions</code>: for disable onboarding survey
- <code class="language-bash">disable-onboarding</code>: for disable onboarding modal
- <code class="language-bash">disable-dashboard-templates-section</code>: for hide the templates section from dashboard
- <code class="language-bash">enable-webhooks</code>: for enable webhooks
- <code class="language-bash">enable-access-tokens</code>: for enable access tokens
- <code class="language-bash">disable-google-fonts-provider</code>: disables the google fonts provider (frontend)
- <code class="language-bash">disable-onboarding</code>: disables the onboarding modals.
- <code class="language-bash">disable-dashboard-templates-section</code>: hides the templates section from dashboard.
- <code class="language-bash">enable-webhooks</code>: enables webhooks. More detail about this configuration in [webhooks section][6].
- <code class="language-bash">enable-access-tokens</code>: enables access tokens. More detail about this configuration in [access tokens section][7].
- <code class="language-bash">disable-google-fonts-provider</code>: disables the google fonts provider.
[1]: /technical-guide/getting-started#configure-penpot-with-elestio
[2]: /technical-guide/getting-started#configure-penpot-with-docker
[3]: /technical-guide/developer/common#dev-environment
[4]: https://github.com/penpot/penpot/blob/main/docker/images/files/nginx.conf
[5]: /technical-guide/getting-started/#using-the-cli-for-administrative-tasks
[6]: /technical-guide/integration/#webhooks
[7]: /technical-guide/integration/#access-tokens

View File

@@ -1,435 +0,0 @@
---
title: 1. Self-hosting Guide
---
# Self-hosting Guide
This guide explains how to get your own Penpot instance, running on a machine you control,
to test it, use it by you or your team, or even customize and extend it any way you like.
If you need more context you can look at the <a
href="https://community.penpot.app/t/self-hosting-penpot-i/2336" target="_blank">post
about self-hosting</a> in Penpot community.
**There is absolutely no difference between <a
href="https://design.penpot.app">our SaaS offer</a> for Penpot and your
self-hosted Penpot platform!**
There are three main options for creating a Penpot instance:
1. Using the platform of our partner <a href="https://elest.io/open-source/penpot" target="_blank">Elestio</a>.
2. Using <a href="https://docker.com" target="_blank">Docker</a> tool.
3. Using <a href="https://kubernetes.io/" target="_blank">Kubernetes</a>.
<p class="advice">
The recommended way is to use Elestio, since it's simpler, fully automatic and still greatly flexible.
Use Docker if you already know the tool, if need full control of the process or have extra requirements
and do not want to depend on any external provider, or need to do any special customization.
</p>
Or you can try <a href="#unofficial-self-host-options">other options</a>,
offered by Penpot community.
## Recommended settings
To self-host Penpot, youll need a server with the following specifications:
* **CPU:** 1-2 CPUs
* **RAM:** 4 GiB of RAM
* **Disk Space:** Disk requirements depend on your usage. Disk usage primarily involves the database and any files uploaded by users.
This setup should be sufficient for a smooth experience with typical usage (your mileage may vary).
## Install with Elestio
This section explains how to get Penpot up and running using <a href="https://elest.io/open-source/penpot"
target="_blank">Elestio</a>.
This platform offers a fully managed service for on-premise instances of a selection of
open-source software! This means you can deploy a dedicated instance of Penpot in just 3
minutes. Youll be relieved of the need to worry about DNS configuration, SMTP, backups,
SSL certificates, OS & Penpot upgrades, and much more.
It uses the same Docker configuration as the other installation option, below, so all
customization options are the same.
### Get an Elestio account
<p class="advice">
Skip this section if you already have an Elestio account.
</p>
To create your Elestio account <a href="https://dash.elest.io/deploy?soft=Penpot&id=121"
target="_blank">click here</a>. You can choose to deploy on any one of five leading cloud
providers or on-premise.
### Deploy Penpot using Elestio
Now you can Create your service in “Services”:
1. Look for Penpot.
2. Select a Service Cloud Provider.
3. Select Service Cloud Region.
4. Select Service Plan (for a team of 20 you should be fine with 2GB RAM).
5. Select Elestio Service Support.
6. Provide Service Name (this will show in the URL of your instance) & Admin email (used
to create the admin account).
7. Select Advanced Configuration options (you can also do this later).
8. Hit “Create Service” on the bottom right.
It will take a couple of minutes to get the instance launched. When the status turns to
“Service is running” you are ready to get started.
By clicking on the Service you go to all the details and configuration options.
In Network/CNAME you can find the URL of your instance. Copy and paste this into a browser
and start using Penpot.
### Configure Penpot with Elestio
If you want to make changes to your Penpot setup click on the “Update config” button in
Software. Here you can see the “Docker compose” used to create the instance. In “ENV” top
middle left you can make configuration changes that will be reflected in the Docker
compose.
In this file, a “#” at the start of the line means it is text and not considered part of
the configuration. This means you will need to delete it to get some of the configuration
options to work. Once you made all your changes hit “Update & restart”. After a couple of
minutes, your changes will be active.
You can find all configuration options in the [Configuration][1] section.
Get in contact with us through <a href="mailto:support@penpot.app">support@penpot.app</a>
if you have any questions or need help.
### Update Penpot
Elestio will update your instance automatically to the latest release unless you don't
want this. In that case you need to “Disable auto updates” in Software auto updates.
## Install with Docker
This section details everything you need to know to get Penpot up and running in
production environments using Docker. For this, we provide a series of *Dockerfiles* and a
*docker-compose* file that orchestrate all.
### Install Docker
<p class="advice">
Skip this section if you already have docker installed, up and running.
</p>
Currently, Docker comes into two different flavours:
#### Docker Desktop
This is the only option to have Docker in a Windows or MacOS. Recently it's also available
for Linux, in the most popular distributions (Debian, Ubuntu and Fedora).
You can install it following the <a href="https://docs.docker.com/desktop/"
target="_blank">official guide</a>.
Docker Desktop has a graphical control panel (GUI) to manage the service and view the
containers, images and volumes. But need the command line (Terminal in Linux and Mac, or
PowerShell in Windows) to build and run the containers, and execute other operations.
It already includes **docker compose** utility, needed by Penpot.
#### Docker Engine
This is the classic and default Docker setup for Linux machines, and the only option for a
Linux VPS without graphical interface.
You can install it following the <a href="https://docs.docker.com/engine/"
target="_blank">official guide</a>.
And you also need the [docker
compose](https://docs.docker.com/compose/cli-command/#installing-compose-v2) (V2)
plugin. You can use the old **docker-compose** tool, but all the documentation supposes
you are using the V2.
You can easily check which version of **docker compose** you have. If you can execute
<code class="language-bash">docker compose</code> command, then you have V2. If you need to write <code class="language-bash">docker-compose</code> (with a
<code class="language-bash">-</code>) for it to work, you have the old version.
### Start Penpot
As first step you will need to obtain the <code class="language-bash">docker-compose.yaml</code> file. You can download it
<a
href="https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml"
target="_blank">from Penpot repository</a>.
```bash
wget https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
```
or
```bash
curl -o docker-compose.yaml https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
```
Then simply launch composer:
```bash
docker compose -p penpot -f docker-compose.yaml up -d
```
At the end it will start listening on http://localhost:9001
<p class="advice">
If you don't change anything, by default this will use the latest image published in dockerhub.
</p>
If you want to have more control over the version (which is recommended), you can use the PENPOT_VERSION envvar in the common ways:
- setting the value in the .env file
- or passing the envvar in the command line
```bash
PENPOT_VERSION=2.4.3 docker compose -p penpot -f docker-compose.yaml up -d
```
### Stop Penpot
If you want to stop running Penpot, just type
```bash
docker compose -p penpot -f docker-compose.yaml down
```
### Configure Penpot with Docker
The configuration is defined using environment variables in the <code class="language-bash">docker-compose.yaml</code>
file. The default downloaded file already comes with the essential variables already set,
and other ones commented out with some explanations.
#### Create users using CLI
By default (or when <code class="language-bash">disable-email-verification</code> flag is used), the email verification process
is completely disabled for new registrations but it is highly recommended enabling email
verification or disabling registration if you are going to expose your penpot instance to
the internet.
If you have registration disabled, you can create additional profiles using the
command line interface:
```bash
docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile
```
**NOTE:** the exact container name depends on your docker version and platform.
For example it could be <code class="language-bash">penpot-penpot-backend-1</code> or <code class="language-bash">penpot_penpot-backend-1</code>.
You can check the correct name executing <code class="language-bash">docker ps</code>.
**NOTE:** This script only will works when you properly have the <code class="language-bash">enable-prepl-server</code>
flag set on backend (is set by default on the latest docker-compose.yaml file)
You can find all configuration options in the [Configuration][1] section.
### Update Penpot
To get the latest version of Penpot in your local installation, you just need to
execute:
```bash
docker compose -f docker-compose.yaml pull
```
This will fetch the latest images. When you do <code class="language-bash">docker compose up</code> again, the containers will be recreated with the latest version.
<p class="advice">
It is strongly recommended to update the Penpot version in small increments, rather than updating between two distant versions.
</p>
**Important: Upgrade from version 1.x to 2.0**
The migration to version 2.0, due to the incorporation of the new v2 components, includes
an additional process that runs automatically as soon as the application starts. If your
on-premises Penpot instance contains a significant amount of data (such as hundreds of
penpot files, especially those utilizing SVG components and assets extensively), this
process may take a few minutes.
In some cases, such as when the script encounters an error, it may be convenient to run
the process manually. To do this, you can disable the automatic migration process using
the <code class="language-bash">disable-v2-migration</code> flag in <code
class="language-bash">PENPOT_FLAGS</code> environment variable. You can then execute the
migration process manually with the following command:
```bash
docker exec -ti <container-name-or-id> ./run.sh app.migrations.v2
```
**IMPORTANT:** this script should be executed on passing from 1.19.x to 2.0.x. Executing
it on versions greater or equal to 2.1 of penpot will not work correctly. It is known that
this script is removed since 2.4.3
### Backup Penpot
Penpot uses <a href="https://docs.docker.com/storage/volumes" target="_blank">Docker
volumes</a> to store all persistent data. This allows you to delete and recreate
containers whenever you want without losing information.
This also means you need to do regular backups of the contents of the volumes. You cannot
directly copy the contents of the volume data folder. Docker provides you a <a
href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"
target="_blank">volume backup procedure</a>, that uses a temporary container to mount one
or more volumes, and copy their data to an archive file stored outside of the container.
If you use Docker Desktop, <a
href="https://www.docker.com/blog/back-up-and-share-docker-volumes-with-this-extension/"
target="_blank">there is an extension</a> that may ease the backup process.
If you use the default **docker compose** file, there are two volumes used: one for the
Postgres database and another one for the assets uploaded by your users (images and svg
clips). There may be more volumes if you enable other features, as explained in the file
itself.
## Install with Kubernetes
This section details everything you need to know to get Penpot up and running in
production environments using a Kubernetes cluster of your choice. To do this, we have
created a <a href="https://helm.sh/" target="_blank">Helm</a> repository with everything
you need.
Therefore, your prerequisite will be to have a Kubernetes cluster on which we can install
Helm.
### What is Helm
*Helm* is the package manager for Kubernetes. A *Chart* is a Helm package. It contains
all of the resource definitions necessary to run an application, tool, or service inside
of a Kubernetes cluster. Think of it like the Kubernetes equivalent of a Homebrew
formula, an Apt dpkg, or a Yum RPM file.
A Repository is the place where charts can be collected and shared. It's like Perl's CPAN
archive or the Fedora Package Database, but for Kubernetes packages.
A Release is an instance of a chart running in a Kubernetes cluster. One chart can often
be installed many times into the same cluster. And each time it is installed, a new
release is created. Consider a MySQL chart. If you want two databases running in your
cluster, you can install that chart twice. Each one will have its own release, which will
in turn have its own release name.
With these concepts in mind, we can now explain Helm like this:
> Helm installs charts into Kubernetes clusters, creating a new release for each
> installation. To find new charts, you can search Helm chart repositories.
### Install Helm
<p class="advice">
Skip this section if you already have Helm installed in your system.
</p>
You can install Helm by following the <a href="https://helm.sh/docs/intro/install/" target="_blank">official guide</a>.
There are different ways to install Helm, depending on your infrastructure and operating
system.
### Add Penpot repository
To add the Penpot Helm repository, run the following command:
```bash
helm repo add penpot http://helm.penpot.app
```
This will add the Penpot repository to your Helm configuration, so you can install all
the Penpot charts stored there.
### Install Penpot Chart
To install the chart with the release name `my-release`:
```bash
helm install my-release penpot/penpot
```
You can customize the installation specify each parameter using the `--set key=value[,key=value]`
argument to helm install. For example,
```bash
helm install my-release \
--set global.postgresqlEnabled=true \
--set global.redisEnabled=true \
--set persistence.assets.enabled=true \
penpot/penpot
```
Alternatively, a YAML file that specifies the values for the above parameters can be
provided while installing the chart. For example,
```bash
helm install my-release -f values.yaml penpot/penpot
```
### Configure Penpot with Helm Chart
In the previous section we have shown how to configure penpot during installation by
using parameters or by using a yaml file.
The default values are defined in the
<a href="https://github.com/penpot/penpot-helm/blob/main/charts/penpot/values.yaml" target="_blank">`values.yml`</a>
file itself, which you can use as a basis for creating your own settings.
You can also consult the list of parameters on the
<a href="https://artifacthub.io/packages/helm/penpot/penpot#parameters" target="_blank">ArtifactHub page of the project</a>.
### Upgrade Penpot
When a new version of Penpot's chart is released, or when you want to change the
configuration of your release, you can use the helm upgrade command.
```bash
helm upgrade my-release -f values.yaml penpot/penpot
```
An upgrade takes an existing release and upgrades it according to the information you
provide. Because Kubernetes charts can be large and complex, Helm tries to perform the
least invasive upgrade. It will only update things that have changed since the last
release.
After each upgrade, a new *revision* will be generated. You can check the revision
history of a release with `helm history my-release` and go back to the previous revision
if something went wrong with `helm rollback my-release 1` (`1` is the revision number of
the previous release revision).
### Backup Penpot
The Penpot's Helm Chart uses different Persistent Volumes to store all persistent data.
This allows you to delete and recreate the instance whenever you want without losing
information.
You back up data from a Persistent Volume via snapshots, so you will want to ensure that
your container storage interface (CSI) supports volume snapshots. There are a couple of
different options for the CSI driver that you choose. All of the major cloud providers
have their respective CSI drivers.
At last, there are two Persistent Volumes used: one for the Postgres database and another
one for the assets uploaded by your users (images and svg clips). There may be more
volumes if you enable other features, as explained in the file itself.
You have to back up your custom settings too (the yaml file or the list of parameters you
are using during you setup).
## Unofficial self-host options
There are some other options, **NOT SUPPORTED BY PENPOT**:
* Install with <a href="https://community.penpot.app/t/how-to-develop-penpot-with-podman-penpotman/2113" target="_blank">Podman</a> instead of Docker.
* Try the under development <a href="https://github.com/author-more/penpot-desktop/releases/latest" target="_blank">Penpot Desktop app</a>.
* Try a simple Kubernetes Deployment option <a href="https://github.com/degola/penpot-kubernetes" target="_blank">penpot-kubernetes</a>.
* Or try a fully manual installation if you have a really specific use case.. For help, you can look at the [Architecture][2] section and the <a href="https://github.com/penpot/penpot/tree/develop/docker/images" target="_blank">Docker configuration files</a>.
[1]: /technical-guide/configuration/
[2]: /technical-guide/developer/architecture

View File

@@ -0,0 +1,239 @@
---
title: 1.3 Install with Docker
---
# Install with Docker
This section details everything you need to know to get Penpot up and running in
production environments using Docker. For this, we provide a series of *Dockerfiles* and a
*docker-compose* file that orchestrate all.
## Install Docker
<p class="advice">
Skip this section if you already have docker installed, up and running.
</p>
Currently, Docker comes into two different flavours:
### Docker Desktop
This is the only option to have Docker in a Windows or MacOS. Recently it's also available
for Linux, in the most popular distributions (Debian, Ubuntu and Fedora).
You can install it following the <a href="https://docs.docker.com/desktop/"
target="_blank">official guide</a>.
Docker Desktop has a graphical control panel (GUI) to manage the service and view the
containers, images and volumes. But you need the command line (Terminal in Linux and Mac, or
PowerShell in Windows) to build and run the containers, and execute other operations.
It already includes **docker compose** utility, needed by Penpot.
### Docker Engine
This is the classic and default Docker setup for Linux machines, and the only option for a
Linux VPS without graphical interface.
You can install it following the <a href="https://docs.docker.com/engine/"
target="_blank">official guide</a>.
And you also need the [docker
compose](https://docs.docker.com/compose/cli-command/#installing-compose-v2) (V2)
plugin. You can use the old **docker-compose** tool, but all the documentation supposes
you are using the V2.
You can easily check which version of **docker compose** you have. If you can execute
<code class="language-bash">docker compose</code> command, then you have V2. If you need to write <code class="language-bash">docker-compose</code> (with a
<code class="language-bash">-</code>) for it to work, you have the old version.
## Start Penpot
As a first step you will need to obtain the <code class="language-bash">docker-compose.yaml</code> file. You can download it
<a
href="https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml"
target="_blank">from the Penpot repository</a>.
```bash
wget https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
```
or
```bash
curl -o docker-compose.yaml https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
```
Then simply launch composer:
```bash
docker compose -p penpot -f docker-compose.yaml up -d
```
At the end it will start listening on http://localhost:9001
<p class="advice">
If you don't change anything, by default this will use the latest image published in dockerhub.
</p>
If you want to have more control over the version (which is recommended), you can use the PENPOT_VERSION envvar in the common ways:
- setting the value in the .env file
- or passing the envvar in the command line
```bash
PENPOT_VERSION=2.4.3 docker compose -p penpot -f docker-compose.yaml up -d
```
## Stop Penpot
If you want to stop running Penpot, just type
```bash
docker compose -p penpot -f docker-compose.yaml down
```
## Configure Penpot with Docker
The configuration is defined using flags and environment variables in the <code class="language-bash">docker-compose.yaml</code>
file. The default downloaded file comes with the essential flags and variables already set,
and other ones commented out with some explanations.
You can find all configuration options in the [Configuration][1] section.
## Using the CLI for administrative tasks
Penpot provides a script (`manage.py`) with some administrative tasks to perform in the server.
**NOTE**: this script will only work with the <code class="language-bash">enable-prepl-server</code>
flag set in the docker-compose.yaml file. For older versions of docker-compose.yaml file,
this flag is set in the backend service.
For instance, if the registration is disabled, the only way to create a new user is with this script:
```bash
docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile
```
**NOTE:** the exact container name depends on your docker version and platform.
For example it could be <code class="language-bash">penpot-penpot-backend-1</code> or <code class="language-bash">penpot_penpot-backend-1</code>.
You can check the correct name executing <code class="language-bash">docker ps</code>.
## Update Penpot
To get the latest version of Penpot in your local installation, you just need to
execute:
```bash
docker compose -f docker-compose.yaml pull
```
This will fetch the latest images. When you do <code class="language-bash">docker compose up</code> again, the containers will be recreated with the latest version.
<p class="advice">
It is strongly recommended to update the Penpot version in small increments, rather than updating between two distant versions.
</p>
**Important: Upgrade from version 1.x to 2.0**
The migration to version 2.0, due to the incorporation of the new v2 components, includes
an additional process that runs automatically as soon as the application starts. If your
on-premises Penpot instance contains a significant amount of data (such as hundreds of
penpot files, especially those utilizing SVG components and assets extensively), this
process may take a few minutes.
In some cases, such as when the script encounters an error, it may be convenient to run
the process manually. To do this, you can disable the automatic migration process using
the <code class="language-bash">disable-v2-migration</code> flag in <code
class="language-bash">PENPOT_FLAGS</code> environment variable. You can then execute the
migration process manually with the following command:
```bash
docker exec -ti <container-name-or-id> ./run.sh app.migrations.v2
```
**IMPORTANT:** this script should be executed on passing from 1.19.x to 2.0.x. Executing
it on versions greater or equal to 2.1 of penpot will not work correctly. It is known that
this script is removed since 2.4.3
## Backup Penpot
Penpot uses <a href="https://docs.docker.com/storage/volumes" target="_blank">Docker
volumes</a> to store all persistent data. This allows you to delete and recreate
containers whenever you want without losing information.
This also means you need to do regular backups of the contents of the volumes. You cannot
directly copy the contents of the volume data folder. Docker provides you a <a
href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"
target="_blank">volume backup procedure</a>, that uses a temporary container to mount one
or more volumes, and copy their data to an archive file stored outside of the container.
If you use Docker Desktop, <a
href="https://www.docker.com/blog/back-up-and-share-docker-volumes-with-this-extension/"
target="_blank">there is an extension</a> that may ease the backup process.
If you use the default **docker compose** file, there are two volumes used: one for the
Postgres database and another one for the assets uploaded by your users (images and svg
clips). There may be more volumes if you enable other features, as explained in the file
itself.
## Configure the proxy
Your host configuration needs to make a proxy to http://localhost:9001.
### Example with NGINX
```bash
server {
listen 80;
server_name penpot.mycompany.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name penpot.mycompany.com;
# This value should be in sync with the corresponding in the docker-compose.yml
# PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
client_max_body_size 31457280;
# Logs: Configure your logs following the best practices inside your company
access_log /path/to/penpot.access.log;
error_log /path/to/penpot.error.log;
# TLS: Configure your TLS following the best practices inside your company
ssl_certificate /path/to/fullchain;
ssl_certificate_key /path/to/privkey;
# Websockets
location /ws/notifications {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_pass http://localhost:9001/ws/notifications;
}
# Proxy pass
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://localhost:9001/;
}
}
```
### Example with CADDY SERVER
```bash
penpot.mycompany.com {
reverse_proxy :9001
tls /path/to/fullchain.pem /path/to/privkey.pem
log {
output file /path/to/penpot.log
}
}
```
[1]: /technical-guide/configuration/

View File

@@ -0,0 +1,68 @@
---
title: 1.2 Install with Elestio
---
# Install with Elestio
This section explains how to get Penpot up and running using <a href="https://elest.io/open-source/penpot"
target="_blank">Elestio</a>.
This platform offers a fully managed service for on-premise instances of a selection of
open-source software! This means you can deploy a dedicated instance of Penpot in just 3
minutes. Youll be relieved of the need to worry about DNS configuration, SMTP, backups,
SSL certificates, OS & Penpot upgrades, and much more.
## Get an Elestio account
<p class="advice">
Skip this section if you already have an Elestio account.
</p>
To create your Elestio account <a href="https://dash.elest.io/deploy?soft=Penpot&id=121"
target="_blank">click here</a>. You can choose to deploy on any one of five leading cloud
providers or on-premise.
## Deploy Penpot using Elestio
Now you can Create your service in “Services”:
1. Look for Penpot.
2. Select a Service Cloud Provider.
3. Select Service Cloud Region.
4. Select Service Plan (for a team of 20 you should be fine with 2GB RAM).
5. Select Elestio Service Support.
6. Provide Service Name (this will show in the URL of your instance) & Admin email (used
to create the admin account).
7. Select Advanced Configuration options (you can also do this later).
8. Hit “Create Service” on the bottom right.
It will take a couple of minutes to get the instance launched. When the status turns to
“Service is running” you are ready to get started.
By clicking on the Service you go to all the details and configuration options.
In Network/CNAME you can find the URL of your instance. Copy and paste this into a browser
and start using Penpot.
## Configure Penpot with Elestio
If you want to make changes to your Penpot setup click on the “Update config” button in
Software. Here you can see the “Docker compose” used to create the instance. In “ENV” top
middle left you can make configuration changes that will be reflected in the Docker
compose.
In this file, a “#” at the start of the line means it is text and not considered part of
the configuration. This means you will need to delete it to get some of the configuration
options to work. Once you made all your changes hit “Update & restart”. After a couple of
minutes, your changes will be active.
You can find all configuration options in the [Configuration][1] section.
Get in contact with us through <a href="mailto:support@penpot.app">support@penpot.app</a>
if you have any questions or need help.
## Update Penpot
Elestio will update your instance automatically to the latest release unless you don't
want this. In that case you need to “Disable auto updates” in Software auto updates.
[1]: /technical-guide/configuration/

View File

@@ -0,0 +1,31 @@
---
title: 1. Self-hosting Guide
---
# Self-hosting Guide
This guide explains how to get your own Penpot instance, running on a machine you control,
to test it, use it by you or your team, or even customize and extend it any way you like.
If you need more context you can look at the <a
href="https://community.penpot.app/t/self-hosting-penpot-i/2336" target="_blank">post
about self-hosting</a> in Penpot community.
**There is absolutely no difference between <a
href="https://design.penpot.app">our SaaS offer</a> for Penpot and your
self-hosted Penpot platform!**
There are three main options for creating a Penpot instance:
1. Using the platform of our partner <a href="https://elest.io/open-source/penpot" target="_blank">Elestio</a>.
2. Using <a href="https://docker.com" target="_blank">Docker</a> tool.
3. Using <a href="https://kubernetes.io/" target="_blank">Kubernetes</a>.
<p class="advice">
The recommended way is to use Elestio, since it's simpler, fully automatic and still greatly flexible.
Use Docker if you already know the tool, if need full control of the process or have extra requirements
and do not want to depend on any external provider, or need to do any special customization.
</p>
Or you can try <a href="#unofficial-self-host-options">other options</a>,
offered by Penpot community.

View File

@@ -0,0 +1,136 @@
---
title: 1.4 Install with Kubernetes
---
# Install with Kubernetes
This section details everything you need to know to get Penpot up and running in
production environments using a Kubernetes cluster of your choice. To do this, we have
created a <a href="https://helm.sh/" target="_blank">Helm</a> repository with everything
you need.
Therefore, your prerequisite will be to have a Kubernetes cluster on which we can install
Helm.
## What is Helm
*Helm* is the package manager for Kubernetes. A *Chart* is a Helm package. It contains
all of the resource definitions necessary to run an application, tool, or service inside
of a Kubernetes cluster. Think of it like the Kubernetes equivalent of a Homebrew
formula, an Apt dpkg, or a Yum RPM file.
A Repository is the place where charts can be collected and shared. It's like Perl's CPAN
archive or the Fedora Package Database, but for Kubernetes packages.
A Release is an instance of a chart running in a Kubernetes cluster. One chart can often
be installed many times into the same cluster. And each time it is installed, a new
release is created. Consider a MySQL chart. If you want two databases running in your
cluster, you can install that chart twice. Each one will have its own release, which will
in turn have its own release name.
With these concepts in mind, we can now explain Helm like this:
> Helm installs charts into Kubernetes clusters, creating a new release for each
> installation. To find new charts, you can search Helm chart repositories.
## Install Helm
<p class="advice">
Skip this section if you already have Helm installed in your system.
</p>
You can install Helm by following the <a href="https://helm.sh/docs/intro/install/" target="_blank">official guide</a>.
There are different ways to install Helm, depending on your infrastructure and operating
system.
## Add Penpot repository
To add the Penpot Helm repository, run the following command:
```bash
helm repo add penpot http://helm.penpot.app
```
This will add the Penpot repository to your Helm configuration, so you can install all
the Penpot charts stored there.
## Install Penpot Chart
To install the chart with the release name `my-release`:
```bash
helm install my-release penpot/penpot
```
You can customize the installation by specifying each parameter using the `--set key=value[,key=value]`
argument to helm install. For example,
```bash
helm install my-release \
--set global.postgresqlEnabled=true \
--set global.redisEnabled=true \
--set persistence.assets.enabled=true \
penpot/penpot
```
Alternatively, a YAML file that specifies the values for the above parameters can be
provided while installing the chart. For example,
```bash
helm install my-release -f values.yaml penpot/penpot
```
## Configure Penpot with Helm Chart
In the previous section we have shown how to configure penpot during installation by
using parameters or by using a yaml file.
The default values are defined in the
<a href="https://github.com/penpot/penpot-helm/blob/main/charts/penpot/values.yaml" target="_blank">`values.yml`</a>
file itself, which you can use as a basis for creating your own settings.
You can also consult the list of parameters on the
<a href="https://artifacthub.io/packages/helm/penpot/penpot#parameters" target="_blank">ArtifactHub page of the project</a>.
## Upgrade Penpot
When a new version of Penpot's chart is released, or when you want to change the
configuration of your release, you can use the helm upgrade command.
```bash
helm upgrade my-release -f values.yaml penpot/penpot
```
An upgrade takes an existing release and upgrades it according to the information you
provide. Because Kubernetes charts can be large and complex, Helm tries to perform the
least invasive upgrade. It will only update things that have changed since the last
release.
After each upgrade, a new *revision* will be generated. You can check the revision
history of a release with `helm history my-release` and go back to the previous revision
if something went wrong with `helm rollback my-release 1` (`1` is the revision number of
the previous release revision).
## Backup Penpot
The Penpot's Helm Chart uses different Persistent Volumes to store all persistent data.
This allows you to delete and recreate the instance whenever you want without losing
information.
You back up data from a Persistent Volume via snapshots, so you will want to ensure that
your container storage interface (CSI) supports volume snapshots. There are a couple of
different options for the CSI driver that you choose. All of the major cloud providers
have their respective CSI drivers.
At last, there are two Persistent Volumes used: one for the Postgres database and another
one for the assets uploaded by your users (images and svg clips). There may be more
volumes if you enable other features, as explained in the file itself.
You have to back up your custom settings too (the yaml file or the list of parameters you
are using during you setup).

View File

@@ -0,0 +1,13 @@
---
title: 1.1 Recommended Settings
---
# Recommended settings
To self-host Penpot, youll need a server with the following specifications:
* **CPU:** 1-2 CPUs
* **RAM:** 4 GiB of RAM
* **Disk Space:** Disk requirements depend on your usage. Disk usage primarily involves the database and any files uploaded by users.
This setup should be sufficient for a smooth experience with typical usage (your mileage may vary).

View File

@@ -0,0 +1,14 @@
---
title: 1.5 Unofficial self-host options
---
# Unofficial self-host options
There are some other options, **NOT SUPPORTED BY PENPOT**:
* Install with <a href="https://community.penpot.app/t/how-to-develop-penpot-with-podman-penpotman/2113" target="_blank">Podman</a> instead of Docker.
* Try the under development <a href="https://github.com/author-more/penpot-desktop/releases/latest" target="_blank">Penpot Desktop app</a>.
* Try a simple Kubernetes Deployment option <a href="https://github.com/degola/penpot-kubernetes" target="_blank">penpot-kubernetes</a>.
* Or try a fully manual installation if you have a really specific use case.. For help, you can look at the [Architecture][1] section and the <a href="https://github.com/penpot/penpot/tree/develop/docker/images" target="_blank">Docker configuration files</a>.
[1]: /technical-guide/developer/architecture

View File

@@ -37,6 +37,11 @@ Also, if you are a developer, you can get into the code, to explore it, learn ho
or extend it and contribute with new functionality. For this, we have a different Docker installation.
In the [Developer Guide][6] you can find how to setup a development environment and many other dev-oriented documentation.
## Troubleshooting Penpot
The [Troubleshooting][8] section guides you through the different logs in Penpot so you can easily identify
any issue that may arise as well as report it comprehensively.
[1]: /technical-guide/getting-started/#install-with-elestio
[2]: /technical-guide/getting-started/#install-with-docker
[3]: /technical-guide/configuration/
@@ -44,3 +49,4 @@ In the [Developer Guide][6] you can find how to setup a development environment
[5]: /technical-guide/integration/
[6]: /technical-guide/developer/
[7]: /technical-guide/getting-started/#install-with-kubernetes
[8]: /technical-guide/troubleshooting/

View File

@@ -0,0 +1,62 @@
---
title: 5. Troubleshooting Penpot
---
# Troubleshooting Penpot
Knowing how to do Penpot troubleshooting can be very useful; on the one hand, it helps to create issues easier to resolve,
since they include relevant information from the beginning which also makes them get solved faster;
on the other hand, many times troubleshooting gives the necessary information to resolve a problem autonomously,
without even creating an issue.
Troubleshooting requires patience and practice; you have to read the stacktrace carefully, even if it looks like a mess at first.
It takes some practice to learn how to read the traces properly and extract important information.
So, if your Penpot installation is not working as intended, there are several places to look up searching for hints.
## Browser logs
Regardless of the type of installation you have performed, you can find useful information about Penpot in your browser.
First, use the devtools to ensure which version and flags you're using. Go to your Penpot instance in the browser and press F12;
you'll see the devtools. In the <code class="language-bash">Console</code>, you can see the exact version that's being used.
![Console](/img/dev-tools-1.png)
Other interesting tab in the devtools is the <code class="language-bash">Network</code> tab, to check if there is a request that throws errors.
![Network](/img/dev-tools-2.png)
## Penpot report
When Penpot crashes, it provides a report with very useful information. Don't miss it!
![Penpot Report](/img/penpot-report.png)
## Docker logs
If you are using the Docker installation, this is an easy way to take a look at the logs.
Check if all containers are up and running:
```bash
docker compose -p penpot -f docker-compose.yaml ps
```
Check logs of all Penpot:
```bash
docker compose -p penpot -f docker-compose.yaml logs -f
```
If there is too much information and you'd like to check just one service at a time:
```bash
docker compose -p penpot -f docker-compose.yaml logs penpot-frontend -f
```
You can always check the logs form a specific container:
```bash
docker logs -f penpot-penpot-postgres-1
```

View File

@@ -27,7 +27,7 @@ title: 07· Exporting objects
<ul>
<li><strong>Size</strong> - Options for the most common sizing scales.</li>
<li><strong>Suffix</strong> - Especially useful if you are exporting at different scales.</li>
<li><strong>File format</strong> - PNG, SVG, JPEG, PDF.</li>
<li><strong>File format</strong> - PNG, JPEG, WEBP, SVG, PDF.</li>
</ul>
<h2 id="export-multiple-elements">Exporting multiple elements</h2>

View File

@@ -166,7 +166,7 @@ a design.</p>
<h2 id="curves">Curves (freehand)</h2>
<p>The curve tool allows a path to be created directly in a freehand mode.
Select the curve tool by clicking on the icon at the toolbar or pressing <kbd>Ctrl/⌘</kbd> + <kbd>c</kbd>.
Select the curve tool by clicking on the icon at the toolbar or pressing <kbd>Shift/⇧</kbd> + <kbd>c</kbd>.
<p>The path created will contain a lot of points, but it is edited the same way as any other curve.</p>
<h2 id="paths">Paths (bezier)</h2>
@@ -206,7 +206,7 @@ You can choose to edit individual nodes or create new ones. Press <kbd>Esc</kbd>
<h3>Insert images</h3>
<p>There are several options for inserting an image into a Penpot file:</p>
<ul>
<li>Use the <strong>image tool</strong> at the toolbar or press <kbd>K</kbd> to inspect images in your file system.</li>
<li>Use the <strong>image tool</strong> at the toolbar or press <kbd>K</kbd> to insert images in your file system.</li>
<li><strong>Drag</strong> an image from your computer to the viewport.</li>
<li>Copy an image & paste it or drag it right from a <strong>browser</strong>.</li>
<li>Drag an image from a Penpot <strong>library</strong>.</li>

View File

@@ -51,13 +51,24 @@ title: 06· Styling
<ol>
<li><strong>Eyedropper</strong> - Allows you to pick any color of the objects at the viewport.</li>
<li><strong>Color profiles</strong> - Select between RGB, the Harmony Wheel or HSV.</li>
<li><strong>Color type</strong> - Solid, linear gradient, radial gradient or image.</li>
<li><strong>Color type</strong> - Solid, gradient, or image.</li>
<li><strong>Sliders</strong> - Easily manage settings like brightness, saturation or opacity.</li>
<li><strong>Values</strong> - Set precise color values of red(R), green(G), blue(B) and transparency(A).</li>
<li><strong>Libraries</strong> - Switch between recent colors and libraries.</li>
<li><strong>Color palette</strong> - A quick launcher of the palette with the selected library.</li>
</ol>
<h3 id="color-picker-gradients">Gradients</h3>
<p>You can apply gradient fills to layers. To do that select the Gradient type at the color picker.</p>
<figure>
<img alt="Gradient" src="/img/styling/color-picker-gradient.webp"/>
</figure>
<p>You can choose between two types of gradients:</p>
<ul>
<li><strong>Linear Gradient:</strong> A smooth transition between two or more colors along a straight line, with the option to adjust the angle.</li>
<li><strong>Radial Gradient:</strong> A circular color transition that starts with one color at the center and gradually shifts to another at the edges, which could be a different color or a fade to transparency.</li>
</ul>
<h2 id="color-palette">Color palette</h2>
<p>The color palette allows you to have a selected color library in plain sight.</p>
<figure>

View File

@@ -5,52 +5,6 @@ title: 02· The interface
<h1 id="the-interface">The interface</h1>
<p class="main-paragraph">The Penpot interface has three main areas: Dashboard, Workspace and View mode. Lets take a look at their composition and main features.</p>
<h2 id="interface-dashboard">Dashboard</h2>
<p>The Dashboard is the place where you will be able to organize your files, libraries, projects and teams.</p>
<figure>
<a href="/img/interface/dashboard-dark.webp" target="_blank">
<img src="/img/interface/dashboard-dark.webp" alt="Penpot's dashboard" />
</a>
</figure>
<p class="hint">
<strong>1)</strong> Teams
<strong>2)</strong> Search files
<strong>3)</strong> Projects
<strong>4)</strong> Drafts
<strong>5)</strong> Shared Libraries
<strong>6)</strong> Custom fonts
<strong>7)</strong> Pinned projects
<strong>8)</strong> User area
<strong>9)</strong> Comment notifications
<strong>10)</strong> Create project
<strong>11)</strong> File card
<strong>12)</strong> Libraries & Templates module
</p>
<ol>
<li><strong>Teams:</strong> A team allows you to collaborate with other Penpot users. Team members are allowed to work with any project or file within the team depending on their permissions. Members with admin permissions can also invite other members. Create or join as many teams as you need with different groups of people.</li>
<li><strong>Search:</strong> If you are looking for a specific file just type its title at the search box.</li>
<li><strong>Projects:</strong> A project allows you to group design files. It works pretty much like a folder in a file system. You can create as many projects as you need. If you are going to work with more people in a project, you should create it inside a team.</li>
<li><strong>Drafts:</strong> The drafts section is where you can find the design files that are not inside any project.</li>
<li><strong>Shared Libraries:</strong> In this section you will find all the design files that have been added as shared libraries. That way you will be able to better control the files that are sharing their assets. </li>
<li><strong>Custom fonts:</strong> If you have purchased or own personal fonts that are not included in the catalog provided by Penpot, you can upload them from your computer and use them across the files of a team.</li>
<li><strong>Pinned projects:</strong> If you want to keep some projects handy (for instance because youre currently working on them) you can pin them to make them quickly available at the sidebar.</li>
<li><strong>User area:</strong> This must be you! Access your profile settings, Penpot tutorials, the Penpot Community and more. You can also find here a way to leave us feedback. Wed love to read your thoughts :)</li>
<li><strong>Comments notifications:</strong> Here you will be able to see if you have unread comments inside the files of the team.</li>
<li><strong>Create project:</strong> Create as many projects as you need to organize your designs.</li>
<li><strong>File card:</strong> Basic information about a file at plain sight. A preview, update info or if its added as a Shared Library. From there you can perform several actions over the file (rename, duplicate, move, download, delete).</li>
<li><strong>Libraries & Templates module:</strong> A curated selection of Libraries & Templates files ready to import.</li>
</ol>
<h3 id="your-account">Your account</h3>
<p>Your account settings can be changed at the user area, in <b>Your account</b>. Here you can make changes to your profile, password or account language, as well as generate personal access tokens and access release notes.</p>
<p>If you want to change the email address associated to your account or remove your account entirely, this can be done in the <b>Profile</b> section.</p>
<h2 id="interface-workspace">Workspace</h2>
<p>The Workspace is where you actually create your designs. You have an infinite canvas where you can work directly but you also have the ability to create and work inside boards that will help you to create pages and exportation units.</p>
@@ -102,8 +56,6 @@ title: 02· The interface
<li><strong>Assets panel:</strong> Each file has a default library (File Library) where you can store elements and styles that are likely to be reused within a project. That includes components, colors and typographies. To add an asset to a library just click the “+” button at the header of each asset group.</li>
</ol>
<h2 id="interface-viewmode">View mode</h2>
<p>Launch the view mode to present and share your designs, comment on them and play with the interactions set at the workspace. You also have an Inspect mode where you can get properties specifications and code snippets. <a href="/user-guide/view-mode/">More about the View mode.</a></p>
@@ -145,6 +97,83 @@ title: 02· The interface
<li><strong>Navigation buttons:</strong> Forward and backwards buttons.</li>
</ol>
<h2 id="interface-dashboard">Dashboard</h2>
<p>The Dashboard is the place where you will be able to organize your files, libraries, projects and teams.</p>
<figure>
<a href="/img/interface/dashboard-dark.webp" target="_blank">
<img src="/img/interface/dashboard-dark.webp" alt="Penpot's dashboard" />
</a>
</figure>
<p class="hint">
<strong>1)</strong> Teams
<strong>2)</strong> Search files
<strong>3)</strong> Projects
<strong>4)</strong> Drafts
<strong>5)</strong> Shared Libraries
<strong>6)</strong> Custom fonts
<strong>7)</strong> Pinned projects
<strong>8)</strong> User area
<strong>9)</strong> Comment notifications
<strong>10)</strong> Create project
<strong>11)</strong> File card
<strong>12)</strong> Libraries & Templates module
</p>
<ol>
<li><strong>Teams:</strong> A team allows you to collaborate with other Penpot users. Team members are allowed to work with any project or file within the team depending on their permissions. Members with admin permissions can also invite other members. Create or join as many teams as you need with different groups of people.</li>
<li><strong>Search:</strong> If you are looking for a specific file just type its title at the search box.</li>
<li><strong>Projects:</strong> A project allows you to group design files. It works pretty much like a folder in a file system. You can create as many projects as you need. If you are going to work with more people in a project, you should create it inside a team.</li>
<li><strong>Drafts:</strong> The drafts section is where you can find the design files that are not inside any project.</li>
<li><strong>Shared Libraries:</strong> In this section you will find all the design files that have been added as shared libraries. That way you will be able to better control the files that are sharing their assets. </li>
<li><strong>Custom fonts:</strong> If you have purchased or own personal fonts that are not included in the catalog provided by Penpot, you can upload them from your computer and use them across the files of a team.</li>
<li><strong>Pinned projects:</strong> If you want to keep some projects handy (for instance because youre currently working on them) you can pin them to make them quickly available at the sidebar.</li>
<li><strong>User area:</strong> This must be you! Access your profile settings, Penpot tutorials, the Penpot Community and more. You can also find here a way to leave us feedback. Wed love to read your thoughts :)</li>
<li><strong>Comments notifications:</strong> Here you will be able to see if you have unread comments inside the files of the team.</li>
<li><strong>Create project:</strong> Create as many projects as you need to organize your designs.</li>
<li><strong>File card:</strong> Basic information about a file at plain sight. A preview, update info or if its added as a Shared Library. From there you can perform several actions over the file (rename, duplicate, move, download, delete).</li>
<li><strong>Libraries & Templates module:</strong> A curated selection of Libraries & Templates files ready to import.</li>
</ol>
<h3 id="your-account">Your account</h3>
<p>Your account settings can be changed at the user area, in <b>Your account</b>. Here you can make changes to your profile, password or account language, as well as generate personal access tokens and access release notes.</p>
<h4 id="your-account-profile">Profile
<a class="direct-link" href="#your-account-profile">#</a>
</h3>
<p>If you want to change the email address associated to your account or remove your account entirely, this can be done in the <b>Profile</b> section.</p>
<figure>
<img src="/img/interface/youraccount-profile.webp" alt="Penpot's profile" />
</figure>
<h4 id="your-account-password">Password
<a class="direct-link" href="#your-account-password">#</a>
</h3>
<p>If you want to change your password to a new one, this can be done in the <b>Password</b> section.</p>
<figure>
<img src="/img/interface/youraccount-password.webp" alt="Penpot's password" />
</figure>
<h4 id="your-account-notifications">Notifications
<a class="direct-link" href="#your-account-notifications">#</a>
</h3>
<p>At the <strong>Notifications</strong> section you can configure the email and dashboard notifications.</p>
<figure>
<img src="/img/interface/youraccount-notifications.webp" alt="Penpot's notifications" />
</figure>
<h4 id="your-account-settings">Settings
<a class="direct-link" href="#your-account-settings">#</a>
</h3>
<p>At the <strong>Settings</strong> section you can change the language and the UI color theme.</p>
<figure>
<img src="/img/interface/youraccount-settings.webp" alt="Penpot's settings" />
</figure>
<h4 id="your-account-accesstokens">Access tokens
<a class="direct-link" href="#your-account-accesstokens">#</a>
</h3>
<p>At the <strong>Asset tokens</strong> section you can manage your access tokens. <a href="https://help.penpot.app/technical-guide/integration/#access-tokens" target="_blank">Read more about access tokens here</a>.</p>
<h2 id="interface-ui-theme">UI Theme</h2>
<p>Penpot's default interface is dark but you can switch anytime to a light option. You have 2 ways to change the theme:</p>
@@ -170,4 +199,4 @@ title: 02· The interface
<img src="/img/interface/viewmode-light.webp" alt="Penpot's view mode" />
</a>
<figcaption>Penpot's view mode in light mode</figcaption>
</figure>
</figure>

View File

@@ -15,7 +15,7 @@
(s/def ::name ::us/string)
(s/def ::suffix ::us/string)
(s/def ::type #{:jpeg :png :pdf :svg})
(s/def ::type #{:png :jpeg :webp :pdf :svg})
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::share-id ::us/uuid)
@@ -40,6 +40,7 @@
(case type
:png (rb/render params on-object)
:jpeg (rb/render params on-object)
:webp (rb/render params on-object)
:pdf (rp/render params on-object)
:svg (rs/render params on-object)))

View File

@@ -34,7 +34,11 @@
(bw/wait-for node)
(case type
:png (bw/screenshot node {:omit-background? true :type type :path path})
:jpeg (bw/screenshot node {:omit-background? false :type type :path path}))
:jpeg (bw/screenshot node {:omit-background? false :type type :path path})
:webp (p/let [png-path (sh/tempfile :prefix "penpot.tmp.render.bitmap." :suffix ".png")]
;; playwright only supports jpg and png, we need to convert it afterwards
(bw/screenshot node {:omit-background? true :type :png :path png-path})
(sh/run-cmd! (str "convert " png-path " -quality 100 WEBP:" path))))
(on-object (assoc object :path path))))
(render [uri page]

View File

@@ -15,6 +15,7 @@
(case type
:png ".png"
:jpeg ".jpg"
:webp ".webp"
:svg ".svg"
:pdf ".pdf"
:zip ".zip"))
@@ -26,6 +27,7 @@
:pdf "application/pdf"
:svg "image/svg+xml"
:jpeg "image/jpeg"
:png "image/png"))
:png "image/png"
:webp "image/webp"))

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,43 @@
{
"~#set": [
{
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:name": "Lorem Ipsum",
"~:revn": 2,
"~:modified-at": "~m1739356261950",
"~:vern": 0,
"~:id": "~u69b52fcf-7de0-81cd-8005-b9b180a0bfb5",
"~:thumbnail-id": "~u55bb9e08-6eed-4a64-a94d-2bcce7006e79",
"~:is-shared": true,
"~:project-id": "~u1ad2931c-eb80-8098-8005-b86c1d9d26c2",
"~:created-at": "~m1739356217030",
"~:library-summary": {
"~:components": {
"~:count": 0,
"~:sample": []
},
"~:media": {
"~:count": 0,
"~:sample": []
},
"~:colors": {
"~:count": 1,
"~:sample": [
{
"~:path": "",
"~:color": "#0087ff",
"~:name": "#0087ff",
"~:modified-at": "~m1739356244863",
"~:opacity": 1,
"~:id": "~u0449ccff-62fe-805c-8005-b9b194b094dd"
}
]
},
"~:typographies": {
"~:count": 0,
"~:sample": []
}
}
}
]
}

View File

@@ -0,0 +1,48 @@
[
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116375",
"~:is-default": true
},
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Second team",
"~:modified-at": "~m1701164272671",
"~:id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:created-at": "~m1701164272671",
"~:is-default": false
}
]

View File

@@ -0,0 +1,115 @@
{
"~:features": {
"~#set": [
"layout/grid",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "10113 - Emtpy lib",
"~:revn": 1,
"~:modified-at": "~m1739365936352",
"~:vern": 0,
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67"
]
},
"~:version": 67,
"~:project-id": "~u1ad2931c-eb80-8098-8005-b86c1d9d26c2",
"~:created-at": "~m1739365911709",
"~:data": {
"~:pages": [
"~u5b7ebd2b-2907-80db-8005-b9d67c20cf2f"
],
"~:pages-index": {
"~u5b7ebd2b-2907-80db-8005-b9d67c20cf2f": {
"~#penpot/pointer": [
"~u5b7ebd2b-2907-80db-8005-b9d67c21cbd3",
{
"~:created-at": "~m1739365911687"
}
]
}
},
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
"~:options": {
"~:components-v2": true
},
"~:colors": {
"~u84a1567d-3f0f-804e-8005-b9d6907e3c8a": {
"~:path": "",
"~:color": "#0087ff",
"~:name": "#0087ff",
"~:modified-at": "~m1739365936355",
"~:opacity": 1,
"~:id": "~u84a1567d-3f0f-804e-8005-b9d6907e3c8a"
}
}
}
}

View File

@@ -0,0 +1,101 @@
{
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c21cbd3",
"~:file-id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
"~:created-at": "~m1739365911680",
"~:data": {
"~:options": {},
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0.0,
"~:y": 0.0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0.0,
"~:y": 0.01
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:r3": 0,
"~:r1": 0,
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1.0,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 0.01,
"~:height": 0.01,
"~:x1": 0,
"~:y1": 0,
"~:x2": 0.01,
"~:y2": 0.01
}
},
"~:fills": [
{
"~:fill-color": "#FFFFFF",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": []
}
}
},
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2f",
"~:name": "Page 1"
}
}

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,5 @@
{
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
"~:name": "10113 - Emtpy lib",
"~:is-shared": true
}

View File

@@ -72,7 +72,7 @@ export class DashboardPage extends BaseWebSocketPage {
this.draftsLink = this.sidebar.getByText("Drafts");
this.fontsLink = this.sidebar.getByText("Fonts");
this.libsLink = this.sidebar.getByText("Libraries");
this.librariesLink = this.sidebar.getByText("Libraries");
this.searchButton = page.getByRole("button", { name: "dashboard-search" });
this.searchInput = page.getByPlaceholder("Search…");
@@ -155,6 +155,9 @@ export class DashboardPage extends BaseWebSocketPage {
await this.mockRPC("search-files", "dashboard/search-files.json", {
method: "POST",
});
await this.mockRPC("delete-team", "dashboard/delete-team.json", {
method: "POST",
});
await this.mockRPC("search-files", "dashboard/search-files.json");
await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json");
}
@@ -278,6 +281,13 @@ export class DashboardPage extends BaseWebSocketPage {
await this.userProfileOption.click();
}
async goToLibraries() {
await this.page.goto(
`#/dashboard/libraries?team-id=${DashboardPage.anyTeamId}`,
);
await expect(this.mainHeading).toHaveText("Libraries");
}
}
export default DashboardPage;

View File

@@ -70,6 +70,7 @@ export class WorkspacePage extends BaseWebSocketPage {
);
this.toolbarOptions = page.getByTestId("toolbar-options");
this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
this.moveButton = page.getByRole("button", { name: "Move (V)" });
this.boardButton = page.getByRole("button", { name: "Board (B)" });
this.toggleToolbarButton = page.getByRole("button", {
name: "Toggle toolbar",
@@ -221,7 +222,7 @@ export class WorkspacePage extends BaseWebSocketPage {
}
async openLibrariesModal(clickOptions = {}) {
await this.sidebar.getByText("Libraries").click(clickOptions);
await this.sidebar.getByTestId("libraries").click(clickOptions);
await expect(this.librariesModal).toBeVisible();
}

View File

@@ -13,6 +13,7 @@ test("Bug 7549 - User clicks on color swatch to display the color picker next to
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
await workspacePage.moveButton.click();
const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" });
const swatchBox = await swatch.boundingBox();
await swatch.click();
@@ -171,6 +172,7 @@ test("Bug 9900 - Color picker has no inputs for HSV values", async ({
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
await workspacePage.moveButton.click();
const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" });
await swatch.click();

View File

@@ -0,0 +1,33 @@
import { test, expect } from "@playwright/test";
import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test("BUG 10421 - Fix libraries context menu", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.mockRPC(
"get-team-shared-files?team-id=*",
"dashboard/get-team-shared-files-10142.json",
);
await dashboardPage.mockRPC(
"get-all-projects",
"dashboard/get-all-projects.json",
);
await dashboardPage.goToLibraries();
const libraryItem = page.getByTitle(/Lorem Ipsum/);
await expect(libraryItem).toBeVisible();
await libraryItem.getByRole("button", { name: "Options" }).click();
await expect(page.getByText("Rename")).toBeVisible();
});

View File

@@ -84,6 +84,33 @@ test("User has context menu options for edit file", async ({ page }) => {
await expect(dashboardPage.page.getByText("delete")).toBeVisible();
});
test("Multiple elements in context", async ({ page }) => {
await DashboardPage.mockRPC(
page,
"get-all-projects",
"dashboard/get-all-projects.json",
);
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDrafts();
await dashboardPage.goToDrafts();
const button = dashboardPage.page.getByRole("button", { name: /New File 1/ });
await button.click();
const button2 = dashboardPage.page.getByRole("button", {
name: /New File 2/,
});
await button2.click({ modifiers: ["Shift"] });
await button.click({ button: "right" });
await expect(page.getByTestId("duplicate-multi")).toBeVisible();
await expect(page.getByTestId("file-move-multi")).toBeVisible();
await expect(page.getByTestId("file-binary-export-multi")).toBeVisible();
await expect(page.getByTestId("file-delete-multi")).toBeVisible();
});
test("User has create file button", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDrafts();
@@ -131,3 +158,31 @@ test("Bug 9927, Don't show the banner to invite team members if the user has dis
await expect(page.getByText("Second team")).toBeVisible();
await expect(page.getByText("Team Up")).not.toBeVisible();
});
test("Bug 10141, The team does not disappear from the team list after deletion", async ({
page,
}) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-teams",
"logged-in-user/get-teams-complete-owner.json",
);
await dashboardPage.goToDashboard();
await dashboardPage.teamDropdown.click();
await expect(page.getByText("Second Team")).toBeVisible();
await page.getByText("Second Team").click();
await page.getByRole("button", { name: "team-management" }).click();
await page.getByTestId("delete-team").click();
await DashboardPage.mockRPC(
page,
"get-teams",
"logged-in-user/get-teams-default.json",
);
await page.getByRole("button", { name: "Delete team" }).click();
await dashboardPage.teamDropdown.click();
await expect(page.getByText("Second Team")).not.toBeVisible();
});

View File

@@ -72,3 +72,41 @@ test("Bug 9056 - 'More info' doesn't open the update tab", async ({ page }) => {
/library updates/i,
);
});
test("Bug 10113 - Empty library modal for non-empty library", async ({
page,
}) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile(page);
await workspace.mockRPC(/get\-file\?/, "workspace/get-file-10113.json");
await workspace.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"workspace/get-file-fragment-10113.json",
);
await workspace.mockRPC(/get\-file\?/, "workspace/get-file-10113.json");
await workspace.mockRPC(
"get-team-shared-files?team-id=*",
"workspace/get-team-shared-files-empty.json",
);
await workspace.mockRPC(
"set-file-shared",
"workspace/set-file-shared-10113.json",
);
await workspace.goToWorkspace({
fileId: "5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
pageId: "5b7ebd2b-2907-80db-8005-b9d67c20cf2f",
});
await workspace.clickAssets();
await workspace.openLibrariesModal();
await workspace.librariesModal
.getByRole("button", { name: "Publish" })
.click();
await expect(
workspace.page.getByText("Publish empty library"),
).not.toBeVisible();
});

View File

@@ -15,6 +15,17 @@ test("User loads worskpace with empty file", async ({ page }) => {
await expect(workspacePage.pageName).toHaveText("Page 1");
});
test("User opens a file with a bad page id", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace({
pageId: "badpage",
});
await expect(workspacePage.pageName).toHaveText("Page 1");
});
test("User receives presence notifications updates in the workspace", async ({
page,
}) => {
@@ -161,6 +172,49 @@ test("User adds a library and its automatically selected in the color palette",
).toBeVisible();
});
test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.moveButton.click();
await workspacePage.page.keyboard.press("Alt+p");
await expect(
workspacePage.palette.getByText(
"There are no color styles in your library yet",
),
).toBeVisible();
await page.getByRole("button", { name: "#E8E9EA" }).click();
await expect(page.getByTestId("colorpicker")).toBeVisible();
const handler = await page.getByTestId("ramp-handler");
await expect(handler).toBeVisible();
const saturation_selection = await page.getByTestId(
"value-saturation-selector",
);
await expect(saturation_selection).toBeVisible();
const saturation_box = await saturation_selection.boundingBox();
const handler_box = await handler.boundingBox();
await page.mouse.move(
handler_box.x + handler_box.width,
handler_box.y + handler_box.height / 2,
);
await page.mouse.down();
await page.mouse.move(
saturation_box.x + saturation_box.width / 2,
saturation_box.y + saturation_box.height / 2,
);
await page.mouse.up();
await expect(
workspacePage.palette.getByText(
"There are no color styles in your library yet",
),
).not.toBeVisible();
});
test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-shortcut", async ({
page,
}) => {

View File

@@ -47,7 +47,7 @@ test("User goes to an empty libraries page", async ({ page }) => {
await dashboardPage.setupLibrariesEmpty();
await dashboardPage.goToDashboard();
await dashboardPage.libsLink.click();
await dashboardPage.librariesLink.click();
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
await expect(dashboardPage.page).toHaveScreenshot();
@@ -100,7 +100,7 @@ test("User goes to a full library page", async ({ page }) => {
await dashboardPage.setupDashboardFull();
await dashboardPage.goToDashboard();
await dashboardPage.libsLink.click();
await dashboardPage.librariesLink.click();
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
await expect(dashboardPage.page).toHaveScreenshot();

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

View File

@@ -0,0 +1,10 @@
// 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
.sb-show-main.sb-main-fullscreen,
.sb-show-main.sb-main-padded {
overflow-y: auto;
}

View File

@@ -215,7 +215,7 @@
--menu-shortcut-foreground-color: var(--color-foreground-secondary);
--menu-shortcut-foreground-color-selected: var(--color-foreground-primary);
--menu-shortcut-foreground-color-hover: var(--color-foreground-primary);
--menu-shadow-color: var(--color-shadow);
--menu-shadow-color: var(--color-shadow-dark);
--menu-background-color-disabled: var(--color-background-primary);
--menu-foreground-color-disabled: var(--color-foreground-secondary);
--menu-border-color-disabled: var(--color-background-quaternary);

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