Compare commits

...

192 Commits

Author SHA1 Message Date
Andrey Antukh
7ca98ddf21 📎 Add missing entry on changelog 2025-01-22 16:07:07 +01:00
Pablo Alba
15157c54b1 🐛 Fix shape-ref cycles 2025-01-22 16:05:50 +01:00
Pablo Alba
232b29cd89 Merge pull request #5627 from penpot/niwinz-file-gc-improvements
 Minor improvements to file-gc code merged in other commit
2025-01-21 09:58:29 +01:00
Andrey Antukh
da0704081f 📎 Normalize default opts for db/plan function 2025-01-20 23:23:31 +01:00
Andrey Antukh
066b1235a6 🐛 Pass correct default options on db/plan fn 2025-01-20 23:23:31 +01:00
Pablo Alba
141694dc8d Merge pull request #5598 from penpot/niwinz-file-gc-improvements
 Add efficiency improvements to file-gc task
2025-01-20 21:14:32 +01:00
Andrey Antukh
151aedcf91 ♻️ Make the components cleaning mechanism on file-gc task more efficient 2025-01-20 16:35:14 +01:00
Andrey Antukh
5513daf17d ♻️ Make the media cleaning mechanism on file-gc task more efficient
Replaces the use of db/cursor with db/plan, that teorethically allows
processing large results without consuming all result set in memory
2025-01-20 16:34:05 +01:00
Andrey Antukh
fde0f3c182 🐛 Pass correct default options on db/plan fn 2025-01-20 16:34:05 +01:00
Aitor Moreno
7b408e4db1 Merge pull request #5622 from penpot/niwinz-api-doc-fix
🐛 Fix api docs page issue
2025-01-20 15:21:45 +01:00
Pablo Alba
b8fd829f9d Merge pull request #5621 from penpot/marina/testab-hover-state-add-file
 A/B test switching '+' to 'Add file' in an empty project
2025-01-20 12:49:19 +01:00
Andrey Antukh
089a66881c Make frontend app setup logging message more easy to be read
Mainly printing flag per line, making it more easily for human eye look
if some feature is active or not
2025-01-20 12:40:28 +01:00
Andrey Antukh
667b5fb6ee 🐛 Fix missing methods reference from api docs page 2025-01-20 12:30:20 +01:00
Andrey Antukh
f0f89151c5 Merge pull request #5620 from penpot/palba-consolidate-board-icon-change
Consolidate board icon change
2025-01-20 12:22:07 +01:00
Andrey Antukh
1221d60357 Merge pull request #5617 from penpot/alotor-hotfix-plugins
🐛 Fix problem in plugins with `replaceColor` method
2025-01-20 12:21:16 +01:00
alonso.torres
f553fa10d8 🐛 Fix problem in plugins with replaceColor method 2025-01-20 12:02:54 +01:00
Marina López
96947b0219 A/B test switching '+' to 'Add file' in an empty project 2025-01-20 11:56:15 +01:00
Pablo Alba
e2900d9012 🎉 Change of boards icon 2025-01-20 11:07:13 +01:00
Pablo Alba
1f0e470419 Revert "🎉 Add A/B test of use of boards if we just change the icon for “standard” one"
This reverts commit 0c586551c4.
2025-01-20 11:06:25 +01:00
Andrey Antukh
079a945c2f Merge branch 'main' into staging 2025-01-20 11:02:08 +01:00
Madalena Melo
542d709541 📚 Add viewer role documentation
Add viewer role to the team roles; also made some tweaks to the descriptions of the other roles
2025-01-20 11:01:38 +01:00
Madalena Melo
4f1d5a19e4 Change the order to add clarity to admin and owner roles
Switched the order of the roles to make it more logical and add more clarity about admins and owners ability to edit
2025-01-20 10:26:52 +01:00
Madalena Melo
91b0c47244 Add detail to role descriptions
Added more context to each role's description; I tried to keep it brief while including more information about what each role can do both within the team as well as in terms of team management
2025-01-20 10:26:46 +01:00
Madalena Melo
a7a49e4b39 Viewer Role - Update index.njk
Add viewer role to the team roles; also made some tweaks to the descriptions of the other roles
2025-01-20 10:26:39 +01:00
Andrey Antukh
ba81b2b14d Merge pull request #5613 from penpot/superalex-fix-staging-reply-to-comment
🐛 Fix reply to comment
2025-01-17 15:23:28 +01:00
Alejandro Alonso
423c237d42 🐛 Fix reply to comment 2025-01-17 11:57:44 +01:00
Andrey Antukh
5a55884b9f Merge pull request #5602 from penpot/hiru-fix-detach-2
🐛 Fix detach when top copy is dangling and nested copy is not
2025-01-16 12:21:33 +01:00
Andrey Antukh
38fd343c53 Merge remote-tracking branch 'origin/main' into staging 2025-01-16 12:20:27 +01:00
adi-lb-phoenix
94976aa2b1 📚 Fix incorrect flag on configuration.md 2025-01-16 12:19:03 +01:00
Andrés Moya
5247d217ab 🐛 Fix detach when top copy is dangling and nested copy is not 2025-01-16 10:45:54 +01:00
Andrey Antukh
40693e6857 🐛 Make the PENPOT_SECRET_KEY optional
Fix a regression introduced with 2.4
2025-01-16 09:59:19 +01:00
Andrey Antukh
5c428b5aa5 🐛 Fix repeated password update on login
because the default options were not being passed in the verification
2025-01-16 09:59:19 +01:00
Andrey Antukh
e92ddee33a 🐳 Move devenv secret key env asignation to scripts
from the docker compose
2025-01-16 09:59:19 +01:00
Eva Marco
c121f459ba Merge pull request #5571 from penpot/andy-docs-radius
📚 Change term for border radius
2025-01-15 10:18:05 +01:00
Andrey Antukh
698a258290 Merge remote-tracking branch 'origin/main' into staging 2025-01-14 17:47:32 +01:00
Yamila Moreno
aa023d847d 🐳 Set correct internal url frontend url for exporter 2025-01-14 17:37:14 +01:00
Andrey Antukh
53f57dad0b Merge pull request #5575 from penpot/azazeln28-fix-text-editor-issue-9285
🐛 Fix text editor copy/paste issue
2025-01-14 16:50:34 +01:00
AzazelN28
d7d7535ab4 🐛 Fix text editor copy/paste issue 2025-01-14 11:20:38 +01:00
Pablo Alba
accc662e1c 🐛 Fix login is not shown on 404 2025-01-14 09:51:12 +01:00
andy
1efc1516e2 📚 Change term for border radius 2025-01-13 15:53:06 +01:00
Alejandro
b5d731ca72 Merge pull request #5559 from penpot/palba-fix-flags-not-setting-login
🐛 Fix feature flags not setting on login
2025-01-13 10:57:37 +01:00
Pablo Alba
e380289e34 🐛 Fix feature flags not setting on login 2025-01-10 21:25:55 +01:00
Andrey Antukh
b22323a484 Merge pull request #5547 from penpot/alotor-bugs-1
🐛 Fix problem when changing color libraries
2025-01-10 15:16:01 +01:00
alonso.torres
58dd23f9c7 🐛 Fix problem when changing color libraries 2025-01-10 12:33:16 +01:00
Andrey Antukh
54e7551d56 🐛 Backport comments decoding from develop
Mainly for backward compatibility with database
layout on comments tables from develop / v2.5
2025-01-10 12:20:53 +01:00
Andrey Antukh
404297f837 Merge pull request #5537 from penpot/azazeln28-fix-text-editor-issue-9716
🐛 Fix text editor issue 9716
2025-01-10 10:24:23 +01:00
Andrey Antukh
33f853ff2e Merge pull request #5511 from penpot/alotor-fix-touched-import
🐛 Fix error when importing files with touched components
2025-01-09 17:13:22 +01:00
alonso.torres
d16513be9d 🐛 Fix error when importing files with touched components 2025-01-09 16:58:40 +01:00
AzazelN28
ad077696b0 🐛 Fix Unknown node type when replacing whole root node 2025-01-09 16:28:32 +01:00
Andrey Antukh
1cbeafe85c Merge pull request #5525 from penpot/palba-testabc-add-library
A/B/C test for rename and change look and feel of "libraries" button
2025-01-09 08:34:05 +01:00
Pablo Alba
76c8523f44 Add test ABC renaming "Libraries" to "Add library" 2025-01-08 13:20:52 +01:00
Pablo Alba
f277d8b125 Revert " Add test AB renaming "Libraries" to "Add library""
This reverts commit 664cacbe9d.
2025-01-08 11:05:50 +01:00
Andrey Antukh
60af8d0bcb Merge pull request #5520 from penpot/azazeln28-fix-text-editor-issue-9285
Fix Copy/Paste text into the text block
2025-01-07 14:26:11 +01:00
Juanfran
d652ed8e68 Merge pull request #5519 from penpot/alotor-fix-plugins-current-user
🐛 Fix problem with currentUser in plugins
2025-01-07 14:05:07 +01:00
AzazelN28
09d73a2f51 🐛 Fix pasting text changes opacity to 0 2025-01-07 13:48:32 +01:00
alonso.torres
7d4535ebd4 🐛 Fix problem with currentUser in plugins 2025-01-07 13:03:56 +01:00
Andrey Antukh
a5a53219bf Merge remote-tracking branch 'origin/main' into staging 2025-01-07 12:00:09 +01:00
Marina López
8716f81765 Merge pull request #5456 from daledesilva/patch-1
📚 Add submission form link to deployment page
2025-01-07 10:58:05 +01:00
Andrey Antukh
7aa46a1f62 🔥 Remove 2.4.1 from changelog 2025-01-07 10:51:46 +01:00
Andrey Antukh
d62eb3d3f4 Merge pull request #5504 from penpot/palba-add-feature-flags-to-events
 Add feature flags info to posthog events
2025-01-07 09:34:22 +01:00
Pablo Alba
e4c427609d Add feature flags info to posthog events 2025-01-07 09:19:04 +01:00
Aitor Moreno
883a26845a Merge pull request #5510 from penpot/alotor-fix-thumbnail-generation
🐛 Fix problem with thumbnail generation when changing between ver…
2025-01-02 12:46:59 +01:00
alonso.torres
bcdf5d86ae 🐛 Fix problem with thumbnail generation when changing between versions 2025-01-02 12:01:30 +01:00
Pablo Alba
3eab9da74e Merge pull request #5492 from penpot/alotor-christmas-bugfixes
🐛 Fix problem with component swap style
2024-12-26 16:39:23 +01:00
alonso.torres
2813fda136 🐛 Fix problem with component swap style 2024-12-23 12:37:24 +01:00
Andrey Antukh
a0022a804b Merge branch 'backrunner-patch/backrunner_nginx-conf' into staging 2024-12-23 10:28:34 +01:00
BackRunner
068acb4303 🐛 Fix assets proxy ssl handshake error 2024-12-23 10:28:10 +01:00
Andrey Antukh
d6f98a6c79 📎 Add externs for worker build 2024-12-23 10:03:10 +01:00
Andrey Antukh
7b6c2da6da 📎 Add externs for main build
With all combinations of small symbols for indicate
to compiler to not use them for internal renaming
2024-12-23 10:03:10 +01:00
Andrey Antukh
affed049ee Merge remote-tracking branch 'origin/main' into staging 2024-12-19 10:27:12 +01:00
alonso.torres
377f636b8e 🐛 Fix problem with reorder on grid layout layers 2024-12-18 13:32:30 +01:00
Alejandro Alonso
2d512ef273 🐛 Fix path edition for plugins 2024-12-18 10:12:01 +01:00
Andrey Antukh
b8ebbe8c3c Merge pull request #5468 from penpot/palba-abtest-add-library-button2
 Add test AB renaming "Libraries" to "Add library"
