Compare commits

...

100 Commits

Author SHA1 Message Date
Andrey Antukh
6e3673136a 📎 Update changelog 2025-04-09 09:19:05 +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
165 changed files with 2923 additions and 1586 deletions

View File

@@ -1,9 +1,11 @@
# CHANGELOG
## 2.6.0 (Unreleased)
## 2.6.0
### :rocket: Epics and highlights
- Design Tokens
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
@@ -13,6 +15,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 +36,23 @@
- 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

@@ -156,9 +156,9 @@
[mw/params]
[mw/format-response]
[mw/parse-request]
[mw/errors errors/handle]
[session/soft-auth cfg]
[actoken/soft-auth cfg]
[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

@@ -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

@@ -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

@@ -212,7 +212,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)))
{: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?)

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

@@ -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

@@ -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

@@ -29,7 +29,6 @@
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[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]))
@@ -97,13 +96,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]
@@ -1226,32 +1225,7 @@
(update :pages-index update-vals update-container)
(update :components 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
@@ -1320,6 +1294,4 @@
"legacy-65"
"legacy-66"
"legacy-67"
"Ensure hidden theme"
"Add token theme id"
"Remove tokens from groups"]))
"0001-remove-tokens-from-groups"]))

View File

@@ -1639,7 +1639,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

@@ -1019,26 +1019,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

