Compare commits

..

162 Commits

Author SHA1 Message Date
Andrey Antukh
c2b13a6d5d 📚 Update changelog 2025-04-29 14:46:15 +02:00
Andrey Antukh
6935d54870 Merge branch 'main' into staging 2025-04-28 08:43:54 +02:00
Andrey Antukh
65e8526ee2 Merge tag '2.6.2-RC4' 2025-04-28 08:43:04 +02:00
Andrés Moya
202762027f 🐛 Handle swapped nested instances when detaching 2025-04-25 10:00:14 +02:00
Andrés Moya
d95551e651 🔧 Add debug traces to detach copy operation 2025-04-25 10:00:14 +02:00
Xaviju
c96fbfdcd6 📚 Update tokens changelog for 2.6.2 (#6364)
Co-authored-by: Xavier Julian <xaviju@proton.me>
2025-04-24 13:29:56 +02:00
Alejandro Alonso
6e5d64d403 Merge pull request #6362 from penpot/niwinz-staging-bugfixes-2
🐛 Add migration for fix old broken root shapes (file migration)
2025-04-24 09:28:04 +02:00
Andrey Antukh
3e0c2bf1a1 🐛 Add migration for fix root shape 2025-04-24 09:17:33 +02:00
Andrey Antukh
283cdee5d6 Ensure consistency on using d/update-vals on file migrations 2025-04-24 08:55:54 +02:00
Andrey Antukh
ab5e01e54a Ensure we don't leave :components with nil on file data
after aplying migrations
2025-04-24 08:53:30 +02:00
Alejandro Alonso
373248e304 Merge pull request #6360 from penpot/niwinz-staging-bugfixes-2
🐛 Fix issues on file data migration handling
2025-04-24 07:30:50 +02:00
Andrey Antukh
80308ceafa 🐛 Make http cache aware of missing file data migrations 2025-04-23 18:15:33 +02:00
Andrey Antukh
f65518f865 🐛 Fix incorrect migration application after binfile import 2025-04-23 18:10:52 +02:00
Alejandro Alonso
f155042958 Merge pull request #6345 from penpot/niwinz-staging-add-interaction-cleaning
🐛 Add migration for decoding and cleaning shape interactions
2025-04-23 08:08:31 +02:00
Andrey Antukh
1dd23a3f47 🐛 Invalidate http cache on apply migrations to file on read operation 2025-04-23 07:57:56 +02:00
Andrey Antukh
1194e40222 🐛 Properly dispose rx subscription on grid thumbnail component 2025-04-22 21:39:57 +02:00
Andrey Antukh
05fac41534 🐛 Remove feature checking from get-file-data-for-thumbnail rpc method
The prev code has feature resolution race condition and it
in reallity does not need that check.
2025-04-22 21:38:40 +02:00
Andrey Antukh
3f85e89f62 🐛 Send frontend version on worker http requests 2025-04-22 21:26:51 +02:00
Alonso Torres
ee0f8ad19a 🐛 Fix horizontal scroll in viewer (#6347) 2025-04-22 21:03:45 +02:00
Andrey Antukh
b7d7cf233a Fix shadow colors on import penpot files 2025-04-22 19:58:10 +02:00
Alonso Torres
bd208c31e2 🐛 Fix update layout on component restore (#6348) 2025-04-22 18:46:21 +02:00
Andrey Antukh
151dc352c8 Don't register shadow schema
It is not really necessary, we can use the
schema var directly.
2025-04-22 17:21:52 +02:00
Andrey Antukh
ccbf17106d 🐛 Add migration for decoding and cleaning shape interactions 2025-04-22 15:04:22 +02:00
Andrey Antukh
95c4d95fd3 📎 Use d/update-vals instead of update-vals on migrations 2025-04-22 15:01:33 +02:00
Andrey Antukh
a72c07b657 Merge pull request #6309 from penpot/niwinz-staging-bugfixes-2
🐛 Several bugfixes
2025-04-22 09:15:02 +02:00
Andrey Antukh
708492afeb 💄 Add mainly cosmetic changes to dashboard placeholder components 2025-04-17 09:20:35 +02:00
Andrey Antukh
1305ab3cc6 🐛 Fix issue with empty placeholder on team change 2025-04-17 09:20:34 +02:00
Andrey Antukh
29cc6b4f9c Print the current seed on test.check fail 2025-04-17 09:20:34 +02:00
Andrey Antukh
cc7f0b145c 🐛 Make shape interaction properly decode on binfile import 2025-04-17 09:20:34 +02:00
Andrey Antukh
e69c0c3e27 Make schema uuid parsing fns private 2025-04-17 09:20:34 +02:00
Andrey Antukh
a209966427 🐛 Don't use schema uuid parsing function on websocket ns 2025-04-17 09:20:34 +02:00
Andrey Antukh
d5abbd4220 📎 Add missing entries on the changelog 2025-04-17 09:20:32 +02:00
Pablo Alba
70a23a14c4 🐛 Fix allow moving a main component into another 2025-04-16 22:54:30 +02:00
Marina López
93c81ea49c 🐛 Fix pricing CTA to be under a config flag (#6304) 2025-04-16 17:17:47 +02:00
Alejandro Alonso
ddc41027ab Merge pull request #6316 from penpot/palba-fix-instanciate-component
🐛 Fix error while drag an drop a component to the canvas
2025-04-16 13:15:07 +02:00
Pablo Alba
4f931fbe6a 🐛 Fix error while drag an drop a component to the canvas 2025-04-16 13:05:56 +02:00
Andrey Antukh
b49a4734ff 🐛 Fix srepl helper for restore file snapshots 2025-04-15 11:03:50 +02:00
Alejandro Alonso
2aaa2f3033 🐛 Fix template import (#6299) 2025-04-15 10:39:22 +02:00
Alejandro Alonso
202b9f3075 Merge pull request #6284 from penpot/niwinz-staging-several-bugfixes
🐛 Several bugfixes and enhacements
2025-04-15 10:33:59 +02:00
Andrey Antukh
be0814cdac Improve internal error reporting 2025-04-14 13:26:12 +02:00
Andrey Antukh
80d719353c Make auth data available before request parsing
For properly report profile-id
2025-04-14 09:23:41 +02:00
Andrey Antukh
fa3fc12594 Sanitize uuid on the rest of code 2025-04-14 09:23:29 +02:00
Andrey Antukh
422a9db07b Sanitize uuid parsing on legacy zip import code 2025-04-14 09:13:35 +02:00
Andrey Antukh
a4145a30f5 🐛 Fix uuid encode/decode on schema 2025-04-14 09:13:34 +02:00
andrés gonzález
e004671346 📚 Update Recommended storage info (#6275) 2025-04-11 14:02:35 +02:00
Andrey Antukh
38e5c161e7 Sanitize plugins uuid parsing 2025-04-11 13:21:26 +02:00
Andrey Antukh
a7c1f7ba69 🐛 Fix incorrect undo handling on path edition 2025-04-11 08:54:02 +02:00
Florian Schroedl
e9755d437e 🐛 Fix sets and set groups with same name cannot be renamed 2025-04-10 13:27:49 +02:00
Eva Marco
e5db66351e 🐛 Fix scroll on token themes modal (#6251)
* 🐛 Fix scroll on token themes modal

* 🐛 Fix collapse set group error
2025-04-10 10:25:08 +02:00
ºelhombretecla
89153eef23 🎉 Increase height presets dropdown (#6185)
* 🎉 Add new measures dropdown height

* 🎉 Add enhancement to CHANGES.md
2025-04-10 10:01:52 +02:00
Alejandro
b7a8677036 Merge pull request #6262 from penpot/niwinz-staging-clean-data
🐛 Clean workspace state on exit or url change
2025-04-10 08:38:55 +02:00
Andrey Antukh
9ff2160c77 🐛 Clean workspace state on exit or url change 2025-04-09 16:31:49 +02:00
Alejandro
4c77b32171 Merge pull request #6256 from penpot/niwinz-staging-fix-backend-tests
📎 Fix backend tests
2025-04-09 13:52:30 +02:00
Andrey Antukh
34141ce9af 📎 Fix backend tests
Caused by update of image procesing libraries on the devenv docker
image update from debian to ubuntu
2025-04-09 13:37:52 +02:00
Alejandro
58c867885c Merge pull request #6250 from penpot/alotor-bug-colorpicker
🐛 Fix colorpicker scroll when dropdown displayed
2025-04-09 12:51:17 +02:00
alonso.torres
ccb6e25914 🐛 Fix colorpicker scroll when dropdown displayed 2025-04-09 12:50:55 +02:00
Alejandro
965d2d4036 🐛 Fix webhooks not shown in list (#6254) 2025-04-09 12:46:00 +02:00
Yamila Moreno
9f8d7c9e41 🐳 Improve https documentation 2025-04-09 12:24:43 +02:00
Andrey Antukh
8d352c1f82 Merge branch 'main' into staging 2025-04-09 10:59:37 +02:00
Andrey Antukh
faead09174 Merge tag '2.6.0' 2025-04-09 10:58:39 +02:00
Yamila Moreno
ae3ce1220b 🐳 Improve https documentation 2025-04-09 10:05:18 +02:00
Andrey Antukh
6e3673136a 📎 Update changelog 2025-04-09 09:19:05 +02:00
Yamila Moreno
28caa1d47d 🐛 Fix docker-compose.yaml (#6236) 2025-04-07 16:29:47 +02:00
Andrey Antukh
ea6f0abf7c 🐛 Fix regresion on features calculate method on workspace load 2025-04-07 14:32:48 +02:00
Andrey Antukh
45cdfff128 🐛 Fix backend notifications on dashboard 2025-04-07 14:00:26 +02:00
Marina López
8c38e41261 🎉 Consolidate first state of a project (#6150) 2025-04-07 13:15:32 +02:00
Alejandro
3197dfddd9 Merge pull request #6234 from penpot/niwinz-staging-bugfixes-3
🐛 Several bugfixes and backports
2025-04-07 11:15:32 +02:00
Andrey Antukh
d900516302 Merge branch 'main' into staging 2025-04-07 09:59:27 +02:00
Andrey Antukh
fa68a25bea Merge branch 'warrenjokinen-patch-1' 2025-04-07 09:59:08 +02:00
warrenjokinen
2cc2d34719 📚 Update shortcuts.njk (docs)
minor typo
2025-04-07 09:57:05 +02:00
Andrey Antukh
4640d043e3 ⬆️ Update yarn 2025-04-07 09:21:56 +02:00
Andrey Antukh
bc957893f4 Make feature resolved on team load
That simplifies features retrieval to simple get
2025-04-07 07:50:40 +02:00
Andrey Antukh
b8107ee497 Ensure workspace page loading and intialization process 2025-04-07 07:42:09 +02:00
Andrey Antukh
6b3a988526 Send version and build data to worker configuration 2025-04-07 07:10:40 +02:00
Andrey Antukh
5cb39874a2 Add better error hints on auth ns 2025-04-07 07:10:40 +02:00
Marina López
9fc671cc17 🐛 Fix wrong path to list all icons in storybook 2025-04-04 10:36:51 +02:00
Pablo Alba
3fb3b45fdc Merge pull request #6219 from penpot/niwinz-staging-bugfixes-2
🐛 Several bugfixes and enhancements
2025-04-03 15:49:09 +02:00
Andrey Antukh
0816adbaec Send ws messages in verbose format when on development build 2025-04-03 11:40:40 +02:00
Andrey Antukh
1d69941882 🐛 Fix backend notification dialogs 2025-04-03 11:40:40 +02:00
Andrey Antukh
8f600f334f 🐛 Make accept and cancel handlers optional on actionable* 2025-04-03 11:21:02 +02:00
Andrey Antukh
cf55d12991 📚 Add better docstring for srepl.main/notify! helper 2025-04-03 11:21:02 +02:00
Andrey Antukh
78919df886 🐛 Fix incorrect topic sending on internal srepl notify helper 2025-04-03 10:58:10 +02:00
Andrés Moya
5d600c6715 Change behavior of single set json file import to be coherent (#6211) 2025-04-02 09:41:12 +02:00
Andrey Antukh
ea031a2161 Merge pull request #6210 from penpot/niwinz-staging-bugfixes
🐛 Several bugfixes
2025-04-02 09:19:57 +02:00
Andrey Antukh
4d4a04e9aa Add minor enhacement for error reporting 2025-04-01 20:43:55 +02:00
Andrey Antukh
3ec797f56e 🐛 Validate and decode params on export-binfile 2025-04-01 19:16:35 +02:00
Eva Marco
74f11859e4 🐛 Fix max lenght on assets inputs (#6201) 2025-04-01 13:28:35 +02:00
Andrey Antukh
47f80cf3db 🐛 Make error middleware capture profile-id 2025-04-01 12:30:51 +02:00
Andrey Fedorov
a20dd3f955 Fix single set import 2025-04-01 10:57:17 +02:00
Andrey Antukh
982118c942 🐳 Update devenv corepack setup 2025-04-01 10:47:49 +02:00
Andrey Antukh
a51feb8638 📎 Update changelog 2025-04-01 10:47:13 +02:00
Yamila Moreno
9663964790 🐳 Make traefik example easier (#6198) 2025-04-01 09:34:46 +02:00
Andrés Moya
2c0e18ce1c 🐛 Fix sync of margin and padding tokens in components 2025-03-31 16:19:45 +02:00
Eva Marco
89876ef96f 🐛 Fix UI with long named colors (#6193) 2025-03-31 15:33:30 +02:00
Xavier Julian
c259b8ed46 🐛 Fix overflow on tokens sidebar 2025-03-28 23:45:01 +01:00
Xavier Julian
b1df0ac194 Add a default 256 maxlength value to all input fields 2025-03-28 23:45:01 +01:00
Eva Marco
c1853a71a9 🐛 Fix available resize area (#6186) 2025-03-28 13:15:35 +01:00
Eva Marco
cbb3f6672f 🐛 Fix asset name on inspect tab (#6173)
Signed-off-by: Eva Marco <eva.marco@kaleidos.net>
2025-03-28 10:38:35 +01:00
Alejandro
cc97a8ffcc Merge pull request #6158 from penpot/niwinz-staging-task-result
🐛 Fix incorrect task result handling
2025-03-28 09:43:11 +01:00
ºelhombretecla
535e8653a0 🎉 Add slides for version 2.6 (#6176) 2025-03-28 09:42:09 +01:00
Andrey Antukh
210e5b0023 🐛 Fix incorrect task result handling
That caused that many task rows in a table not properly marked
as completed and leaved just as scheduled.
2025-03-28 09:10:46 +01:00
Andrés Moya
6a87d5eea9 🐛 Rewrite active tokens calculation algorithm (#6165) 2025-03-27 15:53:17 +00:00
Alejandro Alonso
237d9d067d Merge remote-tracking branch 'origin/main' into staging 2025-03-27 12:12:37 +01:00
Alejandro
6519db82d1 Merge pull request #6171 from penpot/mavalroot-install-plugin-bug
🐛 Fix plugin installation error by penpot hub
2025-03-27 12:12:19 +01:00
María Valderrama
0a60cbedb5 🐛 Fix plugin installation error by penpot hub 2025-03-27 11:57:23 +01:00
Eva Marco
d7c709607d 🐛 Fix line height on token pills (#6164) 2025-03-26 13:46:55 +01:00
Andrey Antukh
bb7301fb63 Improve libraries loading on workspace (#6141)
*  Improve libraries loading on workspace

*  Add improvements to CSS

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2025-03-26 13:19:48 +01:00
Eva Marco
2918c57fb8 🐛 Show broken pills when all sets are disabled (#6161) 2025-03-26 13:13:45 +01:00
Eva Marco
f55e0bf6e3 🐛 Eva Fix context menu for viewer role (#6159) 2025-03-26 12:50:23 +01:00
andrés gonzález
3d16fa6f19 📚 Add Design Tokens documentation (#6026)
* 📚 Add Design Tokens documentation

* 📚 Update docs/user-guide/design-tokens/index.njk

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

* 📚 Update docs/user-guide/design-tokens/index.njk

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

* 📚 Update docs/user-guide/design-tokens/index.njk

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

* 📚 Update docs/user-guide/design-tokens/index.njk

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

* 📚 Update docs/user-guide/design-tokens/index.njk

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

* 📚 Changing several things after the PR review

---------

Co-authored-by: Madalena Melo <madalena.melo@kaleidos.net>
2025-03-26 10:51:17 +01:00
Yamila Moreno
c65c4270c3 Merge pull request #6128 from orhtej2/patch-1
📚 Add YunoHost as a self-hosting option
2025-03-26 10:17:51 +01:00
Andrey Antukh
0099c282b6 🐛 Fix tokens set reordering corner case 2025-03-26 09:06:54 +01:00
Andrés Moya
9115e1a3a3 🐛 Fix resolved value when opening the token edit form 2025-03-25 17:12:04 +01:00
ºelhombretecla
a7044c73ba 🐛 Fix libraries carrousel styles (#6140) 2025-03-25 15:51:34 +01:00
luisδμ
dc84ab3e41 🐛 Fix calculate zoom to avoid bubbles to get outside viewbox (#6138)
* 🐛 Fix calculate zoom to avoid bubbles to get outside vbox

* 📎 PR changes
2025-03-25 15:39:47 +01:00
Eva Marco
e81adb241b 🐛 Add underscore as posible name character (#6135) 2025-03-25 10:57:23 +01:00
Marina López
a6133e9c48 🐛 Fix actions when workspace is visited first time (#6129)
* 🐛 Fix actions when workspace is visited first time

* 📎 Fix linter errors

* 🐛 Fix problem with integration test

* 📎 Fix linter errors

* 📎 Fix linter errors

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2025-03-24 18:02:05 +01:00
Eva Marco
7bc000517f 🐛 Fix modal position when colopicker is open (#6139) 2025-03-24 16:10:16 +01:00
Xavier Julian
95d9403790 Resize tokens panel on resize 2025-03-24 13:59:51 +01:00
Alejandro
5d66eedcc7 Merge pull request #6134 from penpot/azazeln28-fix-render-wasm-build-env
🐛 Fix _build_env release EMCC_CFLAGS
2025-03-24 10:28:28 +01:00
Aitor Moreno
974d43cb08 🐛 Fix _build_env release EMCC_CFLAGS 2025-03-24 10:17:16 +01:00
Eva Marco
752f74767e 🐛 Fix error copy (#6132) 2025-03-21 10:47:32 +01:00
Andrey Antukh
e5319e04c7 ♻️ Fix naming on token-set group move change operation 2025-03-21 10:23:27 +01:00
Andrey Antukh
e8e9037ef1 🐛 Fix inconsistencies on parsing tokens dtcg json 2025-03-21 10:23:27 +01:00
Andrey Antukh
c6bfae0d63 🐛 Normalize set names on importing themes dtcg json 2025-03-21 10:23:27 +01:00
Andrey Antukh
93bf198073 🐛 Prevent theme replacement on ranaming 2025-03-21 09:22:16 +01:00
orhtej2
4be8d77a79 📚 Update unofficial-options.md
Signed-off-by: Mateusz Szymański <2871798+orhtej2@users.noreply.github.com>
2025-03-20 23:59:30 +01:00
Alejandro
9fb7456b38 Merge pull request #6111 from penpot/superalex-fix-pen-shortcut-multiple-times
🐛 Fix opening pen with shortcut multiple times breaks toolbar
2025-03-20 18:05:35 +01:00
Eva Marco
b3a3cca9fe 🐛 Fix stroke width validation (#6124) 2025-03-20 17:35:19 +01:00
andrés gonzález
f98009ec54 📚 Add Design Tokens to the Changelog (#6112) 2025-03-20 16:39:14 +01:00
Andrey Antukh
669533cae6 Merge pull request #6115 from penpot/niwinz-staging-tokens-1
🐛 Fix incorrect absolute frame positioning with measures sidebar
2025-03-20 13:20:08 +01:00
Andrey Antukh
d6efd469e4 🎉 Make the design tokens feature enabled by default 2025-03-20 12:22:37 +01:00
Andrey Antukh
0d4a6fc75f 🐛 Clear selected token set on leave file on workspace 2025-03-20 12:22:37 +01:00
Andrey Antukh
e403194bba 💄 Remove incorrect use of rx/concat on update-shape-position 2025-03-20 12:22:37 +01:00
Andrey Antukh
b8c5a10551 💄 Add minor cosmetic changes to measures menu 2025-03-20 12:22:37 +01:00
Andrey Antukh
7fdb0873db 🐛 Fix incorrect absolute frame positioning with measures sidebar 2025-03-20 12:22:37 +01:00
Xavier Julian
68a89556d6 🐛 Add tooltip to empty sets button on theme creation modal 2025-03-20 10:18:28 +01:00
Alejandro Alonso
1a77c1fe36 🐛 Fix opening pen with shortcut multiple times breaks toolbar 2025-03-20 09:19:27 +01:00
Aitor Moreno
4aa1bb7246 Merge pull request #6116 from penpot/alotor-fix-group-constraints
🐛 Fix problem with constraints when creating group
2025-03-19 16:43:54 +01:00
alonso.torres
3a80120bf6 🐛 Fix problem with constraints when creating group 2025-03-19 16:33:50 +01:00
Aitor Moreno
d01eccf912 Merge pull request #6114 from penpot/alotor-fix-plugins-shapows
🐛 Fix problem with default shadows in plugins
2025-03-19 16:15:41 +01:00
alonso.torres
25621f8deb 🐛 Fix problem with default shadows in plugins 2025-03-19 14:51:39 +01:00
Yamila Moreno
dc006bd7f2 Merge pull request #6108 from penpot/yms-improve-self-host-documentation
📚 Improve self host documentation
2025-03-19 14:05:22 +01:00
Eva Marco
629f09089b 🐛 Fix tooltip with only one set and no active (#6107) 2025-03-19 13:54:00 +01:00
Andrey Antukh
344ec94a3f Merge pull request #6109 from penpot/superalex-fix-hovering-dashboard-templates
🐛 FIx hovering dashboard templates
2025-03-19 13:42:04 +01:00
Andrey Antukh
62e89258e4 Merge pull request #6101 from penpot/niwinz-develop-token-fixes-4
 Add several improvements to tokens (part 4)
2025-03-19 13:38:46 +01:00
Andrey Antukh
b6bb93f0b6 Improve code convetion related to changes protocol
Partial work, still pending to make changes to other related
changes definitions
2025-03-19 12:52:03 +01:00
Andrey Antukh
39a1d5cc89 🐛 Fix set unexpected deletion on reordering 2025-03-19 12:42:05 +01:00
Andrey Antukh
8fa24de3d4 Merge pull request #6096 from penpot/niwinz-develop-token-fixes-3
 Add several improvements and fixes to tokens (part 3)
2025-03-19 12:30:05 +01:00
Alejandro Alonso
1def5015fb 🐛 FIx hovering dashboard templates 2025-03-19 12:27:13 +01:00
Yamila Moreno
1dbc924d31 📚 Remove docker installation in favour of the official documentation 2025-03-19 12:19:07 +01:00
Yamila Moreno
95da007107 📚 Add a warning about technical knowledge 2025-03-19 12:18:04 +01:00
Andrey Antukh
4453eec687 Persist migrated files on srepl process-file helper 2025-03-18 17:57:52 +01:00
Andrey Antukh
c169eef161 ♻️ Remove tokens lib migrations from file migrations 2025-03-18 17:57:52 +01:00
Andrey Antukh
8df12e5e9c Remove state assignation round-trip on update-dimensions event
Using the lower-level apply-modifiers event, introduced in previous
commit
2025-03-18 16:19:55 +01:00
Andrey Antukh
cd423f23c6 Remove get-hidden-theme from tokens lib protocol 2025-03-18 16:19:55 +01:00
Andrey Antukh
86c2c4cd41 ♻️ Add lower-level impl of apply-modifiers event 2025-03-18 16:19:55 +01:00
Andrey Antukh
d9c4fc3721 Calculate uuid lazily on creating token theme 2025-03-18 16:19:55 +01:00
Andrey Antukh
b91e72d8a1 🐛 Fix typo 2025-03-18 16:19:55 +01:00
Andrey Antukh
6cc96ef679 Add logging for tokens update event operation 2025-03-18 16:19:55 +01:00
Andrey Antukh
28fe951c40 ♻️ Replace usage of dm/assert on several namespaces
And remove the `!` from the name on check functions
2025-03-18 16:19:55 +01:00
Andrey Antukh
22f789e77c Don't put timeout on tokens changes transaction 2025-03-18 16:19:55 +01:00
210 changed files with 3783 additions and 2111 deletions

View File

@@ -1,9 +1,36 @@
# CHANGELOG
## 2.6.0 (Unreleased)
## 2.6.2
### :bug: Bugs fixed
- Increase the height of the right sidebar dropdowns [Taiga #10615](https://tree.taiga.io/project/penpot/issue/10615)
- Fix scroll on token themes modal [Taiga #10745](https://tree.taiga.io/project/penpot/issue/10745)
- Fix collapsing grouped sets in "edit Theme" closes the dialog [Taiga #10771](https://tree.taiga.io/project/penpot/issue/10771)
- Fix unexpected exception on path editor on merge segments when undo stack is empty
- Fix pricing CTA to be under a config flag [Taiga #10808](https://tree.taiga.io/project/penpot/issue/10808)
- Fix allow moving a main component into another [Taiga #10818](https://tree.taiga.io/project/penpot/issue/10818)
- Fix several issues with internal srepl helpers
- Fix unexpected exception on template import from libraries
- Fix incorrect uuid parsing from different parts of code
- Fix update layout on component restore [Taiga #10637](https://tree.taiga.io/project/penpot/issue/10637)
- Fix horizontal scroll in viewer [Github #6290](https://github.com/penpot/penpot/issues/6290)
- Fix detach component in a particular case [Taiga #10837](https://tree.taiga.io/project/penpot/issue/10837)
## 2.6.1
### :bug: Bugs fixed
- Fix webhooks not shown in list [Taiga #10763](https://tree.taiga.io/project/penpot/issue/10763)
- Fix colorpicker scroll when dropdown displayed [Taiga #10696](https://tree.taiga.io/project/penpot/issue/10696)
- Clean internal workspace state on exit or url changed [Taiga #10619](https://tree.taiga.io/project/penpot/issue/10619)
## 2.6.0
### :rocket: Epics and highlights
- Design Tokens
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
@@ -13,6 +40,17 @@
- [COMMENTS] "Mark All as Read" Functionality in Dashboard [Taiga #9235](https://tree.taiga.io/project/penpot/us/9235)
- [COMMENTS] Bubble Groups [Taiga #9236](https://tree.taiga.io/project/penpot/us/9236)
- Change templates carrousel [Taiga #9803](https://tree.taiga.io/project/penpot/us/9803)
- [DESIGN TOKENS] Tokens CRUD. Types added: Color, Opacity, Border radius, Dimension, Sizing, Spacing, Rotation and Stroke.
- [DESIGN TOKENS] Create references (alias) that point to other tokens.
- [DESIGN TOKENS] Math operations in token values.
- [DESIGN TOKENS] Sets CRUD, grouping and reordering.
- [DESIGN TOKENS] Multidimensional Themes and Sets management.
- [DESIGN TOKENS] Apply/Remove tokens to/from elements from the Tokens tab.
- [DESIGN TOKENS] Integration with components.
- [DESIGN TOKENS] Import and export tokens from a JSON file.
- [DESIGN TOKENS] Apply Themes and Sets at document level.
- Add more descriptive tooltip to boards for first time users [Taiga #9426](https://tree.taiga.io/project/penpot/us/9426)
- First State of a Project Changes Consolidation [Taia #10605](https://tree.taiga.io/project/penpot/us/10605)
### :bug: Bugs fixed
@@ -23,11 +61,24 @@
- Fix duplicate page with component over frame [Taiga #8151](https://tree.taiga.io/project/penpot/issue/8151) and [Taiga #9698](https://tree.taiga.io/project/penpot/issue/9698)
- The plugin list in the navigation menu lacks scrolling, some plugins are not visible when a large number are installed [Taiga #9360](https://tree.taiga.io/project/penpot/us/9360)
- Fix hidden toolbar click event still available [Taiga #10437](https://tree.taiga.io/project/penpot/us/10437)
- Fix hovering over templates [Taiga #10545](https://tree.taiga.io/project/penpot/issue/10545)
- Fix problem with default shadows value in plugins [Plugins #191](https://github.com/penpot/penpot-plugins/issues/191)
- Fix problem with constraints when creating group [Taiga #10455](https://tree.taiga.io/project/penpot/issue/10455)
- Fix opening pen with shortcut multiple times breaks toolbar [Taiga #10566](https://tree.taiga.io/project/penpot/issue/10566)
- Fix actions when workspace is visited first time [Taiga #10548](https://tree.taiga.io/project/penpot/issue/10548)
- Chat icon overlaps "Show" button in carrousel section [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542)
- Fix assets name on inspect tab [Taiga #10630](https://tree.taiga.io/project/penpot/issue/10630)
- Fix chat icon overlaps "Show" button in carrousel section [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542)
- Fix incorrect handling of background task result (now task rows are properly marked as completed)
- Fix available size of resize handler [Taiga #10639](https://tree.taiga.io/project/penpot/issue/10639)
- Internal error when install a plugin by penpothub - Try plugin [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542)
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
## 2.5.4
### :sparkles: New features
### :heart: Community contributions (Thank you!)
- Add support for WEBP format on shape export [Github #6053](https://github.com/penpot/penpot/pull/6053) and [Github #6074](https://github.com/penpot/penpot/pull/6074)

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"

View File

@@ -30,7 +30,8 @@ export PENPOT_FLAGS="\
enable-access-tokens \
enable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation";
enable-file-schema-validation \
enable-subscriptions-old";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"

View File

@@ -23,7 +23,8 @@ export PENPOT_FLAGS="\
enable-access-tokens \
enable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation";
enable-file-schema-validation \
enable-subscriptions-old";
export OPTIONS="
-A:jmx-remote -A:dev \

View File

@@ -0,0 +1,44 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.binfile.cleaner
"A collection of helpers for perform cleaning of artifacts; mainly
for recently imported shapes."
(:require
[app.common.data :as d]
[app.common.uuid :as uuid]))
(defn- fix-shape-shadow-color
"Some shapes can come with invalid `id` property on shadow colors
caused by incorrect uuid parsing bug that should be already fixed;
this function removes the invalid id from the data structure."
[shape]
(let [fix-color
(fn [{:keys [id] :as color}]
(if (uuid? id)
color
(if (and (string? id)
(re-matches uuid/regex id))
(assoc color :id (uuid/uuid id))
(dissoc color :id))))
fix-shadow
(fn [shadow]
(d/update-when shadow :color fix-color))
xform
(map fix-shadow)]
(d/update-when shape :shadow
(fn [shadows]
(into [] xform shadows)))))
(defn clean-shape-post-decode
"A shape procesor that expected to be executed after schema decoding
process but before validation."
[shape]
(-> shape
(fix-shape-shadow-color)))

View File

@@ -8,12 +8,14 @@
"A ZIP based binary file exportation"
(:refer-clojure :exclude [read])
(:require
[app.binfile.cleaner :as bfl]
[app.binfile.common :as bfc]
[app.binfile.migrations :as bfm]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.files.migrations :as-alias fmg]
[app.common.json :as json]
[app.common.logging :as l]
[app.common.schema :as sm]
@@ -594,16 +596,25 @@
(defn- read-file-components
[{:keys [::bfc/input ::file-id ::entries]}]
(->> (keep (match-component-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry)
(decode-component)
(validate-component))]
(if (= id (:id object))
(assoc result id object)
result)))
{})
(not-empty)))
(let [clean-component-post-decode
(fn [component]
(d/update-when component :objects
(fn [objects]
(reduce-kv (fn [objects id shape]
(assoc objects id (bfl/clean-shape-post-decode shape)))
objects
objects))))]
(->> (keep (match-component-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry)
(decode-component)
(clean-component-post-decode)
(validate-component))]
(if (= id (:id object))
(assoc result id object)
result)))
{})
(not-empty))))
(defn- read-file-typographies
[{:keys [::bfc/input ::file-id ::entries]}]
@@ -631,7 +642,9 @@
(reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry)
(decode-shape)
(bfl/clean-shape-post-decode)
(validate-shape))]
(if (= id (:id object))
(assoc result id object)
result)))
@@ -733,7 +746,14 @@
(assoc :name file-name)
(assoc :project-id project-id)
(dissoc :options)
(bfc/process-file))]
(bfc/process-file)
;; NOTE: this is necessary because when we just
;; creating a new file from imported artifact,
;; there are no migrations registered on the
;; database, so we need to persist all of them, not
;; only the applied
(vary-meta dissoc ::fmg/migrated))]
(bfm/register-pending-migrations! cfg file)

View File

@@ -155,10 +155,10 @@
[["" {:middleware [[mw/server-timing]
[mw/params]
[mw/format-response]
[mw/parse-request]
[mw/errors errors/handle]
[session/soft-auth cfg]
[actoken/soft-auth cfg]
[mw/parse-request]
[mw/errors errors/handle]
[mw/restrict-methods]]}
(::mtx/routes cfg)

View File

@@ -25,7 +25,6 @@
(let [claims (-> {}
(into (::session/token-claims request))
(into (::actoken/token-claims request)))]
{:request/path (:path request)
:request/method (:method request)
:request/params (:params request)
@@ -62,7 +61,8 @@
::yres/body data}
(binding [l/*context* (request->context request)]
(l/err :hint "restriction error" :data data)
(l/err :hint "restriction error"
:cause err)
{::yres/status 400
::yres/body data}))))
@@ -102,7 +102,7 @@
(= code :invalid-image)
(binding [l/*context* (request->context request)]
(let [cause (or parent-cause err)]
(l/warn :hint "unexpected error on processing image" :cause cause)
(l/warn :hint "image process error" :cause cause)
{::yres/status 400 ::yres/body data}))
:else
@@ -177,7 +177,7 @@
(let [state (.getSQLState ^java.sql.SQLException error)
cause (or parent-cause error)]
(binding [l/*context* (request->context request)]
(l/error :hint "PSQL error"
(l/error :hint "postgresql error"
:cause cause)
(cond
(= state "57014")

View File

@@ -273,7 +273,7 @@
(defn- http-handler
[cfg {:keys [params ::session/profile-id] :as request}]
(let [session-id (some-> params :session-id sm/parse-uuid)]
(let [session-id (some-> params :session-id uuid/parse*)]
(when-not (uuid? session-id)
(ex/raise :type :validation
:code :missing-session-id

View File

@@ -53,11 +53,16 @@
(assoc :logger/name logger)
(assoc :logger/level level)
(dissoc :request/params :value :params :data))]
(merge
{:context (-> (into (sorted-map) ctx)
(pp/pprint-str :length 50))
:props (pp/pprint-str props :length 50)
:hint (or (ex-message cause) @message)
:hint (or (when-let [message (ex-message cause)]
(if-let [props-hint (:hint props)]
(str props-hint ": " message)
message))
@message)
:trace (or (::trace record)
(some-> cause (ex/format-throwable :data? false :explain? false :header? false :summary? false)))}

View File

@@ -55,7 +55,7 @@
(contains? cf/flags :login-with-password))
(ex/raise :type :restriction
:code :login-disabled
:hint "login is disabled in this instance"))
:hint "login is disabled"))
(letfn [(check-password [cfg profile password]
(if (= (:password profile) "!")
@@ -79,7 +79,8 @@
:code :wrong-credentials))
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
:code :profile-blocked
:hint "profile is marked as blocked"))
(when-not (check-password cfg profile password)
(ex/raise :type :validation
:code :wrong-credentials))
@@ -183,11 +184,11 @@
(defn- validate-register-attempt!
[cfg params]
(when (or
(not (contains? cf/flags :registration))
(not (contains? cf/flags :login-with-password)))
(when (or (not (contains? cf/flags :registration))
(not (contains? cf/flags :login-with-password)))
(ex/raise :type :restriction
:code :registration-disabled))
:code :registration-disabled
:hint "registration disabled"))
(when (contains? params :invitation-token)
(let [invitation (tokens/verify (::setup/props cfg)
@@ -201,12 +202,14 @@
(when (and (email.blacklist/enabled? cfg)
(email.blacklist/contains? cfg (:email params)))
(ex/raise :type :restriction
:code :email-domain-is-not-allowed))
:code :email-domain-is-not-allowed
:hint "email domain in blacklist"))
(when (and (email.whitelist/enabled? cfg)
(not (email.whitelist/contains? cfg (:email params))))
(ex/raise :type :restriction
:code :email-domain-is-not-allowed))
:code :email-domain-is-not-allowed
:hint "email domain not in whitelist"))
;; Perform a basic validation of email & password
(when (= (str/lower (:email params))
@@ -219,13 +222,13 @@
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email (:email params)
:hint "looks like the email has bounce reports"))
:hint "email has bounce reports"))
(when (eml/has-complaint-reports? cfg (:email params))
(ex/raise :type :restriction
:code :email-has-complaints
:email (:email params)
:hint "looks like the email has complaint reports")))
:hint "email has complaint reports")))
(defn prepare-register
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]

View File

@@ -38,7 +38,6 @@
(def ^:private
schema:export-binfile
[:map {:title "export-binfile"}
[:name [:string {:max 250}]]
[:file-id ::sm/uuid]
[:version {:optional true} ::sm/int]
[:include-libraries ::sm/boolean]
@@ -78,7 +77,7 @@
"Export a penpot file in a binary format."
{::doc/added "1.15"
::webhooks/event? true
::sm/result schema:export-binfile}
::sm/params schema:export-binfile}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id version file-id] :as params}]
(files/check-read-permissions! pool profile-id file-id)
(fn [_]

View File

@@ -292,7 +292,7 @@
(defn get-file-etag
[{:keys [::rpc/profile-id]} {:keys [modified-at revn vern permissions]}]
(str profile-id "/" revn "/" vern "/"
(str profile-id "/" revn "/" vern "/" (hash fmg/available-migrations) "/"
(dt/format-instant modified-at :iso)
"/"
(uri/map->query-string permissions)))
@@ -328,7 +328,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
(cfeat/check-file-features! (:features file)))
;; This operation is needed for backward comapatibility with frontends that
;; does not support pointer-map resolution mechanism; this just resolves the
@@ -490,7 +490,7 @@
_ (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
(cfeat/check-file-features! (:features file)))
page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
(let [page-id (or page-id (-> file :data :pages first))
@@ -737,7 +737,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
(cfeat/check-file-features! (:features file)))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
{:name (:name file)

View File

@@ -91,9 +91,6 @@
:project-id project-id)
team-id (:id team)
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)))
@@ -107,7 +104,7 @@
params (-> params
(assoc :profile-id profile-id)
(assoc :features (set/difference features cfeat/frontend-only-features)))]
(assoc :features features))]
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
@@ -120,7 +117,7 @@
;; to lost team features updating
;; When newly computed features does not match exactly with
;; the features defined on team row, we update it.
;; the features defined on team row, we update it
(when (not= features (:features team))
(let [features (db/create-array conn "text" features)]
(db/update! conn :team

View File

@@ -180,8 +180,7 @@
(def ^:private
schema:get-file-data-for-thumbnail
[:map {:title "get-file-data-for-thumbnail"}
[:file-id ::sm/uuid]
[:features {:optional true} ::cfeat/features]])
[:file-id ::sm/uuid]])
(def ^:private
schema:partial-file
@@ -211,8 +210,7 @@
(fmg/migrate-file)))]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
(cfeat/check-file-features! (:features file)))
{:file-id file-id
:revn (:revn file)

View File

@@ -142,7 +142,7 @@
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params)))
(cfeat/check-file-features! (:features file)))
changes (if changes-with-metadata
(->> changes-with-metadata (mapcat :changes) vec)

View File

@@ -78,7 +78,10 @@
:always
(update :data select-keys [:id :options :pages :pages-index :components]))
libs (files/get-file-libraries conn file-id)
libs (->> (files/get-file-libraries conn file-id)
(mapv (fn [{:keys [id] :as lib}]
(merge lib (files/get-file cfg id)))))
links (->> (db/query conn :share-link {:file-id file-id})
(mapv (fn [row]
(-> row

View File

@@ -10,6 +10,7 @@
(:require
[app.binfile.common :as bfc]
[app.common.data :as d]
[app.common.files.migrations :as fmg]
[app.common.files.validate :as cfv]
[app.db :as db]
[app.features.components-v2 :as feat.comp-v2]
@@ -142,7 +143,9 @@
(update-fn file opts)))]
(when (and (some? file')
(not (identical? file file')))
(or (fmg/migrated? file)
(not (identical? file file'))))
(when validate?
(cfv/validate-file-schema! file'))

View File

@@ -209,100 +209,116 @@
This method allows send flash notifications to specified target destinations.
The message can be a free text or a preconfigured one.
The destination can be: all, profile-id, team-id, or a coll of them."
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
:or {code :generic level :info}
:as params}]
The destination can be: all, profile-id, team-id, or a coll of them.
It also can be:
{:email \"some@example.com\"}
[[:email \"some@example.com\"], ...]
Command examples:
(notify! :dest :all :code :maintenance)
(notify! :dest :all :code :upgrade-version)
"
[& {:keys [dest code message level]
:or {code :generic level :info}
:as params}]
(when-not (contains? #{:success :error :info :warning} level)
(ex/raise :type :assertion
:code :incorrect-level
:hint (str "level '" level "' not supported")))
(letfn [(send [dest]
(l/inf :hint "sending notification" :dest (str dest))
(let [message {:type :notification
:code code
:level level
:version (:full cf/version)
:subs-id dest
:message message}
message (->> (dissoc params :dest :code :message :level)
(merge message))]
(mbus/pub! msgbus
:topic (str dest)
:message message)))
(let [{:keys [::mbus/msgbus ::db/pool]} main/system
(resolve-profile [email]
(some-> (db/get* pool :profile {:email (str/lower email)} {:columns [:id]}) :id vector))
send
(fn [dest]
(l/inf :hint "sending notification" :dest (str dest))
(let [message {:type :notification
:code code
:level level
:version (:full cf/version)
:subs-id dest
:message message}
message (->> (dissoc params :dest :code :message :level)
(merge message))]
(mbus/pub! msgbus
:topic dest
:message message)))
(resolve-team [team-id]
(->> (db/query pool :team-profile-rel
{:team-id team-id}
{:columns [:profile-id]})
(map :profile-id)))
resolve-profile
(fn [email]
(some-> (db/get* pool :profile {:email (str/lower email)} {:columns [:id]}) :id vector))
(resolve-dest [dest]
(cond
(= :all dest)
[uuid/zero]
resolve-team
(fn [team-id]
(->> (db/query pool :team-profile-rel
{:team-id team-id}
{:columns [:profile-id]})
(map :profile-id)))
(uuid? dest)
[dest]
resolve-dest
(fn resolve-dest [dest]
(cond
(= :all dest)
[uuid/zero]
(string? dest)
(some-> dest h/parse-uuid resolve-dest)
(uuid? dest)
[dest]
(nil? dest)
(resolve-dest uuid/zero)
(string? dest)
(some-> dest h/parse-uuid resolve-dest)
(map? dest)
(sequence (comp
(map vec)
(mapcat resolve-dest))
dest)
(nil? dest)
[uuid/zero]
(and (vector? dest)
(every? vector? dest))
(sequence (comp
(map vec)
(mapcat resolve-dest))
dest)
(map? dest)
(sequence (comp
(map vec)
(mapcat resolve-dest))
dest)
(and (vector? dest)
(keyword? (first dest)))
(let [[op param] dest]
(and (vector? dest)
(every? vector? dest))
(sequence (comp
(map vec)
(mapcat resolve-dest))
dest)
(and (vector? dest)
(keyword? (first dest)))
(let [[op param] dest]
(cond
(= op :email)
(cond
(= op :email)
(cond
(and (coll? param)
(every? string? param))
(sequence (comp
(keep resolve-profile)
(mapcat identity))
param)
(and (coll? param)
(every? string? param))
(sequence (comp
(keep resolve-profile)
(mapcat identity))
param)
(string? param)
(resolve-profile param))
(string? param)
(resolve-profile param))
(= op :team-id)
(cond
(coll? param)
(sequence (comp
(mapcat resolve-team)
(keep h/parse-uuid))
param)
(= op :team-id)
(cond
(coll? param)
(sequence (comp
(mapcat resolve-team)
(keep h/parse-uuid))
param)
(uuid? param)
(resolve-team param)
(uuid? param)
(resolve-team param)
(string? param)
(some-> param h/parse-uuid resolve-team))
(string? param)
(some-> param h/parse-uuid resolve-team))
(= op :profile-id)
(if (coll? param)
(sequence (keep h/parse-uuid) param)
(resolve-dest param))))))]
(= op :profile-id)
(if (coll? param)
(sequence (keep h/parse-uuid) param)
(resolve-dest param))))))]
(->> (resolve-dest dest)
(filter some?)
@@ -321,14 +337,23 @@
(db/tx-run! main/system fsnap/create-file-snapshot! {:file-id file-id :label label})))
(defn restore-file-snapshot!
[file-id label]
(let [file-id (h/parse-uuid file-id)]
[file-id & {:keys [label id]}]
(let [file-id (h/parse-uuid file-id)
snapshot-id (some-> id h/parse-uuid)]
(db/tx-run! main/system
(fn [{:keys [::db/conn] :as system}]
(when-let [snapshot (->> (h/search-file-snapshots conn #{file-id} label)
(map :id)
(first))]
(fsnap/restore-file-snapshot! system file-id (:id snapshot)))))))
(cond
(uuid? snapshot-id)
(fsnap/restore-file-snapshot! system file-id snapshot-id)
(string? label)
(->> (h/search-file-snapshots conn #{file-id} label)
(map :id)
(first)
(fsnap/restore-file-snapshot! system file-id))
:else
(throw (ex-info "snapshot id or label should be provided" {})))))))
(defn list-file-snapshots!
[file-id & {:as _}]

View File

@@ -24,6 +24,33 @@
(set! *warn-on-reflection* true)
(def schema:task
[:map {:title "Task"}
[:id ::sm/uuid]
[:queue :string]
[:name :string]
[:created-at ::sm/inst]
[:modified-at ::sm/inst]
[:scheduled-at {:optional true} ::sm/inst]
[:completed-at {:optional true} ::sm/inst]
[:error {:optional true} :string]
[:max-retries :int]
[:retry-num :int]
[:priority :int]
[:status [:enum "scheduled" "completed" "new" "retry" "failed"]]
[:label {:optional true} :string]
[:props :map]])
(def schema:result
[:map {:title "TaskResult"}
[:status [:enum "retry" "failed" "completed"]]
[:error {:optional true} [:fn ex/exception?]]
[:inc-by {:optional true} :int]
[:delay {:optional true} :int]])
(def valid-task-result?
(sm/validator schema:result))
(defn- decode-task-row
[{:keys [props] :as row}]
(cond-> row
@@ -51,10 +78,11 @@
:retry (:retry-num task))
(let [tpoint (dt/tpoint)
task-fn (wrk/get-task registry (:name task))
result (if task-fn
(task-fn task)
{:status :completed :task task})
elapsed (dt/format-duration (tpoint))]
result (when task-fn (task-fn task))
elapsed (dt/format-duration (tpoint))
result (if (valid-task-result? result)
result
{:status "completed"})]
(when-not task-fn
(l/wrn :hint "no task handler found" :name (:name task)))
@@ -76,7 +104,7 @@
(if (and (< (:retry-num task)
(:max-retries task))
(= ::retry (:type edata)))
(cond-> {:status :retry :task task :error cause}
(cond-> {:status "retry" :error cause}
(dt/duration? (:delay edata))
(assoc :delay (:delay edata))
@@ -87,8 +115,8 @@
::l/context (get-error-context cause task)
:cause cause)
(if (>= (:retry-num task) (:max-retries task))
{:status :failed :task task :error cause}
{:status :retry :task task :error cause})))))))
{:status "failed" :error cause}
{:status "retry" :error cause})))))))
(defn- run-task!
[{:keys [::id ::timeout] :as cfg} task-id]
@@ -116,12 +144,17 @@
:task-id task-id)
:else
(run-task cfg task))))
(let [result (run-task cfg task)]
(with-meta result
{::task task})))))
(defn- run-worker-loop!
[{:keys [::db/pool ::rds/rconn ::timeout ::queue] :as cfg}]
(letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}]
(let [explain (ex-message error)
(letfn [(handle-task-retry [{:keys [error inc-by delay] :or {inc-by 1 delay 1000} :as result}]
(let [explain (if (ex/exception? error)
(ex-message error)
(str error))
task (-> result meta ::task)
nretry (+ (:retry-num task) inc-by)
now (dt/now)
delay (->> (iterate #(* % 2) delay) (take nretry) (last))]
@@ -134,8 +167,9 @@
{:id (:id task)})
nil))
(handle-task-failure [{:keys [task error]}]
(let [explain (ex-message error)]
(handle-task-failure [{:keys [error] :as result}]
(let [task (-> result meta ::task)
explain (ex-message error)]
(db/update! pool :task
{:error explain
:modified-at (dt/now)
@@ -143,8 +177,9 @@
{:id (:id task)})
nil))
(handle-task-completion [{:keys [task]}]
(let [now (dt/now)]
(handle-task-completion [result]
(let [task (-> result meta ::task)
now (dt/now)]
(db/update! pool :task
{:completed-at now
:modified-at now
@@ -168,10 +203,11 @@
(process-result [{:keys [status] :as result}]
(ex/try!
(case status
:retry (handle-task-retry result)
:failed (handle-task-failure result)
:completed (handle-task-completion result)
nil)))
"retry" (handle-task-retry result)
"failed" (handle-task-failure result)
"completed" (handle-task-completion result)
(throw (IllegalArgumentException.
(str "invalid status received: " status))))))
(run-task-loop [task-id]
(loop [result (run-task! cfg task-id)]

View File

@@ -48,7 +48,7 @@
(t/is (sto/object? mobj1))
(t/is (sto/object? mobj2))
(t/is (= 122785 (:size mobj1)))
(t/is (= 3302 (:size mobj2)))))))
(t/is (= 3299 (:size mobj2)))))))
(t/deftest media-object-upload
(let [prof (th/create-profile* 1)
@@ -85,7 +85,7 @@
(t/is (sto/object? mobj1))
(t/is (sto/object? mobj2))
(t/is (= 312043 (:size mobj1)))
(t/is (= 3887 (:size mobj2)))))))
(t/is (= 3901 (:size mobj2)))))))
(t/deftest media-object-upload-idempotency
@@ -163,7 +163,7 @@
(t/is (sto/object? mobj1))
(t/is (sto/object? mobj2))
(t/is (= 122785 (:size mobj1)))
(t/is (= 3302 (:size mobj2)))))))
(t/is (= 3299 (:size mobj2)))))))
(t/deftest media-object-upload-command
(let [prof (th/create-profile* 1)
@@ -200,7 +200,7 @@
(t/is (sto/object? mobj1))
(t/is (sto/object? mobj2))
(t/is (= 312043 (:size mobj1)))
(t/is (= 3887 (:size mobj2)))))))
(t/is (= 3901 (:size mobj2)))))))
(t/deftest media-object-upload-idempotency-command

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
"type": "module",
"repository": {
"type": "git",

View File

@@ -82,15 +82,22 @@
"Assoc a k v pair, in the order position just before the other key."
[o ks k v before-k]
(let [f (fn [o']
(cond-> (reduce
(fn [acc [k' v']]
(cond
(and before-k (= k' before-k)) (assoc acc k v k' v')
(= k k') acc
:else (assoc acc k' v')))
(ordered-map)
o')
(not before-k) (assoc k v)))]
(let [found (volatile! false)
result (reduce
(fn [acc [k' v']]
(cond
(and before-k (= k' before-k))
(do
(vreset! found true)
(assoc acc k v k' v'))
(= k k') acc
:else (assoc acc k' v')))
(ordered-map)
o')]
(if (or (not before-k) (not @found))
(assoc result k v)
result)))]
(if (seq ks)
(oupdate-in o ks f)
(f o))))

View File

@@ -47,7 +47,7 @@
(defn undo
[stack]
(update stack :index dec))
(update stack :index #(max 0 (dec %))))
(defn redo
[{index :index items :items :as stack}]

View File

@@ -61,7 +61,8 @@
"styles/v2"
"layout/grid"
"components/v2"
"plugins/runtime"})
"plugins/runtime"
"design-tokens/v1"})
;; A set of features which only affects on frontend and can be enabled
;; and disabled freely by the user any time. This features does not
@@ -84,12 +85,11 @@
;; be applied (per example backend can operate in both modes with or
;; without migration applied)
(def no-migration-features
(-> #{"fdata/objects-map"
"fdata/pointer-map"
"layout/grid"
(-> #{"layout/grid"
"fdata/shape-data-type"
"design-tokens/v1"}
(into frontend-only-features)))
(into frontend-only-features)
(into backend-only-features)))
(sm/register!
^{::sm/type ::features}
@@ -157,7 +157,6 @@
team-features (into #{} xf-remove-ephimeral (:features team))]
(-> enabled-features
(set/intersection no-migration-features)
(set/difference frontend-only-features)
(set/union team-features))))
(defn check-client-features!
@@ -166,6 +165,8 @@
frontend client"
[enabled-features client-features]
(when (set? client-features)
;; Check if client declares support for features enabled on
;; backend side
(let [not-supported (-> enabled-features
(set/difference client-features)
(set/difference frontend-only-features)
@@ -175,14 +176,6 @@
:code :feature-not-supported
:feature (first not-supported)
:hint (str/ffmt "client declares no support for '%' features"
(str/join "," not-supported)))))
(let [not-supported (set/difference client-features supported-features)]
(when (seq not-supported)
(ex/raise :type :restriction
:code :feature-not-supported
:feature (first not-supported)
:hint (str/ffmt "backend does not support '%' features requested by client"
(str/join "," not-supported))))))
enabled-features)
@@ -193,57 +186,49 @@
supported by the current backend"
[enabled-features]
(let [not-supported (set/difference enabled-features supported-features)]
(when (seq not-supported)
(when-let [not-supported (first not-supported)]
(ex/raise :type :restriction
:code :feature-not-supported
:feature (first not-supported)
:hint (str/ffmt "features '%' not supported"
(str/join "," not-supported)))))
enabled-features)
:feature not-supported
:hint (str/ffmt "feature '%' not supported on this backend" not-supported)))
enabled-features))
(defn check-file-features!
"Function used for check feature compability between currently
enabled features set on backend with the provided featured set by
the penpot file"
([enabled-features file-features]
(check-file-features! enabled-features file-features #{}))
([enabled-features file-features client-features]
(let [file-features (into #{} xf-remove-ephimeral file-features)
;; We should ignore all features that does not match with the
;; `no-migration-features` set because we can't enable them
;; as-is, because they probably need migrations
client-features (set/intersection client-features no-migration-features)]
(let [not-supported (-> enabled-features
(set/union client-features)
(set/difference file-features)
;; NOTE: we don't want to raise a feature-mismatch
;; exception for features which don't require an
;; explicit file migration process or has no real
;; effect on file data structure
(set/difference no-migration-features))]
(when (seq not-supported)
(ex/raise :type :restriction
:code :file-feature-mismatch
:feature (first not-supported)
:hint (str/ffmt "enabled features '%' not present in file (missing migration)"
(str/join "," not-supported)))))
[enabled-features file-features]
(let [file-features (into #{} xf-remove-ephimeral file-features)
not-supported (-> enabled-features
(set/difference file-features)
;; NOTE: we don't want to raise a feature-mismatch
;; exception for features which don't require an
;; explicit file migration process or has no real
;; effect on file data structure
(set/difference no-migration-features))]
(check-supported-features! file-features)
(when-let [not-supported (first not-supported)]
(ex/raise :type :restriction
:code :file-feature-mismatch
:feature not-supported
:hint (str/ffmt "enabled feature '%' not present in file (missing migration)"
not-supported)))
(let [not-supported (-> file-features
(set/difference enabled-features)
(set/difference client-features)
(set/difference backend-only-features)
(set/difference frontend-only-features))]
(check-supported-features! file-features)
(when (seq not-supported)
(ex/raise :type :restriction
:code :file-feature-mismatch
:feature (first not-supported)
:hint (str/ffmt "file features '%' not enabled"
(str/join "," not-supported))))))
(let [not-supported (-> file-features
(set/difference enabled-features)
(set/difference backend-only-features)
(set/difference frontend-only-features))]
enabled-features))
;; Check if file has a feature but that feature is not enabled
(when-let [not-supported (first not-supported)]
(ex/raise :type :restriction
:code :file-feature-mismatch
:feature not-supported
:hint (str/ffmt "file feature '%' not enabled" not-supported))))
enabled-features))
(defn check-teams-compatibility!
[{source-features :features} {destination-features :features}]

View File

@@ -382,21 +382,21 @@
[:set-group-path [:vector :string]]
[:set-group-fname :string]]]
[:move-token-set-before
[:map {:title "MoveTokenSetBefore"}
[:type [:= :move-token-set-before]]
[:move-token-set
[:map {:title "MoveTokenSet"}
[:type [:= :move-token-set]]
[:from-path [:vector :string]]
[:to-path [:vector :string]]
[:before-path [:maybe [:vector :string]]]
[:before-group? [:maybe :boolean]]]]
[:before-group [:maybe :boolean]]]]
[:move-token-set-group-before
[:map {:title "MoveTokenSetGroupBefore"}
[:type [:= :move-token-set-group-before]]
[:move-token-set-group
[:map {:title "MoveTokenSetGroup"}
[:type [:= :move-token-set-group]]
[:from-path [:vector :string]]
[:to-path [:vector :string]]
[:before-path [:maybe [:vector :string]]]
[:before-group? [:maybe :boolean]]]]
[:before-group [:maybe :boolean]]]]
[:set-token-theme
[:map {:title "SetTokenThemeChange"}
@@ -1051,17 +1051,17 @@
(ctob/ensure-tokens-lib)
(ctob/rename-set-group set-group-path set-group-fname)))))
(defmethod process-change :move-token-set-before
[data {:keys [from-path to-path before-path before-group?] :as changes}]
(defmethod process-change :move-token-set
[data {:keys [from-path to-path before-path before-group] :as changes}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/move-set from-path to-path before-path before-group?))))
(ctob/move-set from-path to-path before-path before-group))))
(defmethod process-change :move-token-set-group-before
[data {:keys [from-path to-path before-path before-group?]}]
(defmethod process-change :move-token-set-group
[data {:keys [from-path to-path before-path before-group]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/move-set-group from-path to-path before-path before-group?))))
(ctob/move-set-group from-path to-path before-path before-group))))
;; === Operations

View File

@@ -809,34 +809,34 @@
(update :undo-changes conj {:type :rename-token-set-group :set-group-path undo-path :set-group-fname undo-fname})
(apply-changes-local))))
(defn move-token-set-before
(defn move-token-set
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?] :as opts}]
(-> changes
(update :redo-changes conj {:type :move-token-set-before
(update :redo-changes conj {:type :move-token-set
:from-path from-path
:to-path to-path
:before-path before-path
:before-group? before-group?})
(update :undo-changes conj {:type :move-token-set-before
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group? prev-before-group?})
:before-group prev-before-group?})
(apply-changes-local)))
(defn move-token-set-group-before
(defn move-token-set-group
[changes {:keys [from-path to-path before-path before-group? prev-before-path prev-before-group?]}]
(-> changes
(update :redo-changes conj {:type :move-token-set-group-before
(update :redo-changes conj {:type :move-token-set-group
:from-path from-path
:to-path to-path
:before-path before-path
:before-group? before-group?})
(update :undo-changes conj {:type :move-token-set-group-before
:before-group before-group?})
(update :undo-changes conj {:type :move-token-set-group
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group? prev-before-group?})
:before-group prev-before-group?})
(apply-changes-local)))
(defn set-tokens-lib

View File

@@ -283,6 +283,22 @@
:else
(get-root-frame objects (:frame-id frame)))))
(defn get-parent-frame
"Similar to `get-frame, but always return the parent frame. When root
frame is provided, then itself is returned."
[objects shape-or-id]
(cond
(map? shape-or-id)
(get objects (dm/get-prop shape-or-id :frame-id))
(= uuid/zero shape-or-id)
(get objects uuid/zero)
:else
(some->> shape-or-id
(get objects)
(get-frame objects))))
(defn valid-frame-target?
[objects parent-id shape-id]
(let [shape (get objects shape-id)]

View File

@@ -28,17 +28,15 @@
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.shadow :as ctss]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[clojure.set :as set]
[cuerdas.core :as str]))
#?(:cljs (l/set-level! :info))
(declare ^:private available-migrations)
(declare ^:private migration-up-index)
(declare ^:private migration-down-index)
(declare available-migrations)
(def version cfd/version)
@@ -50,7 +48,10 @@
[file]
(or (nil? (:version file))
(not= cfd/version (:version file))
(not= available-migrations (:migrations file))))
(boolean
(->> (:migrations file #{})
(set/difference available-migrations)
(not-empty)))))
(def xf:map-name
(map :name))
@@ -97,13 +98,13 @@
(if (nil? migrations)
(generate-migrations-from-version version)
migrations)))
(migrate)
(update :features (fnil into #{}) (deref cfeat/*new*))
;; NOTE: in some future we can consider to apply
;; a migration to the whole database and remove
;; this code from this function that executes on
;; each file migration operation
(update :features cfeat/migrate-legacy-features)))))
(update :features cfeat/migrate-legacy-features)
(migrate)))))
(defn migrated?
[file]
@@ -120,9 +121,9 @@
(into [] shapes)
shapes))))
(update-page [page]
(update page :objects update-vals update-object))]
(update page :objects d/update-vals update-object))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
(defmethod migrate-data "legacy-3"
[data _]
@@ -173,9 +174,9 @@
(fix-empty-points)))
(update-page [page]
(update page :objects update-vals update-object))]
(update page :objects d/update-vals update-object))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
;; Put the id of the local file in :component-file in instances of
;; local components
@@ -188,9 +189,9 @@
object))
(update-page [page]
(update page :objects update-vals update-object))]
(update page :objects d/update-vals update-object))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
;; Fixes issues with selrect/points for shapes with width/height =
;; 0 (line-like paths)
@@ -213,11 +214,11 @@
shape))
(update-container [container]
(update container :objects update-vals fix-line-paths))]
(update container :objects d/update-vals fix-line-paths))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
;; Remove interactions pointing to deleted frames
(defmethod migrate-data "legacy-7"
@@ -228,9 +229,9 @@
(filterv #(get-in page [:objects (:destination %)]) interactions))))
(update-page [page]
(update page :objects update-vals (partial update-object page)))]
(update page :objects d/update-vals (partial update-object page)))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
;; Remove groups without any shape, both in pages and components
(defmethod migrate-data "legacy-8"
@@ -270,8 +271,8 @@
(assoc container :objects objects)))))]
(-> data
(update :pages-index update-vals clean-container)
(update :components update-vals clean-container))))
(update :pages-index d/update-vals clean-container)
(d/update-when :components d/update-vals clean-container))))
(defmethod migrate-data "legacy-9"
[data _]
@@ -305,7 +306,7 @@
[data _]
(letfn [(update-page [page]
(d/update-in-when page [:objects uuid/zero] dissoc :points :selrect))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
(defmethod migrate-data "legacy-11"
[data _]
@@ -319,7 +320,7 @@
(update page :objects (fn [objects]
(update-vals objects (partial update-object objects)))))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
(defmethod migrate-data "legacy-12"
[data _]
@@ -329,9 +330,9 @@
(assoc :size nil)))
(update-page [page]
(d/update-in-when page [:options :saved-grids] update-vals update-grid))]
(d/update-in-when page [:options :saved-grids] d/update-vals update-grid))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
;; Add rx and ry to images
(defmethod migrate-data "legacy-13"
@@ -349,9 +350,9 @@
(fix-radius)))
(update-page [page]
(update page :objects update-vals update-object))]
(update page :objects d/update-vals update-object))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
(defmethod migrate-data "legacy-14"
[data _]
@@ -381,8 +382,8 @@
container))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-16"
[data _]
@@ -424,11 +425,11 @@
(assign-fills)))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-17"
[data _]
@@ -453,11 +454,11 @@
(assoc :fills [])))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
;; Remove position-data to solve a bug with the text positioning
(defmethod migrate-data "legacy-18"
@@ -468,11 +469,11 @@
(dissoc :position-data)))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-19"
[data _]
@@ -484,11 +485,11 @@
(dissoc :position-data)))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-25"
[data _]
@@ -500,10 +501,10 @@
(update :selrect grc/make-rect)
(cts/create-shape))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-26"
[data _]
@@ -516,11 +517,11 @@
(assoc :transform-inverse (gmt/matrix))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-27"
[data _]
@@ -547,11 +548,11 @@
(dissoc :saved-component-root?))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-28"
[data _]
@@ -576,8 +577,8 @@
(d/update-when container :objects #(update-vals % (partial update-object %))))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-29"
[data _]
@@ -608,11 +609,11 @@
(update :content #(txt/transform-nodes invalid-node? fix-node %)))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-31"
[data _]
@@ -623,10 +624,10 @@
(dissoc :use-for-thumbnail?))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-32"
[data _]
@@ -641,11 +642,11 @@
object)))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-33"
[data _]
@@ -663,9 +664,9 @@
object))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container))))
(update :pages-index d/update-vals update-container))))
(defmethod migrate-data "legacy-34"
[data _]
@@ -675,10 +676,10 @@
(dissoc object :x :y :width :height)
object))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-36"
[data _]
@@ -688,8 +689,8 @@
(dissoc objects nil)
objects))))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-37"
[data _]
@@ -717,11 +718,11 @@
shape)))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(d/update-when container :objects d/update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-39"
[data _]
@@ -739,11 +740,11 @@
shape))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(d/update-when container :objects d/update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-40"
[data _]
@@ -763,11 +764,11 @@
shape))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(d/update-when container :objects d/update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-41"
[data _]
@@ -796,11 +797,11 @@
shape))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(d/update-when container :objects d/update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-42"
[data _]
@@ -813,11 +814,11 @@
object))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(def ^:private valid-fill?
(sm/lazy-validator ::cts/fill))
@@ -842,14 +843,11 @@
object))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(def ^:private valid-shadow?
(sm/lazy-validator ::ctss/shadow))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-44"
[data _]
@@ -862,14 +860,14 @@
(update-object [object]
(let [xform (comp (map fix-shadow)
(filter valid-shadow?))]
(filter ctss/valid-shadow?))]
(d/update-when object :shadow #(into [] xform %))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-45"
[data _]
@@ -882,9 +880,9 @@
:parent-id parent-id)))
(update-container [container]
(d/update-when container :objects update-vals fix-shape))]
(d/update-when container :objects d/update-vals fix-shape))]
(-> data
(update :pages-index update-vals update-container))))
(update :pages-index d/update-vals update-container))))
(defmethod migrate-data "legacy-46"
[data _]
@@ -892,10 +890,10 @@
(dissoc object :thumbnail))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-47"
[data _]
@@ -916,9 +914,9 @@
shape)))
(update-page [page]
(d/update-when page :objects update-vals (partial fix-shape page)))]
(d/update-when page :objects d/update-vals (partial fix-shape page)))]
(-> data
(update :pages-index update-vals update-page))))
(update :pages-index d/update-vals update-page))))
(defmethod migrate-data "legacy-48"
[data _]
@@ -930,9 +928,9 @@
shape)))
(update-page [page]
(d/update-when page :objects update-vals fix-shape))]
(d/update-when page :objects d/update-vals fix-shape))]
(-> data
(update :pages-index update-vals update-page))))
(update :pages-index d/update-vals update-page))))
;; Remove hide-in-viewer for shapes that are origin or destination of an interaction
(defmethod migrate-data "legacy-49"
@@ -950,9 +948,9 @@
(mapcat :interactions)
(map :destination)
(set))]
(update page :objects update-vals (partial update-object destinations))))]
(update page :objects d/update-vals (partial update-object destinations))))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
;; This migration mainly fixes paths with curve-to segments
;; without :c1x :c1y :c2x :c2y properties. Additionally, we found a
@@ -995,11 +993,11 @@
update-container
(fn [page]
(d/update-when page :objects update-vals update-shape))]
(d/update-when page :objects d/update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(def ^:private valid-color?
(sm/lazy-validator ::ctc/color))
@@ -1019,9 +1017,9 @@
shape))
(update-page [page]
(d/update-when page :objects update-vals update-shape))]
(d/update-when page :objects d/update-vals update-shape))]
(update data :pages-index update-vals update-page)))
(update data :pages-index d/update-vals update-page)))
(defmethod migrate-data "legacy-53"
@@ -1037,15 +1035,15 @@
(update-shape [shape]
(let [xform (comp (map fix-shadow)
(filter valid-shadow?))]
(filter ctss/valid-shadow?))]
(d/update-when shape :shadow #(into [] xform %))))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(d/update-when container :objects d/update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
;; This migration moves page options to the page level
(defmethod migrate-data "legacy-55"
@@ -1097,11 +1095,11 @@
(update :content (partial txt/transform-nodes identity fix-fills)))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-57"
@@ -1128,7 +1126,7 @@
(-> data
(update :pages (fn [pages] (into [] (remove nil?) pages)))
(update :pages-index dissoc nil)
(update :pages-index update-vals update-page))))
(update :pages-index d/update-vals update-page))))
(defmethod migrate-data "legacy-59"
[data _]
@@ -1139,11 +1137,11 @@
(d/update-when shape :touched #(into #{} (map fix-touched) %)))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(d/update-when container :objects d/update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-62"
[data _]
@@ -1176,7 +1174,7 @@
;; so the relevant objects are inside the component
(d/update-when component :objects remove-cycles))]
(update data :components update-vals update-component)))
(d/update-when data :components d/update-vals update-component)))
(defmethod migrate-data "legacy-65"
[data _]
@@ -1187,14 +1185,14 @@
update-page
(fn [page]
(-> (update-object page)
(update :objects update-vals update-object)))]
(update :objects d/update-vals update-object)))]
(-> data
(update-object)
(d/update-when :pages-index update-vals update-page)
(d/update-when :colors update-vals update-object)
(d/update-when :typographies update-vals update-object)
(d/update-when :components update-vals update-object))))
(update :pages-index d/update-vals update-page)
(d/update-when :colors d/update-vals update-object)
(d/update-when :typographies d/update-vals update-object)
(d/update-when :components d/update-vals update-object))))
(defmethod migrate-data "legacy-66"
[data _]
@@ -1208,11 +1206,11 @@
object))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "legacy-67"
[data _]
@@ -1220,38 +1218,13 @@
(d/update-when object :shadow #(into [] (reverse %))))
(update-container [container]
(d/update-when container :objects update-vals update-object))]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "Ensure hidden theme"
[data _]
(letfn [(update-tokens-lib [tokens-lib]
(let [hidden-theme (ctob/get-hidden-theme tokens-lib)]
(if (nil? hidden-theme)
(ctob/add-theme tokens-lib (ctob/make-hidden-token-theme))
tokens-lib)))]
(if (contains? data :tokens-lib)
(update data :tokens-lib update-tokens-lib)
data)))
(defmethod migrate-data "Add token theme id"
[data _]
(letfn [(update-tokens-lib [tokens-lib]
(let [themes (ctob/get-themes tokens-lib)]
(reduce (fn [lib theme]
(if (:id theme)
lib
(ctob/update-theme lib (:group theme) (:name theme) #(assoc % :id (str (uuid/next))))))
tokens-lib
themes)))]
(if (contains? data :tokens-lib)
(update data :tokens-lib update-tokens-lib)
data)))
(defmethod migrate-data "Remove tokens from groups"
(defmethod migrate-data "0001-remove-tokens-from-groups"
[data _]
(letfn [(update-object [object]
(cond-> object
@@ -1263,8 +1236,55 @@
(dissoc :applied-tokens)))
(update-page [page]
(d/update-when page :objects update-vals update-object))]
(update data :pages-index update-vals update-page)))
(d/update-when page :objects d/update-vals update-object))]
(update data :pages-index d/update-vals update-page)))
(defmethod migrate-data "0002-clean-shape-interactions"
[data _]
(let [decode-fn (sm/decoder ctsi/schema:interaction sm/json-transformer)
validate-fn (sm/validator ctsi/schema:interaction)
xform
(comp
(map decode-fn)
(filter validate-fn))
update-object
(fn [object]
(d/update-when object :interactions
(fn [interactions]
(into [] xform interactions))))
update-container
(fn [container]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "0003-fix-root-shape"
[data _]
(letfn [(update-object [shape]
(if (= (:id shape) uuid/zero)
(-> shape
(assoc :parent-id uuid/zero)
(assoc :frame-id uuid/zero)
;; We explicitly dissoc them and let the shape-setup
;; to regenerate it with valid values.
(dissoc :selrect)
(dissoc :points)
(cts/setup-shape))
shape))
(update-container [container]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container)
(d/without-nils))))
(def available-migrations
(into (d/ordered-set)
@@ -1320,6 +1340,6 @@
"legacy-65"
"legacy-66"
"legacy-67"
"Ensure hidden theme"
"Add token theme id"
"Remove tokens from groups"]))
"0001-remove-tokens-from-groups"
"0002-clean-shape-interactions"
"0003-fix-root-shape"]))

View File

@@ -124,7 +124,8 @@
;; TODO: deprecate this flag and consolidate the code
:export-file-v3
:render-wasm-dpr
:hide-release-modal})
:hide-release-modal
:subscriptions-old})
(def all-flags
(set/union email login varia))

View File

@@ -36,6 +36,8 @@
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
;; Add uuids here to filter logs to only show specific shapes or containers (and all shapes
;; contained in them).
(def log-shape-ids #{})
(def log-container-ids #{})
@@ -293,6 +295,7 @@
(declare generate-detach-recursive)
(declare generate-advance-nesting-level)
(declare generate-detach-immediate)
(defn generate-detach-instance
"Generate changes to remove the links between a shape and all its children
@@ -306,60 +309,84 @@
(defn- generate-detach-recursive
[changes container libraries shape-id first component-root?]
(let [shape (ctn/get-shape container shape-id)]
(shape-log :trace shape-id container
:msg " Processing" :shape-id shape-id)
(if (and (ctk/instance-head? shape) (not first))
; Subinstances are not detached
(cond-> changes
component-root?
; If the initial shape was component-root, first level subinstances are converted in top instances
(pcb/update-shapes [shape-id] #(assoc % :component-root true))
(pcb/update-shapes [shape-id] #(do (log/trace :msg " -> promote to root")
(assoc % :component-root true)))
:always
; First level subinstances of a detached component can't have swap-slot
(pcb/update-shapes [shape-id] ctk/remove-swap-slot)
(pcb/update-shapes [shape-id] #(do (log/trace :msg " -> remove swap-slot")
(ctk/remove-swap-slot %)))
(nil? (ctk/get-swap-slot shape))
; Near shape-refs need to be advanced one level (except if the head is already swapped)
; Near shape-ref of shape and children need to be advanced one level
; (except if the head is already swapped)
(generate-advance-nesting-level nil container libraries (:id shape)))
;; Otherwise, detach the shape and all children
(let [children-ids (:shapes shape)]
(log/trace :msg " -> detach")
(reduce #(generate-detach-recursive %1 container libraries %2 false component-root?)
(pcb/update-shapes changes [(:id shape)] ctk/detach-shape)
children-ids)))))
(defn- generate-advance-nesting-level
[changes file container libraries shape-id]
(let [children (cfh/get-children-with-self (:objects container) shape-id)
skip-near (fn [changes shape]
(let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})]
(cond-> changes
(some? (:shape-ref ref-shape))
(pcb/update-shapes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape)))
(log/trace :msg " -> advance-nesting-level")
(let [detached-ids (atom #{})
children (cfh/get-children-with-self (:objects container) shape-id) ;; TODO: this function should be refactored to be a recursive tree traversal.
skip-near (fn [changes shape] ;; this way we could shake the tree more easily when detaching shapes
(shape-log :trace (:id shape) container ;; and perhaps even allow to recover nested instances that have been
:msg " * advancing" :shape-id (:id shape)) ;; swapped and so we can access the main instance again.
(if (contains? @detached-ids (:id shape))
(do (log/trace :msg " (detached)")
changes)
(let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})]
(cond-> changes
(some? (:shape-ref ref-shape))
(pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (advanced)")
(assoc % :shape-ref (:shape-ref ref-shape))))
;; When advancing level, the normal touched groups (not swap slots) of the
;; ref-shape must be merged into the current shape, because they refer to
;; the new referenced shape.
(some? ref-shape)
(pcb/update-shapes
[(:id shape)]
#(assoc % :touched
(clojure.set/union (:touched shape)
(ctk/normal-touched-groups ref-shape))))
;; When advancing level, the normal touched groups (not swap slots) of the
;; ref-shape must be merged into the current shape, because they refer to
;; the new referenced shape.
(some? ref-shape)
(pcb/update-shapes
[(:id shape)]
#(do (log/trace :msg " (merge touched)")
(assoc % :touched
(clojure.set/union (:touched shape)
(ctk/normal-touched-groups ref-shape)))))
;; Swap slot must also be copied if the current shape has not any,
;; except if this is the first level subcopy.
(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)))
;; Swap slot must also be copied if the current shape has not any,
;; except if this is the first level subcopy.
(and (some? (ctk/get-swap-slot ref-shape))
(nil? (ctk/get-swap-slot shape))
(not= (:id shape) shape-id))
(pcb/update-shapes [(:id shape)] #(do (log/trace :msg " (got swap-slot)")
(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))))]
;; 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 and all its
;; children (and add to detached-ids so they are not processed again).
(nil? ref-shape)
(generate-detach-immediate container (:id shape) detached-ids)))))]
(reduce skip-near changes children)))
(defn- generate-detach-immediate
[changes container shape-id detached-ids]
(let [shape-and-children (cfh/get-children-ids-with-self (:objects container) shape-id)]
(log/trace :msg " (cannot advance; detach shape and children)")
(swap! detached-ids #(into % shape-and-children))
(pcb/update-shapes changes shape-and-children ctk/detach-shape)))
(defn prepare-restore-component
([changes library-data component-id current-page]
(let [component (ctkl/get-deleted-component library-data component-id)
@@ -1639,7 +1666,21 @@
(if (and (empty? roperations) (empty? applied-tokens))
changes
(let [all-parents (cfh/get-parent-ids (:objects container)
(:id dest-shape))]
(:id dest-shape))
;; Sync tokens of attributes ignored above.
;; FIXME: this probably may be merged with the other calculation
;; of applied tokens, below, and to the calculation only once
;; for all sync-attrs.
applied-tokens (reduce (fn [applied-tokens attr]
(let [attr-group (get ctk/sync-attrs attr)
token-attrs (cto/shape-attr->token-attrs attr)]
(if (not (and (touched attr-group)
omit-touched?))
(into applied-tokens token-attrs)
applied-tokens)))
applied-tokens
ctk/swap-keep-attrs)]
(cond-> changes
(seq roperations)
(-> (update :redo-changes conj (make-change

View File

@@ -1,11 +1,21 @@
;; 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.common.logic.tokens
(:require
[app.common.files.changes-builder :as pcb]
[app.common.types.tokens-lib :as ctob]))
(defn generate-update-active-sets
"Copy the active sets from the currently active themes and move them to the hidden token theme and update the theme with `update-theme-fn`.
Use this for managing sets active state without having to modify a user created theme (\"no themes selected\" state in the ui)."
"Copy the active sets from the currently active themes and move them
to the hidden token theme and update the theme with
`update-theme-fn`.
Use this for managing sets active state without having to modify a
user created theme (\"no themes selected\" state in the ui)."
[changes tokens-lib update-theme-fn]
(let [prev-active-token-themes (ctob/get-active-theme-paths tokens-lib)
active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
@@ -21,7 +31,8 @@
hidden-token-theme))))
(defn generate-toggle-token-set
"Toggle a token set at `set-name` in `tokens-lib` without modifying a user theme."
"Toggle a token set at `set-name` in `tokens-lib` without modifying a
user theme."
[changes tokens-lib set-name]
(generate-update-active-sets changes tokens-lib #(ctob/toggle-set % set-name)))
@@ -49,12 +60,14 @@
:or {collapsed-paths #{}}}]
(let [tree (-> (ctob/get-set-tree tokens-lib)
(ctob/walk-sets-tree-seq :skip-children-pred #(contains? collapsed-paths %)))
from (nth tree from-index)
to (nth tree to-index)
before (case position
:top to
:bot (nth tree (inc to-index) nil)
:center nil)
prev-before (if (:group? from)
(->> (drop (inc from-index) tree)
(filter (fn [element]
@@ -68,6 +81,7 @@
(= :bot position)
(:group? to)
(not (get collapsed-paths (:path to)))))
from-path (:path from)
to-parent-path (if drop-as-direct-group-child?
(:path to)
@@ -113,15 +127,15 @@
(defn generate-move-token-set
"Create changes for dropping a token set or token set.
Throws for impossible moves."
[changes tokens-lib drop-opts]
(if-let [drop-opts' (calculate-move-token-set-or-set-group tokens-lib drop-opts)]
(pcb/move-token-set-before changes drop-opts')
[changes tokens-lib params]
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
(pcb/move-token-set changes params)
changes))
(defn generate-move-token-set-group
"Create changes for dropping a token set or token set group.
Throws for impossible moves"
[changes tokens-lib drop-opts]
(if-let [drop-opts' (calculate-move-token-set-or-set-group tokens-lib drop-opts)]
(pcb/move-token-set-group-before changes drop-opts')
[changes tokens-lib params]
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
(pcb/move-token-set-group changes params)
changes))

View File

@@ -390,14 +390,22 @@
(register! :merge (mu/-merge))
(register! :union (mu/-union))
(def uuid-rx
#"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
(defn parse-uuid
(defn- parse-uuid
[s]
(if (string? s)
(some->> (re-matches uuid-rx s) uuid/uuid)
s))
(if (uuid? s)
s
(if (str/empty? s)
nil
(try
(uuid/parse s)
(catch #?(:clj Exception :cljs :default) _cause
s)))))
(defn- encode-uuid
[v]
(if (uuid? v)
(str v)
v))
(register!
{:type ::uuid
@@ -409,8 +417,8 @@
:gen/gen (sg/uuid)
:decode/string parse-uuid
:decode/json parse-uuid
:encode/string str
:encode/json str
:encode/string encode-uuid
:encode/json encode-uuid
::oapi/type "string"
::oapi/format "uuid"}})
@@ -856,7 +864,7 @@
choices))]
{:pred pred
:type-properties
{:title "contains"
{:title "contains any"
:description "contains predicate"}}))})
(register!
@@ -1019,26 +1027,26 @@
(def valid-text?
(validator ::text))
(def check-safe-int!
(def check-safe-int
(check-fn ::safe-int))
(def check-set-of-strings!
(def check-set-of-strings
(check-fn ::set-of-strings))
(def check-email!
(def check-email
(check-fn ::email))
(def check-uuid!
(def check-uuid
(check-fn ::uuid :hint "expected valid uuid instance"))
(def check-string!
(def check-string
(check-fn :string :hint "expected string"))
(def check-coll-of-uuid!
(def check-coll-of-uuid
(check-fn ::coll-of-uuid))
(def check-set-of-uuid!
(def check-set-of-uuid
(check-fn ::set-of-uuid))
(def check-set-of-emails!
(def check-set-of-emails
(check-fn [::set ::email]))

View File

@@ -60,6 +60,7 @@
(let [smallest (-> params :shrunk :smallest vec)]
(println)
(println "Condition failed with the following params:")
(println "Seed:" (:seed params))
(println)
(pp/pprint smallest)))

View File

@@ -543,14 +543,23 @@
;; We can always move the children to the parent they already have.
;; But if we are pasting, those are new items, so it is considered a change
no-changes?
(and (->> children (every? #(= parent-id (:parent-id %))))
(and (every? #(= parent-id (:parent-id %)) children)
(not pasting?))
all-main?
(->> children (every? #(ctk/main-instance? %)))]
(every? ctk/main-instance? children)
any-main-descendant
(some
(fn [shape]
(some ctk/main-instance? (cfh/get-children-with-self objects (:id shape))))
children)]
(if (or no-changes?
(and (not (invalid-structure-for-component? objects parent children pasting? libraries))
;; If we are moving into a variant-container, all the items should be main
(or all-main? (not (ctk/is-variant-container? parent)))))
(or all-main? (not (ctk/is-variant-container? parent)))
;; If we are moving into a main component, no descendant can be main
(or (nil? any-main-descendant) (not (ctk/main-instance? parent)))))
[parent-id (get-frame parent-id)]
(recur (:parent-id parent) objects children pasting? libraries))))))

View File

@@ -211,7 +211,7 @@
[:interactions {:optional true}
[:vector {:gen/max 2} ::ctsi/interaction]]
[:shadow {:optional true}
[:vector {:gen/max 1} ::ctss/shadow]]
[:vector {:gen/max 1} ctss/schema:shadow]]
[:blur {:optional true} ::ctsb/blur]
[:grow-type {:optional true}
[::sm/one-of grow-types]]

View File

@@ -7,7 +7,6 @@
(ns app.common.types.shape.interactions
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.bounds :as gsb]
@@ -110,13 +109,27 @@
(def check-animation!
(sm/check-fn schema:animation))
(def schema:interaction-attrs
[:map {:title "InteractionAttrs"}
[:action-type {:optional true} [::sm/one-of action-types]]
[:event-type {:optional true} [::sm/one-of event-types]]
[:destination {:optional true} [:maybe ::sm/uuid]]
[:preserve-scroll {:optional true} :boolean]
[:animation {:optional true} schema:animation]
[:overlay-position {:optional true} ::gpt/point]
[:overlay-pos-type {:optional true} [::sm/one-of overlay-positioning-types]]
[:close-click-outside {:optional true} :boolean]
[:background-overlay {:optional true} :boolean]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]
[:url {:optional true} :string]])
(def schema:navigate-interaction
[:map
[:action-type [:= :navigate]]
[:event-type [::sm/one-of event-types]]
[:destination {:optional true} [:maybe ::sm/uuid]]
[:preserve-scroll {:optional true} :boolean]
[:animation {:optional true} ::animation]])
[:animation {:optional true} schema:animation]])
(def schema:open-overlay-interaction
[:map
@@ -127,7 +140,7 @@
[:destination {:optional true} [:maybe ::sm/uuid]]
[:close-click-outside {:optional true} :boolean]
[:background-overlay {:optional true} :boolean]
[:animation {:optional true} ::animation]
[:animation {:optional true} schema:animation]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
(def schema:toggle-overlay-interaction
@@ -139,7 +152,7 @@
[:destination {:optional true} [:maybe ::sm/uuid]]
[:close-click-outside {:optional true} :boolean]
[:background-overlay {:optional true} :boolean]
[:animation {:optional true} ::animation]
[:animation {:optional true} schema:animation]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
(def schema:close-overlay-interaction
@@ -147,7 +160,7 @@
[:action-type [:= :close-overlay]]
[:event-type [::sm/one-of event-types]]
[:destination {:optional true} [:maybe ::sm/uuid]]
[:animation {:optional true} ::animation]
[:animation {:optional true} schema:animation]
[:position-relative-to {:optional true} [:maybe ::sm/uuid]]])
(def schema:prev-scren-interaction
@@ -162,25 +175,25 @@
[:url :string]])
(def schema:interaction
[:multi {:dispatch :action-type
:title "Interaction"
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
(sg/generator schema:open-overlay-interaction)
(sg/generator schema:close-overlay-interaction)
(sg/generator schema:toggle-overlay-interaction)
(sg/generator schema:prev-scren-interaction)
(sg/generator schema:open-url-interaction))
:decode/json #(update % :action-type keyword)}
[:navigate schema:navigate-interaction]
[:open-overlay schema:open-overlay-interaction]
[:toggle-overlay schema:toggle-overlay-interaction]
[:close-overlay schema:close-overlay-interaction]
[:prev-screen schema:prev-scren-interaction]
[:open-url schema:open-url-interaction]])
[:and {:title "Interaction"
:gen/gen (sg/one-of (sg/generator schema:navigate-interaction)
(sg/generator schema:open-overlay-interaction)
(sg/generator schema:close-overlay-interaction)
(sg/generator schema:toggle-overlay-interaction)
(sg/generator schema:prev-scren-interaction)
(sg/generator schema:open-url-interaction))}
schema:interaction-attrs
[:multi {:dispatch :action-type}
[:navigate schema:navigate-interaction]
[:open-overlay schema:open-overlay-interaction]
[:toggle-overlay schema:toggle-overlay-interaction]
[:close-overlay schema:close-overlay-interaction]
[:prev-screen schema:prev-scren-interaction]
[:open-url schema:open-url-interaction]]])
(sm/register! ::interaction schema:interaction)
(def check-interaction!
(def check-interaction
(sm/check-fn schema:interaction))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -203,18 +216,13 @@
(defn set-event-type
[interaction event-type shape]
(dm/assert!
"Should be an interraction map"
(check-interaction! interaction))
(assert (check-interaction interaction))
(assert (contains? event-types event-type)
"should be a valid event type")
(dm/assert!
"Should be a valid event type"
(contains? event-types event-type))
(dm/assert!
"The `:after-delay` event type incompatible with not frame shapes"
(or (not= event-type :after-delay)
(cfh/frame-shape? shape)))
(assert (or (not= event-type :after-delay)
(cfh/frame-shape? shape))
"the `:after-delay` event type incompatible with not frame shapes")
(if (= (:event-type interaction) event-type)
interaction
@@ -230,14 +238,9 @@
(defn set-action-type
[interaction action-type]
(dm/assert!
"Should be an interraction map"
(check-interaction! interaction))
(dm/assert!
"Should be a valid event type"
(contains? action-types action-type))
(assert (check-interaction interaction))
(assert (contains? action-types action-type)
"Should be a valid event type")
(let [new-interaction
(if (= (:action-type interaction) action-type)
@@ -284,18 +287,10 @@
(defn set-delay
[interaction delay]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected valid delay"
(sm/check-safe-int! delay))
(dm/assert!
"expected compatible interaction event type"
(has-delay interaction))
(assert (check-interaction interaction))
(assert (sm/check-safe-int delay))
(assert (has-delay interaction)
"expected compatible interaction event type")
(assoc interaction :delay delay))
@@ -315,14 +310,9 @@
(defn set-destination
[interaction destination]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected compatible interaction event type"
(has-destination interaction))
(assert (check-interaction interaction))
(assert (has-destination interaction)
"expected compatible interaction event type")
(cond-> interaction
:always
@@ -340,17 +330,11 @@
(defn set-preserve-scroll
[interaction preserve-scroll]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected boolean for `preserve-scroll`"
(boolean? preserve-scroll))
(dm/assert!
"expected compatible interaction map with preserve-scroll"
(has-preserve-scroll interaction))
(assert (check-interaction interaction))
(assert (boolean? preserve-scroll)
"expected boolean for `preserve-scroll`")
(assert (has-preserve-scroll interaction)
"expected compatible interaction map with preserve-scroll")
(assoc interaction :preserve-scroll preserve-scroll))
@@ -361,17 +345,11 @@
(defn set-url
[interaction url]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected a string for `url`"
(string? url))
(dm/assert!
"expected compatible interaction map with url param"
(has-url interaction))
(assert (check-interaction interaction))
(assert (string? url)
"expected a string for `url`")
(assert (has-url interaction)
"expected compatible interaction map with url param")
(assoc interaction :url url))
@@ -382,17 +360,12 @@
(defn set-overlay-pos-type
[interaction overlay-pos-type shape objects]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(assert (check-interaction interaction))
(dm/assert!
"expected valid overlay positioning type"
(contains? overlay-positioning-types overlay-pos-type))
(dm/assert!
"expected compatible interaction map"
(has-overlay-opts interaction))
(assert (contains? overlay-positioning-types overlay-pos-type)
"expected valid overlay positioning type")
(assert (has-overlay-opts interaction)
"expected compatible interaction map")
(assoc interaction
:overlay-pos-type overlay-pos-type
@@ -403,17 +376,11 @@
(defn toggle-overlay-pos-type
[interaction overlay-pos-type shape objects]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected valid overlay positioning type"
(contains? overlay-positioning-types overlay-pos-type))
(dm/assert!
"expected compatible interaction map"
(has-overlay-opts interaction))
(assert (check-interaction interaction))
(assert (contains? overlay-positioning-types overlay-pos-type)
"expected valid overlay positioning type")
(assert (has-overlay-opts interaction)
"expected compatible interaction map")
(let [new-pos-type (if (= (:overlay-pos-type interaction) overlay-pos-type)
:manual
@@ -427,17 +394,12 @@
(defn set-overlay-position
[interaction overlay-position]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(assert (check-interaction interaction))
(assert (gpt/point? overlay-position)
"expected valid overlay position")
(assert (has-overlay-opts interaction)
"expected compatible interaction map")
(dm/assert!
"expected valid overlay position"
(gpt/point? overlay-position))
(dm/assert!
"expected compatible interaction map"
(has-overlay-opts interaction))
(assoc interaction
:overlay-pos-type :manual
@@ -446,52 +408,34 @@
(defn set-close-click-outside
[interaction close-click-outside]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected boolean value for `close-click-outside`"
(boolean? close-click-outside))
(dm/assert!
"expected compatible interaction map"
(has-overlay-opts interaction))
(assert (check-interaction interaction))
(assert (boolean? close-click-outside)
"expected boolean value for `close-click-outside`")
(assert (has-overlay-opts interaction)
"expected compatible interaction map")
(assoc interaction :close-click-outside close-click-outside))
(defn set-background-overlay
[interaction background-overlay]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected boolean value for `background-overlay`"
(boolean? background-overlay))
(dm/assert!
"expected compatible interaction map"
(has-overlay-opts interaction))
(assert (check-interaction interaction))
(assert (boolean? background-overlay)
"expected boolean value for `background-overlay`")
(assert (has-overlay-opts interaction)
"expected compatible interaction map")
(assoc interaction :background-overlay background-overlay))
(defn set-position-relative-to
[interaction position-relative-to]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected valid uuid for `position-relative-to`"
(or (nil? position-relative-to)
(uuid? position-relative-to)))
(dm/assert!
"expected compatible interaction map"
(has-overlay-opts interaction))
(assert (check-interaction interaction))
(assert (or (nil? position-relative-to)
(uuid? position-relative-to))
"expected valid uuid for `position-relative-to`")
(assert (has-overlay-opts interaction)
"expected compatible interaction map")
(assoc interaction :position-relative-to position-relative-to))
@@ -519,13 +463,9 @@
frame-offset] ;; if this interaction starts in a frame opened
;; on another interaction, this is the position
;; of that frame
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected compatible interaction map"
(has-overlay-opts interaction))
(assert (check-interaction interaction))
(assert (has-overlay-opts interaction)
"expected compatible interaction map")
(let [;; When the interactive item is inside a nested frame we need to add to the offset the position
;; of the parent-frame otherwise the position won't match
@@ -617,22 +557,15 @@
(defn set-animation-type
[interaction animation-type]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected valid value for `animation-type`"
(or (nil? animation-type)
(contains? animation-types animation-type)))
(dm/assert!
"expected interaction map compatible with animation"
(has-animation? interaction))
(dm/assert!
"expected allowed animation type"
(allowed-animation? (:action-type interaction) animation-type))
(assert (check-interaction interaction))
(assert (or (nil? animation-type)
(contains? animation-types animation-type))
"expected valid value for `animation-type`")
(assert (has-animation? interaction)
"expected interaction map compatible with animation")
(assert (allowed-animation? (:action-type interaction) animation-type)
"expected allowed animation type")
(if (= (-> interaction :animation :animation-type) animation-type)
interaction
@@ -668,17 +601,10 @@
(defn set-duration
[interaction duration]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected valid duration"
(sm/check-safe-int! duration))
(dm/assert!
"expected compatible interaction map"
(has-duration? interaction))
(assert (check-interaction interaction))
(assert (sm/check-safe-int duration))
(assert (has-duration? interaction)
"expected compatible interaction map")
(update interaction :animation assoc :duration duration))
@@ -689,17 +615,11 @@
(defn set-easing
[interaction easing]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected valid easing"
(contains? easing-types easing))
(dm/assert!
"expected compatible interaction map"
(has-easing? interaction))
(assert (check-interaction interaction))
(assert (contains? easing-types easing)
"expected valid easing")
(assert (has-easing? interaction)
"expected compatible interaction map")
(update interaction :animation assoc :easing easing))
@@ -712,17 +632,11 @@
(defn set-way
[interaction way]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected valid way"
(contains? way-types way))
(dm/assert!
"expected compatible interaction map"
(has-way? interaction))
(assert (check-interaction interaction))
(assert (contains? way-types way)
"expected valid way")
(assert (has-way? interaction)
"expected compatible interaction map")
(update interaction :animation assoc :way way))
@@ -733,26 +647,20 @@
(defn set-direction
[interaction direction]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(assert (check-interaction interaction))
(assert (contains? direction-types direction)
"expected valid direction")
(dm/assert!
"expected valid direction"
(contains? direction-types direction))
(dm/assert!
"expected compatible interaction map"
(has-direction? interaction))
(assert (has-direction? interaction)
"expected compatible interaction map")
(update interaction :animation assoc :direction direction))
(defn invert-direction
[animation]
(dm/assert!
"expected valid animation map"
(or (nil? animation)
(check-animation! animation)))
(assert (or (nil? animation)
(check-animation! animation))
"expected valid animation map")
(case (:direction animation)
:right
@@ -768,24 +676,18 @@
(defn has-offset-effect?
[interaction]
; Offset-effect is ignored in slide animations of overlay actions
;; Offset-effect is ignored in slide animations of overlay actions
(and (= (:action-type interaction) :navigate)
(= (-> interaction :animation :animation-type) :slide)))
(defn set-offset-effect
[interaction offset-effect]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected valid boolean for `offset-effect`"
(boolean? offset-effect))
(dm/assert!
"expected compatible interaction map"
(has-offset-effect? interaction))
(assert (check-interaction interaction))
(assert (boolean? offset-effect)
"expected valid boolean for `offset-effect`")
(assert (has-offset-effect? interaction)
"expected compatible interaction map")
(update interaction :animation assoc :offset-effect offset-effect))

View File

@@ -26,7 +26,9 @@
[:hidden :boolean]
[:color ::ctc/color]])
(sm/register! ::shadow schema:shadow)
(def check-shadow
(sm/check-fn schema:shadow))
(def valid-shadow?
(sm/validator schema:shadow))

View File

@@ -60,7 +60,7 @@
(token-types t))
(def token-name-ref
[:and :string [:re #"^(?!\$)([a-zA-Z0-9-$]+\.?)*(?<!\.)$"]])
[:and :string [:re #"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"]])
(defn valid-token-name-ref?
[n]
@@ -182,14 +182,27 @@
([shape-attr] (shape-attr->token-attrs shape-attr nil))
([shape-attr changed-sub-attr]
(cond
(= :fills shape-attr) #{:fill}
(and (= :strokes shape-attr) (nil? changed-sub-attr)) #{:stroke-width :stroke-color}
(= :fills shape-attr)
#{:fill}
(and (= :strokes shape-attr) (nil? changed-sub-attr))
#{:stroke-width :stroke-color}
(= :strokes shape-attr)
(cond
(some #{:stroke-color} changed-sub-attr) #{:stroke-color}
(some #{:stroke-width} changed-sub-attr) #{:stroke-width})
(and (= :layout-padding shape-attr) (seq changed-sub-attr)) changed-sub-attr
(and (= :layout-item-margin shape-attr) (seq changed-sub-attr)) changed-sub-attr
(= :layout-padding shape-attr)
(if (seq changed-sub-attr)
changed-sub-attr
#{:p1 :p2 :p3 :p4})
(= :layout-item-margin shape-attr)
(if (seq changed-sub-attr)
changed-sub-attr
#{:m1 :m2 :m3 :m4})
(border-radius-keys shape-attr) #{shape-attr}
(sizing-keys shape-attr) #{shape-attr}
(opacity-keys shape-attr) #{shape-attr}

View File

@@ -588,7 +588,7 @@
(update :group d/nilv top-level-theme-group-name)
(update :description d/nilv "")
(update :is-source d/nilv false)
(update :id d/nilv (str (uuid/next)))
(update :id #(or % (str (uuid/next))))
(update :modified-at #(or % (dt/now)))
(update :sets set)
(check-token-theme-attrs)
@@ -612,7 +612,6 @@
(get-theme-tree [_] "get a nested tree of all themes in the library")
(get-themes [_] "get an ordered sequence of all themes in the library")
(get-theme [_ group name] "get one theme looking for name")
(get-hidden-theme [_] "get the theme hidden from the user, used for managing active sets without a user created theme.")
(get-theme-groups [_] "get a sequence of group names by order")
(get-active-theme-paths [_] "get the active theme paths")
(get-active-themes [_] "get an ordered sequence of active themes in the library")
@@ -666,20 +665,20 @@
["$value" :map]
["$type" :string]]]))
(defn has-legacy-format?
(defn get-json-format
"Searches through parsed token file and returns:
- true when first node satisfies `legacy-node?` predicate
- false when first node satisfies `dtcg-node?` predicate
- nil if neither combination is found"
- `:json-format/legacy` when first node satisfies `legacy-node?` predicate
- `:json-format/dtcg` when first node satisfies `dtcg-node?` predicate
- `nil` if neither combination is found"
([data]
(has-legacy-format? data legacy-node? dtcg-node?))
(get-json-format data legacy-node? dtcg-node?))
([data legacy-node? dtcg-node?]
(let [branch? map?
children (fn [node] (vals node))
check-node (fn [node]
(cond
(legacy-node? node) true
(dtcg-node? node) false
(legacy-node? node) :json-format/legacy
(dtcg-node? node) :json-format/dtcg
:else nil))
walk (fn walk [node]
(lazy-seq
@@ -691,6 +690,10 @@
(filter some?)
first))))
(defn single-set? [data]
(and (not (contains? data "$metadata"))
(not (contains? data "$themes"))))
;; DEPRECATED
(defn walk-sets-tree-seq
"Walk sets tree as a flat list.
@@ -800,7 +803,7 @@
(map-indexed (fn [index item]
(assoc item :index index))))))
(defn flatten-nested-tokens-json
(defn- flatten-nested-tokens-json
"Recursively flatten the dtcg token structure, joining keys with '.'."
[tokens token-path]
(reduce-kv
@@ -827,6 +830,24 @@
(declare make-tokens-lib)
(defn- legacy-nodes->dtcg-nodes [sets-data]
(walk/postwalk
(fn [node]
(cond-> node
(and (map? node)
(contains? node "value")
(sequential? (get node "value")))
(update "value"
(fn [seq-value]
(map #(set/rename-keys % {"type" "$type"}) seq-value)))
(and (map? node)
(and (contains? node "type")
(contains? node "value")))
(set/rename-keys {"value" "$value"
"type" "$type"})))
sets-data))
(defprotocol ITokensLib
"A library of tokens, sets and themes."
(set-path-exists? [_ path] "if a set at `path` exists")
@@ -845,6 +866,8 @@ Will return a value that matches this schema:
(get-active-themes-set-tokens [_] "set of set names that are active in the the active themes")
(encode-dtcg [_] "Encodes library to a dtcg compatible json string")
(decode-dtcg-json [_ parsed-json] "Decodes parsed json containing tokens and converts to library")
(decode-single-set-json [_ set-name tokens] "Decodes parsed json containing single token set and converts to library")
(decode-single-set-legacy-json [_ set-name tokens] "Decodes parsed legacy json containing single token set and converts to library")
(decode-legacy-json [_ parsed-json] "Decodes parsed legacy json containing tokens and converts to library")
(get-all-tokens [_] "all tokens in the lib")
(validate [_]))
@@ -946,14 +969,21 @@ Will return a value that matches this schema:
(let [prefixed-from-path (set-full-path->set-prefixed-full-path from-path)
prev-set (get-in sets prefixed-from-path)]
(if (instance? TokenSet prev-set)
(let [prefixed-to-path (set-full-path->set-prefixed-full-path to-path)
prefixed-before-path (when before-path
(if before-group?
(mapv add-set-path-group-prefix before-path)
(set-full-path->set-prefixed-full-path before-path)))
(let [prefixed-to-path
(set-full-path->set-prefixed-full-path to-path)
prefixed-before-path
(when before-path
(if before-group?
(mapv add-set-path-group-prefix before-path)
(set-full-path->set-prefixed-full-path before-path)))
set
(assoc prev-set :name (join-set-path to-path))
reorder?
(= prefixed-from-path prefixed-to-path)
set (assoc prev-set :name (join-set-path to-path))
reorder? (= prefixed-from-path prefixed-to-path)
sets'
(if reorder?
(d/oreorder-before sets
@@ -965,6 +995,7 @@ Will return a value that matches this schema:
(d/oassoc-in-before sets prefixed-before-path prefixed-to-path set)
(d/oassoc-in sets prefixed-to-path set))
(d/dissoc-in prefixed-from-path)))]
(TokensLib. sets'
(if reorder?
themes
@@ -1130,9 +1161,6 @@ Will return a value that matches this schema:
(get-theme [_ group name]
(dm/get-in themes [group name]))
(get-hidden-theme [this]
(get-theme this hidden-token-theme-group hidden-token-theme-name))
(set-active-themes [_ active-themes]
(TokensLib. sets
themes
@@ -1223,18 +1251,15 @@ Will return a value that matches this schema:
:none)))
(get-active-themes-set-tokens [this]
(let [sets-order (get-ordered-set-names this)
active-themes (get-active-themes this)
order-theme-set (fn [theme]
(filter #(contains? (set (:sets theme)) %) sets-order))]
(reduce
(fn [tokens theme]
(reduce
(fn [tokens' cur]
(merge tokens' (:tokens (get-set this cur))))
tokens (order-theme-set theme)))
(d/ordered-map)
active-themes)))
(let [theme-set-names (get-active-themes-set-names this)
all-set-names (get-ordered-set-names this)
active-set-names (filter theme-set-names all-set-names)
tokens (reduce (fn [tokens set-name]
(let [set (get-set this set-name)]
(merge tokens (:tokens set))))
(d/ordered-map)
active-set-names)]
tokens))
(encode-dtcg [this]
(let [themes-xform
@@ -1286,74 +1311,101 @@ Will return a value that matches this schema:
(assoc-in ["$metadata" "activeThemes"] active-themes-clear)
(assoc-in ["$metadata" "activeSets"] active-sets))))
(decode-dtcg-json [_ parsed-json]
(let [metadata (get parsed-json "$metadata")
sets-data (dissoc parsed-json "$themes" "$metadata")
themes-data (->> (get parsed-json "$themes")
(map (fn [theme]
(-> theme
(set/rename-keys {"selectedTokenSets" "sets"})
(update "sets" keys)))))
active-sets (get metadata "activeSets")
active-themes (get metadata "activeThemes")
active-themes (if (empty? active-themes)
#{hidden-token-theme-path}
active-themes)
(decode-single-set-json [this set-name tokens]
(assert (map? tokens) "expected a map data structure for `data`")
set-order (get metadata "tokenSetOrder")
name->pos (into {} (map-indexed (fn [idx itm] [itm idx]) set-order))
sets-data' (sort-by (comp name->pos first) sets-data)
lib (make-tokens-lib)
lib' (->> sets-data'
(reduce (fn [lib [set-name tokens]]
(add-set lib (make-token-set
:name set-name
:tokens (flatten-nested-tokens-json tokens ""))))
lib))
lib' (cond-> lib'
(and (seq active-sets) (= #{hidden-token-theme-path} active-themes))
(update-theme hidden-token-theme-group hidden-token-theme-name
#(assoc % :sets active-sets)))]
(if-let [themes-data (seq themes-data)]
(as-> lib' $
(reduce
(fn [lib {:strs [name group description is-source id modified-at sets]}]
(add-theme lib (TokenTheme. name
(or group "")
description
(some? is-source)
(or id (str (uuid/next)))
(or (some-> modified-at
(dt/parse-instant))
(dt/now))
(set sets))))
$ themes-data)
(reduce
(fn [lib active-theme]
(let [[group name] (split-token-theme-path active-theme)]
(activate-theme lib group name)))
$ active-themes))
lib')))
(add-set this (make-token-set :name (normalize-set-name set-name)
:tokens (flatten-nested-tokens-json tokens ""))))
(decode-single-set-legacy-json [this set-name tokens]
(assert (map? tokens) "expected a map data structure for `data`")
(decode-single-set-json this set-name (legacy-nodes->dtcg-nodes tokens)))
(decode-dtcg-json [_ data]
(assert (map? data) "expected a map data structure for `data`")
(let [metadata (get data "$metadata")
xf-normalize-set-name
(map normalize-set-name)
sets
(dissoc data "$themes" "$metadata")
ordered-sets
(-> (d/ordered-set)
(into xf-normalize-set-name (get metadata "tokenSetOrder"))
(into xf-normalize-set-name (keys sets)))
active-sets
(or (->> (get metadata "activeSets")
(into #{} xf-normalize-set-name)
(not-empty))
#{})
active-themes
(or (->> (get metadata "activeThemes")
(into #{})
(not-empty))
#{hidden-token-theme-path})
themes
(->> (get data "$themes")
(map (fn [theme]
(make-token-theme
:name (get theme "name")
:group (get theme "group")
:is-source (get theme "is-source")
:id (get theme "id")
:modified-at (some-> (get theme "modified-at")
(dt/parse-instant))
:sets (into #{}
(comp (map key)
xf-normalize-set-name
(filter #(contains? ordered-sets %)))
(get theme "selectedTokenSets")))))
(not-empty))
library
(make-tokens-lib)
sets
(reduce-kv (fn [result name tokens]
(assoc result
(normalize-set-name name)
(flatten-nested-tokens-json tokens "")))
{}
sets)
library
(reduce (fn [library name]
(if-let [tokens (get sets name)]
(add-set library (make-token-set :name name :tokens tokens))
library))
library
ordered-sets)
library
(update-theme library hidden-token-theme-group hidden-token-theme-name
#(assoc % :sets active-sets))
library
(reduce add-theme library themes)
library
(reduce (fn [library theme-path]
(let [[group name] (split-token-theme-path theme-path)]
(activate-theme library group name)))
library
active-themes)]
library))
(decode-legacy-json [this parsed-legacy-json]
(let [other-data (select-keys parsed-legacy-json ["$themes" "$metadata"])
sets-data (dissoc parsed-legacy-json "$themes" "$metadata")
dtcg-sets-data (walk/postwalk
(fn [node]
(cond-> node
(and (map? node)
(contains? node "value")
(sequential? (get node "value")))
(update "value"
(fn [seq-value]
(map #(set/rename-keys % {"type" "$type"}) seq-value)))
(and (map? node)
(and (contains? node "type")
(contains? node "value")))
(set/rename-keys {"value" "$value"
"type" "$type"})))
sets-data)]
dtcg-sets-data (legacy-nodes->dtcg-nodes sets-data)]
(decode-dtcg-json this (merge other-data
dtcg-sets-data))))
(get-all-tokens [this]
@@ -1368,6 +1420,10 @@ Will return a value that matches this schema:
(valid-token-themes? themes)
(valid-active-token-themes? active-themes))))
(defn get-hidden-theme
[tokens-lib]
(get-theme tokens-lib hidden-token-theme-group hidden-token-theme-name))
(defn valid-tokens-lib?
[o]
(and (instance? TokensLib o)
@@ -1382,27 +1438,32 @@ Will return a value that matches this schema:
(def ^:private check-active-themes
(sm/check-fn schema:active-themes :hint "expected valid active themes"))
(defn- ensure-hidden-theme
"A helper that is responsible to ensure that the hidden theme always
exists on the themes data structure"
[themes]
(update themes hidden-token-theme-group
(fn [data]
(if (contains? data hidden-token-theme-name)
data
(d/oassoc data hidden-token-theme-name (make-hidden-token-theme))))))
;; NOTE: is possible that ordered map is not the most apropriate
;; data structure and maybe we need a specific that allows us an
;; easy way to reorder it, or just store inside Tokens data
;; structure the data and the order separately as we already do
;; with pages and pages-index.
(defn make-tokens-lib
"Create an empty or prepopulated tokens library."
([]
;; NOTE: is possible that ordered map is not the most apropriate
;; data structure and maybe we need a specific that allows us an
;; easy way to reorder it, or just store inside Tokens data
;; structure the data and the order separately as we already do
;; with pages and pages-index.
(make-tokens-lib :sets (d/ordered-map)
:themes (d/ordered-map)
:active-themes #{hidden-token-theme-path}))
([& {:keys [sets themes active-themes]}]
(let [active-themes (d/nilv active-themes #{hidden-token-theme-path})
themes (if (empty? themes)
(update themes hidden-token-theme-group d/oassoc hidden-token-theme-name (make-hidden-token-theme))
themes)]
(TokensLib.
(check-token-sets sets)
(check-token-themes themes)
(check-active-themes active-themes)))))
[& {:keys [sets themes active-themes]}]
(let [sets (or sets (d/ordered-map))
themes (-> (or themes (d/ordered-map))
(ensure-hidden-theme))
active-themes (or active-themes #{hidden-token-theme-path})]
(TokensLib.
(check-token-sets sets)
(check-token-themes themes)
(check-active-themes active-themes))))
(defn ensure-tokens-lib
[tokens-lib]
@@ -1445,6 +1506,71 @@ Will return a value that matches this schema:
:wfn #(into {} %)
:rfn #(map->Token %)})
#?(:clj
(defn- read-tokens-lib-v1-0
"Reads the first version of tokens lib, now completly obsolete"
[r]
(let [;; Migrate sets tree without prefix to new format
prev-sets (->> (fres/read-object! r)
(tree-seq d/ordered-map? vals)
(filter (partial instance? TokenSet)))
sets (-> (reduce add-set (make-tokens-lib) prev-sets)
(deref)
(:sets))
_set-groups (fres/read-object! r)
themes (fres/read-object! r)
active-themes (fres/read-object! r)]
(->TokensLib sets themes active-themes))))
#?(:clj
(defn- read-tokens-lib-v1-1
"Reads the tokens lib data structure and ensures that hidden
theme exists and adds missing ID on themes"
[r]
(let [sets (fres/read-object! r)
themes (fres/read-object! r)
active-themes (fres/read-object! r)
;; Ensure we have at least a hidden theme
themes
(ensure-hidden-theme themes)
;; Ensure we add an :id field for each existing theme
themes
(reduce (fn [result group-id]
(update result group-id
(fn [themes]
(reduce (fn [themes theme-id]
(update themes theme-id
(fn [theme]
(if (get theme :id)
theme
(assoc theme :id (str (uuid/next)))))))
themes
(keys themes)))))
themes
(keys themes))]
(->TokensLib sets themes active-themes))))
#?(:clj
(defn- write-tokens-lib
[n w ^TokensLib o]
(fres/write-tag! w n 3)
(fres/write-object! w (.-sets o))
(fres/write-object! w (.-themes o))
(fres/write-object! w (.-active-themes o))))
#?(:clj
(defn- read-tokens-lib
[r]
(let [sets (fres/read-object! r)
themes (fres/read-object! r)
active-themes (fres/read-object! r)]
(->TokensLib sets themes active-themes))))
#?(:clj
(fres/add-handlers!
{:name "penpot/token/v1"
@@ -1474,32 +1600,15 @@ Will return a value that matches this schema:
(let [obj (fres/read-object! r)]
(map->TokenTheme obj)))}
;; LEGACY TOKENS LIB READERS (with migrations)
{:name "penpot/tokens-lib/v1"
:rfn (fn [r]
(let [;; Migrate sets tree without prefix to new format
prev-sets (->> (fres/read-object! r)
(tree-seq d/ordered-map? vals)
(filter (partial instance? TokenSet)))
;; FIXME: wtf we usind deref here?
sets (-> (reduce add-set (make-tokens-lib) prev-sets)
(deref)
(:sets))
_set-groups (fres/read-object! r)
themes (fres/read-object! r)
active-themes (fres/read-object! r)]
(->TokensLib sets themes active-themes)))}
:rfn read-tokens-lib-v1-0}
{:name "penpot/tokens-lib/v1.1"
:rfn read-tokens-lib-v1-1}
;; CURRENT TOKENS LIB READER & WRITTER
{:name "penpot/tokens-lib/v1.2"
:class TokensLib
:wfn (fn [n w o]
(fres/write-tag! w n 3)
(fres/write-object! w (.-sets o))
(fres/write-object! w (.-themes o))
(fres/write-object! w (.-active-themes o)))
:rfn (fn [r]
(let [sets (fres/read-object! r)
themes (fres/read-object! r)
active-themes (fres/read-object! r)]
(->TokensLib sets themes active-themes)))}))
:wfn write-tokens-lib
:rfn read-tokens-lib}))

View File

@@ -17,9 +17,14 @@
java.util.UUID
java.nio.ByteBuffer)))
(def regex
#"^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]-[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$")
(defn uuid
"Creates an UUID instance from string, expectes valid uuid strings,
the existense of validation is implementation detail"
the existense of validation is implementation detail.
UNSAFE: this can accept invalid uuids or incomplete uuids"
[s]
#?(:clj (UUID/fromString s)
:cljs (c/uuid s)))
@@ -27,8 +32,21 @@
(defn parse
"Parse string uuid representation into proper UUID instance, validates input"
[s]
#?(:clj (UUID/fromString s)
:cljs (c/parse-uuid s)))
(if (and (string? s) ^boolean (re-matches regex s))
#?(:clj (UUID/fromString s)
:cljs (uuid s))
(let [message (str "invalid string '" s "' for uuid")]
(throw #?(:clj (IllegalArgumentException. message)
:cljs (js/Error. message))))))
(defn parse*
"Exception safe version of `parse`."
[s]
(try
(parse s)
(catch #?(:clj Exception :cljs :default) _cause
nil)))
(defn next
[]

View File

@@ -0,0 +1,6 @@
{"color":
{"red":
{"100":
{"value":"red",
"type":"color",
"description":""}}}}

View File

@@ -0,0 +1,6 @@
{"color":
{"red":
{"100":
{"$value":"red",
"$type":"color",
"$description":""}}}}

View File

@@ -84,52 +84,65 @@
(t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) #"expected valid params for token-set"
(ctob/make-token-set params)))))
(t/deftest move-token-set
(t/testing "flat"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "A"))
(ctob/add-set (ctob/make-token-set :name "B"))
(ctob/add-set (ctob/make-token-set :name "Move")))
move (fn [from-path to-path before-path before-group?]
(->> (ctob/move-set tokens-lib from-path to-path before-path before-group?)
(ctob/get-ordered-set-names)
(into [])))]
(t/testing "move to top"
(t/is (= ["Move" "A" "B"] (move ["Move"] ["Move"] ["A"] false))))
(t/deftest move-token-set-flat
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "A"))
(ctob/add-set (ctob/make-token-set :name "B"))
(ctob/add-set (ctob/make-token-set :name "Move")))
move (fn [from-path to-path before-path before-group?]
(->> (ctob/move-set tokens-lib from-path to-path before-path before-group?)
(ctob/get-ordered-set-names)
(into [])))]
(t/testing "move to top"
(t/is (= ["Move" "A" "B"] (move ["Move"] ["Move"] ["A"] false))))
(t/testing "move in-between"
(t/is (= ["A" "Move" "B"] (move ["Move"] ["Move"] ["B"] false))))
(t/testing "move in-between"
(t/is (= ["A" "Move" "B"] (move ["Move"] ["Move"] ["B"] false))))
(t/testing "move to bottom"
(t/is (= ["A" "B" "Move"] (move ["Move"] ["Move"] nil false))))))
(t/testing "move to bottom"
(t/is (= ["A" "B" "Move"] (move ["Move"] ["Move"] nil false))))))
(t/testing "nested"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "Foo/Baz"))
(ctob/add-set (ctob/make-token-set :name "Foo/Bar"))
(ctob/add-set (ctob/make-token-set :name "Foo")))
move (fn [from-path to-path before-path before-group?]
(->> (ctob/move-set tokens-lib from-path to-path before-path before-group?)
(ctob/get-ordered-set-names)
(into [])))]
(t/testing "move outside of group"
(t/is (= ["Foo/Baz" "Bar" "Foo"] (move ["Foo" "Bar"] ["Bar"] ["Foo"] false)))
(t/is (= ["Bar" "Foo/Baz" "Foo"] (move ["Foo" "Bar"] ["Bar"] ["Foo" "Baz"] true)))
(t/is (= ["Foo/Baz" "Foo" "Bar"] (move ["Foo" "Bar"] ["Bar"] nil false))))
(t/deftest move-token-set-nested
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "Foo/Baz"))
(ctob/add-set (ctob/make-token-set :name "Foo/Bar"))
(ctob/add-set (ctob/make-token-set :name "Foo")))
move (fn [from-path to-path before-path before-group?]
(->> (ctob/move-set tokens-lib from-path to-path before-path before-group?)
(ctob/get-ordered-set-names)
(into [])))]
(t/testing "move outside of group"
(t/is (= ["Foo/Baz" "Bar" "Foo"] (move ["Foo" "Bar"] ["Bar"] ["Foo"] false)))
(t/is (= ["Bar" "Foo/Baz" "Foo"] (move ["Foo" "Bar"] ["Bar"] ["Foo" "Baz"] true)))
(t/is (= ["Foo/Baz" "Foo" "Bar"] (move ["Foo" "Bar"] ["Bar"] nil false))))
(t/testing "move inside of group"
(t/is (= ["Foo/Foo" "Foo/Baz" "Foo/Bar"] (move ["Foo"] ["Foo" "Foo"] ["Foo" "Baz"] false)))
(t/is (= ["Foo/Baz" "Foo/Bar" "Foo/Foo"] (move ["Foo"] ["Foo" "Foo"] nil false))))))
(t/testing "move inside of group"
(t/is (= ["Foo/Foo" "Foo/Baz" "Foo/Bar"] (move ["Foo"] ["Foo" "Foo"] ["Foo" "Baz"] false)))
(t/is (= ["Foo/Baz" "Foo/Bar" "Foo/Foo"] (move ["Foo"] ["Foo" "Foo"] nil false))))))
;; FIXME
(t/testing "updates theme set names"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "Foo/Bar/Baz"))
(ctob/add-set (ctob/make-token-set :name "Other"))
(ctob/add-theme (ctob/make-token-theme :name "Theme"
:sets #{"Foo/Bar/Baz"}))
(ctob/move-set ["Foo" "Bar" "Baz"] ["Other/Baz"] nil nil))]
(t/is (= #{"Other/Baz"} (:sets (ctob/get-theme tokens-lib "" "Theme")))))))
(t/deftest move-token-set-nested-2
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "a/b"))
(ctob/add-set (ctob/make-token-set :name "a/a"))
(ctob/add-set (ctob/make-token-set :name "b/a"))
(ctob/add-set (ctob/make-token-set :name "b/b")))
move (fn [from-path to-path before-path before-group?]
(->> (ctob/move-set tokens-lib from-path to-path before-path before-group?)
(ctob/get-ordered-set-names)
(vec)))]
(t/testing "move within group"
(t/is (= ["a/b" "a/a" "b/a" "b/b"] (vec (ctob/get-ordered-set-names tokens-lib))))
(t/is (= ["a/a" "a/b" "b/a" "b/b"] (move ["a" "b"] ["a" "b"] nil true))))))
(t/deftest move-token-set-nested-3
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "Foo/Bar/Baz"))
(ctob/add-set (ctob/make-token-set :name "Other"))
(ctob/add-theme (ctob/make-token-theme :name "Theme"
:sets #{"Foo/Bar/Baz"}))
(ctob/move-set ["Foo" "Bar" "Baz"] ["Other/Baz"] nil nil))]
(t/is (= #{"Other/Baz"} (:sets (ctob/get-theme tokens-lib "" "Theme"))))))
(t/deftest move-token-set-group
(t/testing "reordering"
@@ -213,7 +226,7 @@
(t/is (= (ctob/set-count tokens-lib) 0))))
(t/deftest make-invalid-tokens-lib
(let [params {:sets nil :themes nil}]
(let [params {:sets {} :themes {}}]
(t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) #"expected valid token sets"
(ctob/make-tokens-lib params)))))
@@ -428,32 +441,225 @@
(t/is (nil? token'))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
(t/deftest list-active-themes-tokens-in-order
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :name "out-of-order-theme"
;; Out of order sets in theme
:sets ["unknown-set" "set-b" "set-a"]))
(ctob/set-active-themes #{"/out-of-order-theme"})
(t/deftest get-ordered-sets
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "group-1/set-a"))
(ctob/add-set (ctob/make-token-set :name "group-1/set-b"))
(ctob/add-set (ctob/make-token-set :name "group-2/set-a"))
(ctob/add-set (ctob/make-token-set :name "group-1/set-c")))
(ctob/add-set (ctob/make-token-set :name "set-a"))
(ctob/add-token-in-set "set-a" (ctob/make-token :name "set-a-token"
:type :boolean
:value true))
(ctob/add-set (ctob/make-token-set :name "set-b"))
(ctob/add-token-in-set "set-b" (ctob/make-token :name "set-b-token"
:type :boolean
:value true))
;; Ignore this set
(ctob/add-set (ctob/make-token-set :name "inactive-set"))
(ctob/add-token-in-set "inactive-set" (ctob/make-token :name "inactive-set-token"
:type :boolean
:value true)))
ordered-sets (ctob/get-ordered-set-names tokens-lib)]
expected-order (ctob/get-ordered-set-names tokens-lib)
expected-tokens (ctob/get-active-themes-set-tokens tokens-lib)
expected-token-names (mapv key expected-tokens)]
(t/is (= '("set-a" "set-b" "inactive-set") expected-order))
(t/is (= ["set-a-token" "set-b-token"] expected-token-names))))
(t/is (= ordered-sets '("group-1/set-a"
"group-1/set-b"
"group-1/set-c"
"group-2/set-a")))))
(t/deftest list-active-themes-tokens-no-theme
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "set-a"
:tokens {"token-1"
(ctob/make-token :name "token-1"
:type :border-radius
:value 10)
"token-2"
(ctob/make-token :name "token-2"
:type :border-radius
:value 20)}))
(ctob/add-set (ctob/make-token-set :name "set-b"
:tokens {"token-1"
(ctob/make-token :name "token-1"
:type :border-radius
:value 100)
"token-3"
(ctob/make-token :name "token-3"
:type :border-radius
:value 300)}))
(ctob/add-set (ctob/make-token-set :name "set-c"
:tokens {"token-1"
(ctob/make-token :name "token-1"
:type :border-radius
:value 1000)
"token-2"
(ctob/make-token :name "token-2"
:type :border-radius
:value 2000)
"token-3"
(ctob/make-token :name "token-3"
:type :border-radius
:value 3000)
"token-4"
(ctob/make-token :name "token-4"
:type :border-radius
:value 4000)}))
(ctob/update-theme ctob/hidden-token-theme-group ctob/hidden-token-theme-name
#(ctob/enable-sets % #{"set-a" "set-b"})))
tokens (ctob/get-active-themes-set-tokens tokens-lib)]
(t/is (= (mapv key tokens) ["token-1" "token-2" "token-3"]))
(t/is (= (get-in tokens ["token-1" :value]) 100))
(t/is (= (get-in tokens ["token-2" :value]) 20))
(t/is (= (get-in tokens ["token-3" :value]) 300))))
(t/deftest list-active-themes-tokens-one-theme
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "set-a"
:tokens {"token-1"
(ctob/make-token :name "token-1"
:type :border-radius
:value 10)
"token-2"
(ctob/make-token :name "token-2"
:type :border-radius
:value 20)}))
(ctob/add-set (ctob/make-token-set :name "set-b"
:tokens {"token-1"
(ctob/make-token :name "token-1"
:type :border-radius
:value 100)
"token-3"
(ctob/make-token :name "token-3"
:type :border-radius
:value 300)}))
(ctob/add-set (ctob/make-token-set :name "set-c"
:tokens {"token-1"
(ctob/make-token :name "token-1"
:type :border-radius
:value 1000)
"token-2"
(ctob/make-token :name "token-2"
:type :border-radius
:value 2000)
"token-3"
(ctob/make-token :name "token-3"
:type :border-radius
:value 3000)
"token-4"
(ctob/make-token :name "token-4"
:type :border-radius
:value 4000)}))
(ctob/add-theme (ctob/make-token-theme :name "single-theme"
:sets #{"set-b" "set-c" "set-a"}))
(ctob/set-active-themes #{"/single-theme"}))
tokens (ctob/get-active-themes-set-tokens tokens-lib)]
;; Note that sets order inside the theme is undefined. What matters is order in that the
;; sets have been added to the library.
(t/is (= (mapv key tokens) ["token-1" "token-2" "token-3" "token-4"]))
(t/is (= (get-in tokens ["token-1" :value]) 1000))
(t/is (= (get-in tokens ["token-2" :value]) 2000))
(t/is (= (get-in tokens ["token-3" :value]) 3000))
(t/is (= (get-in tokens ["token-4" :value]) 4000))))
(t/deftest list-active-themes-tokens-two-themes
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "set-a"
:tokens {"token-1"
(ctob/make-token :name "token-1"
:type :border-radius
:value 10)
"token-2"
(ctob/make-token :name "token-2"
:type :border-radius
:value 20)}))
(ctob/add-set (ctob/make-token-set :name "set-b"
:tokens {"token-1"
(ctob/make-token :name "token-1"
:type :border-radius
:value 100)
"token-3"
(ctob/make-token :name "token-3"
:type :border-radius
:value 300)}))
(ctob/add-set (ctob/make-token-set :name "set-c"
:tokens {"token-1"
(ctob/make-token :name "token-1"
:type :border-radius
:value 1000)
"token-2"
(ctob/make-token :name "token-2"
:type :border-radius
:value 2000)
"token-3"
(ctob/make-token :name "token-3"
:type :border-radius
:value 3000)
"token-4"
(ctob/make-token :name "token-4"
:type :border-radius
:value 4000)}))
(ctob/add-theme (ctob/make-token-theme :name "theme-1"
:sets #{"set-b"}))
(ctob/add-theme (ctob/make-token-theme :name "theme-2"
:sets #{"set-b" "set-a"}))
(ctob/set-active-themes #{"/theme-1" "/theme-2"}))
tokens (ctob/get-active-themes-set-tokens tokens-lib)]
;; Note that themes order is irrelevant. What matters is the union of the active sets
;; and the order of the sets in the library.
(t/is (= (mapv key tokens) ["token-1" "token-2" "token-3"]))
(t/is (= (get-in tokens ["token-1" :value]) 100))
(t/is (= (get-in tokens ["token-2" :value]) 20))
(t/is (= (get-in tokens ["token-3" :value]) 300))))
(t/deftest list-active-themes-tokens-bug-taiga-10617
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "Mode / Dark"
:tokens {"red"
(ctob/make-token :name "red"
:type :color
:value "#700000")}))
(ctob/add-set (ctob/make-token-set :name "Mode / Light"
:tokens {"red"
(ctob/make-token :name "red"
:type :color
:value "#ff0000")}))
(ctob/add-set (ctob/make-token-set :name "Device / Desktop"
:tokens {"border1"
(ctob/make-token :name "border1"
:type :border-radius
:value 30)}))
(ctob/add-set (ctob/make-token-set :name "Device / Mobile"
:tokens {"border1"
(ctob/make-token :name "border1"
:type :border-radius
:value 50)}))
(ctob/add-theme (ctob/make-token-theme :group "App"
:name "Mobile"
:sets #{"Mode / Dark" "Device / Mobile"}))
(ctob/add-theme (ctob/make-token-theme :group "App"
:name "Web"
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop"}))
(ctob/add-theme (ctob/make-token-theme :group "Brand"
:name "Brand A"
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop" "Device / Mobile"}))
(ctob/add-theme (ctob/make-token-theme :group "Brand"
:name "Brand B"
:sets #{}))
(ctob/set-active-themes #{"App/Web" "Brand/Brand A"}))
tokens (ctob/get-active-themes-set-tokens tokens-lib)]
(t/is (= (mapv key tokens) ["red" "border1"]))
(t/is (= (get-in tokens ["red" :value]) "#ff0000"))
(t/is (= (get-in tokens ["border1" :value]) 50))))
(t/deftest list-active-themes-tokens-no-tokens
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "set-a")))
tokens (ctob/get-active-themes-set-tokens tokens-lib)]
(t/is (empty? tokens))))
(t/deftest list-active-themes-tokens-no-sets
(let [tokens-lib (ctob/make-tokens-lib)
tokens (ctob/get-active-themes-set-tokens tokens-lib)]
(t/is (empty? tokens))))
(t/deftest sets-at-path-active-state
(let [tokens-lib (-> (ctob/make-tokens-lib)
@@ -1165,6 +1371,30 @@
(t/testing "invalid tokens got discarded"
(t/is (nil? (get-set-token "typography" "H1.Bold")))))))
#?(:clj
(t/deftest single-set-legacy-json-decoding
(let [json (-> (slurp "test/common_tests/types/data/legacy-single-set.json")
(tr/decode-str))
lib (ctob/decode-single-set-legacy-json (ctob/ensure-tokens-lib nil) "single_set" json)
get-set-token (fn [set-name token-name]
(some-> (ctob/get-set lib set-name)
(ctob/get-token token-name)))]
(t/is (= '("single_set") (ctob/get-ordered-set-names lib)))
(t/testing "token added"
(t/is (some? (get-set-token "single_set" "color.red.100")))))))
#?(:clj
(t/deftest single-set-dtcg-json-decoding
(let [json (-> (slurp "test/common_tests/types/data/single-set.json")
(tr/decode-str))
lib (ctob/decode-single-set-json (ctob/ensure-tokens-lib nil) "single_set" json)
get-set-token (fn [set-name token-name]
(some-> (ctob/get-set lib set-name)
(ctob/get-token token-name)))]
(t/is (= '("single_set") (ctob/get-ordered-set-names lib)))
(t/testing "token added"
(t/is (some? (get-set-token "single_set" "color.red.100")))))))
#?(:clj
(t/deftest dtcg-encoding-decoding-json
(let [json (-> (slurp "test/common_tests/types/data/tokens-multi-set-example.json")

View File

@@ -156,10 +156,13 @@ http {
}
location / {
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
location ~ ^/github/penpot-files/(.+)$ {
rewrite ^/github/penpot-files/(.+) /penpot/penpot-files/refs/heads/main/$1 break;
proxy_pass https://raw.githubusercontent.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_set_header User-Agent "curl/7.74.0";
proxy_hide_header Cookies;
proxy_set_header User-Agent "curl/8.5.0";
proxy_set_header Host "raw.githubusercontent.com";
proxy_set_header Accept "*/*";
add_header Access-Control-Allow-Origin $http_origin;

View File

@@ -8,12 +8,12 @@ source ~/.bashrc
echo "[start-tmux.sh] Installing node dependencies"
pushd ~/penpot/frontend/
corepack up;
corepack install;
yarn install;
yarn run playwright install --with-deps chromium
popd
pushd ~/penpot/exporter/
corepack up;
corepack install;
yarn install
yarn run playwright install --with-deps chromium
popd

View File

@@ -52,7 +52,7 @@ services:
## penpot to the internet, or a different host than `localhost`.
# traefik:
# image: traefik:v2.9
# image: traefik:v3.3
# networks:
# - penpot
# command:
@@ -87,29 +87,16 @@ services:
networks:
- penpot
labels:
- "traefik.enable=true"
# labels:
# - "traefik.enable=true"
## HTTP: example of labels for the case where penpot will be exposed to the
## internet with only HTTP (without HTTPS) using traefik.
# ## HTTPS: example of labels for the case where penpot will be exposed to the
# ## internet with HTTPS using traefik.
# - "traefik.http.routers.penpot-http.entrypoints=web"
# - "traefik.http.routers.penpot-http.rule=Host(`<DOMAIN_NAME>`)"
# - "traefik.http.services.penpot-http.loadbalancer.server.port=80"
## HTTPS: example of labels for the case where penpot will be exposed to the
## internet with HTTPS using traefik.
# - "traefik.http.middlewares.http-redirect.redirectscheme.scheme=https"
# - "traefik.http.middlewares.http-redirect.redirectscheme.permanent=true"
# - "traefik.http.routers.penpot-http.entrypoints=web"
# - "traefik.http.routers.penpot-http.rule=Host(`<DOMAIN_NAME>`)"
# - "traefik.http.routers.penpot-http.middlewares=http-redirect"
# - "traefik.http.routers.penpot-https.entrypoints=websecure"
# - "traefik.http.routers.penpot-https.rule=Host(`<DOMAIN_NAME>`)"
# - "traefik.http.services.penpot-https.loadbalancer.server.port=80"
# - "traefik.http.routers.penpot-https.tls=true"
# - "traefik.http.routers.penpot-https.entrypoints=websecure"
# - "traefik.http.routers.penpot-https.tls.certresolver=letsencrypt"
# - "traefik.http.routers.penpot-https.tls=true"
environment:
<< : [*penpot-flags, *penpot-http-body-size]
@@ -130,8 +117,7 @@ services:
networks:
- penpot
## Configuration envronment variables for the backend
## container.
## Configuration envronment variables for the backend container.
environment:
<< : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size]

View File

@@ -135,10 +135,13 @@ http {
}
location / {
location ~ ^/github/penpot-files/(?<template_file>[a-zA-Z0-9\-\_\.]+) {
proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file;
location ~ ^/github/penpot-files/(.+)$ {
rewrite ^/github/penpot-files/(.+) /penpot/penpot-files/refs/heads/main/$1 break;
proxy_pass https://raw.githubusercontent.com;
proxy_hide_header Access-Control-Allow-Origin;
proxy_set_header User-Agent "curl/7.74.0";
proxy_hide_header Cookies;
proxy_set_header User-Agent "curl/8.5.0";
proxy_set_header Host "raw.githubusercontent.com";
proxy_set_header Accept "*/*";
add_header Access-Control-Allow-Origin $http_origin;

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -2,6 +2,14 @@
title: 1.3 Install with Docker
---
<p class="advice">
Installing and maintaining a self-hosted Penpot instance requires some technical knowledge:
Docker and Docker Compose, basic DNS management, and proxy configuration.
If you're not comfortable with this stack, we encourage you to try
more straight-forward installations with <a href="https://help.penpot.app/technical-guide/getting-started/elestio/" target="_blank">Elestio</a>
or use the SAAS at <a href="https://design.penpot.app" targret="_blank">https://design.penpot.app</a>.
</p>
# Install with Docker
This section details everything you need to know to get Penpot up and running in
@@ -10,42 +18,10 @@ production environments using Docker. For this, we provide a series of *Dockerfi
## Install Docker
<p class="advice">
Skip this section if you already have docker installed, up and running.
</p>
Currently, Docker comes into two different flavours:
### Docker Desktop
This is the only option to have Docker in a Windows or MacOS. Recently it's also available
for Linux, in the most popular distributions (Debian, Ubuntu and Fedora).
You can install it following the <a href="https://docs.docker.com/desktop/"
target="_blank">official guide</a>.
Docker Desktop has a graphical control panel (GUI) to manage the service and view the
containers, images and volumes. But you need the command line (Terminal in Linux and Mac, or
PowerShell in Windows) to build and run the containers, and execute other operations.
It already includes **docker compose** utility, needed by Penpot.
### Docker Engine
This is the classic and default Docker setup for Linux machines, and the only option for a
Linux VPS without graphical interface.
You can install it following the <a href="https://docs.docker.com/engine/"
target="_blank">official guide</a>.
And you also need the [docker
compose](https://docs.docker.com/compose/cli-command/#installing-compose-v2) (V2)
plugin. You can use the old **docker-compose** tool, but all the documentation supposes
you are using the V2.
You can easily check which version of **docker compose** you have. If you can execute
<code class="language-bash">docker compose</code> command, then you have V2. If you need to write <code class="language-bash">docker-compose</code> (with a
<code class="language-bash">-</code>) for it to work, you have the old version.
To host a Penpot instance with Docker, it's necessary to have
<code class="language-bash">docker</code> and <code class="language-bash">docker compose</code>
installed. Check the comprehensive <a href="https://docs.docker.com/" target="_blank">official documentation</a>
to install and maintain docker.
## Start Penpot
@@ -175,9 +151,20 @@ Postgres database and another one for the assets uploaded by your users (images
clips). There may be more volumes if you enable other features, as explained in the file
itself.
## Configure the proxy
## Configure the proxy and HTTPS
Your host configuration needs to make a proxy to http://localhost:9001.
We strongly recommend to use Penpot under HTTPS/SSL, which will require specific server configurations for DNS and SSL certificates.
Besides, your host configuration needs to make a proxy to http://localhost:9001.
<p class="advice">
If you plan to serve Penpot under different domain than `localhost` without HTTPS,
you need to disable the `secure` flag on cookies, with the `disable-secure-session-cookies` flag.
This is a configuration NOT recommended for production environments; as some browser APIs do
not work properly under non-https environments, this unsecure configuration
may limit the usage of Penpot; as an example, the clipboard does not work with HTTP.
</p>
Below, you can see three examples with three different proxys:
### Example with NGINX
@@ -224,6 +211,8 @@ server {
}
```
For full documentation, go to the [official website][2]
### Example with CADDY SERVER
```bash
@@ -236,4 +225,15 @@ penpot.mycompany.com {
}
```
For full documentation, go to the [official website][3]
### Example with TRAEFIK
In the [Penpot's docker-compose.yaml][4] file, there is a simple example with Traefik.
For full documentation, go to the [official website][5]
[1]: /technical-guide/configuration/
[2]: https://nginx.org/en/docs/index.html
[3]: https://caddyserver.com/docs/
[4]: https://github.com/penpot/penpot/blob/develop/docker/images/docker-compose.yaml
[5]: https://doc.traefik.io/traefik/

View File

@@ -1,13 +1,11 @@
---
title: 1.1 Recommended Settings
title: 1.1 Recommended storage
---
# Recommended settings
# Recommended storage
To self-host Penpot, youll need a server with the following specifications:
Disk requirements depend on your usage, with the primary factors being database storage and user-uploaded files.
* **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.
As a rule of thumb, start with a **minimum** database size of **50GB** to **100GB** with elastic sizing capability — this configuration should adequately support up to 10 editors. For environments with **more than 10 users**, we recommend adding approximately **5GB** of capacity per additional editor.
This setup should be sufficient for a smooth experience with typical usage (your mileage may vary).
Keep in mind that database size doesn't grow strictly proportionally with user count, as it depends heavily on how Penpot is used and the complexity of files created. Most organizations begin with this baseline and elastic sizing approach, then monitor usage patterns monthly until resource requirements stabilize.

View File

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

View File

@@ -1,5 +1,5 @@
---
title: 10· Components
title: 11· Components
---
<h1 id="components">Components</h1>

View File

@@ -1,5 +1,5 @@
---
title: 16· Custom fonts
title: 17· Custom fonts
---
<h1 id="customfonts">Custom fonts</h1>

View File

@@ -0,0 +1,377 @@
---
title: 10· Design Tokens
---
<h1 id="design-tokens">Design Tokens</h1>
<p class="main-paragraph">Design tokens are the building blocks of all UI elements, the same tokens are used in designs, tools, and code. They include colors, typography, spacing, shadows, and any visual element that affects an object: all these properties collectively make up a design system or a visual inheritance.</p>
<figure>
<img src="/img/design-tokens/01-tokens-cover.webp" alt="Tokens cover" />
</figure>
<h3 id="design-tokens-why">Why Design Tokens?</h3>
<p>Design tokens act as a single source of truth, a common language that can be translated and used in any other tool or framework capable of reading the token format. With Design Tokens, you can create, manage, and synchronize these visual elements within Penpot and across other design tools, keeping your designs consistent and making your workflows faster and easier to maintain.</p>
<p>You can also integrate Design Tokens with other core Penpot features, such as components and grid & flex layout, plus plugins will be able to access the tokens API (coming soon) making it even more powerful.</p>
<h3 id="design-tokens-format">W3C DTCG Format</h3>
<p>Penpot Design Tokens adhere to the <a href="https://design-tokens.github.io/community-group/format/" target="_blank">Design Tokens Format Module</a> and <a href="https://www.designtokens.org/glossary/" target="_blank">its definitions</a>, a draft by the <a href="https://www.w3.org/community/design-tokens/" target="_blank">W3C DTCG</a>. Penpot ensures compatibility across various disciplines, tools, and technologies by following the most standardized approach available for design tokens.</p>
<p>Tokens can be exported from Penpot or integrated into other tools directly, without conversion. Additionally, the knowledge gained from using Design Tokens in Penpot remains valuable, regardless of whether you continue using Penpot or a different tool or technology.</p>
<h2 id="design-tokens-use">Using Tokens</h2>
<h3 id="design-tokens-use-create">Creating a token</h3>
<p>You can create reusable and semantic tokens to be referenced in your designs at the <strong>Tokens</strong> panel. In this panel, youll find all the available types of tokens in Penpot arranged alphabetically, with existing tokens being shown at the top of the list.</p>
<figure>
<img src="/img/design-tokens/02-tokens-create.webp" alt="Tokens create" />
</figure>
<p>To create a token, click on the + next to the type of token you want to create. Youll then be prompted to define the tokens:</p>
<ul>
<li><strong>Name:</strong> The name should be specific to that token, as it is not possible to create multiple tokens with the same name; for example, <strong>dimension.small</strong>.</li>
<li><strong>Value:</strong> The value you wish to attribute to the token; depending on the token type, values may be numerical or color spaces (Hex, RGB, RGBA, ARGB, HSL or HSLA).</li>
<li><strong>Description:</strong> You can also choose to add a description to your token.</li>
</ul>
<p>Once you have named the token and assigned it a value, click <strong>Save</strong> to store the token and start referencing it.</p>
<h3 id="design-tokens-aliases">Referencing tokens into values (aliases)</h3>
<p>When assigning a value to a token, you can reference existing tokens - these are called aliases at the <a href="https://www.designtokens.org/glossary/" target="_blank">DTCG Glossary</a>.</p>
<figure>
<img src="/img/design-tokens/03-tokens-aliases.webp" alt="Tokens aliases" />
</figure>
<p>For example, if you have created a <strong>dimension.small</strong> token, with a value of <strong">64</strong>, you could create a <strong>spacing.small</strong> token with a value of <code class="language-js">{dimension.small}</code>. The <strong>spacing.small</strong> token would thereby have a value of 64.</p>
<p>When referencing an existing token in the value of a new token, you must reference it within <strong>{}</strong>.</p>
<p>If the value of the referenced token changes, this will also change the value of the tokens where it is referenced.</p>
<p class="advice">References to existing tokens are case sensitive.</p>
<h3 id="design-tokens-equations">Using equations</h3>
<p>Token types with numerical values also accept mathematical equations. If, for example, you create a <strong>spacing.small</strong> token with the value of <strong>2</strong>, and you then want to create a <strong>spacing.medium</strong> token that is twice as large, you could do so by writing <code class="language-js">{spacing.small} * 2</code> in its value. As a result, <strong>spacing.medium</strong> would have a value of <strong>4</strong>.</p>
<p>Say you have a <strong>spacing.scale</strong> token with a value of <strong>2</strong>. You could also use this token in the equation to calculate the value of <strong>spacing.medium</strong> by writing <code class="language-js">{spacing.small} * {spacing.scale}</code> in its value.</p>
<figure>
<img src="/img/design-tokens/04-tokens-math.webp" alt="Tokens math" />
</figure>
<p>Mathematical equations can be performed using:</p>
<ul>
<li><code class="language-js">+</code> for addition.</li>
<li><code class="language-js">-</code> for subtraction.</li>
<li><code class="language-js">*</code> for multiplication.</li>
<li><code class="language-js">/</code> for division.</li>
</ul>
<h3 id="design-tokens-edit">Editing a token</h3>
<p>Tokens can be edited by right-clicking the token and selecting <strong>Edit token</strong>. This will allow you to change the tokens name, value and description. Once the changes are made, click <strong>Save</strong>.</p>
<figure>
<img src="/img/design-tokens/05-tokens-edit.webp" alt="Tokens edit" />
</figure>
<p class="advice">Renaming tokens will break any references to their old names. If a token is already applied somewhere, you'll need to reapply it after renaming. This can lead to extra work, so rename with caution. We're actively working on a solution to handle this automatically, ensuring renamed tokens stay linked to their properties without additional effort.</p>
<h3 id="design-tokens-duplicate">Duplicating a token</h3>
<p>Tokens can be duplicated by right-clicking the token you wish to duplicate and selecting <strong>Duplicate token</strong>. This will create a copy of the selected token within the same set, with <code class="language-js">-copy</code> added to its name.</p>
<h3 id="design-tokens-delete">Deleting a token</h3>
<p>Tokens can be deleted by right-clicking the token you wish to delete and selecting <strong>Delete token</strong>.</p>
<h2 id="design-tokens-available">Available tokens</h2>
<p>You can apply tokens to the properties of any <a href="/user-guide/objects/" target="_blank">object</a>. There are two ways to apply tokens to a selection:</p>
<ul>
<li>Right-click on tokens to specify a particular property that you want to apply.</li>
<li>Left-click on tokens to apply the assumtion. Assumptions can vary across different token types. For example, for the <strong>color</strong> type the assumtion is that you want to apply the token as a <strong>fill</strong>.</li>
</ul>
<p>Tokens can be applied to multiple selected elements, but not to groups.</p>
<h3 id="design-tokens-radius">Border radius</h3>
<p>Border radius tokens allow you to define specific values for border-radius properties, offering flexibility in how you style the corners of elements.</p>
<figure>
<img src="/img/design-tokens/06-tokens-radius.webp" alt="Tokens radius" />
</figure>
<h4>Applying Border Radius Tokens</h4>
<p>To apply the border radius token to an element, select the element and choose the token from the list:</p>
<ul>
<li>Clicking on a border radius token will apply it to all corners of the element simultaneously.</li>
<li>Right-clicking on a border radius token, allows you to select which corners the token should be applied to.</li>
</ul>
<h3 id="design-tokens-color">Color</h3>
<p>Color tokens support color properties that can be applied to many different design elements, including boards, groups, shapes, and text.</p>
<figure>
<img src="/img/design-tokens/07-tokens-color-create.webp" alt="Tokens color create" />
</figure>
<p>You can define a color tokens value using:</p>
<ol>
<li><strong>The color picker</strong>, select the color switch to the left of the token <strong>Value</strong> input to open the color picker; here youll also be able to define the colors opacity.</li>
<li>
<strong>The color Spaces</strong>, define your color token in the following color spaces:
<ul>
<li>Hex: #ff0000</li>
<li>RGB: rgb(255, 0, 0)</li>
<li>RGBA: rgba(255, 0, 0, 1)</li>
<li>ARGB: #80FFFF00 (also known as Hex8)</li>
<li>HSL: hsl(120, 50%, 50%)</li>
<li>HSLA: hsla(120, 50%, 50%, 1)</li>
</ul>
</li>
</ol>
<h4>Applying Color Tokens</h4>
<p>Color tokens can define a design element's <strong>fill</strong> or <strong>stroke</strong> color. To apply the color token to an element, select the element and choose the token from the list:</p>
<ul>
<li>Clicking on a color token will apply it to the element as a fill by default.</li>
<li>Right-clicking on a color token, allows you to select whether it should be applied as a fill or stroke color.</li>
</ul>
<figure>
<img src="/img/design-tokens/08-tokens-color.webp" alt="Tokens color" />
</figure>
<h3 id="design-tokens-dimensions">Dimensions</h3>
<p>Dimension tokens allow you to define an amount of distance that can be used to set the size, space, radius or position of specific elements within a design.</p>
<h4>Applying Dimension Tokens</h4>
<p>To apply a dimension token, select the element and choose the token from the list:</p>
<ul>
<li>Clicking on a dimension token will apply it to the elements width and height by default.</li>
<li>
Right-clicking on a dimension token, allows you to select where you want the token to be used in the element. You can use it for:
<ol>
<li>Sizing</li>
<li>Spacing</li>
<li>Border radius</li>
<li>Stroke width</li>
<li>X position</li>
<li>Y position</li>
</ol>
</li>
</ul>
<h4>Sizing (dimension)</h4>
<p>The sizing property of the dimension token defines the height or width of design elements like boards, shapes, and groups.</p>
<figure>
<img src="/img/design-tokens/09-tokens-dimensions-sizing.webp" alt="Tokens dimensions sizing" />
</figure>
<p>When using dimension tokens for sizing, you can apply the token to an element's size by selecting:</p>
<ul>
<li><strong>All</strong> to apply the same size value to both the width and height of an element;</li>
<li><strong>Width</strong> to apply the token value to the horizontal size of an element;</li>
<li><strong>Height</strong> to apply the token value to the vertical size of an element;</li>
</ul>
<p>If you are working with flex-layout boards, you can also apply the token to an elements size by selecting:</p>
<ul>
<li><strong>All</strong> to apply the same size value to both the <strong>Min width</strong> and <strong>Min height</strong> of an element;</li>
<li><strong>Min width</strong> to define the smallest allowed horizontal size of an element but allow a larger size;</li>
<li><strong>Min height</strong> to define the smallest allowed vertical size of an element but allow a large size;</li>
<li><strong>All</strong> to apply the same size value to both the <strong>Max width</strong> and <strong>Max height</strong> of an element;</li>
<li><strong>Max width</strong> to define the largest allowed horizontal size of an element but allow a smaller size;</li>
<li><strong>Max height</strong> to define the largest allowed vertical size of an element but allow a smaller size.</li>
</ul>
<p class="advice">If you apply the <strong>min/max height/width properties</strong> to a board before flex-layout is applied to it, you may have to remove and re-apply the token for it to take effect.</p>
<h4>Spacing (dimension)</h4>
<p>The spacing property of the dimension token defines the distance between elements and it must be applied to flex-layout boards.</p>
<figure>
<img src="/img/design-tokens/10-tokens-dimensions-spacing.webp" alt="Tokens dimensions spacing" />
</figure>
<p class="advice">If you apply the token to a board before flex-layout is applied to it, you may have to remove and re-apply the token for it to take effect.</p>
<p>When using dimension tokens for spacing, you can apply the token to an element's padding and gap. Specifically, you can select:</p>
<ul>
<li>
<strong>Gaps</strong>
<ul>
<li><strong>All</strong> to apply the gap to all sides of the element;</li>
<li><strong>Column Gap</strong> to add a gap between child elements within a parent element;</li>
<li><strong>Row Gap</strong> to add vertical space between rows on flex-layout board elements set to wrap. </li></li>
</ul>
</li>
<li>
<strong>Paddings & Margins:</strong>
<ul>
<li><strong>Horizontal</strong> applies to the left and right of the container;</li>
<li><strong>Vertical</strong> and to the top and bottom sides of the container;</li>
<li><strong>Top, right, bottom or left</strong> apply space to individual sides of the element.</li>
</ul>
</li>
</ul>
<h4>Border radius (dimension)</h4>
<p>The border radius property of the dimension token defines the roundness of the corner of elements like boards, shapes, and groups.</p>
<p>You can apply the border radius property by right-clicking on the dimension token and selecting:</p>
<ul>
<li><strong>All</strong> to apply the radius to all sides of the selected element;</li>
<li><strong>Top Left</strong> to apply the radius to the top-left corner of the selected element;</li>
<li><strong>Top Right</strong> to apply the radius to the top-right corner of the selected element;</li>
<li><strong>Bottom Right</strong> to apply the radius to the bottom-right corner of the selected element;</li>
<li><strong>Bottom Left</strong> to apply the radius to the bottom-left corner of the selected element.</li>
</ul>
<h4>Stroke width (dimension)</h4>
<p>The stroke width property specifies the thickness of a border for elements that already have a stroke property applied.</p>
<p class="advice">If you apply the border property to an element before it has a stroke applied to it, you may have to remove and re-apply the token for it to take effect.</p>
<h4>X Position (dimension)</h4>
<p>The X property specifies the position of the element on the X axis of the canvas.</p>
<h4>Y Position (dimension)</h4>
<p>The Y property specifies the position of the element on the Y axis of the canvas.</p>
<h3 id="design-tokens-opacity">Opacity</h3>
<p>Opacity tokens allow you to define the opacity of a layer, ranging from fully opaque to fully transparent.</p>
<p>Opacity tokens can be applied to any design element that supports transparency. You can use any decimal value between 0 and 1 to set varying levels of opacity or you can use any value between 0 and 100 with <strong>`%`</strong> sign at the end of the value. For example, you can use <strong>45%</strong> which would resolve to <strong>.45</strong>.</p>
<h4>Applying Opacity Tokens</h4>
<p>To apply the opacity token to an element, select the element and click on the desired token.</p>
<h3 id="design-tokens-rotation">Rotation</h3>
<p>Rotation tokens are used to define and standardize rotational values within a design system. These tokens represent rotation angles, typically measured in degrees, and can be applied to elements such as icons or images, to ensure consistent rotation throughout a design.</p>
<h4>Applying Rotation Tokens</h4>
<p>To apply a rotation token, select the element and choose the token from the list.</p>
<h3 id="design-tokens-sizing">Sizing</h3>
<p>Sizing tokens can define various size-related design properties, namely the height and width of design elements.The sizing token supports numeric values, which include negative values.</p>
<figure>
<img src="/img/design-tokens/11-tokens-spacing.webp" alt="Tokens spacing" />
</figure>
<h4>Applying Sizing Tokens</h4>
<p>To apply the sizing token to an element, select the element and choose the token from the list:</p>
<ul>
<li>Clicking on a sizing token will apply it to all sides of the element by default;</li>
<li>
Right-clicking on a sizing token, allows you to specify which side of the element the token should apply to:
<ul>
<li><strong>All</strong> applies the same size value to both the width and height of an element;</li>
<li><strong>Width</strong> applies the token to the horizontal size of an element;</li>
<li><strong>Height</strong> applies the token to the vertical size of an element;</li>
</ul>
</li>
</ul>
<p>If you are working with flex-layout boards, you can also apply the token to an elements size by selecting:</p>
<ul>
<li><strong>Min width</strong> defines the smallest allowed horizontal size of an element, allowing larger sizes;</li>
<li><strong>Min height</strong> defines the smallest allowed vertical size of an element, allowing larger sizes;</li>
<li><strong>Max width</strong> defines the largest allowed horizontal size of an element, allowing smaller sizes;</li>
<li><strong>Max height</strong> defines the largest allowed vertical size of an element, allowing smaller sizes.</li>
</ul>
<h3 id="design-tokens-spacing">Spacing</h3>
<p>The spacing token defines the distance between design elements and supports numeric values, which include negative values. Spacing tokens must be applied to Flex Layout boards. </p>
<p class="advice">If you apply the token to a board before flex-layout is applied to it, you may have to remove and re-apply the token for it to take effect.</p>
<h4>Applying Spacing Tokens</h4>
<p>To apply the spacing token to an element, select the element and choose the token from the list:</p>
<ul>
<li>Clicking on a spacing token will apply it to all gaps (row and column).</li>
<li>Right-clicking on a spacing token, allows you to specify which property of the element the token should apply to. In this menu, properties are divided by blocks. In each property you can choose to apply to All sides in a specific property, or choose to apply them to individual sides:</li>
<li><strong>Gaps:</strong> Applies spaces between child elements within a parent container (both column and row gaps). This only works on flex and grid layouts.</li>
<li>
<strong>Paddings & Margins:</strong>
<ul>
<li><strong>Horizontal</strong> applies to the left and right of the container;</li>
<li><strong>Vertical</strong> and to the top and bottom sides of the container;</li>
<li><strong>Top, right, bottom or left</strong> apply space to individual sides of the element.</li>
</ul>
</li>
</ul>
<h4>Stroke Width</h4>
<p>The stroke width token, also known as the border width token, defines the thickness of a stroke around a design element. It can be applied to boards, groups, rectangles and text elements.</p>
<h2 id="design-tokens-sets">Token Sets</h2>
<p>Token Sets allow you to split your tokens up into multiple files in order to create organized groups or collections of tokens. It enables efficient management and customization within design files. For example you can group all your color sets, sizing sets or platform-specific sets. The purpose of tokens sets is to organize them in a way that matches your needs.</p>
<figure>
<img src="/img/design-tokens/12-tokens-sets.webp" alt="Tokens sets" />
</figure>
<p>When you create your first token, a default set is created. You can rename, group or move it later. As you create new token sets, theyll be added sequentially after existing ones. You can reorder token sets by dragging and dropping them.</p>
<p class="advice">The order of the token sets is essential! If you have tokens with the same name (and different values) across multiple sets, the tokens that are in the set that appears last in the list will override the previous ones - similar to how Cascading Style Sheets work.</p>
<p>When creating a token set, its recommended that you assign it a unique name to ensure clarity. Token set names are not included in individual token names by default so it is possible to have tokens with the same name belonging to different token sets.</p>
<p>Token sets can be enabled or disabled. If a set is disabled, its tokens will be excluded from the token resolution process.</p>
<h3 id="design-tokens-sets-create">Creating Token Sets</h3>
<p>There are two ways to create a token set at the <strong>Tokens</strong> tab:</p>
<ol>
<li>Click on the <strong>+</strong> next to <strong>Sets</strong>;</li>
<li>Click on the <strong>Create one</strong> button.</li>
</ol>
<p>Youll then need to name your token set. Set names should be specific, as it is not possible to create multiple token sets with the same name.</p>
<p>When a token set is selected, the tokens within the selected set are displayed on the panel below.</p>
<h3 id="design-tokens-sets-delete">Deleting and Renaming a Token Set</h3>
<p>Token sets can be renamed or deleted by right-clicking on the token set and:</p>
<ol>
<li>Selecting <strong>Rename</strong>, entering a new name, and hitting Enter.</li>
<li>Selecting <strong>Delete</strong>.</li>
</ol>
<figure>
<img src="/img/design-tokens/14-tokens-sets-edit.webp" alt="Tokens sets edit" />
</figure>
<h3 id="design-tokens-sets-create-within"></h3>
<p>Once you have created a token set, you can start creating tokens within that token set. To do so, simply select the token set and create a new token.</p>
<p class="advice">If a token with the same name already exists in another set, a new token can still be created in the current set.</p>
<h3 id="design-tokens-groups">Creating Token Groups</h3>
<p>You can create a token set group by simply naming your token sets to have a folder path. For example, you can create a <strong><i>Light</i></strong> group with a <strong><i>Global</i></strong> set and a <strong><i>Colors</i></strong> set using: <code class="language-js">Light/Global</code>, <code class="language-js">Light/Colors</code>. </p>
<figure>
<img src="/img/design-tokens/15-tokens-sets-group.webp" alt="Tokens sets group" />
</figure>
<h2 id="design-tokens-themes">Token Themes</h2>
<p>Themes are a way to configure your sets to be applied in a specific context, such as a brand, a mode or a touchpoint. Themes enable switching between different styles dynamically by applying different token values depending on the selected theme.</p>
<p>Themes are multidimensional, this means that you can have more than one theme active at the same time, combining the values of the active themes.</p>
<h4>Theme groups</h4>
<p>Using Theme Groups you can categorise your themes into groups. This will allow you to generate a number of combinations involving color themes, brands, platforms, density, and more. Using groups will reduce the need to create an excessive number of individual themes with every combination.</p>
<figure>
<img src="/img/design-tokens/16-tokens-themes.webp" alt="Tokens themes" />
</figure>
<p>For example:</p>
<p><i>Group - theme options</i></p>
<ul>
<li><strong>Mode</strong> - Light, Dark</li>
<li><strong>Brand</strong> - RedPlanet, YellowCab</li>
<li><strong>Contrast</strong> - High, Low, Dim</li>
<li><strong>Platform</strong> - Web, App</li>
</ul>
<p>When you have various themes inside a group, only one of the themes in this group can be active.</p>
<p>Having your sets clubbed under groups makes it more accessible to switch from a matrix of themes.</p>
<h3 id="design-tokens-themes-create">Creating Token Themes</h3>
<p>To create a new theme, click the <strong>Create one</strong> button in the Themes section. You can create a group (this is optional) or add an existing one, and then you then need to assign a name to your theme and click on <strong>Save Theme</strong>.</p>
<p>Your new theme will now appear on the Theme lists. Youll need to enable the tokens sets that you want to include in the theme, clicking on the button “no active sets”. Here you can also activate and deactivate it, as well as delete the theme.</p>
<figure>
<img src="/img/design-tokens/17-tokens-themes-create.webp" alt="Tokens themes create" />
</figure>
<h3 id="design-tokens-themes-edit">Editing Themes</h3>
<p>In the <strong>Themes</strong> section, you can find a dropdown to activate and deactivate themes. If there are no active themes, the dropdown shows a message of: “no theme active”.</p>
<figure>
<img src="/img/design-tokens/19-tokens-themes-edit.webp" alt="Tokens themes edit" />
</figure>
<p>To edit existing themes, you can click on the <strong>Edit</strong> button next to the dropdown in the <strong>Themes</strong> section or open the dropdown and select <strong>Edit themes</strong>:</p>
<figure>
<img src="/img/design-tokens/18-tokens-themes-list.webp" alt="Tokens themes list" />
</figure>
<p>This action will open a modal window where you can activate or deactivate themes, as well as select which Token sets should be part of the theme.</p>
<ol>
<li>You can enable and disable the themes.</li>
<li>Configure the token sets you want to be included in the theme.</li>
<li>Deletes the theme.</li>
<li>Creates a new theme.</li>
</ol>
<h3 id="design-tokens-themes-group">Grouping Themes</h3>
<p>You can categorize your themes into groups. This allows you to generate a matrix of potential combinations involving color themes, brands, modes, and more.</p>
<figure>
<img src="/img/design-tokens/20-tokens-themes-group.webp" alt="Tokens themes group" />
</figure>
<ol>
<li>Select a particular group.</li>
<li>Select the theme from the group.</li>
<li>You can define what token sets should be used as part of this theme option.</li>
<li>Click save theme to see the changes.</li>
<li>Cancel to clear the edits.</li>
<li>Delete.</li>
</ol>
<h2 id="design-tokens-import-export">Importing and Exporting Tokens</h2>
<p>You can export Tokens from Penpot and import them from your computer to a Penpot file. Tokens can be imported from the <strong>Tools</strong> option at the bottom of the <strong>Tokens</strong> tab.</p>
<p>The <strong>Import</strong> functionality allows you to upload and replace the global token set using a single file, while the <strong>Export</strong> functionality lets you download the current global token set using a single file to your system.</p>
<p>These features support JSON files formatted according to specific guidelines and preserve the ability to undo changes if needed.</p>
<figure>
<img src="/img/design-tokens/21-tokens-import-export.webp" alt="Tokens import export" />
</figure>
<ol>
<li><strong>Import:</strong> At the <strong>Tools</strong> option, select <strong>Import</strong>, then select your <code class="language-js">tokens.json</code> file. </li>
<li><strong>Export:</strong> At the <strong>Tools</strong> option, select <strong>Export</strong>. This will export all the tokens, including token sets and themes.</li>
</ol>

View File

@@ -1,5 +1,5 @@
---
title: 14· Import/export files
title: 15· Import/export files
---
<h1 id="import-export">Import and export files</h1>

View File

@@ -1,5 +1,5 @@
---
title: 13· Inspect designs
title: 14· Inspect designs
---
<h1 id="inspect">Inspect designs</h1>

View File

@@ -848,7 +848,7 @@ title: Shortcuts
</table>
<h2 id="viewer-section"> View mode </h2>
<p>The View mode is the area to present and share designs and play the proptotype interactions. <a href="/user-guide/the-interface/#interface-viewmode">More about the View mode</a>.</p>
<p>The View mode is the area to present and share designs and play the prototype interactions. <a href="/user-guide/the-interface/#interface-viewmode">More about the View mode</a>.</p>
<h3 id="generic-viewer">Generic</h3>
<table cellspacing="0" cellpadding="1" border="1" width="100%">

View File

@@ -1,5 +1,5 @@
---
title: 17· Plugins
title: 18· Plugins
---
<h1 id="penpot-plugins">Penpot Plugins</h1>

View File

@@ -1,5 +1,5 @@
---
title: 11· Prototyping
title: 12· Prototyping
---
<h1 id="prototype">Prototyping interactions</h1>

View File

@@ -1,5 +1,5 @@
---
title: 15· Teams
title: 16· Teams
---
<h1 id="teams">Teams</h1>

View File

@@ -1,5 +1,5 @@
---
title: 12· View mode
title: 13· View mode
---
<h1 id="viewmode">View mode</h1>

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.7.0+sha512.5a0afa1d4c1d844b3447ee3319633797bcd6385d9a44be07993ae52ff4facabccafb4af5dcd1c2f9a94ac113e5e9ff56f6130431905884414229e284e37bb7c9",
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.7.0+sha512.5a0afa1d4c1d844b3447ee3319633797bcd6385d9a44be07993ae52ff4facabccafb4af5dcd1c2f9a94ac113e5e9ff56f6130431905884414229e284e37bb7c9",
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6",
"browserslist": [
"defaults"
],

View File

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

View File

@@ -245,6 +245,9 @@ export class WorkspacePage extends BaseWebSocketPage {
async clickAssets(clickOptions = {}) {
await this.sidebar.getByText("Assets").click(clickOptions);
}
async clickLayers(clickOptions = {}) {
await this.sidebar.getByText("Layers").click(clickOptions);
}
async openLibrariesModal(clickOptions = {}) {
await this.sidebar.getByTestId("libraries").click(clickOptions);

View File

@@ -70,3 +70,55 @@ test("[Taiga #9116] Copy CSS background color in the selected format in the INSP
);
expect(rgbaColorText).toContain("background: rgba(");
});
test("[Taiga #10630] [INSPECT] Style assets not being displayed on info tab", async ({
page,
context,
}) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
await workspacePage.mockRPC(
"link-file-to-library",
"workspace/link-file-to-library.json",
);
await workspacePage.mockRPC(
"unlink-file-from-library",
"workspace/unlink-file-from-library.json",
);
await workspacePage.mockRPC(
"get-team-shared-files?team-id=*",
"workspace/get-team-shared-libraries-non-empty.json",
);
await workspacePage.clickColorPalette();
await workspacePage.clickAssets();
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json");
await workspacePage.openLibrariesModal();
await workspacePage.clickLibrary("Testing library 1");
await workspacePage.closeLibrariesModal();
await expect(
workspacePage.palette.getByRole("button", { name: "test-color-187cd5" }),
).toBeVisible();
await workspacePage.clickLayers();
await workspacePage.rectShapeButton.click();
await workspacePage.clickWithDragViewportAt(128, 128, 200, 100);
await workspacePage.clickLeafLayer("Rectangle");
await workspacePage.palette
.getByRole("button", { name: "test-color-187cd5" })
.click();
const inspectButton = workspacePage.page.getByRole("tab", {
name: "Inspect",
});
await inspectButton.click();
const colorLibraryName = workspacePage.page.getByTestId("color-library-name");
await expect(colorLibraryName).toHaveText("test-color-187cd5");
});

View File

@@ -511,9 +511,7 @@ test.describe("Tokens: Sets Tab", () => {
// Creates nesting by renaming set with double click
await tokenThemesSetsSidebar
.getByRole("button", { name: "light-renamed" })
.click({ button: "right" });
await expect(tokenContextMenuForSet).toBeVisible();
await tokenContextMenuForSet.getByText("Rename").click();
.dblclick();
await changeSetInput(tokenThemesSetsSidebar, "nested/light");
await assertSetsList(tokenThemesSetsSidebar, [
@@ -558,6 +556,45 @@ test.describe("Tokens: Sets Tab", () => {
]);
});
test("User can create & edit sets and set groups with an identical name", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokenContextMenuForSet } =
await setupEmptyTokensFile(page);
const tokensTabButton = tokenThemesSetsSidebar
.getByRole("button", { name: "Add set" })
.click();
await createSet(tokenThemesSetsSidebar, "core/colors");
await createSet(tokenThemesSetsSidebar, "core");
await assertSetsList(tokenThemesSetsSidebar, ["core", "colors", "core"]);
await tokenThemesSetsSidebar
.getByRole("button", { name: "core" })
.nth(0)
.dblclick();
await changeSetInput(tokenThemesSetsSidebar, "core-group-renamed");
await assertSetsList(tokenThemesSetsSidebar, [
"core-group-renamed",
"colors",
"core",
]);
await page.keyboard.press(`ControlOrMeta+z`);
await assertSetsList(tokenThemesSetsSidebar, ["core", "colors", "core"]);
await tokenThemesSetsSidebar
.getByRole("button", { name: "core" })
.nth(1)
.dblclick();
await changeSetInput(tokenThemesSetsSidebar, "core-set-renamed");
await assertSetsList(tokenThemesSetsSidebar, [
"core",
"colors",
"core-set-renamed",
]);
});
test("Fold/Unfold set", async ({ page }) => {
const { tokenThemesSetsSidebar, tokenSetGroupItems } =
await setupTokensFile(page);

View File

@@ -20,11 +20,23 @@ test("Save and restore version", async ({ page }) => {
"workspace/update-file-create-rect.json",
);
await workspacePage.mockRPC(
"push-audit-events",
"workspace/audit-event-empty.json",
);
await workspacePage.mockRPC(
"update-profile-props",
"workspace/update-profile-empty.json",
);
await workspacePage.goToWorkspace({
fileId: "406b7b01-d3e2-80e4-8005-3138ac5d449c",
pageId: "406b7b01-d3e2-80e4-8005-3138ac5d449d",
});
await workspacePage.moveButton.click();
await workspacePage.mockRPC(
"get-file-snapshots?file-id=*",
"workspace/versions-snapshot-1.json",

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -239,15 +239,15 @@
(str (:last-id file)))
(lookupShape [_ shape-id]
(clj->js (fb/lookup-shape file (uuid/uuid shape-id))))
(clj->js (fb/lookup-shape file (uuid/parse shape-id))))
(updateObject [_ id new-obj]
(let [old-obj (fb/lookup-shape file (uuid/uuid id))
(let [old-obj (fb/lookup-shape file (uuid/parse id))
new-obj (d/deep-merge old-obj (parse-data new-obj))]
(set! file (fb/update-object file old-obj new-obj))))
(deleteObject [_ id]
(set! file (fb/delete-object file (uuid/uuid id))))
(set! file (fb/delete-object file (uuid/parse id))))
(getId [_]
(:id file))

View File

@@ -15,7 +15,7 @@
[file ^string page-id]
;; Better to expose the api as a promise to be consumed from JS
(let [page-id (uuid/uuid page-id)
(let [page-id (uuid/parse page-id)
file-data (.-file file)
data (get-in file-data [:data :pages-index page-id])]
(p/create

View File

@@ -15,7 +15,6 @@
[app.main.data.profile :as dp]
[app.main.data.websocket :as ws]
[app.main.errors]
[app.main.features :as feat]
[app.main.rasterizer :as thr]
[app.main.store :as st]
[app.main.ui :as ui]
@@ -67,7 +66,6 @@
(watch [_ _ stream]
(rx/merge
(rx/of (ev/initialize)
(feat/initialize)
(dp/refresh-profile))
;; Watch for profile deletion events

View File

@@ -192,3 +192,5 @@
:height 720}])
(def zoom-half-pixel-precision 8)
(def max-input-length 255)

View File

@@ -13,7 +13,6 @@
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.main.data.helpers :as dsh]
[app.main.features :as features]
[app.main.worker :as uw]
[app.util.time :as dt]
[beicon.v2.core :as rx]
@@ -182,8 +181,8 @@
(let [file-id (or file-id (:current-file-id state))
uchg (vec undo-changes)
rchg (vec redo-changes)
features (features/get-team-enabled-features state)
permissions (:permissions state)]
features (get state :features)
permissions (get state :permissions)]
;; Prevent commit changes by a viewer team member (it really should never happen)
(when (:can-edit permissions)

View File

@@ -69,7 +69,7 @@
"Retrieves the mentions in the content as an array of uuids"
[content]
(->> (re-seq r-mentions content)
(mapv (fn [[_ _ id]] (uuid/uuid id)))))
(mapv (fn [[_ _ id]] (uuid/parse id)))))
(defn update-mentions
"Updates the params object with the mentiosn"
@@ -648,9 +648,7 @@
(defn detach-comment-thread
"Detach comment threads that are inside a frame when that frame is deleted"
[ids]
(dm/assert!
"expected a valid coll of uuid's"
(sm/check-coll-of-uuid! ids))
(assert (sm/check-coll-of-uuid ids))
(ptk/reify ::detach-comment-thread
ptk/WatchEvent

View File

@@ -16,7 +16,6 @@
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as-alias dps]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
@@ -73,7 +72,7 @@
(st/emit! (ntf/hide)))
(defn handle-notification
[{:keys [message code level] :as params}]
[{:keys [message code] :as params}]
(ptk/reify ::show-notification
ptk/WatchEvent
(watch [_ _ _]
@@ -81,9 +80,6 @@
:upgrade-version
(rx/of (ntf/dialog
:content (tr "notifications.by-code.upgrade-version")
:controls :inline-actions
:type :inline
:level level
:accept {:label (tr "labels.refresh")
:callback force-reload!}
:tag :notification))
@@ -91,16 +87,14 @@
:maintenance
(rx/of (ntf/dialog
:content (tr "notifications.by-code.maintenance")
:controls :inline-actions
:type level
:accept {:label (tr "labels.accept")
:callback hide-notifications!}
:tag :notification))
(rx/of (ntf/dialog
:content message
:controls :close
:type level
:accept {:label (tr "labels.close")
:callback hide-notifications!}
:tag :notification))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -112,7 +106,7 @@
(ptk/reify ::show-shared-dialog
ptk/WatchEvent
(watch [_ state _]
(let [features (features/get-team-enabled-features state)
(let [features (get state :features)
file (dsh/lookup-file state)
data (get file :data)]
@@ -169,8 +163,8 @@
(ptk/reify ::export-files
ptk/WatchEvent
(watch [_ state _]
(let [features (features/get-team-enabled-features state)
team-id (:current-team-id state)]
(let [features (get state :features)
team-id (get state :current-team-id)]
(->> (rx/from files)
(rx/mapcat
(fn [file]

View File

@@ -19,7 +19,6 @@
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
[app.main.data.websocket :as dws]
[app.main.features :as features]
[app.main.repo :as rp]
[app.util.i18n :as i18n :refer [tr]]
[app.util.sse :as sse]
@@ -57,8 +56,7 @@
(rx/filter (fn [{:keys [topic] :as msg}]
(or (= topic uuid/zero)
(= topic profile-id))))
(rx/map process-message)
(rx/ignore)))
(rx/map process-message)))
(rx/take-until stopper))))))
@@ -497,7 +495,7 @@
base-name (tr "dashboard.new-file-prefix")
name (or name
(cfh/generate-unique-name base-name unames :immediate-suffix? true))
features (-> (features/get-team-enabled-features state)
features (-> (get state :features)
(set/difference cfeat/frontend-only-features))
params (-> params
(assoc :name name)
@@ -536,11 +534,8 @@
(defn move-files
[{:keys [ids project-id] :as params}]
(dm/assert! (uuid? project-id))
(dm/assert!
"expected a valid set of uuids"
(sm/check-set-of-uuid! ids))
(assert (uuid? project-id))
(assert (sm/check-set-of-uuid ids))
(ptk/reify ::move-files
ev/Event

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