2024-12-13 12:20:56 +01:00
Pablo Alba
09c184200d Merge pull request #5471 from penpot/niwinz-bugfix-5
🐛 Fix unhandled exception on accepting invitation
2024-12-13 12:15:35 +01:00
Andrey Antukh
bbe0b22a8b 🐛 Fix unhandled exception on accepting invitation
In an interaction with the audit log; happens when an old invitation
(with created-by as nil) is accepted.
2024-12-13 11:48:37 +01:00
Andrey Antukh
8603085a69 Merge pull request #5470 from penpot/palba-add-config-not-show-release
🎉 Add config flag to hide release info
2024-12-13 11:28:27 +01:00
Andrey Antukh
29ec44482d Merge pull request #5463 from penpot/andy-docs-adjustments
📚 Remove broken video about History versions
2024-12-13 11:17:27 +01:00
Andrey Antukh
fd4d4ec6e3 Merge pull request #5465 from penpot/palba-fix-viewer-role-thumbnails
🐛 Fix viewer role is unable to generate thumbnails on dashboard
2024-12-13 11:16:32 +01:00
Pablo Alba
0945dd2920 🎉 Add config flag to hide release info 2024-12-13 11:14:59 +01:00
Pablo Alba
664cacbe9d Add test AB renaming "Libraries" to "Add library" 2024-12-13 10:51:41 +01:00
Pablo Alba
08516ac7ca Revert " Add test AB renaming "Libraries" to "Add library""
This reverts commit ec7f8a6aa7.
2024-12-13 10:51:25 +01:00
Pablo Alba
24e51eef5b 🐛 Fix viewer role is unable to generate thumbnails on dashboard 2024-12-12 15:15:27 +01:00
Andrey Antukh
ee62016c34 Merge pull request #5462 from penpot/palba-dont-show-release-info
🐛 Fix a new user shouldn't see the "What's new" popup
2024-12-12 12:27:14 +01:00
andy
7c10f20b95 📚 Remove deprecated video at History version 2024-12-12 11:57:08 +01:00
Pablo Alba
4958da63e5 🐛 Fix a new user shouldn't see the "What's new" popup 2024-12-12 11:45:58 +01:00
andy
f39a994fed 📚 Import/export images replacements and other adjustments 2024-12-12 08:38:00 +01:00
andy
5ef59d5e2e 📚 New penpot file format 2024-12-12 08:37:53 +01:00
andy
98221c6b51 📚 Images and text adjustments 2024-12-12 08:37:40 +01:00
Madalena Melo
74713cde63 File history versions
https://tree.taiga.io/project/penpot/task/9377
2024-12-12 08:37:32 +01:00
andrés gonzález
a5084c35b5 Merge pull request #5461 from penpot/andy-docs-blending
📚 Info about layers blend and opacity
2024-12-12 08:29:41 +01:00
andrés gonzález
cdce1df919 Merge pull request #5460 from penpot/andy-shortcuts-update
📚 History shortcut removed
2024-12-12 08:27:48 +01:00
andy
c75b886548 📚 Info about layers blend and opacity 2024-12-12 07:03:54 +01:00
andy
abd41e825e 📚 History shortcut removed 2024-12-12 06:22:51 +01:00
Andrey Antukh
97b9a7d31c 🐛 Remove hardcoded limit of 20 on the snapshot list rpc method 2024-12-11 14:13:04 +01:00
Dale de Silva
dbeebf181f 📚 Add submission form link to deployment page
The plugin submission page is hard to find while looking at the plugin help docs (As it's not linked from there). It should eventually be a page of its own but there isn't enough content yet (or an illustration) to support it.
2024-12-11 23:51:11 +11:00
Andrey Antukh
0eec09acbf Merge pull request #5441 from penpot/juan-slides-2.4
Juan slides 2.4
2024-12-11 12:49:31 +01:00
Elhombretecla
81e250e27d 🎉 Add slides for 2.4 release 2024-12-11 12:36:29 +01:00
Alejandro
9f1f8cc80c Merge pull request #5438 from penpot/andy-docs-selfhostsettings
📚 Recommended settings for selfhost
2024-12-11 07:57:23 +01:00
andy
2440c81b42 📚 Recommended settings for selfhost 2024-12-11 07:42:01 +01:00
Alejandro
ada078abab Merge pull request #5439 from penpot/niwinz-delete-xlog-gc-task
🔥 Remove file-xlog-gc task
2024-12-11 07:29:39 +01:00
Andrey Antukh
31319a0d04 Merge pull request #5442 from penpot/alotor-view-only-menu
🐛 Remove file history versions menu options on view mode
2024-12-10 15:20:57 +01:00
alonso.torres
b9cb415507 🐛 Remove file history versions menu options on view mode 2024-12-10 14:41:00 +01:00
Andrey Antukh
36121d862d Merge pull request #5440 from penpot/palba-ab-share-in-workspace
 Fix test A/B add share button to the workspace
2024-12-10 13:48:49 +01:00
Pablo Alba
d8964a69bc Fix test A/B add share button to the workspace 2024-12-10 13:33:04 +01:00
Andrey Antukh
39da7d7ab6 Merge pull request #5429 from penpot/palba-bugfixing-01
🐛 Palba bugfixing 01
2024-12-10 12:33:38 +01:00
Andrey Antukh
1bb25bb89d 🔥 Remove file-xlog-gc task
It is no longer necessary because snapshots are managed by the objects-gc task
2024-12-10 12:19:12 +01:00
Pablo Alba
b0a3f2b72a 🐛 Fix history panel remains open after restoring a version 2024-12-10 11:00:54 +01:00
Pablo Alba
f2f3d9f7eb 💄 Fix css format (spacing) 2024-12-10 10:53:12 +01:00
Pablo Alba
cf72b35e73 🐛 Fix separator lines have no color at some menus 2024-12-10 10:53:12 +01:00
Andrey Antukh
fe8d9cf159 Merge pull request #5418 from penpot/palba-abtest-add-sugested-libraries
 Add test AB for adding a few "Suggested" libraries
2024-12-10 09:50:41 +01:00
Andrey Antukh
4cfe33bc5c Merge pull request #5436 from penpot/azazeln28-fix-text-editor-v2-copy-paste-issues
🐛 Fix copy/paste issues
2024-12-10 09:29:31 +01:00
Andrey Antukh
e5d8bc91fb 💄 Fix describe-library-blocks component syntax decl style 2024-12-10 09:26:07 +01:00
Andrey Antukh
ce1ba3f28f 💄 Fix sample-library-entry component syntax style 2024-12-10 09:21:45 +01:00
Pablo Alba
257d72ee9d Add test AB for adding a few "Suggested" libraries 2024-12-10 09:16:55 +01:00
Alejandro
0766b341bd Merge pull request #5432 from penpot/niwinz-bugfix-2
🐛 Bug fixes
2024-12-10 08:05:00 +01:00
AzazelN28
4ef631fd6a 🐛 Fix copy/paste issues 2024-12-09 16:10:39 +01:00
Andrey Antukh
a923d39603 🐛 Fix incorrect teams query on profile deletion
The current approach prevents profile deletion when
there are some extra (soft)deleted teams where the profile
is owner
2024-12-09 10:15:13 +01:00
Andrey Antukh
2f79d71262 🐛 Fix incorrect event handling on file-menu
Don't wait team to be present for open the menu,
because with slow connection speed it can cause
unexpected ux glitche showing menu when the component
inner request is resoved
2024-12-09 10:15:13 +01:00
Andrey Antukh
4881bf3619 Merge pull request #5412 from penpot/palba-abtest-add-library-button
 Add test AB renaming "Libraries" to "Add library"
2024-12-09 09:53:35 +01:00
Eva Marco
69df69c4bb Merge pull request #5424 from penpot/superalex-bugfixing-30
🐛 Bug fixing
2024-12-05 16:12:16 +01:00
Alejandro
2c36a4076f Merge pull request #5427 from penpot/eva-fix-firefox-scrollbar
🐛 Fix horizontal scroll on firefox
2024-12-05 13:30:52 +01:00
Andrey Antukh
3ac6f59b7b Merge pull request #5428 from penpot/qol-versions-limit-warning
 Add autosaved versions warning
2024-12-05 13:26:59 +01:00
alonso.torres
c68a0d3967 Add autosaved versions warning 2024-12-05 13:18:03 +01:00
Andrey Antukh
aeb1ac41da 🐛 Prevent upload media objects to deleted files 2024-12-05 12:39:43 +01:00
Eva Marco
b58830260c 🐛 Fix horizontal scroll on firefox 2024-12-05 12:21:22 +01:00
Alejandro Alonso
4114d9b56f 🐛 Fix shortcut for history not working 2024-12-05 10:52:59 +01:00
Alejandro
553b9eb4bb Merge pull request #5425 from penpot/ladybenko-9477-backport
[backport] 🐛 Fix missing rename shortcut translation
2024-12-05 10:43:37 +01:00
María Valderrama
12e97c73f3 🐛 Fix missing rename shortcut translation 2024-12-05 09:59:00 +01:00
Alejandro
bd1286aace Merge pull request #5422 from penpot/eva-fix-demote-owner
🐛  Fix admin can demote owner
2024-12-05 09:37:22 +01:00
Alejandro Alonso
630f42f7ac 🐛 Fix internal error when creating a guide 2024-12-05 09:30:10 +01:00
Eva Marco
bf40cd98e8 🐛 Fix admin can demote owner 2024-12-05 09:27:32 +01:00
Pablo Alba
ec7f8a6aa7 Add test AB renaming "Libraries" to "Add library" 2024-12-04 13:55:55 +01:00
andrés gonzález
4d3192546c 📚 Update changelog with some links format 2024-12-04 09:49:33 +01:00
andrés gonzález
2184926bbb 📚 Update changelog with "New .penpot file format" 2024-12-04 09:42:20 +01:00
Aitor Moreno
fffc3b1b58 Merge pull request #5396 from penpot/niwinz-bugfix-1
🐛 Fix regression on drawing text with auto-size
2024-11-29 15:55:34 +01:00
Andrey Antukh
e0cc999345 🐛 Fix regression on drawing text with auto-size
Using editor v1
Caused by 2ed743b6be
2024-11-29 13:23:39 +01:00
Andrey Antukh
8e836f79fb Merge pull request #5390 from penpot/palba-fix-bad-redirect-remove-from-team
🐛 Fix bad redirect after being removed from a team
2024-11-29 13:01:27 +01:00
Pablo Alba
093a58b9ec 🐛 Fix bad redirect after being removed from a team 2024-11-29 10:55:17 +01:00
Andrey Antukh
5e2b847202 Merge pull request #5383 from penpot/azazeln28-fix-text-editor-more-issues
🐛 Fix text editor issues
2024-11-29 10:17:43 +01:00
Andrey Antukh
ef207cfe70 Merge pull request #5386 from penpot/palba-create-events
Create several events
2024-11-28 12:10:45 +01:00
Pablo Alba
f72c37a198 Add events for use library assets 2024-11-28 11:25:12 +01:00
Pablo Alba
dbf3d0d7c1 Add event "navigate-to-library-file" 2024-11-28 11:23:28 +01:00
Pablo Alba
903c8c021d Add event "change-inspect-tab" 2024-11-27 13:51:21 +01:00
Pablo Alba
9b8ef0a2e5 Add event "duplicate-page" 2024-11-27 13:51:21 +01:00
Pablo Alba
22e64c1c81 Add event "resolve-comment-thread" 2024-11-27 13:51:21 +01:00
Pablo Alba
14c917d003 Add event "create-prototype" 2024-11-27 13:51:21 +01:00
AzazelN28
4377a0dcc4 🐛 Fix an error that kept empty inlines 2024-11-27 12:29:38 +01:00
AzazelN28
db6ca6f905 🐛 Fix wrong line-height value 2024-11-27 12:05:42 +01:00
Aitor Moreno
ede8ee6a78 Merge pull request #5384 from penpot/alotor-fix-editor
🐛 Fix problem with editor line-height
2024-11-27 12:01:58 +01:00
alonso.torres
37b50497f3 🐛 Fix problem with editor line-height 2024-11-27 11:51:13 +01:00
Aitor Moreno
e3d2b99acc Merge pull request #5381 from penpot/niwinz-bugfix-1
🐛 Fix incorrect thumbnail rendering on dashboard
2024-11-27 11:48:25 +01:00
AzazelN28
7101b94557 🐛 Fix selectAll when editor is empty 2024-11-27 11:42:18 +01:00
Aitor Moreno
5ffab1953d Merge pull request #5382 from penpot/niwinz-bugfix-2
🐛 Fix regression with component thumbnails
2024-11-27 11:34:50 +01:00
Andrey Antukh
75e7cfb69e 🐛 Fix regression with component thumbnails 2024-11-27 11:15:58 +01:00
Andrey Antukh
65b7e5c3a5 🐛 Fix incorrect thumbnail rendering on dashboard 2024-11-27 10:42:56 +01:00
Andrey Antukh
30a06249ff Merge pull request #5377 from penpot/azazeln28-fix-text-editor-issues
🐛 Fix text editor issues
2024-11-27 09:08:39 +01:00
AzazelN28
59ca09c24e 🐛 Fix text editor issues 2024-11-27 08:56:52 +01:00
Andrey Antukh
1aeafdfca7 Merge pull request #5378 from penpot/yms-fix-k8s-documentation
📚 Add links to Kubernetes documentation
2024-11-27 08:54:37 +01:00
Andrey Antukh
a714085523 Merge branch 'niwinz-plugins-reify' into staging 2024-11-27 08:32:54 +01:00
alonso.torres
eccc4226c7 Migrate proxies to new format 2024-11-27 08:32:07 +01:00
Yamila Moreno
4d6d85b3de 📚 Add links to Kubernetes documentation 2024-11-26 15:25:51 +01:00
Aitor Moreno
c607b61af6 Merge pull request #5349 from penpot/palba-add-event-for-pdf
 Add export format info to export-shapes event
2024-11-26 12:53:52 +01:00
Andrey Antukh
e16ec9c719 Add facility for create anonymous objects
Speciailly designed to be work in plugins where code
is submited to hard deep freeze on the sandboxing
process.
2024-11-25 16:35:44 +01:00
Pablo Alba
59e5656bd7 Merge pull request #5370 from penpot/niwinz-bugfix-4
🐛 Fix incorrect access to flows on add-new-interaction event
2024-11-25 15:41:13 +01:00
Andrey Antukh
723eef9565 🐛 Fix incorrect access to flows on add-new-interaction event 2024-11-25 13:32:45 +01:00
Belén Albeza
8448036d67 Merge pull request #5368 from penpot/niwinz-bugfix-3
🐛 Fix import format detection and error handling
2024-11-25 13:30:46 +01:00
Andrey Antukh
e1c9691567 Improve scss compilation error handling
Don't stop watch scss process on compilation error
2024-11-25 12:44:10 +01:00
Andrey Antukh
577b731b22 🐛 Fix import format detection and error handling 2024-11-25 12:29:59 +01:00
Andrey Antukh
ef3588d05f Merge pull request #5355 from penpot/azazeln28-fix-component-list-jumps
🐛 Fix component list jumps
2024-11-25 11:44:05 +01:00
Andrey Antukh
d4893523bc Merge pull request #5367 from penpot/niwinz-bugfix-2
🐛 Fix release 2.3 onboarding text typos
2024-11-25 11:43:47 +01:00
AzazelN28
f10792619d 🐛 Fix component list jumps 2024-11-25 11:35:33 +01:00
Alejandro
7a0702650a Merge pull request #5366 from penpot/niwinz-bugfix-1
🐛 Bugfixes
2024-11-25 11:32:28 +01:00
Elhombretecla
ee1230c488 🐛 Fix release 2.3 onboarding text typos 2024-11-25 11:30:51 +01:00
Andrey Antukh
ede1176606 Merge pull request #5340 from penpot/palba-testab-board-icon
🎉 Add A/B test of use of boards if we just change the icon for “standard” one
2024-11-25 11:24:23 +01:00
Andrey Antukh
9506606e15 Merge pull request #5359 from penpot/alotor-fixes-2
🐛 Fix problem with scroll in history versions
2024-11-25 11:23:47 +01:00
Andrey Antukh
7e5f93ca3d Merge pull request #5358 from penpot/azazeln28-fix-assets-filters
🐛 Fix assets filters
2024-11-25 11:23:14 +01:00
Andrey Antukh
6655563aba Merge pull request #5357 from penpot/superalex-fix-generate-translation-files-with-markdown-and-links
🐛 Fix generate translation files with markdown and links
2024-11-25 11:18:13 +01:00
Andrey Antukh
8ee9b45243 Merge pull request #5346 from penpot/alotor-fixes-1
Alotor fixes 1
2024-11-25 11:16:25 +01:00
Alejandro
caa6897f81 Merge pull request #5342 from penpot/niwinz-thumbnails-fix
 Make the file and shape thumbnails not dependent on PUBLIC_URI
2024-11-25 11:13:04 +01:00
Andrey Antukh
660bc1a4dd 🐛 Fix incorrect team rename operation 2024-11-25 10:01:36 +01:00
Andrey Antukh
3ddd45e99b 🐛 Fix incorrect internal form initialization 2024-11-25 10:01:36 +01:00
Andrey Antukh
9b71e04e1c 🐛 Fix exception on user-feedback rpc method
And normalizes configuration parameters
2024-11-25 10:01:36 +01:00
alonso.torres
39620fe9c4 🐛 Hover on history version entry to show options 2024-11-25 09:51:41 +01:00
alonso.torres
db7c1fc7dd 🐛 Fix problem with some texts desynchronization 2024-11-25 09:51:41 +01:00
Alejandro Alonso
c89abf56ac 🐛 Fix translate files generations with markdown and links 2024-11-25 07:20:32 +01:00
alonso.torres
d22f6e37c9 Add pin version to main menu 2024-11-22 15:47:19 +01:00
alonso.torres
c1d3e4cd6e 🐛 Fix problem with scroll in history versions 2024-11-22 15:34:32 +01:00
AzazelN28
2164593757 🐛 Fix assets filters 2024-11-22 14:47:43 +01:00
Pablo Alba
53f55444cd Add export format info to export-shapes event 2024-11-21 16:33:06 +01:00
Andrey Antukh
5cc678ddc3 Remove not necessary check on upgrade-version notification 2024-11-20 16:43:14 +01:00
Andrey Antukh
64c8741233 🐛 Make thumbnails independent of current public uri
Mainly always resolve the public uri at frontend, making the
PENPOT_PUBLIC_URI less necessary to be changed. This improves
the experience of on-premise configuration.

Also removes unnecesary calls for thumbnail generation
on components.
2024-11-20 16:43:14 +01:00
Pablo Alba
0c586551c4 🎉 Add A/B test of use of boards if we just change the icon for “standard” one 2024-11-20 15:12:54 +01:00
203 changed files with 11313 additions and 5790 deletions

View File

@@ -1,5 +1,22 @@
# CHANGELOG
## 2.4.2
### :bug: Bugs fixed
- Fix detach when top copy is dangling and nested copy is not [Taiga #9699](https://tree.taiga.io/project/penpot/issue/9699)
- Fix problem in plugins with `replaceColor` method [#174](https://github.com/penpot/penpot-plugins/issues/174)
- Fix issue with recursive commponents [Taiga #9903](https://tree.taiga.io/project/penpot/issue/9903)
- Fix missing methods reference on API Docs
- Fix memory usage issue on file-gc asynchronous task (related to snapshots feature)
## 2.4.1
### :bug: Bugs fixed
- Fix error when importing files with touched components [Taiga #9625](https://tree.taiga.io/project/penpot/issue/9625)
- Fix problem when changing color libraries [Plugins #184](https://github.com/penpot/penpot-plugins/issues/184)
## 2.4.0
### :rocket: Epics and highlights
@@ -23,12 +40,16 @@
### :sparkles: New features
- Viewer role for team members [Taiga #1056 & #6590](https://tree.taiga.io/project/penpot/us/1056 & https://tree.taiga.io/project/penpot/us/6590)
- File history versions management [Taiga](https://tree.taiga.io/project/penpot/us/187?milestone=411120)
- Viewer role for team members [Taiga #1056](https://tree.taiga.io/project/penpot/us/1056) & [Taiga #6590](https://tree.taiga.io/project/penpot/us/6590)
- File history versions management [Taiga #187](https://tree.taiga.io/project/penpot/us/187?milestone=411120)
- Rename selected layer via keyboard shortcut and context menu option [Taiga #8882](https://tree.taiga.io/project/penpot/us/8882)
- New .penpot file format [Taiga #8657](https://tree.taiga.io/project/penpot/us/8657)
### :bug: Bugs fixed
- Fix problem with some texts desynchronization [Taiga #9379](https://tree.taiga.io/project/penpot/issue/9379)
- Fix problem with reoder grid layers [#5446](https://github.com/penpot/penpot/issues/5446)
- Fix problem with swap component style [#9542](https://tree.taiga.io/project/penpot/issue/9542)
## 2.3.3

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env bash
export PENPOT_SECRET_KEY=super-secret-devenv-key
export PENPOT_HOST=devenv
export PENPOT_FLAGS="\
$PENPOT_FLAGS \

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env bash
export PENPOT_SECRET_KEY=super-secret-devenv-key
export PENPOT_HOST=devenv
export PENPOT_FLAGS="\
$PENPOT_FLAGS \

View File

@@ -8,7 +8,7 @@
(:require
[buddy.hashers :as hashers]))
(def default-params
(def ^:private default-options
{:alg :argon2id
:memory 32768 ;; 32 MiB
:iterations 3
@@ -16,12 +16,12 @@
(defn derive-password
[password]
(hashers/derive password default-params))
(hashers/derive password default-options))
(defn verify-password
[attempt password]
(try
(hashers/verify attempt password)
(hashers/verify attempt password default-options)
(catch Throwable _
{:update false
:valid false})))

View File

@@ -42,7 +42,6 @@
:rpc-rlimit-config "resources/rlimit.edn"
:rpc-climit-config "resources/climit.edn"
:auto-file-snapshot-total 10
:auto-file-snapshot-every 5
:auto-file-snapshot-timeout "3h"
@@ -101,7 +100,6 @@
[:telemetry-uri {:optional true} :string]
[:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
[:auto-file-snapshot-total {:optional true} ::sm/int]
[:auto-file-snapshot-every {:optional true} ::sm/int]
[:auto-file-snapshot-timeout {:optional true} ::dt/duration]

View File

@@ -270,19 +270,17 @@
:else (throw (IllegalArgumentException. "unable to resolve connectable"))))
(def ^:private params-mapping
{::return-keys? :return-keys
::return-keys :return-keys})
{::return-keys :return-keys})
(defn rename-opts
[opts]
(set/rename-keys opts params-mapping))
(def ^:private default-insert-opts
{:builder-fn sql/as-kebab-maps
:return-keys true})
(assoc sql/default-opts :return-keys true))
(def ^:private default-opts
{:builder-fn sql/as-kebab-maps})
sql/default-opts)
(defn exec!
([ds sv] (exec! ds sv nil))
@@ -333,7 +331,7 @@
(defn update!
"A helper that build an UPDATE SQL statement and executes it.
Given a connectable object, a table name, a hash map of columns and
Given a connectable object, a table name, a hash map of columns and
values to set, and either a hash map of columns and values to search
on or a vector of a SQL where clause and parameters, perform an
update on the table.
@@ -413,10 +411,20 @@
:hint "database object not found"))
row))
(def ^:private default-plan-opts
(-> default-opts
(assoc :fetch-size 1)
(assoc :concurrency :read-only)
(assoc :cursors :close)
(assoc :result-type :forward-only)))
(defn plan
[ds sql]
(-> (get-connectable ds)
(jdbc/plan sql sql/default-opts)))
([ds sql]
(-> (get-connectable ds)
(jdbc/plan sql default-plan-opts)))
([ds sql opts]
(-> (get-connectable ds)
(jdbc/plan sql (merge default-plan-opts opts)))))
(defn cursor
"Return a lazy seq of rows using server side cursors"

View File

@@ -15,14 +15,15 @@
(defn kebab-case [s] (str/replace s #"_" "-"))
(defn snake-case [s] (str/replace s #"-" "_"))
(def default-opts
{:table-fn snake-case
:column-fn snake-case})
(defn as-kebab-maps
[rs opts]
(jdbc-opt/as-unqualified-modified-maps rs (assoc opts :label-fn kebab-case)))
(def default-opts
{:table-fn snake-case
:column-fn snake-case
:builder-fn as-kebab-maps})
(defn insert
([table key-map]
(insert table key-map nil))

View File

@@ -226,8 +226,8 @@
[:priority {:optional true} [:enum :high :low]]
[:extra-data {:optional true} ::sm/text]])
(def ^:private valid-context?
(sm/validator schema:context))
(def ^:private check-context
(sm/check-fn schema:context))
(defn template-factory
[& {:keys [id schema]}]
@@ -236,10 +236,8 @@
(sm/check-fn schema)
(constantly nil))]
(fn [context]
(assert (valid-context? context) "expected a valid context")
(check-fn context)
(let [email (build-email-template id context)]
(let [context (-> context check-context check-fn)
email (build-email-template id context)]
(when-not email
(ex/raise :type :internal
:code :email-template-does-not-exists
@@ -271,7 +269,7 @@
"Schedule an already defined email to be sent using asynchronously
using worker task."
[{:keys [::conn ::factory] :as context}]
(assert (db/connection? conn) "expected a valid database connection")
(assert (db/connectable? conn) "expected a valid database connection or pool")
(let [email (if factory
(factory context)
@@ -348,7 +346,7 @@
[:subject ::sm/text]
[:content ::sm/text]])
(def feedback
(def user-feedback
"A profile feedback email."
(template-factory
:id ::feedback

View File

@@ -349,7 +349,6 @@
:file-gc (ig/ref :app.tasks.file-gc/handler)
:file-gc-scheduler (ig/ref :app.tasks.file-gc-scheduler/handler)
:offload-file-data (ig/ref :app.tasks.offload-file-data/handler)
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
:telemetry (ig/ref :app.tasks.telemetry/handler)
:storage-gc-deleted (ig/ref ::sto.gc-deleted/handler)
@@ -405,10 +404,6 @@
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.file-xlog-gc/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.telemetry/handler
{::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)

View File

@@ -273,7 +273,8 @@
(merge {:viewed-tutorial? false
:viewed-walkthrough? false
:nudge {:big 10 :small 1}
:v2-info-shown true})
:v2-info-shown true
:release-notes-viewed (:main cf/version)})
(db/tjson))
password (or (:password params) "!")

View File

@@ -29,10 +29,11 @@
;; --- GENERAL PURPOSE INTERNAL HELPERS
(defn- decode-row
[{:keys [participants position] :as row}]
[{:keys [participants position mentions] :as row}]
(cond-> row
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))
(db/pgarray? mentions) (assoc :mentions (db/decode-pgarray mentions #{}))))
(def xf-decode-row
(map decode-row))
@@ -461,8 +462,9 @@
:thread-id thread-id
:owner-id profile-id
:content content})
props {:file-id file-id
:share-id nil}]
comment (decode-row comment)
props {:file-id file-id
:share-id nil}]
;; Update thread modified-at attribute and assoc the current
;; profile to the participant set.

View File

@@ -17,7 +17,7 @@
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]))
(declare ^:private send-feedback!)
(declare ^:private send-user-feedback!)
(def ^:private schema:send-user-feedback
[:map {:title "send-user-feedback"}
@@ -34,14 +34,16 @@
:hint "feedback not enabled"))
(let [profile (profile/get-profile pool profile-id)]
(send-feedback! pool profile params)
(send-user-feedback! pool profile params)
nil))
(defn- send-feedback!
(defn- send-user-feedback!
[pool profile params]
(let [dest (cf/get :feedback-destination)]
(let [dest (or (cf/get :user-feedback-destination)
;; LEGACY
(cf/get :feedback-destination))]
(eml/send! {::eml/conn pool
::eml/factory eml/feedback
::eml/factory eml/user-feedback
:from dest
:to dest
:profile profile

View File

@@ -698,11 +698,7 @@
(defn get-team-recent-files
[conn team-id]
(->> (db/exec! conn [sql:team-recent-files team-id])
(mapv (fn [row]
(if-let [media-id (:thumbnail-id row)]
(assoc row :thumbnail-uri (resolve-public-uri media-id))
(dissoc row :media-id))))))
(db/exec! conn [sql:team-recent-files team-id]))
(def ^:private schema:get-team-recent-files
[:map {:title "get-team-recent-files"}

View File

@@ -28,13 +28,19 @@
[cuerdas.core :as str]))
(def sql:get-file-snapshots
"SELECT id, label, revn, created_at, created_by, profile_id
FROM file_change
WHERE file_id = ?
AND data IS NOT NULL
AND (deleted_at IS NULL OR deleted_at > now())
ORDER BY created_at DESC
LIMIT 20")
"WITH changes AS (
SELECT id, label, revn, created_at, created_by, profile_id
FROM file_change
WHERE file_id = ?
AND data IS NOT NULL
AND (deleted_at IS NULL OR deleted_at > now())
), versions AS (
(SELECT * FROM changes WHERE created_by = 'system' LIMIT 1000)
UNION ALL
(SELECT * FROM changes WHERE created_by != 'system' LIMIT 1000)
)
SELECT * FROM versions
ORDER BY created_at DESC;")
(defn get-file-snapshots
[conn file-id]

View File

@@ -50,8 +50,7 @@
" where file_id=? and tag=? and deleted_at is null")
res (db/exec! conn [sql file-id tag])]
(->> res
(d/index-by :object-id (fn [row]
(files/resolve-public-uri (:media-id row))))
(d/index-by :object-id :media-id)
(d/without-nils))))
(defn- get-object-thumbnails
@@ -62,8 +61,7 @@
" where file_id=? and deleted_at is null")
res (db/exec! conn [sql file-id])]
(->> res
(d/index-by :object-id (fn [row]
(files/resolve-public-uri (:media-id row))))
(d/index-by :object-id :media-id)
(d/without-nils))))
([conn file-id object-ids]
@@ -75,8 +73,7 @@
res (db/exec! conn [sql file-id ids])]
(->> res
(d/index-by :object-id (fn [row]
(files/resolve-public-uri (:media-id row))))
(d/index-by :object-id :media-id)
(d/without-nils)))))
(sv/defmethod ::get-file-object-thumbnails
@@ -127,8 +124,11 @@
(if-let [frame (-> frames first)]
(let [frame-id (:id frame)
object-id (thc/fmt-object-id (:id file) page-id frame-id "frame")
frame (if-let [thumb (get thumbnails object-id)]
(assoc frame :thumbnail thumb :shapes [])
frame (if-let [media-id (get thumbnails object-id)]
(-> frame
(assoc :thumbnail-id media-id)
(assoc :shapes []))
(dissoc frame :thumbnail))
children-ids
@@ -402,7 +402,10 @@
[cfg {:keys [::rpc/profile-id file-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
;; TODO For now we check read permissions instead of write,
;; to allow viewer users to update thumbnails. We might
;; review this approach on the future.
(files/check-read-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [media (create-file-thumbnail! cfg params)]
{:uri (files/resolve-public-uri (:id media))

View File

@@ -223,15 +223,6 @@
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
(some->> (:data-ref-id file) (sto/touch-object! storage))))
(-> cfg
(assoc ::wrk/task :file-xlog-gc)
(assoc ::wrk/label (str "xlog:" (:id file)))
(assoc ::wrk/params {:file-id (:id file)})
(assoc ::wrk/delay (dt/duration "5m"))
(assoc ::wrk/dedupe true)
(assoc ::wrk/priority 1)
(wrk/submit!))
(persist-file! cfg file)
(let [params (assoc params :file file)

View File

@@ -67,7 +67,7 @@
:is-owner true
:is-admin true
:can-edit true}
{::db/return-keys? false}))
{::db/return-keys false}))
(doseq [params (sequence (comp
(map #(bfc/remap-id % :file-id))

View File

@@ -60,15 +60,25 @@
(media/validate-media-type! content)
(media/validate-media-size! content)
(db/run! cfg (fn [cfg]
(let [object (create-file-media-object cfg params)
props {:name (:name params)
:file-id file-id
:is-local (:is-local params)
:size (:size content)
:mtype (:mtype content)}]
(with-meta object
{::audit/replace-props props})))))
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
;; We get the minimal file for proper checking if
;; file is not already deleted
(let [_ (files/get-minimal-file conn file-id)
mobj (create-file-media-object cfg params)]
(db/update! conn :file
{:modified-at (dt/now)
:has-media-trimmed false}
{:id file-id}
{::db/return-keys false})
(with-meta mobj
{::audit/replace-props
{:name (:name params)
:file-id file-id
:is-local (:is-local params)
:size (:size content)
:mtype (:mtype content)}})))))
(defn- big-enough-for-thumbnail?
"Checks if the provided image info is big enough for
@@ -142,20 +152,14 @@
:always
(assoc ::image (process-main-image info)))))
(defn create-file-media-object
[{:keys [::sto/storage ::db/conn ::wrk/executor]}
(defn- create-file-media-object
[{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
{:keys [id file-id is-local name content]}]
(let [result (px/invoke! executor (partial process-image content))
image (sto/put-object! storage (::image result))
thumb (when-let [params (::thumb result)]
(sto/put-object! storage params))]
(db/update! conn :file
{:modified-at (dt/now)
:has-media-trimmed false}
{:id file-id})
(db/exec-one! conn [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
@@ -182,7 +186,18 @@
::sm/params schema:create-file-media-object-from-url}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(files/check-edition-permissions! pool profile-id file-id)
(create-file-media-object-from-url cfg (assoc params :profile-id profile-id)))
;; We get the minimal file for proper checking if file is not
;; already deleted
(let [_ (files/get-minimal-file cfg file-id)
mobj (create-file-media-object-from-url cfg (assoc params :profile-id profile-id))]
(db/update! pool :file
{:modified-at (dt/now)
:has-media-trimmed false}
{:id file-id}
{::db/return-keys false})
mobj))
(defn download-image
[{:keys [::http/client]} uri]

View File

@@ -422,7 +422,9 @@
:deleted-at deleted-at
:id profile-id}})
(rph/with-transform {} (session/delete-fn cfg)))))
(-> (rph/wrap nil)
(rph/with-transform (session/delete-fn cfg))))))
;; --- HELPERS
@@ -431,8 +433,11 @@
"WITH owner_teams AS (
SELECT tpr.team_id AS id
FROM team_profile_rel AS tpr
JOIN team AS t ON (t.id = tpr.team_id)
WHERE tpr.is_owner IS TRUE
AND tpr.profile_id = ?
AND (t.deleted_at IS NULL OR
t.deleted_at > now())
)
SELECT tpr.team_id AS id,
count(tpr.profile_id) - 1 AS participants

View File

@@ -166,23 +166,26 @@
;; invited team.
(let [props {:team-id (:team-id claims)
:role (:role claims)
:invitation-id (:id invitation)}
:invitation-id (:id invitation)}]
accept-invitation-event
(-> (audit/event-from-rpc-params params)
(assoc ::audit/name "accept-team-invitation")
(assoc ::audit/props props))
(audit/submit!
cfg
(-> (audit/event-from-rpc-params params)
(assoc ::audit/name "accept-team-invitation")
(assoc ::audit/props props)))
accept-invitation-from-event
(-> (audit/event-from-rpc-params params)
(assoc ::audit/profile-id (:created-by invitation))
(assoc ::audit/name "accept-team-invitation-from")
(assoc ::audit/props (assoc props
:profile-id (:id profile)
:email (:email profile))))]
(audit/submit! cfg accept-invitation-event)
(audit/submit! cfg accept-invitation-from-event)
;; NOTE: Backward compatibility; old invitations can
;; have the `created-by` to be nil; so in this case we
;; don't submit this event to the audit-log
(when-let [created-by (:created-by invitation)]
(audit/submit!
cfg
(-> (audit/event-from-rpc-params params)
(assoc ::audit/profile-id created-by)
(assoc ::audit/name "accept-team-invitation-from")
(assoc ::audit/props (assoc props
:profile-id (:id profile)
:email (:email profile))))))
(accept-invitation cfg claims invitation profile)
(assoc claims :state :created))

View File

@@ -87,6 +87,7 @@
(let [params (:query-params request)
pstyle (:type params "js")
context (assoc context :param-style pstyle)]
{::yres/status 200
::yres/body (-> (io/resource "app/templates/api-doc.tmpl")
(tmpl/render context))}))
@@ -207,7 +208,7 @@
(assert (sm/valid? ::rpc/methods (::rpc/methods params)) "expected valid methods"))
(defmethod ig/init-key ::routes
[_ {:keys [methods] :as cfg}]
[_ {:keys [::rpc/methods] :as cfg}]
[(let [context (prepare-doc-context methods)]
[["/_doc"
{:handler (doc-handler context)

View File

@@ -74,8 +74,7 @@
(defmethod ig/assert-key ::props
[_ params]
(assert (db/pool? (::db/pool params)) "expected valid database pool")
(assert (string? (::key params)) "expected valid key string"))
(assert (db/pool? (::db/pool params)) "expected valid database pool"))
(defmethod ig/init-key ::props
[_ {:keys [::db/pool ::key] :as cfg}]

View File

@@ -26,7 +26,6 @@
[app.util.pointer-map :as pmap]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.set :as set]
[integrant.core :as ig]))
(declare ^:private get-file)
@@ -53,16 +52,21 @@
RETURNING id")
(def ^:private xf:collect-used-media
(comp (map :data) (mapcat bfc/collect-used-media)))
(comp
(map :data)
(mapcat bfc/collect-used-media)))
(defn- clean-file-media!
"Performs the garbage collection of file media objects."
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
(let [used (into #{}
xf:collect-used-media
(cons file
(->> (db/cursor conn [sql:get-snapshots id])
(map (partial decode-file cfg)))))
(let [xform (comp
(map (partial decode-file cfg))
xf:collect-used-media)
used (->> (db/plan conn [sql:get-snapshots id])
(transduce xform conj #{}))
used (into used xf:collect-used-media [file])
ids (db/create-array conn "uuid" used)
unused (->> (db/exec! conn [sql:mark-file-media-object-deleted id ids])
(into #{} (map :id)))]
@@ -145,51 +149,47 @@
AND f.deleted_at IS null
ORDER BY f.modified_at ASC")
(def ^:private xf:map-id (map :id))
(defn- get-used-components
"Given a file and a set of components marked for deletion, return a
filtered set of component ids that are still un use"
[components library-id {:keys [data]}]
(filter #(ctf/used-in? data library-id % :component) components))
(defn- clean-deleted-components!
"Performs the garbage collection of unreferenced deleted components."
[{:keys [::db/conn] :as cfg} {:keys [data] :as file}]
(let [file-id (:id file)
get-used-components
(fn [data components]
;; Find which of the components are used in the file.
(into #{}
(filter #(ctf/used-in? data file-id % :component))
components))
deleted-components
(ctkl/deleted-components-seq data)
get-unused-components
(fn [components files]
;; Find and return a set of unused components (on all files).
(reduce (fn [components {:keys [data]}]
(if (seq components)
(->> (get-used-components data components)
(set/difference components))
(reduced components)))
xform
(mapcat (partial get-used-components deleted-components file-id))
components
files))
used-remote
(->> (db/plan conn [sql:get-files-for-library file-id])
(transduce (comp (map (partial decode-file cfg)) xform) conj #{}))
process-fdata
(fn [data unused]
(reduce (fn [data id]
(l/trc :hint "delete component"
:component-id (str id)
:file-id (str file-id))
(ctkl/delete-component data id))
data
unused))
used-local
(into #{} xform [file])
deleted (into #{} (ctkl/deleted-components-seq data))
unused (->> (db/cursor conn [sql:get-files-for-library file-id] {:chunk-size 1})
(map (partial decode-file cfg))
(cons file)
(get-unused-components deleted)
(mapv :id)
(set))
file (update file :data process-fdata unused)]
unused
(transduce xf:map-id disj
(into #{} xf:map-id deleted-components)
(concat used-remote used-local))
file
(update file :data
(fn [data]
(reduce (fn [data id]
(l/trc :hint "delete component"
:component-id (str id)
:file-id (str file-id))
(ctkl/delete-component data id))
data
unused)))]
(l/dbg :hint "clean" :rel "components" :file-id (str file-id) :total (count unused))
file))

View File

@@ -1,64 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.tasks.file-xlog-gc
(:require
[app.common.logging :as l]
[app.config :as cf]
[app.db :as db]
[integrant.core :as ig]))
;; Get the latest available snapshots without exceeding the total
;; snapshot limit
(def ^:private sql:get-latest-snapshots
"SELECT fch.id, fch.created_at
FROM file_change AS fch
WHERE fch.file_id = ?
AND fch.created_by = 'system'
AND fch.data IS NOT NULL
AND fch.deleted_at > now()
ORDER BY fch.created_at DESC
LIMIT ?")
;; Mark all snapshots that are outside the allowed total threshold
;; available for the GC
(def ^:private sql:delete-snapshots
"UPDATE file_change
SET deleted_at = now()
WHERE file_id = ?
AND deleted_at > now()
AND data IS NOT NULL
AND created_by = 'system'
AND created_at < ?")
(defn- get-alive-snapshots
[conn file-id]
(let [total (cf/get :auto-file-snapshot-total 10)
snapshots (db/exec! conn [sql:get-latest-snapshots file-id total])]
(not-empty snapshots)))
(defn- delete-old-snapshots!
[{:keys [::db/conn] :as cfg} file-id]
(when-let [snapshots (get-alive-snapshots conn file-id)]
(let [last-date (-> snapshots peek :created-at)
result (db/exec-one! conn [sql:delete-snapshots file-id last-date])]
(l/inf :hint "delete old file snapshots"
:file-id (str file-id)
:current (count snapshots)
:deleted (db/get-update-count result)))))
(defmethod ig/assert-key ::handler
[_ params]
(assert (db/pool? (::db/pool params)) "expected a valid database pool"))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [props] :as task}]
(let [file-id (:file-id props)]
(assert (uuid? file-id) "expected file-id on props")
(-> cfg
(assoc ::db/rollback (:rollback props false))
(db/tx-run! delete-old-snapshots! file-id)))))

View File

@@ -62,7 +62,7 @@
(def default
{:database-uri "postgresql://postgres/penpot_test"
:redis-uri "redis://redis/1"
:file-snapshot-every 1})
:auto-file-snapshot-every 1})
(def config
(cf/read-config :prefix "penpot-test"

View File

@@ -383,8 +383,19 @@
;; as deleted.
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
;; This only clears fragments, the file media objects still referenced because
;; snapshots are preserved
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 3 (:processed res))))
(t/is (= 2 (:processed res))))
;; Mark all snapshots to be a non-snapshot file change
(th/db-exec! ["update file_change set data = null where file_id = ?" (:id file)])
(th/db-exec! ["update file set has_media_trimmed = false where id = ?" (:id file)])
;; Rerun the file-gc and objects-gc
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 2 (:processed res))))
;; Now that file-gc have deleted the file-media-object usage,
;; lets execute the touched-gc task, we should see that two of
@@ -417,20 +428,6 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))
(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::th/type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:revn revn
:vern 0
:components-v2 true
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))]
(let [storage (:app.storage/storage th/*system*)
@@ -550,8 +547,20 @@
;; as deleted.
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
;; This only removes unused fragments, file media are still
;; referenced on snapshots.
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 7 (:processed res))))
(t/is (= 2 (:processed res))))
;; Mark all snapshots to be a non-snapshot file change
(th/db-exec! ["update file set has_media_trimmed = false where id = ?" (:id file)])
(th/db-exec! ["update file_change set data = null where file_id = ?" (:id file)])
;; Rerun file-gc and objects-gc task for the same file once all snapshots are
;; "expired/deleted"
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 6 (:processed res))))
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)
:deleted-at nil})]
@@ -591,20 +600,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))
#_(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::th/type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:revn revn
:features cfeat/supported-features
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))]
(:result out)))]
(let [storage (:app.storage/storage th/*system*)
profile (th/create-profile* 1)
@@ -1090,8 +1086,7 @@
(t/is (contains? result :file-id))
(t/is (= (:id file) (:file-id result)))
(t/is (str/starts-with? (get-in result [:page :objects frame1-id :thumbnail])
"http://localhost:3449/assets/by-id/"))
(t/is (uuid? (get-in result [:page :objects frame1-id :thumbnail-id])))
(t/is (= [] (get-in result [:page :objects frame1-id :shapes]))))
;; Delete thumbnail data
@@ -1337,3 +1332,329 @@
(t/is (every? #(bytes? (:data %)) rows))
(t/is (every? #(nil? (:data-ref-id %)) rows))
(t/is (every? #(nil? (:data-backend %)) rows)))))
(t/deftest file-gc-with-components-1
(let [storage (:app.storage/storage th/*system*)
profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})
s-id-1 (uuid/random)
s-id-2 (uuid/random)
c-id (uuid/random)
page-id (first (get-in file [:data :pages]))]
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)
:deleted-at nil})]
(t/is (= (count rows) 1)))
;; Update file inserting new component
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id page-id
:id s-id-1
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id s-id-1
:name "Board"
:frame-id uuid/zero
:parent-id uuid/zero
:type :frame
:main-instance true
:component-root true
:component-file (:id file)
:component-id c-id})}
{:type :add-obj
:page-id page-id
:id s-id-2
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id s-id-2
:name "Board"
:frame-id uuid/zero
:parent-id uuid/zero
:type :frame
:main-instance false
:component-root true
:component-file (:id file)
:component-id c-id})}
{:type :add-component
:path ""
:name "Board"
:main-instance-id s-id-1
:main-instance-page page-id
:id c-id
:anotation nil}])
;; Run the file-gc task immediately without forced min-age
(t/is (false? (th/run-task! :file-gc {:file-id (:id file)})))
;; Run the task again
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
;; Retrieve file and check trimmed attribute
(let [row (th/db-get :file {:id (:id file)})]
(t/is (true? (:has-media-trimmed row))))
;; Check that component exists
(let [data {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
component (get-in result [:data :components c-id])]
(t/is (some? component))
(t/is (nil? (:objects component)))))
;; Now proceed to delete a component
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :del-component
:id c-id}
{:type :del-obj
:page-id page-id
:id s-id-1
:ignore-touched true}])
;; ;; Check that component is marked as deleted
(let [data {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
component (get-in result [:data :components c-id])]
(t/is (true? (:deleted component)))
(t/is (some? (not-empty (:objects component))))))
;; Re-run the file-gc task
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
(let [row (th/db-get :file {:id (:id file)})]
(t/is (true? (:has-media-trimmed row))))
;; Check that component is still there after file-gc task
(let [data {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
component (get-in result [:data :components c-id])]
(t/is (true? (:deleted component)))
(t/is (some? (not-empty (:objects component))))))
;; Now delete the last instance using deleted component
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :del-obj
:page-id page-id
:id s-id-2
:ignore-touched true}])
;; Now, we have deleted the usage of component if we pass file-gc,
;; that component should be deleted
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
;; Check that component is properly removed
(let [data {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
components (get-in result [:data :components])]
(t/is (not (contains? components c-id)))))))
(t/deftest file-gc-with-components-2
(let [storage (:app.storage/storage th/*system*)
profile (th/create-profile* 1)
file-1 (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared true})
file-2 (th/create-file* 2 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})
rel (th/link-file-to-library*
{:file-id (:id file-2)
:library-id (:id file-1)})
s-id-1 (uuid/random)
s-id-2 (uuid/random)
c-id (uuid/random)
f1-page-id (first (get-in file-1 [:data :pages]))
f2-page-id (first (get-in file-2 [:data :pages]))]
;; Update file library inserting new component
(update-file!
:file-id (:id file-1)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id f1-page-id
:id s-id-1
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id s-id-1
:name "Board"
:frame-id uuid/zero
:parent-id uuid/zero
:type :frame
:main-instance true
:component-root true
:component-file (:id file-1)
:component-id c-id})}
{:type :add-component
:path ""
:name "Board"
:main-instance-id s-id-1
:main-instance-page f1-page-id
:id c-id
:anotation nil}])
;; Instanciate a component in a different file
(update-file!
:file-id (:id file-2)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id f2-page-id
:id s-id-2
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id s-id-2
:name "Board"
:frame-id uuid/zero
:parent-id uuid/zero
:type :frame
:main-instance false
:component-root true
:component-file (:id file-1)
:component-id c-id})}])
;; Run the file-gc on file and library
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-1)})))
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-2)})))
;; Check that component exists
(let [data {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file-1)}
out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
component (get-in result [:data :components c-id])]
(t/is (some? component))
(t/is (nil? (:objects component)))))
;; Now proceed to delete a component
(update-file!
:file-id (:id file-1)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :del-component
:id c-id}
{:type :del-obj
:page-id f1-page-id
:id s-id-1
:ignore-touched true}])
;; Check that component is marked as deleted
(let [data {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file-1)}
out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
component (get-in result [:data :components c-id])]
(t/is (true? (:deleted component)))
(t/is (some? (not-empty (:objects component))))))
;; Re-run the file-gc task
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-1)})))
(t/is (false? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-2)})))
;; Check that component is still there after file-gc task
(let [data {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file-1)}
out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
component (get-in result [:data :components c-id])]
(t/is (true? (:deleted component)))
(t/is (some? (not-empty (:objects component))))))
;; Now delete the last instance using deleted component
(update-file!
:file-id (:id file-2)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :del-obj
:page-id f2-page-id
:id s-id-2
:ignore-touched true}])
;; Mark
(th/db-exec! ["update file set has_media_trimmed = false where id = ?" (:id file-1)])
;; Now, we have deleted the usage of component if we pass file-gc,
;; that component should be deleted
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-1)})))
;; Check that component is properly removed
(let [data {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file-1)}
out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)
components (get-in result [:data :components])]
(t/is (not (contains? components c-id)))))))

View File

@@ -10,6 +10,7 @@
[app.db :as db]
[app.rpc :as-alias rpc]
[app.storage :as sto]
[app.util.time :as dt]
[backend-tests.helpers :as th]
[clojure.test :as t]
[datoteka.fs :as fs]))
@@ -245,3 +246,35 @@
(t/is (= "image/jpeg" (:mtype result)))
(t/is (uuid? (:media-id result)))
(t/is (uuid? (:thumbnail-id result))))))
(t/deftest media-object-upload-command-when-file-is-deleted
(let [prof (th/create-profile* 1)
proj (th/create-project* 1 {:profile-id (:id prof)
:team-id (:default-team-id prof)})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
_ (th/db-update! :file
{:deleted-at (dt/now)}
{:id (:id file)})
mfile {:filename "sample.jpg"
:path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"
:size 312043}
params {::th/type :upload-file-media-object
::rpc/profile-id (:id prof)
:file-id (:id file)
:is-local true
:name "testfile"
:content mfile}
out (th/command! params)]
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found)))))

View File

@@ -203,7 +203,24 @@
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :owner-teams-with-people))))))
(t/is (= (:code edata) :owner-teams-with-people)))
(let [params {::th/type :delete-team
::rpc/profile-id (:id prof1)
:id (:id team1)}
out (th/command! params)]
;; (th/print-result! out)
(let [team (th/db-get :team {:id (:id team1)} {::db/remove-deleted false})]
(t/is (dt/instant? (:deleted-at team)))))
;; Request profile to be deleted
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (nil? (:error out)))))))
(t/deftest profile-deletion-3
(let [prof1 (th/create-profile* 1)
@@ -291,7 +308,7 @@
out (th/command! params)]
;; (th/print-result! out)
(t/is (= {} (:result out)))
(t/is (nil? (:result out)))
(t/is (nil? (:error out))))
;; query files after profile soft deletion
@@ -336,7 +353,7 @@
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (= {} (:result out)))
(t/is (nil? (:result out)))
(t/is (nil? (:error out))))
(th/run-pending-tasks!)

View File

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

View File

@@ -1130,6 +1130,21 @@
(update :pages-index dissoc nil)
(update :pages-index update-vals update-page))))
(defn migrate-up-59
[data]
(letfn [(fix-touched [elem]
(cond-> elem (string? elem) keyword))
(update-shape [shape]
(d/update-when shape :touched #(into #{} (map fix-touched) %)))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(def migrations
"A vector of all applicable migrations"
[{:id 2 :migrate-up migrate-up-2}
@@ -1178,5 +1193,6 @@
{:id 54 :migrate-up migrate-up-54}
{:id 55 :migrate-up migrate-up-55}
{:id 56 :migrate-up migrate-up-56}
{:id 57 :migrate-up migrate-up-57}])
{:id 57 :migrate-up migrate-up-57}
{:id 59 :migrate-up migrate-up-59}])

View File

@@ -320,6 +320,35 @@
(pcb/with-file-data file-data)
(pcb/update-shapes shape-ids detach-shape))))))
(defmethod repair-error :shape-ref-cycle
[_ {:keys [shape args] :as error} file-data _]
(let [repair-component
(fn [component]
(let [objects (:objects component) ;; we only have encounter this on deleted components,
;; so the relevant objects are inside the component
to-detach (->> (:cycles-ids args)
(map #(get objects %))
(map #(ctn/get-head-shape objects %))
(map :id)
distinct
(mapcat #(ctn/get-children-in-instance objects %))
(map :id)
set)]
(update component :objects
(fn [objects]
(reduce-kv (fn [acc k v]
(if (contains? to-detach k)
(assoc acc k (ctk/detach-shape v))
(assoc acc k v)))
{}
objects)))))]
(log/dbg :hint "repairing component :shape-ref-cycle" :id (:id shape) :name (:name shape))
(-> (pcb/empty-changes nil nil)
(pcb/with-library-data file-data)
(pcb/update-component (:id shape) repair-component))))
(defmethod repair-error :shape-ref-in-main
[_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape

View File

@@ -55,7 +55,8 @@
:component-nil-objects-not-allowed
:instance-head-not-frame
:misplaced-slot
:missing-slot})
:missing-slot
:shape-ref-cycle})
(def ^:private schema:error
[:map {:title "ValidationError"}
@@ -482,6 +483,18 @@
"This deleted component has children with the same swap slot"
component file nil))))
(defn check-ref-cycles
[component file]
(let [cycles-ids (->> component
:objects
vals
(filter #(= (:id %) (:shape-ref %)))
(map :id))]
(when (seq cycles-ids)
(report-error :shape-ref-cycle
"This deleted component has shapes with shape-ref pointing to self"
component file nil :cycles-ids cycles-ids))))
(defn- check-component
"Validate semantic coherence of a component. Report all errors found."
@@ -491,7 +504,8 @@
"Objects list cannot be nil"
component file nil))
(when (:deleted component)
(check-component-duplicate-swap-slot component file)))
(check-component-duplicate-swap-slot component file)
(check-ref-cycles component file)))
(defn- get-orphan-shapes
[{:keys [objects] :as page}]

View File

@@ -304,7 +304,12 @@
(and (some? (ctk/get-swap-slot ref-shape))
(nil? (ctk/get-swap-slot shape))
(not= (:id shape) shape-id))
(pcb/update-shapes [(:id shape)] #(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape))))))]
(pcb/update-shapes [(:id shape)] #(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape)))
;; If we can't get the ref-shape (e.g. it's in an external library not linked),
;: we can't do a suitable advance. So it's better to detach the shape
(nil? ref-shape)
(pcb/update-shapes [(:id shape)] ctk/detach-shape))))]
(reduce skip-near changes children)))

View File

@@ -304,7 +304,9 @@
(->> ids
(mapcat #(ctn/get-child-heads objects %))
(map :id)))
cell (or cell (ctl/get-cell-by-index parent to-index))]
index-cell-data (when to-index (ctl/get-cell-by-index parent to-index))
cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)]))]
(-> changes
(pcb/with-page-id page-id)
@@ -409,12 +411,14 @@
;; Resize parent containers that need to
(pcb/resize-parents parents))))
(defn change-show-in-viewer [shape hide?]
(defn change-show-in-viewer
[shape hide?]
(assoc shape :hide-in-viewer hide?))
(defn add-new-interaction [shape interaction]
(-> shape
(update :interactions ctsi/add-interaction interaction)))
(defn add-new-interaction
[shape interaction]
(update shape :interactions ctsi/add-interaction interaction))
(defn show-in-viewer [shape]
(defn show-in-viewer
[shape]
(dissoc shape :hide-in-viewer))

View File

@@ -335,24 +335,28 @@
(true? (= (:id component) (:id ref-component)))))
(defn find-swap-slot
[shape container file libraries]
(if-let [swap-slot (ctk/get-swap-slot shape)]
swap-slot
(let [ref-shape (find-ref-shape file
container
libraries
shape
:include-deleted? true
:with-context? true)
shape-meta (meta ref-shape)
ref-file (:file shape-meta)
ref-container (:container shape-meta)]
(when ref-shape
(if-let [swap-slot (ctk/get-swap-slot ref-shape)]
swap-slot
(if (ctk/main-instance? ref-shape)
(:id shape)
(find-swap-slot ref-shape ref-container ref-file libraries)))))))
([shape container file libraries]
(find-swap-slot shape container file libraries #{}))
([shape container file libraries viewed-ids]
(if (contains? viewed-ids (:id shape)) ;; prevent cycles
nil
(if-let [swap-slot (ctk/get-swap-slot shape)]
swap-slot
(let [ref-shape (find-ref-shape file
container
libraries
shape
:include-deleted? true
:with-context? true)
shape-meta (meta ref-shape)
ref-file (:file shape-meta)
ref-container (:container shape-meta)]
(when ref-shape
(if-let [swap-slot (ctk/get-swap-slot ref-shape)]
swap-slot
(if (ctk/main-instance? ref-shape)
(:id shape)
(find-swap-slot ref-shape ref-container ref-file libraries (conj viewed-ids (:id shape)))))))))))
(defn match-swap-slot?
[shape-main shape-inst container-inst container-main file libraries]
@@ -738,16 +742,20 @@
(:component-id shape) "@"
:else "-")
(when (and (:component-file shape) component-file)
(when (:component-file shape)
(str/format "<%s> "
(if (= (:id component-file) (:id file))
"local"
(:name component-file))))
(if component-file
(if (= (:id component-file) (:id file))
"local"
(:name component-file))
(if show-ids
(str/format "¿%s?" (:component-file shape))
"?"))))
(or (:name component-shape)
(str/format "?%s"
(when show-ids
(str " " (:shape-ref shape)))))
(if show-ids
(str/format "¿%s?" (:shape-ref shape))
"?"))
(when (and show-ids component-shape)
(str/format " %s" (:id component-shape)))

View File

@@ -33,7 +33,7 @@
[:id ::sm/uuid]
[:axis [::sm/one-of #{:x :y}]]
[:position ::sm/safe-number]
[:frame-id {:optional true} ::sm/uuid]])
[:frame-id {:optional true} [:maybe ::sm/uuid]]])
(def schema:guides
[:map-of {:gen/max 2} ::sm/uuid schema:guide])

View File

@@ -154,6 +154,7 @@
[:main-instance {:optional true} :boolean]
[:remote-synced {:optional true} :boolean]
[:shape-ref {:optional true} ::sm/uuid]
[:touched {:optional true} [:maybe [:set :keyword]]]
[:blocked {:optional true} :boolean]
[:collapsed {:optional true} :boolean]
[:locked {:optional true} :boolean]

View File

@@ -1479,7 +1479,7 @@
(defn get-cell-by-index
[parent to-index]
(let [cells (get-cells parent {:sort? true :remove-empty? true})
to-index (- (count cells) to-index)]
to-index (- (count cells) to-index 1)]
(nth cells to-index nil)))
(defn add-children-to-index

View File

@@ -103,6 +103,83 @@
(t/is (= (:shape-ref copy-nested-ellipse) (thi/id :nested-ellipse)))
(t/is (nil? (ctk/get-swap-slot copy-nested-ellipse)))))
(t/deftest test-advance-in-library
(let [;; ==== Setup
library (setup-file)
file (-> (thf/sample-file :file2)
(thc/instantiate-component :c-big-board
:copy-big-board
:library library
:children-labels [:copy-h-board-with-ellipse
:copy-nested-h-ellipse
:copy-nested-ellipse]))
page (thf/current-page file)
;; ==== Action
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
(pcb/with-page page)
(pcb/with-objects (:objects page)))
page
{(:id file) file
(:id library) library}
(thi/id :copy-big-board))
file' (thf/apply-changes file changes)
;; ==== Get
copy-h-board-with-ellipse (ths/get-shape file' :copy-h-board-with-ellipse)
copy-nested-h-ellipse (ths/get-shape file' :copy-nested-h-ellipse)
copy-nested-ellipse (ths/get-shape file' :copy-nested-ellipse)]
;; ==== Check
;; It should the same as above, but in an external library.
(thf/dump-file file)
(t/is (ctk/instance-root? copy-h-board-with-ellipse))
(t/is (= (:shape-ref copy-h-board-with-ellipse) (thi/id :board-with-ellipse)))
(t/is (nil? (ctk/get-swap-slot copy-h-board-with-ellipse)))
(t/is (ctk/instance-head? copy-nested-h-ellipse))
(t/is (= (:shape-ref copy-nested-h-ellipse) (thi/id :nested-h-ellipse)))
(t/is (nil? (ctk/get-swap-slot copy-nested-h-ellipse)))
(t/is (not (ctk/instance-head? copy-nested-ellipse)))
(t/is (= (:shape-ref copy-nested-ellipse) (thi/id :nested-ellipse)))
(t/is (nil? (ctk/get-swap-slot copy-nested-ellipse)))))
(t/deftest test-advance-in-broken-library
(let [;; ==== Setup
library (setup-file)
file (-> (thf/sample-file :file2)
(thc/instantiate-component :c-big-board
:copy-big-board
:library library
:children-labels [:copy-h-board-with-ellipse
:copy-nested-h-ellipse
:copy-nested-ellipse]))
page (thf/current-page file)
;; ==== Action
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
(pcb/with-page page)
(pcb/with-objects (:objects page)))
page
{(:id file) file}
(thi/id :copy-big-board))
file' (thf/apply-changes file changes)
;; ==== Get
copy-h-board-with-ellipse (ths/get-shape file' :copy-h-board-with-ellipse)
copy-nested-h-ellipse (ths/get-shape file' :copy-nested-h-ellipse)
copy-nested-ellipse (ths/get-shape file' :copy-nested-ellipse)]
;; ==== Check
;; If the main component cannot be found, because it's in a library that is
;; not available, the nested copies should be detached too.
(t/is (not (ctk/in-component-copy? copy-h-board-with-ellipse)))
(t/is (not (ctk/in-component-copy? copy-nested-h-ellipse)))
(t/is (not (ctk/in-component-copy? copy-nested-ellipse)))))
(t/deftest test-dont-advance-when-swapped-copy
(let [;; ==== Setup
file (-> (setup-file)

View File

@@ -43,7 +43,6 @@ services:
environment:
- EXTERNAL_UID=${CURRENT_USER_ID}
- PENPOT_SECRET_KEY=super-secret-devenv-key
# SMTP setup
- PENPOT_SMTP_ENABLED=true
- PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com

View File

@@ -90,6 +90,7 @@ http {
proxy_hide_header x-amz-meta-server-side-encryption;
proxy_hide_header x-amz-server-side-encryption;
proxy_pass $redirect_uri;
proxy_ssl_server_name on;
add_header x-internal-redirect "$redirect_uri";
add_header x-cache-control "$redirect_cache_control";

View File

@@ -201,7 +201,7 @@ services:
environment:
# Don't touch it; this uses an internal docker network to
# communicate with the frontend.
PENPOT_PUBLIC_URI: http://penpot-frontend
PENPOT_PUBLIC_URI: http://penpot-frontend:8080
## Redis is used for the websockets notifications.
PENPOT_REDIS_URI: redis://penpot-redis/0

View File

@@ -92,6 +92,7 @@ http {
proxy_hide_header x-amz-request-id;
proxy_hide_header x-amz-meta-server-side-encryption;
proxy_hide_header x-amz-server-side-encryption;
proxy_ssl_server_name on;
proxy_pass $redirect_uri;
add_header x-internal-redirect "$redirect_uri";

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Binary file not shown.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -216,3 +216,9 @@ Success! - Published to example-plugin-penpot.surge.sh
```
5. Done!
## 3.5. Submitting to Penpot
To make your finished plugin available in our catalog, submit in on the [plugin submission page](https://penpot.app/penpothub/plugins/create-plugin). Once it becomes available any Penpot user will be able to install and use it.

View File

@@ -30,7 +30,7 @@ flags (that just enables or disables something). All flags are set in a single
format: <code class="language-bash"><enable|disable>-\<flag-name></code>. For example:
```bash
PENPOT_FLAGS: enable-smpt disable-registration disable-email-verification
PENPOT_FLAGS: enable-smtp disable-registration disable-email-verification
```
### Registration ###

View File

@@ -4,7 +4,8 @@ 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.
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
@@ -14,18 +15,30 @@ about self-hosting</a> in Penpot community.
href="https://design.penpot.app">our SaaS offer</a> for Penpot and your
self-hosted Penpot platform!**
There are two main options for creating a Penpot instance:
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.
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"
@@ -261,7 +274,7 @@ itself.
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
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
@@ -287,7 +300,7 @@ 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. And to find new charts, you can search Helm chart repositories.
> installation. To find new charts, you can search Helm chart repositories.
### Install Helm

View File

@@ -20,6 +20,8 @@ machine.
* In the [Install with Docker][2] section, you can find the official Docker installation guide.
* In the [Install with Kubernetes][7] section, you can find the official Kubernetes installation guide.
* In the [Configuration][3] section, you can find all the customization options you can set up after installing.
* Or you can try other, not supported by Penpot, [Unofficial options][4].
@@ -28,9 +30,11 @@ machine.
The [Integration Guide][5] explains how to connect Penpot with external apps, so they get notified
when certain events occur and may create your own interconnections and collaboration features.
## Developing Penpot
Also, if you are a developer, you can get into the code, to explore it, learn how it is made, or extend it and contribute with new functionality. For this, we have a different Docker installation.
Also, if you are a developer, you can get into the code, to explore it, learn how it is made,
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.
[1]: /technical-guide/getting-started/#install-with-elestio
@@ -39,3 +43,4 @@ In the [Developer Guide][6] you can find how to setup a development environment
[4]: /technical-guide/getting-started/#unofficial-self-host-options
[5]: /technical-guide/integration/
[6]: /technical-guide/developer/
[7]: /technical-guide/getting-started/#install-with-kubernetes

View File

@@ -5,45 +5,25 @@ title: 14· Import/export files
<h1 id="import-export">Import and export files</h1>
<p class="main-paragraph">You can export Penpot files to your computer and import them from your computer to your projects.</p>
<h2 id="penpot-formats">Penpot file formats</h2>
<p>There are two different formats in which you can import/export Penpot files. A standard one and a binary one. You always have the chance to use both for any file.</p>
<h3>Penpot file (.penpot).</h3>
<p>The fast one. Binary Penpot specific.</p>
<ul>
<li>✅ Highly efficient in terms of memory and transfer time when exporting and importing.</li>
<li>❌ It can be opened only in Penpot.</li>
<li>❌ Not transparent, code difficult to explore.</li>
</ul>
<h3>Standard file (.zip).</h3>
<p>The open one. A compressed file that includes SVG and JSON.</p>
<ul>
<li>✅ Allows the file to be opened by other softwares (still, for those cases export to SVG seems to be the common practice).</li>
<li>✅ Allows some automations and integrations.</li>
<li>✅ Is a transparent, existing, open standard format.</li>
<li>❌ Highly inefficient in terms of memory and transfer time when exporting and importing (this is because SVG).</li>
</ul>
<h2 id="files-export">Export Penpot files</h2>
<p>Exporting files is useful for many reasons. Sometimes you want to have a backup of your files and sometimes it is useful to share Penpot files with a user that does not belong to one of your teams, or you want to have a backup of your files outside Penpot, both SaaS (design.penpot.app) or at a self-hosted instance.</p>
<h3 id="export-penpot-files">How to export Penpot files</h3>
<h4>Export a single file</h4>
<p>You can download (export) files from the workspace and from the dashboard.</p>
<ul>
<li>
<strong>From the <a href="/user-guide/the-interface/#interface-workspace">workspace</a></strong>: Select the download option at the main menu.
<figure><img src="/img/import-export/export-card.webp" alt="Export penpot file" /></figure>
</li>
<li>
<strong>From the <a href="/user-guide/the-interface/#interface-dashboard">dashboard</a></strong>: Select the download option at the file card menu.
<figure><img src="/img/import-export/export-menu.webp" alt="Export penpot file" /></figure>
</li>
</ul>
<p>
<strong>From the <a href="/user-guide/the-interface/#interface-dashboard">dashboard</a></strong>: Select the download option at the file card menu.
<figure><img src="/img/import-export/export-card.webp" alt="Export penpot file" /></figure>
</p>
<p>
<strong>From the <a href="/user-guide/the-interface/#interface-workspace">workspace</a></strong>: Select the download option at the main menu.
<figure><img src="/img/import-export/export-menu.webp" alt="Export penpot file" /></figure>
</p>
<h4>Export multiple files</h4>
<p>Select multiple files to export them at the same time. An overlay will show you the progress of the different exports.</p>
<figure>
<video title="Export multiple files" muted="" playsinline="" controls="" width="100%" poster="/img/import-export/export-multiple.webp" height="auto">
<video title="Export multiple files" muted="" playsinline="" controls="" width="auto" poster="/img/import-export/export-multiple.webp" height="auto">
<source src="/img/import-export/export-multiple.mp4" type="video/mp4">
</video>
</figure>
@@ -63,4 +43,27 @@ title: 14· Import/export files
<p>The import option is at the projects menu. Press “Import files” and then select one or more .penpot files to import. You can import a .zip file as well.</p>
<figure><img src="/img/import-export/import-menu.webp" alt="Import penpot file" /></figure>
<p>Right before importing the files to your project, youll still have the opportunity to review the items to be imported, have the information about the ones that can not be imported and also the chance to discard files.</p>
<figure><img src="/img/import-export/import-selection.webp" alt="Import penpot file" /></figure
<figure><img src="/img/import-export/import-selection.webp" alt="Import penpot file" /></figure>
<h2 id="penpot-formats">Penpot file format</h2>
<p>Penpot export to a unique format that streamline the import and export of files and assets by being more efficient and interoperable.</p>
<p>Unlike other design tools, <strong>Penpot's format is built on standard languages</strong>. The exported file is essentially a ZIP archive containing binary assets (such as bitmap and vector images) alongside a readable JSON structure. By avoiding proprietary formats, Penpot empowers users with autonomy from specific tools while enabling seamless third-party integrations.</p>
<h3>Deprecated Penpot file formats</h3>
<p class="advice">These formats can only be exported from version 2.3 or earlier versions, but can be imported to any Penpot version</p>
<p>There are two different deprecated Penpot file formats in which you can import/export Penpot files. A standard one and a binary one. You always have the chance to use both for any file.</p>
<h4>[Deprecated] Penpot file (.penpot).</h4>
<p>The fast one. Binary Penpot specific.</p>
<ul>
<li>✅ Highly efficient in terms of memory and transfer time when exporting and importing.</li>
<li>❌ It can be opened only in Penpot.</li>
<li>❌ Not transparent, code difficult to explore.</li>
</ul>
<h4>[Deprecated] Standard file (.zip).</h4>
<p>The open one. A compressed file that includes SVG and JSON.</p>
<ul>
<li>✅ Allows the file to be opened by other softwares (still, for those cases export to SVG seems to be the common practice).</li>
<li>✅ Allows some automations and integrations.</li>
<li>✅ Is a transparent, existing, open standard format.</li>
<li>❌ Highly inefficient in terms of memory and transfer time when exporting and importing (this is because SVG).</li>
</ul>

View File

@@ -424,11 +424,6 @@ title: Shortcuts
<td style="text-align: center;"><kbd>Alt</kbd><kbd>P</kbd></td>
<td style="text-align: center;"><kbd>⌥</kbd><kbd>P</kbd></td>
</tr>
<tr>
<td>History</td>
<td style="text-align: center;"><kbd>Alt</kbd><kbd>H</kbd></td>
<td style="text-align: center;"><kbd>⌥</kbd><kbd>H</kbd></td>
</tr>
<tr>
<td>Layers</td>
<td style="text-align: center;"><kbd>Alt</kbd><kbd>L</kbd></td>

View File

@@ -124,10 +124,10 @@ title: 06· Styling
<li><strong>Square</strong> - Adds a rectangular ending to the end of the path.</li>
</ul>
<h2 id="radius">Corner radius</h2>
<p>You can set values for corner radius to rectangles and images. Theres also the option to edit each corner individually.</p>
<h2 id="radius">Border radius</h2>
<p>You can customize the border radius of rectangles and images, with the option to customize each corner individually.</p>
<figure>
<video title="Corner radius" muted="" playsinline="" controls="" width="100%" poster="/img/styling/corners.webp" height="auto">
<video title="Border radius" muted="" playsinline="" controls="" width="100%" poster="/img/styling/corners.webp" height="auto">
<source src="/img/styling/corners.mp4" type="video/mp4">
</video>
</figure>
@@ -155,4 +155,30 @@ title: 06· Styling
<video title="Apply blur to a layer" muted="" playsinline="" controls="" width="100%" poster="/img/styling/blur.webp" height="auto">
<source src="/img/styling/blur.mp4" type="video/mp4">
</video>
</figure>
</figure>
<h2 id="blend">Opacity and blend</h2>
<p>Set the overal opacity for layers and their blend mode.</p>
<p>Blend allows you to control how a layer interacts with the layers beneath it, determining how pixels from the current layer are combined with pixels in the underlying layers. Use blend to achive various effects, such as shading, highlights, or creative visual styles.</p>
<figure>
<img alt="Layer blend and opacity" src="/img/styling/blend-opacity.webp"/>
</figure>
<p>Blend options available:</p>
<ul>
<li><strong>Normal</strong></li>
<li><strong>Darken</strong></li>
<li><strong>Multiply</strong></li>
<li><strong>Color burn</strong></li>
<li><strong>Lighten</strong></li>
<li><strong>Screen</strong></li>
<li><strong>Color dodge</strong></li>
<li><strong>Overlay</strong></li>
<li><strong>Soft light</strong></li>
<li><strong>Hard light</strong></li>
<li><strong>Difference</strong></li>
<li><strong>Exclusion</strong></li>
<li><strong>Hue</strong></li>
<li><strong>Saturation</strong></li>
<li><strong>Color</strong></li>
<li><strong>Luminosity</strong></li>
</ul>

View File

@@ -36,9 +36,10 @@ member is allowed to do depends on their permissions.</p>
<h3>Team roles</h3>
<p>These are the team roles currently available at Penpot:</p>
<ul>
<li><strong>Owner:</strong> There's only one owner per team, the role is automatically assigned to the team creator. Owners have permissions to change every other member role, including transfering ownership. Owners can update team settings, invite members and delete teams.</strong></li>
<li><strong>Admin:</strong> Permissions to change every other member role except owners. Can invite members and update team settings.</strong></li>
<li><strong>Editor:</strong> Without permissions to change member roles, invite members or update team settings.</strong></li>
<li><strong>Viewer:</strong> Viewers can view, comment on and inspect files but will not be able to edit them, nor do they have permissions to manage team settings.</strong></li>
<li><strong>Editor:</strong> Editors can create, import, edit and manage files and libraries, but do not have permissions to manage team settings.</strong></li>
<li><strong>Admin:</strong> Admins have the same permissions as editors, with the added ability to change every other member's role except owners. They can invite members and update team settings.</strong></li>
<li><strong>Owner:</strong> There's only one owner per team, the role is automatically assigned to the team creator. Owners have all the permissions of admins, with the additional ability to change any member's role, including transferring ownership. Owners can update team settings, invite members and delete teams.</strong></li>
</ul>
<figure><img src="/img/teams/teams-permissions.webp" alt="Team members" /></figure>
<p class="advice">More team roles will be eventually available, as well as fine grained permissions management to control members access and actions.</p>

View File

@@ -199,26 +199,58 @@ geometric structure. In Penpot there are three types of guides:
<img src="/img/workspace-basics/shortcuts.webp" alt="Shortcuts panel" />
</figure>
<h2 id="history">History</h2>
<p>The history panel keeps track of the latest changes on an opened file.</p>
<h2 id="history">File history versions</h2>
<p>The history panel keeps track of the latest changes on an opened file as well as the different versions of the file, making it easier to track changes, revert to previous states and collaborate.</p>
<h4>View history</h4>
<p>To view the recent history of a file at the workspace press <kbd>Ctrl/⌘</kbd> + <kbd>H</kbd> or click at the history icon on the toolbar at the left.</p>
<p>At the history you can see items with information about the last changes. At first sight you have object type (rectangle, text, image...) and type of change (New, Modified, Deleted...). If you press the item further details are shown.</p>
<h3>View history</h3>
<p>To view the recent history of a file at the workspace click the history icon on the navbar at the left:</p>
<ul>
<li>To see the history of file versions go to the <strong>History</strong> tab.</li>
<li>To see the history of item changes go to the <strong>Actions</strong> tab.</li>
</ul>
<figure>
<img src="/img/workspace-basics/history.webp" alt="History panel" />
<img src="/img/workspace-basics/history-view.webp" alt="History versions button" />
</figure>
<p><strong>Note:</strong> History panel is still in a very early state and shows only a limited list of changes at a current browser tab session. Refreshing the browser means refreshing the History as well. Eventually, Penpot will have a proper version history capacity.</p>
<h4>Navigate history</h4>
<p>To navigate through the history press <kbd>Ctrl/⌘</kbd> + <kbd>Z</kbd> to go backwards and <kbd>Ctrl/⌘</kbd> + <kbd>Shift/⇧</kbd> + <kbd>Z</kbd> to go forward.</p>
<p>You can also press any item of the history list to get to this specific state.</p>
<h3>History panel</h3>
<p>At the History panel, you can save the current version of your file, as well as access previous versions for up to 7 days.</p>
<h4>Restore versions</h4>
<p>All saved versions of the file—whether manually saved, autosaved, or pinned—can be restored, reverting the file back to its state at the selected time.</p>
<figure>
<video title="Navigate history" muted="" playsinline="" controls="" width="auto" poster="/img/workspace-basics/history-navigate.webp" height="auto">
<source src="/img/workspace-basics/history-navigate.mp4" type="video/mp4">
</video>
<img src="/img/workspace-basics/history-restore.webp" alt="Restore versions" />
</figure>
<h4>Saved versions</h4>
<p>You can save the current version of your file by clicking the pin icon at the History tab. This will allow the version to be named and it will add it to your list of versions.</p>
<figure>
<img src="/img/workspace-basics/history-save.webp" alt="Saved versions" />
</figure>
<h4>Autosaved versions</h4>
<p>When you start working on a file, Penpot will start to automatically save versions of that file across time so that you can later restore them as needed.</p>
<p>In the History tab, if you click on the autosaved versions, youll see a list of the exact date and time when the version was automatically saved.</p>
<figure>
<img src="/img/workspace-basics/history-autosaved.webp" alt="Autosaved versions" />
</figure>
<h4>Pinned versions</h4>
<p>File versions can also be pinned. Pinning a file version will allow you to name it, making it easier to access at the History tab. Pinned file versions will be saved forever and can be renamed, restored or deleted at any time.</p>
<figure>
<img src="/img/workspace-basics/history-pin.webp" alt="Pin versions" />
</figure>
<h3>Actions panel</h3>
<p>At the Actions panel, you have the object type (rectangle, text, image...) and type of change (New, Modified, Deleted...). If you press the item, it will be reverted to its state before that specific action was performed.</p>
<figure>
<img src="/img/workspace-basics/history-actions.webp" alt="Actions panel" />
</figure>
<p class="advice">The Actions panel shows only a limited list of changes at a current browser tab session. Refreshing the browser means refreshing the history of actions as well.</p>
<h4>Navigate actions</h4>
<p>To navigate through the actions press <kbd>Ctrl/⌘</kbd> + <kbd>Z</kbd> to go backwards and <kbd>Ctrl/⌘</kbd> + <kbd>Shift/⇧</kbd> + <kbd>Z</kbd> to go forward.</p>
<p>You can also press any item of the actions list to get to this specific state.</p>
<h2 id="comments">Comments</h2>
<p>Comments allow the team to have one priceless conversation getting and providing feedback right over the designs and prototypes.<p>

View File

@@ -43,7 +43,9 @@
{:extra-paths ["dev"]
:extra-deps
{thheller/shadow-cljs {:mvn/version "2.28.18"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
cider/cider-nrepl {:mvn/version "0.48.0"}}}
:shadow-cljs

34
frontend/dev/user.clj Normal file
View File

@@ -0,0 +1,34 @@
;; 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 user
(:require
[app.common.data :as d]
[app.common.pprint :as pp]
[clojure.java.io :as io]
[clojure.tools.namespace.repl :as repl]
[clojure.pprint :refer [pprint print-table]]
[clojure.repl :refer :all]
[clojure.walk :refer [macroexpand-all]]
[criterium.core :as crit]))
;; --- Benchmarking Tools
(defmacro run-quick-bench
[& exprs]
`(crit/with-progress-reporting (crit/quick-bench (do ~@exprs) :verbose)))
(defmacro run-quick-bench'
[& exprs]
`(crit/quick-bench (do ~@exprs)))
(defmacro run-bench
[& exprs]
`(crit/with-progress-reporting (crit/bench (do ~@exprs) :verbose)))
(defmacro run-bench'
[& exprs]
`(crit/bench (do ~@exprs)))

3392
frontend/externs/main.txt Normal file
View File

File diff suppressed because it is too large Load Diff

1
frontend/externs/worker.txt Symbolic link
View File

@@ -0,0 +1 @@
main.txt

View File

@@ -0,0 +1,30 @@
[
{
"~:is-admin": true,
"~:email": "bar@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Han Solo",
"~:fullname": "Han Solo",
"~:is-owner": true,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:profile-id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:created-at": "~m1733324626956"
},
{
"~:is-admin": true,
"~:email": "foo@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Princesa Leia",
"~:fullname": "Princesa Leia",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:profile-id": "~uf56647eb-19a7-8115-8003-b6bc939ecd1b",
"~:created-at": "~m1713533116365"
}
]

View File

@@ -35,7 +35,7 @@
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-owner": false,
"~:is-admin": true,
"~:can-edit": true
},

View File

@@ -52,3 +52,20 @@ test("Lists files in the drafts page", async ({ page }) => {
dashboardPage.page.getByRole("button", { name: /New File 2/ }),
).toBeVisible();
});
test("Bug 9443, Admin can not demote owner", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await DashboardPage.mockRPC(
page,
"get-team-members?team-id=*",
"dashboard/get-team-members-admin.json",
);
await dashboardPage.goToSecondTeamMembersSection();
await expect(page.getByRole("heading", { name: "Members" })).toBeVisible();
await expect(page.getByRole("combobox", { name: "Admin" })).toBeVisible();
await expect(page.getByText("Owner")).toBeVisible();
await expect(page.getByRole("combobox", { name: "Owner" })).toHaveCount(0);
});

View File

@@ -30,7 +30,7 @@ test("Save and restore version", async ({ page }) => {
"workspace/versions-snapshot-1.json",
);
await page.getByLabel("History (Alt+H)").click();
await page.getByLabel("History").click();
await workspacePage.mockRPC(
"create-file-snapshot",
@@ -58,10 +58,11 @@ test("Save and restore version", async ({ page }) => {
await page.getByRole("textbox").press("Enter");
await page
.locator("li")
.filter({ hasText: "INIT" })
.getByRole("button")
.click();
.getByLabel("History", { exact: true })
.locator("div")
.nth(3)
.hover();
await page.getByRole("button", { name: "Open version menu" }).click();
await page.getByRole("button", { name: "Restore" }).click();
await workspacePage.mockRPC(
@@ -70,4 +71,7 @@ test("Save and restore version", async ({ page }) => {
);
await page.getByRole("button", { name: "Restore" }).click();
// check that the history panel is closed after restore
await expect(page.getByRole("tab", { name: "design" })).toBeVisible();
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
<path d="M4.88 1.5H2.945c-.798 0-1.445.647-1.445 1.445v1.74M4.88 14.5H2.945c-.798 0-1.445-.842-1.445-1.64v-1.74m13-6.24V2.945c0-.798-.647-1.445-1.445-1.445H11.12m3.38 9.62v1.935c0 .798-.647 1.445-1.445 1.445H11.12"/>
</svg>
<path d="M3.5 3.5h-2m2 0v-2m0 2h9m-9 0v9m9-9v-2m0 2h2m-2 0v9m0 0h2m-2 0v2m0-2h-9m0 0v2m0-2h-2"/>
</svg>

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 214 B

View File

@@ -41,13 +41,6 @@ body {
scrollbar-width: thin;
}
// Firefox-only hack
@-moz-document url-prefix() {
* {
scrollbar-width: auto;
}
}
img {
height: auto;
width: 100%;

View File

@@ -237,13 +237,18 @@ async function renderTemplate(path, context = {}, partials = {}) {
return mustache.render(content, context, partials);
}
const renderer = {
link(href, title, text) {
return `<a href="${href}" target="_blank">${text}</a>`;
const extension = {
useNewRenderer: true,
renderer: {
link(token) {
const href = token.href;
const text = token.text;
return `<a href="${href}" target="_blank">${text}</a>`;
},
},
};
marked.use({ renderer });
marked.use(extension);
async function readTranslations() {
const langs = [
@@ -503,6 +508,7 @@ export async function compileStyles() {
const start = process.hrtime();
log.info("init: compile styles");
let result = await compileSassAll(worker);
result = concatSass(result);

6
frontend/scripts/repl Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow";
set -ex
exec clojure $OPTIONS -M -m rebel-readline.main

View File

@@ -24,15 +24,22 @@ async function compileSassAll() {
async function compileSass(path) {
const start = process.hrtime();
log.info("changed:", path);
const result = await h.compileSass(worker, path, { modules: true });
sass.index[result.outputPath] = result.css;
const output = h.concatSass(sass);
try {
const result = await h.compileSass(worker, path, { modules: true });
sass.index[result.outputPath] = result.css;
await fs.writeFile("./resources/public/css/main.css", output);
const output = h.concatSass(sass);
const end = process.hrtime(start);
log.info("done:", `(${ppt(end)})`);
await fs.writeFile("./resources/public/css/main.css", output);
const end = process.hrtime(start);
log.info("done:", `(${ppt(end)})`);
} catch (cause) {
console.error(cause);
const end = process.hrtime(start);
log.error("error:", `(${ppt(end)})`);
}
}
await fs.mkdir("./resources/public/css/", { recursive: true });

View File

@@ -143,6 +143,16 @@
(let [f (obj/get global "externalSessionId")]
(when (fn? f) (f))))
(defn external-context-info
[]
(let [f (obj/get global "externalContextInfo")]
(when (fn? f) (f))))
(defn initialize-external-context-info
[]
(let [f (obj/get global "initializeExternalConfigInfo")]
(when (fn? f) (f))))
;; --- Helper Functions
(defn ^boolean check-browser? [candidate]

View File

@@ -30,20 +30,21 @@
[app.util.i18n :as i18n]
[app.util.theme :as theme]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[debug]
[features]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(log/setup! {:app :info})
(log/set-level! :debug)
(when (= :browser cf/target)
(log/info :version (:full cf/version)
:asserts *assert*
:build-date cf/build-date
:public-uri (dm/str cf/public-uri))
(log/info :flags (str/join "," (map name cf/flags))))
(log/inf :version (:full cf/version)
:asserts *assert*
:build-date cf/build-date
:public-uri (dm/str cf/public-uri))
(doseq [flag cf/flags]
(log/dbg :hint "flag enabled" :flag (name flag))))
(declare reinit)

View File

@@ -195,13 +195,16 @@
ptk/WatchEvent
(watch [_ state _]
(let [share-id (-> state :viewer-local :share-id)]
(->> (rp/cmd! :update-comment-thread {:id id :is-resolved is-resolved :share-id share-id})
(rx/catch (fn [{:keys [type code] :as cause}]
(if (and (= type :restriction)
(= code :max-quote-reached))
(rx/throw cause)
(rx/throw {:type :comment-error}))))
(rx/ignore))))))
(rx/concat
(when is-resolved (rx/of
(ptk/event ::ev/event {::ev/name "resolve-comment-thread" :thread-id id})))
(->> (rp/cmd! :update-comment-thread {:id id :is-resolved is-resolved :share-id share-id})
(rx/catch (fn [{:keys [type code] :as cause}]
(if (and (= type :restriction)
(= code :max-quote-reached))
(rx/throw cause)
(rx/throw {:type :comment-error}))))
(rx/ignore)))))))
(defn add-comment
[thread content]

View File

@@ -12,7 +12,6 @@
[app.common.schema :as sm]
[app.common.types.components-list :as ctkl]
[app.common.types.team :as ctt]
[app.config :as cf]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.features :as features]
@@ -75,15 +74,13 @@
(watch [_ _ _]
(case code
:upgrade-version
(when (or (not= (:version params) (:full cf/version))
(true? (:force params)))
(rx/of (ntf/dialog
:content (tr "notifications.by-code.upgrade-version")
:controls :inline-actions
:type :inline
:level level
:actions [{:label "Refresh" :callback force-reload!}]
:tag :notification)))
(rx/of (ntf/dialog
:content (tr "notifications.by-code.upgrade-version")
:controls :inline-actions
:type :inline
:level level
:actions [{:label "Refresh" :callback force-reload!}]
:tag :notification))
:maintenance
(rx/of (ntf/dialog
@@ -227,6 +224,11 @@
[{:keys [team-id team-name change]}]
(dm/assert! (uuid? team-id))
(ptk/reify ::team-membership-change
ptk/UpdateEvent
(update [_ state]
;; FIXME: Remove on 2.5
(assoc state :current-team-id (dm/get-in state [:profile :default-team-id])))
ptk/WatchEvent
(watch [_ state _]
(when (= :removed change)

View File

@@ -450,7 +450,9 @@
(ptk/reify ::update-team
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:teams id :name] name))
(-> state
(assoc-in [:teams id :name] name)
(assoc-in [:team :name] name)))
ptk/WatchEvent
(watch [_ _ _]
@@ -1120,6 +1122,7 @@
(ptk/reify ::go-to-projects-1
ptk/UpdateEvent
(update [_ state]
;; FIXME: Remove on 2.5
(assoc state :current-team-id team-id))
ptk/WatchEvent
(watch [_ _ _]

View File

@@ -8,6 +8,7 @@
(:require
["ua-parser-js" :as ua]
[app.common.data :as d]
[app.common.json :as json]
[app.common.logging :as l]
[app.config :as cf]
[app.main.repo :as rp]
@@ -93,6 +94,11 @@
data
data))
(defn add-external-context-info
[context]
(let [external-context-info (json/->clj (cf/external-context-info))]
(merge context external-context-info)))
(defn- process-event-by-proto
[event]
(let [data (d/deep-merge (-data event) (meta event))
@@ -102,6 +108,7 @@
(assoc :event-origin (::origin data))
(assoc :event-namespace (namespace type))
(assoc :event-symbol ev-name)
(add-external-context-info)
(d/without-nils))
props (-> data d/without-qualified simplify-props)]
@@ -119,6 +126,7 @@
(let [type (::type data "action")
context (-> (::context data)
(assoc :event-origin (::origin data))
(add-external-context-info)
(d/without-nils))
props (-> data d/without-qualified simplify-props)]
{:type type

View File

@@ -7,6 +7,7 @@
(ns app.main.data.exports.assets
(:require
[app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.modal :as modal]
[app.main.data.persistence :as dwp]
[app.main.data.workspace.state-helpers :as wsh]
@@ -247,6 +248,12 @@
(rx/map #(clear-export-state @resource-id))
(rx/take-until (rx/delay 6000 stopper))))))))
(defn request-export
[{:keys [exports] :as params}]
(if (= 1 (count exports))
(request-simple-export (assoc params :export (first exports)))
(request-multiple-export params)))
(defn retry-last-export
[]
(ptk/reify ::retry-last-export
@@ -256,3 +263,16 @@
(when (seq params)
(rx/of (request-multiple-export params)))))))
(defn export-shapes-event
[exports origin]
(let [types (reduce (fn [counts {:keys [type]}]
(if (#{:png :pdf :svg :jpeg} type)
(update counts type inc)
counts))
{:png 0, :pdf 0, :svg 0, :jpeg 0}
exports)]
(ptk/event
::ev/event (merge types
{::ev/name "export-shapes"
::ev/origin origin
:num-shapes (count exports)}))))

View File

@@ -207,6 +207,7 @@
ptk/WatchEvent
(watch [_ _ _]
(when (is-authenticated? profile)
(cf/initialize-external-context-info)
(->> (rx/concat
(rx/of (profile-fetched profile)
(fetch-teams)

View File

@@ -65,6 +65,7 @@
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.thumbnails :as dwth]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
@@ -83,6 +84,7 @@
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.storage :as storage]
[app.util.text.content :as tc]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
@@ -199,18 +201,21 @@
;; Load libraries
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat identity)
(rx/merge-map
(fn [{:keys [id synced-at]}]
(->> (rp/cmd! :get-file {:id id :features features})
(rx/map #(assoc % :synced-at synced-at)))))
(rx/merge-map fpmap/resolve-file)
(rx/merge-map
(fn [{:keys [id] :as file}]
(->> (rp/cmd! :get-file-object-thumbnails {:file-id id :tag "component"})
(rx/map #(assoc file :thumbnails %)))))
(rx/reduce conj [])
(rx/map libraries-fetched)))
(rx/mapcat (fn [libraries]
(rx/merge
(->> (rx/from libraries)
(rx/merge-map
(fn [{:keys [id synced-at]}]
(->> (rp/cmd! :get-file {:id id :features features})
(rx/map #(assoc % :synced-at synced-at)))))
(rx/merge-map fpmap/resolve-file)
(rx/reduce conj [])
(rx/map libraries-fetched))
(->> (rx/from libraries)
(rx/map :id)
(rx/mapcat (fn [file-id]
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
(rx/map dwl/library-thumbnails-fetched)))))))
(rx/of (with-meta (workspace-initialized)
{:file-id file-id})))
@@ -1636,6 +1641,7 @@
(rx/ignore))))))))))
(declare ^:private paste-transit)
(declare ^:private paste-html-text)
(declare ^:private paste-text)
(declare ^:private paste-image)
(declare ^:private paste-svg-text)
@@ -1703,6 +1709,7 @@
(let [pdata (wapi/read-from-paste-event event)
image-data (some-> pdata wapi/extract-images)
text-data (some-> pdata wapi/extract-text)
html-data (some-> pdata wapi/extract-html-text)
transit-data (ex/ignoring (some-> text-data t/decode-str))]
(cond
(and (string? text-data) (re-find #"<svg\s" text-data))
@@ -1715,6 +1722,9 @@
(coll? transit-data)
(rx/of (paste-transit (assoc transit-data :in-viewport in-viewport?)))
(string? html-data)
(rx/of (paste-html-text html-data text-data))
(string? text-data)
(rx/of (paste-text text-data))
@@ -2013,6 +2023,8 @@
(map :id)
(pcb/resize-parents changes))
orig-shapes (map (d/getf all-objects) selected)
selected (into (d/ordered-set)
(comp
(filter add-obj?)
@@ -2025,13 +2037,22 @@
(some? drop-cell)
(pcb/update-shapes [parent-id]
#(ctl/add-children-to-cell % selected all-objects drop-cell)))
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dws/select-shapes selected)
(ptk/data-event :layout/update {:ids [frame-id]})
(dwu/commit-undo-transaction undo-id)))))))
(rx/concat
(->> (filter ctk/instance-head? orig-shapes)
(map (fn [{:keys [component-file]}]
(ptk/event ::ev/event
{::ev/name "use-library-component"
::ev/origin "paste"
:external-library (not= file-id component-file)})))
(rx/from))
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dws/select-shapes selected)
(ptk/data-event :layout/update {:ids [frame-id]})
(dwu/commit-undo-transaction undo-id))))))))
(defn as-content [text]
(let [paragraphs (->> (str/lines text)
@@ -2056,6 +2077,34 @@
:else
(deref ms/mouse-position)))
(defn- paste-html-text
[html text]
(dm/assert! (string? html))
(ptk/reify ::paste-html-text
ptk/WatchEvent
(watch [_ state _]
(let [root (dwtxt/create-root-from-html html)
content (tc/dom->cljs root)
id (uuid/next)
width (max 8 (min (* 7 (count text)) 700))
height 16
{:keys [x y]} (calculate-paste-position state)
shape {:id id
:type :text
:name (txt/generate-shape-name text)
:x x
:y y
:width width
:height height
:grow-type (if (> (count text) 100) :auto-height :auto-width)
:content content}
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/create-and-add-shape :text x y shape)
(dwu/commit-undo-transaction undo-id))))))
(defn- paste-text
[text]
(dm/assert! (string? text))

View File

@@ -17,6 +17,7 @@
[app.common.types.shape.interactions :as ctsi]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.events :as ev]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
@@ -168,7 +169,8 @@
objects (get page :objects)
frame (cfh/get-root-frame objects (:id shape))
flows (get page :objects)
first? (not-any? #(seq (:interactions %)) (vals objects))
flows (get page :flows)
flow (ctp/get-frame-flow flows (:id frame))]
(rx/concat
(rx/of (dwsh/update-shapes
@@ -177,14 +179,17 @@
(let [new-interaction (-> ctsi/default-interaction
(ctsi/set-destination destination)
(assoc :position-relative-to (:id shape)))]
(cls/add-new-interaction shape new-interaction))))
(cls/add-new-interaction shape new-interaction)))))
(when destination
(dwsh/update-shapes [destination] cls/show-in-viewer))
(when destination
(rx/of (dwsh/update-shapes [destination] cls/show-in-viewer)))
(when (and (not (connected-frame? objects (:id frame)))
(nil? flow))
(add-flow (:id frame))))))))))
(when (and (not (connected-frame? objects (:id frame)))
(nil? flow))
(rx/of (add-flow (:id frame))))
(when first?
;; When the first interaction of the page is created we emit the event "create-prototype"
(rx/of (ptk/event ::ev/event {::ev/name "create-prototype"})))))))))
(defn remove-interaction
([shape index]

View File

@@ -191,6 +191,25 @@
(watch [it state _]
(update-color* it state color file-id)))))
(defn update-color-data
"Update color data without affecting the path location"
[color file-id]
(let [color (d/without-nils color)]
(dm/assert!
"expected valid color data structure"
(ctc/check-color! color))
(dm/assert!
"expected file-id"
(uuid? file-id))
(ptk/reify ::update-color-data
ptk/WatchEvent
(watch [it state _]
(let [color (assoc color :name (dm/str (:path color) "/" (:name color)))]
(update-color* it state color file-id))))))
(defn rename-color
[file-id id new-name]
(dm/assert!
@@ -586,7 +605,7 @@
in the given file library. Then selects the newly created instance."
([file-id component-id position]
(instantiate-component file-id component-id position nil))
([file-id component-id position {:keys [start-move? initial-point id-ref]}]
([file-id component-id position {:keys [start-move? initial-point id-ref origin]}]
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? component-id))
(dm/assert! (gpt/point? position))
@@ -600,6 +619,8 @@
changes (-> (pcb/empty-changes it (:id page))
(pcb/with-objects objects))
current-file-id (:current-file-id state)
[new-shape changes]
(cll/generate-instantiate-component changes
objects
@@ -608,12 +629,18 @@
position
page
libraries)
undo-id (js/Symbol)]
(when id-ref
(reset! id-ref (:id new-shape)))
(rx/of (dwu/start-undo-transaction undo-id)
(rx/of (ptk/event
::ev/event
{::ev/name "use-library-component"
::ev/origin origin
:external-library (not= file-id current-file-id)})
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids [(:id new-shape)]})
(dws/select-shapes (d/ordered-set (:id new-shape)))
@@ -881,11 +908,15 @@
(rx/of
(dwu/start-undo-transaction undo-id)
(update-component shape-id undo-group)
(sync-file current-file-id file-id :components (:component-id shape) undo-group)
;; These two calls are necessary for properly sync thumbnails
;; when a main component does not live in the same page
(update-component-thumbnail-sync state component-id file-id "frame")
(update-component-thumbnail-sync state component-id file-id "component")
(sync-file current-file-id file-id :components component-id undo-group)
(when (not current-file?)
(sync-file file-id file-id :components (:component-id shape) undo-group))
(sync-file file-id file-id :components component-id undo-group))
(dwu/commit-undo-transaction undo-id)))))))
(defn launch-component-sync
@@ -937,9 +968,9 @@
;; in the grid creating new rows/columns to make space
(let [file (wsh/get-file state file-id)
libraries (wsh/get-libraries state)
page (wsh/lookup-page state)
objects (wsh/lookup-page-objects state)
parent (get objects (:parent-id shape))
page (wsh/lookup-page state)
objects (wsh/lookup-page-objects state)
parent (get objects (:parent-id shape))
;; If the target parent is a grid layout we need to pass the target cell
target-cell (when (ctl/grid-layout? parent)

View File

@@ -456,22 +456,30 @@
id-duplicated (first new-ids)
frames (into #{}
(map #(get-in objects [% :frame-id]))
ids)
undo-id (js/Symbol)]
frames (into #{}
(map #(get-in objects [% :frame-id]))
ids)
undo-id (js/Symbol)]
(rx/concat
(->> (map (d/getf objects) ids)
(filter ctk/instance-head?)
(map (fn [{:keys [component-file]}]
(ptk/event ::ev/event
{::ev/name "use-library-component"
::ev/origin "duplicate"
:external-library (not= file-id component-file)})))
(rx/from))
;; Warning: This order is important for the focus mode.
(->> (rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(when change-selection?
(select-shapes new-ids))
(ptk/data-event :layout/update {:ids frames})
(memorize-duplicated id-original id-duplicated)
(dwu/commit-undo-transaction undo-id))
(rx/tap #(when (some? return-ref)
(reset! return-ref id-duplicated))))))))))
(->> (rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(when change-selection?
(select-shapes new-ids))
(ptk/data-event :layout/update {:ids frames})
(memorize-duplicated id-original id-duplicated)
(dwu/commit-undo-transaction undo-id))
(rx/tap #(when (some? return-ref)
(reset! return-ref id-duplicated)))))))))))
(defn duplicate-selected
([move-delta?]

View File

@@ -447,11 +447,6 @@
:subsections [:panels]
:fn #(st/emit! (dw/go-to-layout :assets))}
:toggle-history {:tooltip (ds/alt "H")
:command (ds/a-mod "h")
:subsections [:panels]
:fn #(emit-when-no-readonly (dw/go-to-layout :document-history))}
:toggle-colorpalette {:tooltip (ds/alt "P")
:command (ds/a-mod "p")
:subsections [:panels]

View File

@@ -37,6 +37,8 @@
;; -- V2 Editor Helpers
(def ^function create-root-from-string editor.v2/createRootFromString)
(def ^function create-root-from-html editor.v2/createRootFromHTML)
(def ^function create-editor editor.v2/create)
(def ^function set-editor-root! editor.v2/setRoot)
(def ^function get-editor-root editor.v2/getRoot)

View File

@@ -52,6 +52,11 @@
(defonce queue
(q/create find-request (/ 1000 30)))
(defn clear-queue!
[]
(l/dbg :hint "clearing thumbnail queue")
(q/clear! queue))
;; This function first renders the HTML calling `render/render-frame` that
;; returns HTML as a string, then we send that data to the iframe rasterizer
;; that returns the image as a Blob. Finally we create a URI for that blob.
@@ -168,7 +173,7 @@
(.error js/console cause)
(rx/empty)))
(rx/tap #(l/trc :hint "thumbnail updated" :elapsed (dm/str (tp) "ms")))
(rx/tap #(l/dbg :hint "thumbnail updated" :elapsed (dm/str (tp) "ms")))
;; We cancel all the stream if user starts editing while
;; thumbnail is generating

View File

@@ -11,6 +11,7 @@
[app.main.data.events :as ev]
[app.main.data.persistence :as dwp]
[app.main.data.workspace :as dw]
[app.main.data.workspace.thumbnails :as th]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.util.time :as dt]
@@ -126,11 +127,13 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(rx/of ::dwp/force-persist)
(rx/of ::dwp/force-persist
(dw/remove-layout-flag :document-history))
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/tap #(th/clear-queue!))
(rx/map #(dw/initialize-file project-id file-id)))
(case origin
:version

View File

@@ -13,6 +13,7 @@
[app.common.types.shape-tree :as ctt]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.store :as st]
[app.main.ui.workspace.tokens.token-set :as wtts]
@@ -582,7 +583,8 @@
[object-id]
(l/derived
(fn [state]
(dm/get-in state [:workspace-thumbnails object-id]))
(some-> (dm/get-in state [:workspace-thumbnails object-id])
(cf/resolve-media)))
st/state))
(def workspace-text-modifier

View File

@@ -99,7 +99,7 @@
[{:keys [shape]}]
(let [thumbnails? (mf/use-ctx muc/render-thumbnails)
childs (mapv (d/getf objects) (:shapes shape))]
(if (and thumbnails? (some? (:thumbnail shape)))
(if (and thumbnails? (some? (:thumbnail-id shape)))
[:& frame/frame-thumbnail {:shape shape :bounds (:children-bounds shape)}]
[:& frame-shape {:shape shape :childs childs}])))))
@@ -338,7 +338,7 @@
;; used to render thumbnails on assets panel.
(mf/defc component-svg
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
[{:keys [objects root-shape show-grids? zoom class] :or {zoom 1} :as props}]
[{:keys [objects root-shape show-grids? is-hidden zoom class] :or {zoom 1} :as props}]
(when root-shape
(let [root-shape-id (:id root-shape)
include-metadata (mf/use-ctx export/include-metadata-ctx)
@@ -381,13 +381,14 @@
:xmlns:penpot (when include-metadata "https://penpot.app/xmlns")
:fill "none"}
[:*
[:> shape-container {:shape root-shape'}
[:& (mf/provider muc/is-component?) {:value true}
[:& root-shape-wrapper {:shape root-shape' :view-box vbox}]]]
(when-not is-hidden
[:*
[:> shape-container {:shape root-shape'}
[:& (mf/provider muc/is-component?) {:value true}
[:& root-shape-wrapper {:shape root-shape' :view-box vbox}]]]
(when show-grids?
[:& empty-grids {:root-shape-id root-shape-id :objects objects}])]])))
(when show-grids?
[:& empty-grids {:root-shape-id root-shape-id :objects objects}])])])))
(mf/defc component-svg-thumbnail
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}

View File

@@ -67,6 +67,7 @@
show-release-modal?
(and (contains? cf/flags :onboarding)
(not (contains? cf/flags :hide-release-modal))
(:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main cf/version))
(not= "0.0" (:main cf/version)))]
@@ -106,7 +107,7 @@
:dashboard-team-webhooks
:dashboard-team-settings)
[:?
#_[:& app.main.ui.releases/release-notes-modal {:version "2.3"}]
#_[:& app.main.ui.releases/release-notes-modal {:version "2.4"}]
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
#_[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]

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