@@ -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]
@@ -180,7 +179,7 @@
(sm/register! ::interaction schema:interaction)
(def check-interaction!
(def check-interaction
(sm/check-fn schema:interaction))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -203,18 +202,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 +224,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 +273,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 +296,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 +316,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 +331,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 +346,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 +362,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 +380,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 +394,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 +449,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 +543,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 +587,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 +601,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 +618,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 +633,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 +662,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

@@ -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

@@ -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

@@ -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:
@@ -88,28 +88,15 @@ services:
- penpot
labels:
- "traefik.enable=true"
# - "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

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
@@ -224,6 +200,8 @@ server {
}
```
For full documentation, go to the [official website][2]
### Example with CADDY SERVER
```bash
@@ -236,4 +214,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

@@ -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

@@ -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

@@ -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

@@ -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

View File

@@ -12,7 +12,6 @@
[app.common.schema :as sm]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.features :as features]
[app.main.repo :as rp]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -47,7 +46,7 @@
(ptk/reify ::export-files
ptk/WatchEvent
(watch [_ state _]
(let [features (features/get-team-enabled-features state)
(let [features (get state :features)
team-id (:current-team-id state)
evname (if (= format :legacy-zip)
"export-standard-files"

View File

@@ -19,7 +19,7 @@
(def ^:private schema:notification
[:map {:title "Notification"}
[:level [::sm/one-of #{:success :error :info :warning}]]
[:level {:optional true} [::sm/one-of #{:success :error :info :warning}]]
[:status {:optional true}
[::sm/one-of #{:visible :hide}]]
[:position {:optional true}
@@ -129,15 +129,11 @@
:timeout timeout})))
(defn dialog
[& {:keys [content controls actions accept cancel position tag level links]
:or {controls :none position :floating level :info}}]
[& {:keys [content accept cancel tag links]}]
(show (d/without-nils
{:content content
:level level
:links links
:position position
:controls controls
:actions actions
:type :inline
:accept accept
:cancel cancel
:links links
:tag tag})))

View File

@@ -204,6 +204,9 @@
(rx/filter #(= % ::force-persist))))]
(rx/merge
(->> notifier-s
(rx/map #(ptk/data-event ::persistence-notification)))
(->> local-commits-s
(rx/debounce 200)
(rx/map (fn [_]

View File

@@ -101,7 +101,7 @@
(let [permissions (get team :permissions)
features (get team :features)]
(rx/of #(assoc % :permissions permissions)
(features/initialize (or features #{}))
(features/initialize features)
(fetch-members team-id))))))
ptk/EffectEvent
@@ -255,12 +255,12 @@
(dm/assert! (string? name))
(ptk/reify ::create-team
ptk/WatchEvent
(watch [it state _]
(watch [it _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)
params {:name name :features features}]
features features/global-enabled-features
params {:name name :features features}]
(->> (rp/cmd! :create-team (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
@@ -272,11 +272,11 @@
[{:keys [name emails role] :as params}]
(ptk/reify ::create-team-with-invitations
ptk/WatchEvent
(watch [it state _]
(watch [it _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)
features features/global-enabled-features
params {:name name
:emails emails
:role role
@@ -350,12 +350,10 @@
(defn create-invitations
[{:keys [emails role team-id resend?] :as params}]
(dm/assert! (keyword? role))
(dm/assert! (uuid? team-id))
(dm/assert!
"expected a valid set of emails"
(sm/check-set-of-emails! emails))
(assert (keyword? role))
(assert (uuid? team-id))
(assert (sm/check-set-of-emails emails))
(ptk/reify ::create-invitations
ev/Event
@@ -376,11 +374,8 @@
(defn copy-invitation-link
[{:keys [email team-id] :as params}]
(dm/assert!
"expected a valid email"
(sm/check-email! email))
(dm/assert! (uuid? team-id))
(assert (sm/check-email email))
(assert (uuid? team-id))
(ptk/reify ::copy-invitation-link
IDeref
@@ -406,12 +401,9 @@
(defn update-invitation-role
[{:keys [email team-id role] :as params}]
(dm/assert!
"expected a valid email"
(sm/check-email! email))
(dm/assert! (uuid? team-id))
(dm/assert! (contains? ctt/valid-roles role))
(assert (sm/check-email email))
(assert (uuid? team-id))
(assert (contains? ctt/valid-roles role))
(ptk/reify ::update-invitation-role
IDeref
@@ -428,8 +420,9 @@
(defn delete-invitation
[{:keys [email team-id] :as params}]
(dm/assert! (sm/check-email! email))
(dm/assert! (uuid? team-id))
(assert (sm/check-email email))
(assert (uuid? team-id))
(ptk/reify ::delete-invitation
ptk/WatchEvent
(watch [_ _ _]

View File

@@ -91,16 +91,21 @@
(ptk/reify ::update-token-theme
ptk/WatchEvent
(watch [it state _]
(let [tokens-lib (get-tokens-lib state)
data (dsh/lookup-file-data state)
prev-token-theme (some-> tokens-lib (ctob/get-theme group name))
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token-theme (:group prev-token-theme)
(:name prev-token-theme)
token-theme))]
(rx/of
(dch/commit-changes changes))))))
(let [data (dsh/lookup-file-data state)
tokens-lib (get data :tokens-lib)]
(if (and (or (not= group (:group token-theme))
(not= name (:name token-theme)))
(ctob/get-theme tokens-lib
(:group token-theme)
(:name token-theme)))
(rx/of (ntf/show {:content (tr "errors.token-theme-already-exists")
:type :toast
:level :error
:timeout 9000}))
(let [changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token-theme group name token-theme))]
(rx/of (dch/commit-changes changes))))))))
(defn toggle-token-theme-active? [group name]
(ptk/reify ::toggle-token-theme-active?
@@ -252,6 +257,8 @@
:level :error
:timeout 9000})))))))
;; FIXME: add schema for params
(defn drop-token-set-group [drop-opts]
(ptk/reify ::drop-token-set-group
ptk/WatchEvent
@@ -265,17 +272,21 @@
(rx/of
(drop-error (ex-data e))))))))
(defn drop-token-set [drop-opts]
;; FIXME: add schema for params
(defn drop-token-set
[params]
(ptk/reify ::drop-token-set
ptk/WatchEvent
(watch [it state _]
(try
(when-let [changes (clt/generate-move-token-set (pcb/empty-changes it) (get-tokens-lib state) drop-opts)]
(let [tokens-lib (get-tokens-lib state)
changes (-> (pcb/empty-changes it)
(clt/generate-move-token-set tokens-lib params))]
(rx/of (dch/commit-changes changes)
(wtu/update-workspace-tokens)))
(catch :default e
(rx/of
(drop-error (ex-data e))))))))
(catch :default cause
(rx/of (drop-error (ex-data cause))))))))
(defn- create-token-with-set
"A special case when a first token is created and no set exists"

View File

@@ -184,11 +184,13 @@
ptk/UpdateEvent
(update [_ state]
(let [team-id (:id team)
team {:members users}]
team (assoc team :members users)]
(-> state
(assoc :share-links share-links)
(assoc :current-team-id team-id)
(assoc :teams {team-id team})
(assoc :files (-> (d/index-by :id libraries)
(assoc (:id file) file)))
(assoc :viewer {:libraries (d/index-by :id libraries)
:users (d/index-by :id users)
:permissions permissions

View File

@@ -44,6 +44,7 @@
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as-alias dps]
[app.main.data.plugins :as dp]
[app.main.data.profile :as du]
[app.main.data.project :as dpj]
@@ -168,20 +169,12 @@
(let [data (assoc data :pages-index pages-index)]
(assoc file :data (d/removem (comp t/pointer? val) data))))))))))
(defn- libraries-fetched
(defn- check-libraries-synchronozation
[file-id libraries]
(ptk/reify ::libraries-fetched
ptk/UpdateEvent
(update [_ state]
(let [libraries (->> libraries
(map (fn [l] (assoc l :library-of file-id)))
(d/index-by :id))]
(update state :files merge libraries)))
(ptk/reify ::check-libraries-synchronozation
ptk/WatchEvent
(watch [_ state _]
(let [file (dsh/lookup-file state)
file-id (get file :id)
(let [file (dsh/lookup-file state file-id)
ignore-until (get file :ignore-sync-until)
needs-check?
@@ -194,29 +187,47 @@
(->> (rx/of (dwl/notify-sync-file file-id))
(rx/delay 1000)))))))
(defn- library-resolved
[library]
(ptk/reify ::library-resolved
ptk/UpdateEvent
(update [_ state]
(update state :files assoc (:id library) library))))
(defn- libraries-fetched
[file-id libraries]
(ptk/reify ::libraries-fetched
ptk/UpdateEvent
(update [_ state]
(update state :files merge
(->> libraries
(map #(assoc % :library-of file-id))
(d/index-by :id))))))
(defn- fetch-libraries
[file-id]
[file-id features]
(ptk/reify ::fetch-libries
ptk/WatchEvent
(watch [_ state _]
(let [features (features/get-team-enabled-features state)]
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat
(fn [libraries]
(rx/merge
(->> (rx/from libraries)
(rx/merge-map
(fn [{:keys [id synced-at]}]
(->> (rp/cmd! :get-file {:id id :features features})
(rx/map #(assoc % :synced-at synced-at)))))
(rx/merge-map resolve-file)
(rx/reduce conj [])
(rx/map (partial libraries-fetched file-id)))
(->> (rx/from libraries)
(rx/map :id)
(rx/mapcat (fn [file-id]
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
(rx/map dwl/library-thumbnails-fetched))))))))))
(watch [_ _ _]
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat
(fn [libraries]
(rx/concat
(rx/of (libraries-fetched file-id libraries))
(rx/merge
(->> (rx/from libraries)
(rx/merge-map
(fn [{:keys [id synced-at]}]
(->> (rp/cmd! :get-file {:id id :features features})
(rx/map #(assoc % :synced-at synced-at :library-of file-id)))))
(rx/mapcat resolve-file)
(rx/map library-resolved))
(->> (rx/from libraries)
(rx/map :id)
(rx/mapcat (fn [file-id]
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
(rx/map dwl/library-thumbnails-fetched)))
(rx/of (check-libraries-synchronozation file-id libraries)))))))))
(defn- workspace-initialized
[file-id]
@@ -234,29 +245,16 @@
(fbs/fix-broken-shapes)))))
(defn- bundle-fetched
[{:keys [features file thumbnails]}]
[{:keys [file file-id thumbnails] :as bundle}]
(ptk/reify ::bundle-fetched
IDeref
(-deref [_]
{:features features
:file file
:thumbnails thumbnails})
(-deref [_] bundle)
ptk/UpdateEvent
(update [_ state]
(let [file-id (:id file)]
(-> state
(assoc :thumbnails thumbnails)
(update :files assoc file-id file))))
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
file-id (:id file)]
(rx/of (dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout)
(fetch-libraries file-id))))))
(-> state
(assoc :thumbnails thumbnails)
(update :files assoc file-id file)))))
(defn zoom-to-frame
[]
@@ -285,46 +283,30 @@
(defn- fetch-bundle
"Multi-stage file bundle fetch coordinator"
[file-id]
[file-id features]
(ptk/reify ::fetch-bundle
ptk/WatchEvent
(watch [_ state stream]
(let [features (features/get-team-enabled-features state)
render-wasm? (contains? features "render-wasm/v1")
stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
team-id (:current-team-id state)]
(->> (rx/concat
;; Firstly load wasm module if it is enabled and fonts
(rx/merge
(if ^boolean render-wasm?
(->> (rx/from @wasm/module)
(rx/ignore))
(rx/empty))
(->> stream
(rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1)
(rx/ignore))
(rx/of (df/fetch-fonts team-id)))
;; Then fetch file and thumbnails
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
(get-file-object-thumbnails file-id))
(rx/take 1)
(rx/mapcat
(fn [[file thumbnails]]
(->> (resolve-file file)
(rx/map (fn [file]
{:file file
:features features
:thumbnails thumbnails})))))
(rx/map bundle-fetched)))
(watch [_ _ stream]
(let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
(get-file-object-thumbnails file-id))
(rx/take 1)
(rx/mapcat
(fn [[file thumbnails]]
(->> (resolve-file file)
(rx/map (fn [file]
{:file file
:file-id file-id
:features features
:thumbnails thumbnails})))))
(rx/map bundle-fetched)
(rx/take-until stopper-s))))))
(defn initialize-workspace
[file-id]
[team-id file-id]
(assert (uuid? team-id) "expected valud uuid for `team-id`")
(assert (uuid? file-id) "expected valud uuid for `file-id`")
(ptk/reify ::initialize-workspace
ptk/UpdateEvent
(update [_ state]
@@ -336,24 +318,56 @@
ptk/WatchEvent
(watch [_ state stream]
(log/debug :hint "initialize-workspace" :file-id (dm/str file-id))
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state)]
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state)
features (features/get-enabled-features state team-id)
render-wasm? (contains? features "render-wasm/v1")]
(log/debug :hint "initialize-workspace"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(->> (rx/merge
(rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles)
(fetch-bundle file-id))
(rx/concat
;; Fetch all essential data that should be loaded before the file
(rx/merge
(if ^boolean render-wasm?
(->> (rx/from @wasm/module)
(rx/ignore))
(rx/empty))
(->> stream
(rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1)
(rx/ignore))
(rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles)
(df/fetch-fonts team-id)))
;; Once the essential data is fetched, lets proceed to
;; fetch teh file bunldle
(rx/of (fetch-bundle file-id features)))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
(rx/take 1)
(rx/map deref)
(rx/mapcat (fn [{:keys [file]}]
(rx/of (dpj/initialize-project (:project-id file))
(-> (workspace-initialized file-id)
(with-meta {:file-id file-id}))))))
(rx/mapcat
(fn [{:keys [file]}]
(rx/of (dpj/initialize-project (:project-id file))
(dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout)
(fetch-libraries file-id features)
(-> (workspace-initialized file-id)
(with-meta {:team-id team-id
:file-id file-id}))))))
(->> stream
(rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(when-let [component-id (some-> rparams :component-id parse-uuid)]
(->> stream
@@ -394,7 +408,7 @@
(unchecked-set ug/global "name" name)))))
(defn finalize-workspace
[file-id]
[_team-id file-id]
(ptk/reify ::finalize-workspace
ptk/UpdateEvent
(update [_ state]
@@ -406,6 +420,7 @@
:workspace-media-objects
:workspace-persistence
:workspace-presence
:workspace-tokens
:workspace-undo)
(update :workspace-global dissoc :read-only?)
(assoc-in [:workspace-global :options-mode] :design)))
@@ -413,7 +428,6 @@
ptk/WatchEvent
(watch [_ state _]
(let [project-id (:current-project-id state)]
(rx/of (dwn/finalize file-id)
(dpj/finalize-project project-id)
(dwsl/finalize-shape-layout)
@@ -427,14 +441,13 @@
(ptk/reify ::reload-current-file
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)]
(rx/of (initialize-workspace file-id))))))
(let [file-id (:current-file-id state)
team-id (:current-team-id state)]
(rx/of (initialize-workspace team-id file-id))))))
;; Make this event callable through dynamic resolution
(defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file))
(def ^:private xf:collect-file-media
"Resolve and collect all file media on page objects"
(comp (map second)
@@ -471,15 +484,25 @@
(defn initialize-page
[file-id page-id]
(assert (uuid? file-id) "expected valid uuid for `file-id`")
(assert (uuid? page-id) "expected valid uuid for `page-id`")
(ptk/reify ::initialize-page
ptk/WatchEvent
(watch [_ state _]
(if-let [page (dsh/lookup-page state file-id page-id)]
(rx/of (initialize-page* file-id page-id page)
(dwth/watch-state-changes file-id page-id)
(dwl/watch-component-changes)
(select-frame-tool file-id page-id))
(rx/concat
(rx/of (initialize-page* file-id page-id page)
(dwth/watch-state-changes file-id page-id)
(dwl/watch-component-changes))
(let [profile (:profile state)
props (get profile :props)]
(when (not (:workspace-visited props))
(rx/of (select-frame-tool file-id page-id)))))
;; NOTE: this redirect is necessary for cases where user
;; explicitly passes an non-existing page-id on the url
;; params, so on check it we can detect that there are no data
;; for the page and redirect user to an existing page
(rx/of (dcm/go-to-workspace :file-id file-id ::rt/replace true))))))
(defn finalize-page
@@ -1390,7 +1413,7 @@
(let [objects (dsh/lookup-page-objects state)
selected (->> (dsh/lookup-selected state)
(cfh/clean-loops objects))
features (-> (features/get-team-enabled-features state)
features (-> (get state :features)
(set/difference cfeat/frontend-only-features))
file-id (:current-file-id state)
@@ -1628,9 +1651,10 @@
objects (dsh/lookup-page-objects state)]
(when-let [shape (get objects selected)]
(let [props (cts/extract-props shape)
features (-> (features/get-team-enabled-features state)
features (-> (get state :features)
(set/difference cfeat/frontend-only-features))
version (-> (dsh/lookup-file state) :version)
version (-> (dsh/lookup-file state)
(get :version))
copy-data {:type :copied-props
:features features
@@ -1764,8 +1788,8 @@
(ptk/reify ::paste-transit-shapes
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
features (features/get-team-enabled-features state)]
(let [file-id (:current-file-id state)
features (get state :features)]
(when-not (paste-data-valid? pdata)
(ex/raise :type :validation
@@ -1836,7 +1860,7 @@
(ptk/reify ::paste-transit-props
ptk/WatchEvent
(watch [_ state _]
(let [features (features/get-team-enabled-features state)
(let [features (get state :features)
selected (dsh/lookup-selected state)]
(when (paste-data-valid? pdata)

View File

@@ -134,9 +134,7 @@
;; Move comment threads that are inside a frame when that frame is moved"
(defmethod ptk/resolve ::move-frame-comment-threads
[_ 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 ::move-frame-comment-threads
ptk/WatchEvent

View File

@@ -7,7 +7,6 @@
(ns app.main.data.workspace.common
(:require
[app.common.logging :as log]
[app.config :as cf]
[app.main.data.profile :as du]
[app.main.data.workspace.layout :as dwl]
[beicon.v2.core :as rx]
@@ -38,13 +37,9 @@
(watch [_ state _]
(let [profile (:profile state)
props (get profile :props)]
(when (and (cf/external-feature-flag "boards-03" "test") (not (:workspace-visited props)))
(when (not (:workspace-visited props))
(rx/of (du/update-profile-props {:workspace-visited true})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UNDO
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Toolbar
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -120,6 +120,14 @@
(pcb/with-page-id page-id)
(pcb/with-objects objects)
(pcb/add-object group {:index group-idx})
;; Create a group needs to reset the constraints to scale/scale
(pcb/update-shapes
(map :id shapes)
(fn [shape]
(-> shape
(d/assoc-when :constraints-h :scale)
(d/assoc-when :constraints-v :scale))))
(pcb/update-shapes (map :id shapes) ctl/remove-layout-item-data)
(pcb/change-parent (:id group) (reverse shapes))
(pcb/update-shapes (map :id shapes-to-detach) ctk/detach-shape)

View File

@@ -1398,7 +1398,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [features (features/get-team-enabled-features state)]
(let [features (get state :features)]
(rx/concat
(rx/merge
(->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id})

View File

@@ -533,14 +533,98 @@
(assoc state :workspace-modifiers modif-tree)))))
(def ^:private xf:without-uuid-zero
(remove #(= % uuid/zero)))
(def ^:private transform-attrs
#{:selrect
:points
:x
:y
:r1
:r2
:r3
:r4
:shadow
:blur
:strokes
:width
:height
:content
:transform
:transform-inverse
:rotation
:flip-x
:flip-y
:grow-type
:position-data
:layout-gap
:layout-padding
:layout-item-h-sizing
:layout-item-max-h
:layout-item-max-w
:layout-item-min-h
:layout-item-min-w
:layout-item-v-sizing
:layout-padding-type
:layout-item-margin
:layout-item-margin-type
:layout-grid-cells
:layout-grid-columns
:layout-grid-rows})
(defn apply-modifiers*
"A lower-level version of apply-modifiers, that expects receive ready
to use objects, object-modifiers and text-modifiers."
[objects object-modifiers text-modifiers options]
(ptk/reify ::apply-modifiers*
ptk/WatchEvent
(watch [_ _ _]
(let [ids
(into [] xf:without-uuid-zero (keys object-modifiers))
ids-with-children
(into ids
(mapcat (partial cfh/get-children-ids objects))
ids)
ignore-tree
(calculate-ignore-tree object-modifiers objects)
options
(-> options
(assoc :reg-objects? true)
(assoc :ignore-tree ignore-tree)
;; Attributes that can change in the transform. This
;; way we don't have to check all the attributes
(assoc :attrs transform-attrs))
update-shape
(fn [shape]
(let [shape-id (dm/get-prop shape :id)
modifiers (dm/get-in object-modifiers [shape-id :modifiers])
text-shape? (cfh/text-shape? shape)
pos-data (when ^boolean text-shape?
(dm/get-in text-modifiers [shape-id :position-data]))]
(-> shape
(gsh/transform-shape modifiers)
(cond-> (d/not-empty? pos-data)
(assoc-position-data pos-data shape))
(cond-> text-shape?
(update-grow-type shape)))))]
(rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers})
(ptk/event ::dwcm/move-frame-comment-threads ids-with-children)
(dwsh/update-shapes ids update-shape options))))))
(defn apply-modifiers
([]
(apply-modifiers nil))
([{:keys [modifiers undo-transation? stack-undo? ignore-constraints
ignore-snap-pixel ignore-touched undo-group page-id]
:or {undo-transation? true stack-undo? false ignore-constraints false
ignore-snap-pixel false ignore-touched false}}]
([{:keys [modifiers undo-transation? ignore-constraints
ignore-snap-pixel page-id]
:or {undo-transation? true ignore-constraints false
ignore-snap-pixel false}
:as options}]
(ptk/reify ::apply-modifiers
ptk/WatchEvent
(watch [_ state _]
@@ -553,88 +637,17 @@
(calculate-modifiers state ignore-constraints ignore-snap-pixel modifiers page-id)
(get state :workspace-modifiers))
ids
(into []
(remove #(= % uuid/zero))
(keys object-modifiers))
ids-with-children
(into ids
(mapcat (partial cfh/get-children-ids objects))
ids)
ignore-tree
(calculate-ignore-tree object-modifiers objects)
undo-id (js/Symbol)]
undo-id
(js/Symbol)]
(rx/concat
(if undo-transation?
(rx/of (dwu/start-undo-transaction undo-id))
(rx/empty))
(rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers})
(ptk/event ::dwcm/move-frame-comment-threads ids-with-children)
(dwsh/update-shapes
ids
(fn [shape]
(let [modif (get-in object-modifiers [(:id shape) :modifiers])
text-shape? (cfh/text-shape? shape)
position-data (when text-shape?
(dm/get-in text-modifiers [(:id shape) :position-data]))]
(-> shape
(gsh/transform-shape modif)
(cond-> (d/not-empty? position-data)
(assoc-position-data position-data shape))
(cond-> text-shape?
(update-grow-type shape)))))
{:reg-objects? true
:stack-undo? stack-undo?
:ignore-tree ignore-tree
:ignore-touched ignore-touched
:undo-group undo-group
:page-id page-id
;; Attributes that can change in the transform. This way we don't have to check
;; all the attributes
:attrs [:selrect
:points
:x
:y
:r1
:r2
:r3
:r4
:shadow
:blur
:strokes
:width
:height
:content
:transform
:transform-inverse
:rotation
:flip-x
:flip-y
:grow-type
:position-data
:layout-gap
:layout-padding
:layout-item-h-sizing
:layout-item-margin
:layout-item-max-h
:layout-item-max-w
:layout-item-min-h
:layout-item-min-w
:layout-item-v-sizing
:layout-padding-type
:layout-gap
:layout-item-margin
:layout-item-margin-type
:layout-grid-cells
:layout-grid-columns
:layout-grid-rows]})
;; We've applied the text-modifier so we can dissoc the temporary data
(rx/of (apply-modifiers* objects object-modifiers text-modifiers options)
(fn [state]
(update state :workspace-text-modifier #(apply dissoc % ids))))
(let [ids (into [] xf:without-uuid-zero (keys object-modifiers))]
(update state :workspace-text-modifier #(apply dissoc % ids)))))
(if (nil? modifiers)
(rx/of (clear-local-transform))
(rx/empty))

View File

@@ -65,7 +65,6 @@
(->> (rx/from initmsg)
(rx/map dws/send))
;; Subscribe to notifications of the subscription
(->> stream
(rx/filter (ptk/type? ::dws/message))

View File

@@ -324,7 +324,9 @@
(let [id (dm/get-in state [:workspace-local :edition])
objects (dsh/lookup-page-objects state)
content (dm/get-in objects [id :content])]
(update-in state [:workspace-local :edit-path id] assoc :old-content content)))
(if content
(update-in state [:workspace-local :edit-path id] assoc :old-content content)
state)))
ptk/WatchEvent
(watch [_ state stream]

View File

@@ -51,11 +51,8 @@
([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-touched undo-group with-objects? changed-sub-attr]
:or {reg-objects? false save-undo? true stack-undo? false ignore-touched false with-objects? false}}]
(dm/assert!
"expected a valid coll of uuid's"
(sm/check-coll-of-uuid! ids))
(dm/assert! (fn? update-fn))
(assert (sm/check-coll-of-uuid ids))
(assert (fn? update-fn))
(ptk/reify ::update-shapes
ptk/WatchEvent
@@ -162,9 +159,7 @@
([ids] (delete-shapes nil ids {}))
([page-id ids] (delete-shapes page-id ids {}))
([page-id ids options]
(dm/assert!
"expected a valid set of uuid's"
(sm/check-set-of-uuid! ids))
(assert (sm/check-set-of-uuid ids))
(ptk/reify ::delete-shapes
ptk/WatchEvent

View File

@@ -14,7 +14,6 @@
[app.main.data.changes :as dch]
[app.main.data.helpers :as dsh]
[app.main.data.persistence :as-alias dps]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.notifications :as-alias wnt]
[app.main.rasterizer :as thr]
[app.main.refs :as refs]
@@ -293,10 +292,4 @@
(rx/mapcat #(into #{} %))
(rx/map #(update-thumbnail file-id page-id % "frame" "watch-state-changes"))))
;; WARNING: This is a workaround for an AB test, in case we consolidate this change we should
;; find a better way to handle this.
(->> notifier-s
(rx/take 1)
(rx/map dwc/set-workspace-visited))
(rx/take-until stopper-s))))))

View File

@@ -340,35 +340,35 @@
(rx/filter (ptk/type? ::trigger-bounding-box-cloaking) stream)))))))
(defn update-dimensions
"Change size of shapes, from the sideber options form.
Will ignore pixel snap used in the options side panel"
"Change size of shapes, from the sidebar options form
(will ignore pixel snap)"
([ids attr value] (update-dimensions ids attr value nil))
([ids attr value options]
(dm/assert! (number? value))
(dm/assert!
"expected valid coll of uuids"
(every? uuid? ids))
(dm/assert!
"expected valid attr"
(contains? #{:width :height} attr))
(ptk/reify ::update-dimensions
ptk/UpdateEvent
(update [_ state]
(let [page-id (or (get options :page-id)
(get state :current-page-id))
(assert (number? value))
(assert (every? uuid? ids)
"expected valid coll of uuids")
(assert (contains? #{:width :height} attr)
"expected valid attr")
(ptk/reify ::update-dimensions
ptk/WatchEvent
(watch [_ state _]
(let [page-id
(or (get options :page-id)
(get state :current-page-id))
objects
(dsh/lookup-page-objects state page-id)
objects (dsh/lookup-page-objects state page-id)
get-modifier
(fn [shape] (ctm/change-dimensions-modifiers shape attr value))
(fn [shape]
(ctm/change-dimensions-modifiers shape attr value))
modif-tree
(-> (dwm/build-modif-tree ids objects get-modifier)
(gm/set-objects-modifiers objects))]
(assoc state :workspace-modifiers modif-tree)))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwm/apply-modifiers options))))))
(rx/of (dwm/apply-modifiers* objects modif-tree nil options)))))))
(defn change-orientation
"Change orientation of shapes, from the sidebar options form.
@@ -859,42 +859,44 @@
(rx/of (reorder-selected-layout-child direction))
(rx/of (nudge-selected-shapes direction shift?)))))))
(defn- get-delta [position bbox]
(let [cpos (gpt/point (:x bbox) (:y bbox))
pos (gpt/point (or (:x position) (:x bbox))
(or (:y position) (:y bbox)))]
(gpt/subtract pos cpos)))
(defn- get-relative-delta [position bbox frame]
(let [frame-bbox (-> frame :points grc/points->rect)
relative-cpos (gpt/subtract (gpt/point (:x bbox) (:y bbox))
(gpt/point (:x frame-bbox)
(:y frame-bbox)))
cpos (gpt/point (:x relative-cpos) (:y relative-cpos))
pos (gpt/point (or (:x position) (:x relative-cpos))
(or (:y position) (:y relative-cpos)))]
(gpt/subtract pos cpos)))
(defn- calculate-delta
[position bbox relative-to]
(let [current (gpt/point (:x bbox) (:y bbox))
position (gpt/point (or (some-> (:x position) (+ (dm/get-prop relative-to :x)))
(:x bbox))
(or (some-> (:y position) (+ (dm/get-prop relative-to :y)))
(:y bbox)))]
(gpt/subtract position current)))
(defn update-position
"Move shapes to a new position"
"Move shapes to a new position. It will resolve to the current frame
of the shape, unless given the absolute option. In this case it will
resolve to the root frame of the page.
The position is a map that can have a partial position (it means it
can receive {:x 10}."
([id position] (update-position id position nil))
([id position options]
(dm/assert! (uuid? id))
(assert (uuid? id) "expected a valid uuid for `id`")
(assert (map? position) "expected a valid map for `position`")
(ptk/reify ::update-position
ptk/WatchEvent
(watch [_ state _]
(let [page-id (or (get options :page-id)
(get state :current-page-id))
objects (dsh/lookup-page-objects state page-id)
shape (get objects id)
;; FIXME: performance rect
bbox (-> shape :points grc/points->rect)
frame (cfh/get-frame objects shape)
delta (if (:absolute? options)
(get-delta position bbox)
(get-relative-delta position bbox frame))
modif-tree (dwm/create-modif-tree [id] (ctm/move-modifiers delta))]
(rx/of (dwm/apply-modifiers {:modifiers modif-tree
(let [page-id (or (get options :page-id)
(get state :current-page-id))
objects (dsh/lookup-page-objects state page-id)
shape (get objects id)
bbox (-> shape :points grc/points->rect)
frame (if (:absolute? options)
(cfh/get-frame objects)
(cfh/get-parent-frame objects shape))
delta (calculate-delta position bbox frame)
modifiers (dwm/create-modif-tree [id] (ctm/move-modifiers delta))]
(rx/of (dwm/apply-modifiers {:modifiers modifiers
:page-id page-id
:ignore-constraints false
:ignore-touched (:ignore-touched options)

View File

@@ -22,7 +22,8 @@
;; Change this to :info :debug or :trace to debug this module
(log/set-level! :warn)
(def discard-transaction-time-millis (* 20 1000))
(def ^:private
discard-transaction-time-millis (* 20 1000))
(def ^:private
schema:undo-entry
@@ -30,7 +31,7 @@
[:undo-changes [:vector ::cpc/change]]
[:redo-changes [:vector ::cpc/change]]])
(def check-undo-entry!
(def check-undo-entry
(sm/check-fn schema:undo-entry))
(def MAX-UNDO-SIZE 50)
@@ -48,8 +49,7 @@
(ptk/reify ::materialize-undo
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-undo :index] index)))))
(update state :workspace-undo assoc :index index))))
(defn- add-undo-entry
[state entry]
@@ -88,12 +88,9 @@
(defn append-undo
[entry stack?]
(dm/assert!
"expected valid undo entry"
(check-undo-entry! entry))
(dm/assert!
(boolean? stack?))
(assert (check-undo-entry entry))
(assert (boolean? stack?))
(ptk/reify ::append-undo
ptk/UpdateEvent
@@ -118,17 +115,11 @@
(defn start-undo-transaction
"Start a transaction, so that every changes inside are added together in a single undo entry."
[id]
[id & {:keys [timeout] :or {timeout discard-transaction-time-millis}}]
(ptk/reify ::start-undo-transaction
ptk/WatchEvent
(watch [_ _ _]
(->> (rx/of (check-open-transactions))
;; Wait the configured time
(rx/delay discard-transaction-time-millis)))
ptk/UpdateEvent
(update [_ state]
(log/info :msg "start-undo-transaction")
(log/info :hint "start-undo-transaction")
;; We commit the old transaction before starting the new one
(let [current-tx (get-in state [:workspace-undo :transaction])
pending-tx (get-in state [:workspace-undo :transactions-pending])]
@@ -136,20 +127,28 @@
(nil? current-tx) (assoc-in [:workspace-undo :transaction] empty-tx)
(nil? pending-tx) (assoc-in [:workspace-undo :transactions-pending] #{id})
(some? pending-tx) (update-in [:workspace-undo :transactions-pending] conj id)
:always (update-in [:workspace-undo :transactions-pending-ts] assoc id (dt/now)))))))
:always (update-in [:workspace-undo :transactions-pending-ts] assoc id (dt/now)))))
ptk/WatchEvent
(watch [_ _ _]
(when (and timeout (pos? timeout))
(->> (rx/of (check-open-transactions timeout))
;; Wait the configured time
(rx/delay timeout))))))
(defn discard-undo-transaction []
(ptk/reify ::discard-undo-transaction
ptk/UpdateEvent
(update [_ state]
(log/info :msg "discard-undo-transaction")
(log/info :hint "discard-undo-transaction")
(update state :workspace-undo dissoc :transaction :transactions-pending :transactions-pending-ts))))
(defn commit-undo-transaction [id]
(ptk/reify ::commit-undo-transaction
ptk/UpdateEvent
(update [_ state]
(log/info :msg "commit-undo-transaction")
(log/info :hint "commit-undo-transaction")
(let [state (-> state
(update-in [:workspace-undo :transactions-pending] disj id)
(update-in [:workspace-undo :transactions-pending-ts] dissoc id))]
@@ -166,15 +165,15 @@
(assoc state :workspace-undo {}))))
(defn check-open-transactions
[]
[timeout]
(ptk/reify ::check-open-transactions
ptk/WatchEvent
(watch [_ state _]
(log/info :msg "check-open-transactions")
(log/info :hint "check-open-transactions" :timeout timeout)
(let [pending-ts (-> (dm/get-in state [:workspace-undo :transactions-pending-ts])
(update-vals #(.toMillis (dt/diff (dt/now) %))))]
(update-vals #(inst-ms (dt/diff (dt/now) %))))]
(->> pending-ts
(filter (fn [[_ ts]] (>= ts discard-transaction-time-millis)))
(filter (fn [[_ ts]] (>= ts timeout)))
(rx/from)
(rx/tap #(js/console.warn (dm/str "FORCE COMMIT TRANSACTION AFTER " (second %) "MS")))
(rx/map first)

View File

@@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.persistence :as dwp]
[app.main.data.workspace :as dw]
[app.main.data.workspace.thumbnails :as th]
@@ -97,7 +98,8 @@
(ptk/reify ::restore-version
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)]
(let [file-id (:current-file-id state)
team-id (:current-team-id state)]
(rx/concat
(rx/of ::dwp/force-persist
(dw/remove-layout-flag :document-history))
@@ -106,7 +108,7 @@
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/tap #(th/clear-queue!))
(rx/map #(dw/initialize-workspace file-id)))
(rx/map #(dw/initialize-workspace team-id file-id)))
(case origin
:version
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
@@ -200,21 +202,23 @@
(ptk/reify ::restore-version-from-plugins
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"})
::dwp/force-persist)
(watch [_ state _]
(let [file (dsh/lookup-file state file-id)
team-id (or (:team-id file) (:current-file-id state))]
(rx/concat
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"})
::dwp/force-persist)
;; FIXME: we should abstract this
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-workspace file-id)))
;; FIXME: we should abstract this
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-workspace team-id file-id)))
(->> (rx/of 1)
(rx/tap resolve)
(rx/ignore))))))
(->> (rx/of 1)
(rx/tap resolve)
(rx/ignore)))))))

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