Compare commits

...

200 Commits

Author SHA1 Message Date
elhombretecla
c565915007 💄 Add new tier two color tokens for ui elements 2025-10-06 09:53:11 +02:00
Eva Marco
2de6b6460e ♻️ Review on switcher component 2025-10-03 15:02:31 +02:00
elhombretecla
f905dfc699 💄 Fix linter errors 2025-10-02 11:05:53 +02:00
elhombretecla
c79f110177 💄 Fix cli errors 2025-10-02 11:05:34 +02:00
elhombretecla
f644b3744a 💄 Fix light theme styles 2025-10-02 11:05:34 +02:00
elhombretecla
0722af3a2f 🎉 Add translation string to switcher label 2025-10-02 11:05:34 +02:00
elhombretecla
b4c6bbb191 💄 Remove css nesting 2025-10-02 11:05:34 +02:00
Eva Marco
cad9d03ca1 🎉 Some fixes 2025-10-02 11:05:34 +02:00
elhombretecla
1d6389a3eb 🎉 Apply fixes and new doc structure 2025-10-02 11:05:34 +02:00
elhombretecla
913a8d3148 🎉 Fix label color and scss variables 2025-10-02 11:05:34 +02:00
elhombretecla
34e3453f24 Use proper classes name convention 2025-10-02 11:05:34 +02:00
elhombretecla
6f362f9211 🎉 Add new component switcher structure 2025-10-02 11:05:34 +02:00
alonso.torres
979b4276ca 🐛 Fix problem with component swapping panel 2025-10-02 09:10:21 +02:00
Elena Torró
a32fe40528 Merge pull request #7409 from penpot/ladybenko-fix-wasm-playwright-ci
🔧 Fix Playwright config in CI to include the wasm build
2025-10-02 09:03:12 +02:00
Natacha
b602df549e Add new shadow icons (#7416)
*  Adds new shadow icons

Signed-off-by: Natacha <natachamenjibar@gmail.com>

*  Add shadow icons

Signed-off-by: Natacha <natachamenjibar@gmail.com>

*  Adds shadow icons

Signed-off-by: Natacha <natachamenjibar@gmail.com>

* 📎 Fix wrong svg

Signed-off-by: Natacha <natachamenjibar@gmail.com>

* 📎 Fix wrong svg

Signed-off-by: Natacha <natachamenjibar@gmail.com>

* 📎 Fix wrong svg

Signed-off-by: Natacha <natachamenjibar@gmail.com>

---------

Signed-off-by: Natacha <natachamenjibar@gmail.com>
2025-10-01 17:11:19 +02:00
Luis de Dios
7f1ab08ec8 🐛 Fix use a pointer cursor for adding variant from the viewport (#7410) 2025-10-01 17:01:07 +02:00
Luis de Dios
1263ea11fa 🐛 Fix order of component menu options in assets tab (#7388)
* 🐛 Reorder component menu options in assets tab

* ♻️ Use new component syntax

* 📚 Add bugfix to changelog

* ♻️ Code restructuring and SCSS improvements
2025-10-01 17:00:27 +02:00
Yamila Moreno
ce26c52b30 👷 Automate docker images creation 2025-10-01 14:54:00 +02:00
Yamila Moreno
5c8b3ac3d6 👷 Automate docker images creation 2025-10-01 13:41:03 +02:00
Aitor Moreno
bd4d576172 Merge pull request #7412 from penpot/elenatorro-fix-loop-all-ancestors
🐛 Break loop when no parent is present
2025-10-01 13:36:16 +02:00
Elena Torro
e10169b3db 🐛 Break loop when no parent is present 2025-10-01 12:43:56 +02:00
Elena Torró
f119a9548d Merge pull request #7411 from penpot/azazeln28-fix-issue-12185-wrong-text-width-height-layout
🐛 Fix wrong text auto width/height layout
2025-10-01 12:40:58 +02:00
Aitor Moreno
c097aef152 🐛 Fix wrong text auto width/height layout 2025-10-01 12:27:38 +02:00
Andrey Antukh
000fa51c73 🐛 Fix zip handling on exporter 2025-10-01 11:56:57 +02:00
Belén Albeza
d815494ffa 🔧 Fix playwright config to do a wasm build 2025-10-01 11:27:13 +02:00
Andrey Antukh
a25ba6b482 📎 Fix incorrect regex for match merge and revert commits 2025-10-01 11:07:21 +02:00
Andrey Antukh
e8434c3370 📎 Update devenv tmux script to start exporter using yarn 2025-10-01 10:59:41 +02:00
Andrey Antukh
7cf4ec2792 ♻️ Make the exporter build as esm module 2025-10-01 10:58:03 +02:00
Andrey Antukh
365ce25996 Merge remote-tracking branch 'origin/staging' into develop 2025-10-01 10:50:19 +02:00
Andrey Antukh
01ef55e4f4 Revert " Add minor improvement to cljs impl logging"
This reverts commit 960b76f760.
2025-10-01 10:48:24 +02:00
Andrey Antukh
3b81c1d750 Revert "♻️ Make the exporter build as esm module"
This reverts commit d0f34f06a9.
2025-10-01 10:47:47 +02:00
Elena Torró
40b34da788 Merge pull request #7269 from penpot/azazeln28-feat-caret-position
🎉 Feat caret position
2025-10-01 09:43:03 +02:00
Aitor Moreno
732c79b7b5 🎉 Add function to retrieve caret position 2025-10-01 09:18:46 +02:00
Andrey Antukh
d0f34f06a9 ♻️ Make the exporter build as esm module 2025-10-01 08:10:37 +02:00
Andrey Antukh
23d5bdd20b 🐛 Add missing poppler-tools dependency on devenv 2025-10-01 08:10:37 +02:00
Andrey Antukh
9f2dc06c95 Add missing srepl helper for disable objects-map feat 2025-10-01 08:10:37 +02:00
Andrey Antukh
62563d28d0 📎 Bump library version to 1.0.9
Mainly fixes dependencies declaration on package.json file
2025-09-30 21:55:17 +02:00
Andrey Antukh
21e2ee9904 🐛 Fix dependencies on library 2025-09-30 21:53:04 +02:00
brian mwenda
e6c418eb9c 🐛 Improve auto-width to fixed conversion logic in layout contexts
Signed-off-by: Brian Mwenda <brian@nathandigital.com>
2025-09-30 21:48:03 +02:00
Luis de Dios
de5ff227d2 🎉 Create variant from the viewport (#7357)
* 🎉 Create variant from the viewport

* ♻️ Use DS styles and new component syntax

* 📎 PR changes
2025-09-30 18:15:17 +02:00
Florian Schroedl
0f67730198 🐛 Dont forward default-value for mismatching tab-type 2025-09-30 14:27:40 +02:00
Florian Schroedl
3da02e2b6b 🐛 Fixes resolved values being prefilled for existing referenced composite token 2025-09-30 14:27:40 +02:00
Florian Schroedl
ab80021fb1 🐛 Fix performance issue on font-family 2025-09-30 14:27:40 +02:00
Xaviju
f31e9b8ac9 🎉 Add blur panel to inspect styles tab (#7397) 2025-09-30 13:08:52 +02:00
Andrey Antukh
7d16515eb7 Add minor enhacements to logging on frontend (#7401)
*  Add logging consistency enhacements on fonts loading

*  Disable data evens ns logging

*  Simplify flags logging on application initialization

*  Improve features logging
2025-09-30 11:59:41 +02:00
Pablo Alba
cd9ba482e3 🐛 Load dependant libraries, and don't allow unload them 2025-09-30 09:55:21 +02:00
David Barragán Merino
dff1ca23d3 📚 Update changelog 2025-09-29 18:08:28 +02:00
Andrey Antukh
c363d4d937 📎 Bump library version 2025-09-29 13:44:14 +02:00
Andrey Antukh
de25a24a6d 🐛 Fix backend repl start issue with jdk 24 2025-09-29 13:35:48 +02:00
Andrey Antukh
accc9a173f Merge remote-tracking branch 'origin/staging' into develop 2025-09-29 13:24:31 +02:00
Andrey Antukh
2d364dde5c Add several minor enhacements to features subsystem
Mainly fixes the team non-inheritable features handling and
removes unnecesary/duplicate checks.
2025-09-29 13:23:16 +02:00
Andrey Antukh
c892a9f254 Integrate objects-map usage on backend and frontend 2025-09-29 13:23:16 +02:00
Andrey Antukh
aaae35fb51 🎉 Add multiplatform impl of ObjectsMap
The new type get influentiated by the ObjectsMap impl on backend
code but with simplier implementation that no longer restricts keys
to UUID type but preserves the same performance characteristics.

This type encodes and decodes correctly both in fressian (backend)
and transit (backend and frontend).

This is an initial implementation and several memory usage
optimizations are still missing.
2025-09-29 13:23:16 +02:00
Andrey Antukh
960b76f760 Add minor improvement to cljs impl logging
Mainly reduce the emmited code, that will contribute to reduce the
bundle size and also adds timestamp to the default output.
2025-09-29 13:23:16 +02:00
Andrey Antukh
d921e7eaa3 📎 Add not-empty generator to schema generator ns 2025-09-29 13:23:16 +02:00
Andrey Antukh
49f06b25fa 📚 Update changelog 2025-09-29 13:23:01 +02:00
Andrey Antukh
5ffb7ae2ec Add warning on using deprecated storage config 2025-09-29 13:23:01 +02:00
Andrey Antukh
27945ace65 Revert deprecated storage config cleaning 2025-09-29 13:23:01 +02:00
María Valderrama
e39bf0b439 Invitations management improvements (#7230)
*  Invitations management improvements

* 📎 Change invite email subject

* 📎 Update icon usage

* ♻️ Fix css file

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2025-09-29 13:18:57 +02:00
Alonso Torres
deee7f7334 Merge pull request #7366 from penpot/niwinz-develop-page-data-type
 Add several enhancements for reduce workspace file load time
2025-09-29 12:43:34 +02:00
Xaviju
20d61cbce2 Create ghost variant for select DS component (#7392) 2025-09-29 12:24:20 +02:00
Andrés Moya
9ad8d3fd08 🔧 Make small improvements from PR comments 2025-09-29 12:16:42 +02:00
Andrés Moya
4c35571336 🔧 Read and modify token themes by id 2025-09-29 12:16:42 +02:00
Andrés Moya
37679b7ec6 🔧 Organize token changes API 2025-09-29 12:16:42 +02:00
Andrés Moya
194eded930 🔧 Unify path name helper functions 2025-09-29 12:16:42 +02:00
Andrés Moya
4e607d8da2 💄 Clarify and reorder interfaces 2025-09-29 12:16:42 +02:00
Andrés Moya
f5fd978a07 🔧 Retrieve tokens from library and not from set 2025-09-29 12:16:42 +02:00
Andrés Moya
b28be62845 🔧 Fix rebase problems 2025-09-29 12:16:42 +02:00
Andrés Moya
d76a5c615c 🔧 Modify token sets by id instead of name and review usage 2025-09-29 12:16:42 +02:00
Andrés Moya
03e05da41e 💄 Normalize some attributes of changes 2025-09-29 12:16:42 +02:00
Andrés Moya
5f886e141a 💄 Minor changes 2025-09-29 12:16:42 +02:00
Andrés Moya
021b8f81ca 🔧 Read token sets by id instead of name 2025-09-29 12:16:42 +02:00
Andrey Antukh
f32112544e Make deleted fonts fixer to run with more granular stragegy
Instead of running it on all the file, only run it to local library
and the current page, reducing considerably the overhead of analyzing
the whole file on each file load.

It stills executes for page each time the page is loaded, and add
some kind of local cache for not doing repeated work each time page
loads is pending to be implemented in other commit.
2025-09-29 12:07:49 +02:00
Andrey Antukh
27e311277a Add logging to frontend repo namespace 2025-09-29 12:07:49 +02:00
Andrey Antukh
b9030fcc73 Add better workspace file indexing strategy
Improve file indexes initialization on workspace.

Instead of initialize indexes for all pages only initialize
indexes for the loaded page.
2025-09-29 12:07:49 +02:00
Andrey Antukh
e1519f0ee4 Integrate objects-map usage on backend and frontend 2025-09-29 12:07:48 +02:00
Andrey Antukh
7fefe6dbc8 🎉 Add multiplatform impl of ObjectsMap
The new type get influentiated by the ObjectsMap impl on backend
code but with simplier implementation that no longer restricts keys
to UUID type but preserves the same performance characteristics.

This type encodes and decodes correctly both in fressian (backend)
and transit (backend and frontend).

This is an initial implementation and several memory usage
optimizations are still missing.
2025-09-29 12:06:56 +02:00
Andrey Antukh
fdf70ae9c1 Fix docstring on common.weak ns function 2025-09-29 12:06:56 +02:00
Andrey Antukh
528315b75c 📎 Add not-empty generator to schema generator ns 2025-09-29 12:06:56 +02:00
Andrey Antukh
42d03a0325 📎 Add several missing imports on repl related namespaces 2025-09-29 12:06:56 +02:00
Andrey Antukh
0346c48b03 Add several minor enhacements to features subsystem
Mainly fixes the team non-inheritable features handling and
removes unnecesary/duplicate checks.
2025-09-29 12:06:56 +02:00
Andrey Antukh
1d54fe2e24 Add support for emit messages without waiting response on worker 2025-09-29 12:06:56 +02:00
Andrey Antukh
255f5af2e3 Add several enhacements to buffer namespace
The changes are just for completenes.
2025-09-29 12:06:56 +02:00
Andrey Antukh
eea65b12dd Add minor improvement to cljs impl logging
Mainly reduce the emmited code, that will contribute to reduce the
bundle size and also adds timestamp to the default output.
2025-09-29 12:06:56 +02:00
Andrey Antukh
473066cf5c 🔧 Add missing config for on commit checker 2025-09-29 12:04:37 +02:00
Xaviju
5e84bda404 🎉 Add SVG panel to inspect styles tab (#7373) 2025-09-29 09:53:15 +02:00
Andrey Antukh
c1058c7fdb ♻️ Add minor refactor for internal concurrency model
Replace general usage of virtual threads with platform threads
and use virtual threads for lightweight procs such that websocket
connections. This decision is made mainly because virtual threads
does not appear on thread dumps in an easy way so debugging issues
becomes very difficult.

The threads requirement of penpot for serving http requests
is not very big so having so this decision does not really affects
the resource usage.
2025-09-26 14:35:06 +02:00
Andrey Antukh
9d907071aa ⬆️ Update dependencies (#7330)
* ⬆️ Update to JDK25 on the devenv

* ⬆️ Update dependencies

* 🔥 Remove unused flag from devenv backend startup scripts

*  Enable shenandoah gc on backend scripts/repl
2025-09-26 13:43:43 +02:00
Elena Torró
c32b94abcf Merge pull request #7343 from penpot/elenatorro-12118-support-large-svg-files
🐛 Fix parsing large paths with multiple subpaths
2025-09-26 13:35:17 +02:00
Elena Torro
9d8ad0ea6e 🐛 Fix parsing large paths with multiple subpaths 2025-09-26 13:04:47 +02:00
Yamila Moreno
2b1e107a44 Merge pull request #7390 from penpot/yms-add-curl-dependency
🐳 Add curl to the backend image
2025-09-26 11:40:42 +02:00
Yamila Moreno
2196318cfc 🐳 Add curl to the backend image 2025-09-26 11:23:02 +02:00
Yamila Moreno
b3d1701698 Merge pull request #7355 from penpot/yms-docker-update-nginx-entrypoint
🐳 Improve Docker nginx
2025-09-26 10:49:24 +02:00
Yamila Moreno
042bd03beb 🐳 Improve Docker nginx 2025-09-26 10:31:23 +02:00
Juan de la Cruz
a39a127f03 🐛 Fix underline text in template card at carrusel 2025-09-26 09:56:05 +02:00
Pablo Alba
bd665f70bf 💄 Add new library modal UI tweaks 2025-09-25 22:56:27 +02:00
Elena Torró
9b90236b72 Merge pull request #7385 from penpot/elenatorro-improve-image-load-performance
🔧 Improve image parsing performance
2025-09-25 17:20:49 +02:00
Elena Torro
bf6cdf729d 🔧 Improve image parsing performance 2025-09-25 17:17:42 +02:00
Belén Albeza
361bdb4a04 ♻️ Decouple serialization from text/layout models" (#7360)
* ♻️ Move text serialization code to wasm module

* ♻️ Add serializer for TextAlign

* ♻️ Add serializers for TextDirection and TextDecoration

* ♻️ Add serializer for TextTransform

* ♻️ Remove unused font_style from TextLeaf model

* ♻️ Refactor parsing of TextLeaf from bytes

* ♻️ Decouple tight serialization of Paragraph
2025-09-25 16:54:07 +02:00
Elena Torró
3827aa6bd4 Merge pull request #7344 from penpot/elenatorro-11542-truncate-long-font-names-on-fonts-menu
🔧 Use two lines text ellipsis on custom font names
2025-09-25 15:25:50 +02:00
Xaviju
adf7b0df50 🎉 Add visibility panel to inspect styles tab (#7362) 2025-09-25 12:52:43 +02:00
Elena Torro
97b4491a27 🔧 Use two lines text ellipsis on custom font names 2025-09-25 12:49:33 +02:00
Xavier Julian
015bd9e453 🎉 Inspect styles tab: fill panel 2025-09-25 11:31:15 +02:00
Belén Albeza
49d5987b15 💄 Add deprecated namespace and fix import for remaining scss files (#7379) 2025-09-25 11:27:10 +02:00
Belén Albeza
a5e4de97e3 💄 Use deprecated prefix for deprecated scss vars and mixins (#7375) 2025-09-25 09:22:25 +02:00
Alonso Torres
378be9473d 🐛 Fix problem with export size (#7374) 2025-09-25 08:50:31 +02:00
Juan de la Cruz
412cf61d7d 🐛 Remove translations form inspect tab text properties (#7369) 2025-09-25 08:48:41 +02:00
Juan de la Cruz
754a1b6fa2 🐛 Fix loading tips wording (#7368) 2025-09-25 08:48:10 +02:00
Elenzakaleidos
ec94d08f4a 🎉 Update README.md with Variants (#7353)
Update the Readme with new text and image that include Variants as feature

Signed-off-by: Elenzakaleidos <elena.scilinguo@kaleidos.net>
2025-09-25 08:46:02 +02:00
Alonso Torres
b6b2d28464 🐛 Fix problem with flow not being deleted (#7371) 2025-09-24 18:06:26 +02:00
Elena Torro
32770c685a 🐛 Do not add shadows on hidden children 2025-09-24 14:42:57 +02:00
Eva Marco
441dc33e38 ♻️ Add shortcut to scss import paths (#7364)
* 🎉 Add config for shortcut imports

* ♻️ Change import paths
2025-09-24 11:18:34 +02:00
Eva Marco
3f87e768a7 ♻️ Fix color token reviews (#7322)
* ♻️ Fix some review changes

* 🐛 Fix more errors

* 🎉 Create token from colorpicker fixed

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-09-24 11:13:52 +02:00
David Barragán Merino
09e9340ba6 💄 Fix a description and remove an unused event 2025-09-24 09:38:42 +02:00
David Barragán Merino
d5ff7b4144 📎 Add DevEnv builder workflow 2025-09-23 23:26:11 +02:00
David Barragán Merino
ef0aee0a09 📎 Automatically publish github release and docker images with final version tags 2025-09-23 23:25:52 +02:00
Andrey Antukh
1e9682376e Merge remote-tracking branch 'origin/staging' into develop 2025-09-23 12:20:49 +02:00
Pablo Alba
c9b61745a0 🎉 Switch several variant copies at the same time 2025-09-23 11:31:57 +02:00
Aitor Moreno
974b76d7bd Merge pull request #7267 from penpot/azazeln28-feat-text-layout
🎉 Add internal TextContent layout data
2025-09-22 16:21:06 +02:00
Aitor Moreno
f505fcfa0d 🎉 Add internal TextContent layout data 2025-09-22 16:01:23 +02:00
Belén Albeza
e4d610d503 ♻️ Decouple shapes serialization from model (rust) (#7328)
* ♻️ Move shape type serialization to wasm module

* ♻️ Refactor serialization of constraints and vertical alignment into wasm module

* ♻️ Refactor serialization and model of shape blur

* ♻️ Refactor bool serialization to the wasm module

* ♻️ Split wasm::layout into submodules

* ♻️ Refactor serialization of AlignItems, AlignContent, JustifyItems and JustifyContent

* ♻️ Refactor serialization of WrapType and FlexDirection

* ♻️ Refactor serialization of JustifySelf

* ♻️ Refactor serialization of GridCell

* ♻️ Refactor serialization of AlignSelf

* 🐛 Fix AlignSelf not being serialized

* ♻️ Refactor handling of None variants in Raw* enums

* ♻️ Refactor serialization of grid direction

* ♻️ Refactor serialization of GridTrack and GridTrackType

* ♻️ Refactor serialization of Sizing

* ♻️ Refactor serialization of ShadowStyle

* ♻️ Refactor serialization of StrokeCap and StrokeStyle

* ♻️ Refactor serialization of BlendMode

* ♻️ Refactor serialization of FontStyle

* ♻️ Refactor serialization of GrowType
2025-09-22 13:47:54 +02:00
Madalena Melo
5c23a678cc Merge pull request #7342 from penpot/madalenapmelo-kp-patch-1
📚 Add reference to the Teams section on the Dashboard section
2025-09-22 11:23:29 +02:00
David Barragán Merino
fb3923924b 📎 Change the name of some action workflows 2025-09-22 09:58:26 +02:00
Florian Schroedl
c882e8347a Add line-height to composite typography token 2025-09-22 09:52:56 +02:00
Pablo Alba
c1fd1a3b42 📚 Add variants doc for SDK (#7351)
* 📚 Add variants doc for SDK

* 📚 Spelling & style improvements

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2025-09-21 22:15:27 +02:00
Eva Marco
b1fe32baea ♻️ Remove deprecated @import from scss files (#7347)
* 🐛 Fix import warnings 1 of 2

* 🐛 Fix import warnings 2 of 2

* 🐛 Fix visual tests and format files

* 🐛 Fix mixed declarations on scss
2025-09-19 11:50:08 +02:00
Andrey Antukh
fb7a7d02da Merge pull request #7205 from penpot/niwinz-measures-tokens-backup
♻️ Replace numeric inputs on measure options
2025-09-19 11:44:17 +02:00
Eva Marco
20dfc2a216 🐛 Fix typo on event name (#7350) 2025-09-19 11:40:53 +02:00
Eva Marco
d7d2d36e0a ♻️ Replace measure inputs for numeric input component 2025-09-19 11:28:22 +02:00
Andrey Antukh
07904bcc5d ♻️ Add needed changes to get tokens from sidebar
This reverts commit afe149f702148d86d1dea6cb6a537917ce7202aa.
2025-09-19 10:26:29 +02:00
Andrey Antukh
9686075104 🐛 Fix translations 2025-09-18 12:02:45 +02:00
María Valderrama
436e0e847d 🐛 Fix current version on sidebar 2025-09-18 11:56:47 +02:00
Eva Marco
d50b070a64 🎉 Add usefull mixins to DS (#7340) 2025-09-18 10:47:55 +02:00
Andrey Antukh
80cb48fd6a Merge remote-tracking branch 'origin/staging' into develop 2025-09-18 10:44:21 +02:00
Madalena Melo
49c6efbc22 📚 Add reference to the Teams section on the Dashboard section
https://tree.taiga.io/project/penpot/task/11806

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>
2025-09-17 16:17:24 +02:00
Andrey Antukh
4fb1c7a630 Merge remote-tracking branch 'origin/staging' into develop 2025-09-17 13:46:49 +02:00
David Barragán Merino
fd37fdde93 📎 Add release action workflow 2025-09-16 18:06:06 +02:00
David Barragán Merino
66b1d5b7bd Merge remote-tracking branch 'origin/staging' into develop 2025-09-16 16:26:23 +02:00
Xavier Julian
2bf7a9dd5f ♻️ Remove unneeded fn parameters 2025-09-16 14:17:14 +02:00
Xavier Julian
7bacd8fbca ♻️ Refactor defmulti fn into case switches 2025-09-16 14:17:14 +02:00
Aitor Moreno
b883882a32 🐛 Fix onboarding select keyboard interaction (#7295) 2025-09-16 13:59:15 +02:00
Belén Albeza
e5e11b6383 🔧 Autogenerate serialization values for wasm enums (#7296)
* 🔧 Autogenerate serialization values for wasm enums

* 🔧 Add serializer values to the wasm api

*  Avoid converting to a clojure map the serializer js object

* 🔧 Update watch script for autoserialized enums

* 🐛 Fix missing serializer values
2025-09-16 12:29:14 +02:00
Eva Marco
01e963ae35 🐛 Fix font name hot update (#7316) 2025-09-16 12:23:41 +02:00
Eva Marco
90a80c4b63 🐛 Fix Uppercase on add token button (#7314) 2025-09-16 12:05:55 +02:00
Andrey Antukh
b56f237780 Merge remote-tracking branch 'origin/staging' into develop 2025-09-16 11:38:58 +02:00
Xavier Julian
4970ae3eb4 💄 Align tokens panel vertically to the top 2025-09-16 11:38:33 +02:00
Elena Torró
2e21f084fc 🐛 Fix boolean operations on rotated shapes (#7309) 2025-09-15 14:46:56 +02:00
Xavier Julian
55513b9ae5 🎉 Inspect styles tab: layout element panel 2025-09-15 13:39:00 +02:00
Eva Marco
07d0062645 🐛 Fix sets shown without color tokens (#7312) 2025-09-15 10:38:06 +02:00
Xavier Julian
f4b38af649 Display border-radius as logical properties in inspect tab 2025-09-15 09:46:01 +02:00
Andrey Antukh
6e7bcd1243 Merge remote-tracking branch 'origin/staging' into develop 2025-09-12 16:55:25 +02:00
Andrés Moya
ed3fc5b8b2 🐛 Fix detaching a nested copy inside a main component (#7304)
* 🐛 Fix detaching a nested copy inside a main component

* 💄 Rename functions for more semantic precission
2025-09-12 16:00:01 +02:00
Pablo Alba
f5f9157786 🐛 Fix paste behavior according to the selected element 2025-09-12 15:17:26 +02:00
Andrey Antukh
6cb0cb7f98 Merge remote-tracking branch 'origin/staging' into develop 2025-09-12 14:49:52 +02:00
Xavier Julian
0210b310b7 🎉 Inspect styles tab: layout panel 2025-09-12 10:27:41 +02:00
Eva Marco
ce1e44eda4 ♻️ Refactor set titles (#7301) 2025-09-12 08:46:05 +02:00
Marina López
48825e1e59 Show current penpot version 2025-09-11 13:18:42 +02:00
Florian Schroedl
61cfe2d142 🐛 Fix font-family being split up when restoring from backup value 2025-09-11 12:33:26 +02:00
Eva Marco
2d68f4dfd3 🐛 Fix icons (#7299) 2025-09-11 09:42:11 +02:00
Elena Torró
1e23937aa5 Merge pull request #7291 from penpot/superalex-fix-boolean-and-group-shadows
🐛 Fix boolean and group shadows
2025-09-11 09:27:56 +02:00
Eva Marco
aecaf51953 Add color token on colorpicker (#7197)
*  Add token aplication to colorpicker

* 🐛 Change fn name

* 🐛 Change scss from file

* 🐛 Change color for direct-color

* 🐛 Remove vector from fns

* 🐛 Fix CI

* 🐛 Change color-option name

* 🐛 Fix comments

* 🐛 Remove sets without color tokens
2025-09-11 09:13:43 +02:00
Alejandro Alonso
da05d6b67d 🐛 Fix boolean and group shadows 2025-09-10 15:59:39 +02:00
Alejandro Alonso
99a100ad63 Merge pull request #7264 from penpot/elenatorro-12002-draw-shadows-and-blurs-on-texts-on-surfaces
🐛 Fix text shadows and blur and refactor text rendering
2025-09-10 15:50:33 +02:00
Elena Torró
bd3bcb4b18 Merge pull request #7284 from penpot/superalex-fix-blend-mode
🐛 Fix updating blend mode for shapes
2025-09-10 15:03:17 +02:00
Elena Torró
534c7864fc Merge pull request #7285 from penpot/superalex-fix-cornder-radius
🐛 Fix corner radius
2025-09-10 14:59:06 +02:00
Elena Torro
4bd2eba573 🐛 Fix text shadows and blur and refactor text rendering 2025-09-10 14:20:24 +02:00
Xavier Julian
563f608255 🐛 Display token themes as a string 2025-09-10 13:55:54 +02:00
Alejandro Alonso
382b5e7e3a Merge remote-tracking branch 'origin/staging' into develop 2025-09-10 12:33:54 +02:00
Eva Marco
a503f8ae93 ♻️ Refactor composite token UI (#7287)
* ♻️ Refactor composite token UI

* 🐛 Fix comments
2025-09-10 12:16:39 +02:00
Xavier Julian
e1935fb3fb 🎉 Inspect styles tab: geometry panel 2025-09-10 11:01:19 +02:00
Florian Schroedl
b3763dec3f Typography import-export 2025-09-09 13:30:38 +02:00
Alejandro Alonso
41751d60d2 🐛 Fix corner radius 2025-09-09 10:24:56 +02:00
Yamila Moreno
8bd0edca46 Merge pull request #7282 from penpot/yms-update-ci
📎 Update CI
2025-09-09 09:30:18 +02:00
Alejandro Alonso
e2f22b86c7 🐛 Fix updating blend mode for shapes 2025-09-09 09:19:09 +02:00
Alejandro Alonso
108b5ab225 🐛 Fix missing filter-icon 2025-09-09 09:05:42 +02:00
Alejandro Alonso
43a238a896 Merge remote-tracking branch 'origin/staging' into develop 2025-09-09 08:40:35 +02:00
Yamila Moreno
daa408e291 📎 Update CI 2025-09-08 16:51:05 +02:00
Florian Schrödl
8aed47dad3 Allow references to other typography tokens (#7251) 2025-09-08 16:45:18 +02:00
Elena Torró
0e23c9f6ab Merge pull request #7278 from penpot/superalex-fix-fill-stroke-opacity-shouldnt-affect-shadows
🐛 Fix fills and strokes opacity shouldn't affect shadows
2025-09-08 14:08:20 +02:00
Alejandro Alonso
8fff9afee6 🐛 Fix fills and strokes opacity shouldn't affect shadows 2025-09-08 13:04:52 +02:00
Xavier Julian
ff55318c04 🎉 Inspect styles tab: variants panel 2025-09-08 11:59:33 +02:00
Elena Torró
41b7957eff Merge pull request #7274 from penpot/superalex-refactor-drop-shadows
🐛 Fixing nested shadows
2025-09-08 11:38:19 +02:00
Alejandro Alonso
7e52aadb98 🐛 Fixing nested shadows 2025-09-08 11:20:03 +02:00
Alejandro Alonso
69f41c300f Merge pull request #7199 from penpot/elenatorro-11844-fix-font-long-names
🐛 Fix custom font-long names overflow
2025-09-08 10:48:54 +02:00
Elena Torro
18c5e0b9a8 🐛 Fix font long name overflow 2025-09-08 10:31:35 +02:00
Florian Schroedl
5230d54551 🐛 Fix when font-weight is a computed int (math resolver) 2025-09-04 12:23:43 +02:00
Alejandro Alonso
a79be05261 🐛 Fix selection and devtools problem (#7259) 2025-09-04 09:29:38 +02:00
Belén Albeza
9c77296858 🔧 Make the watch script to compile the debug css when not in production env (#7250) 2025-09-03 13:45:11 +02:00
Xavier Julian
34da6b64df 🎉 Inspect styles tab tokens panel 2025-09-03 13:01:38 +02:00
Florian Schroedl
c4481be39f ♻️ Revert trigger interactive via actionize and propagation 2025-09-03 09:42:40 +02:00
Elena Torró
f60b6a4869 Merge pull request #7247 from penpot/ladybenko-11983-textlayout-module
♻️ Refactor into new textlayout module
2025-09-02 17:17:12 +02:00
Belén Albeza
3e02dc550f ♻️ Create type alias for ParagraphBuilderGroup 2025-09-02 15:32:10 +02:00
Belén Albeza
1cf0de395c ♻️ Rename get_children to children (Paragraph) 2025-09-02 15:30:54 +02:00
Belén Albeza
d40b68c004 ♻️ Refactor and rename ParagraphBuilder instantiating from TextContent 2025-09-02 15:22:05 +02:00
Belén Albeza
50b9e8c6e6 ♻️ Rename TextContent::get_width to TextContent::width 2025-09-02 15:07:13 +02:00
Belén Albeza
d25f9cd4bd ♻️ Move auto_width and auto_height to their own textlayout module 2025-09-02 15:03:46 +02:00
Florian Schroedl
bedb98ad9f Add context menu for typography 2025-09-02 13:19:45 +02:00
Elena Torró
5f37601122 🐛 Fix different fonts on texts shadows (#7214)
* 🐛 Fix different fonts on texts shadows

* 🔧 Refactor text rendering and move text-decoration logic outside

* 🔧 Use transparency correctly
2025-09-02 12:56:07 +02:00
Pablo Alba
796aaed11e 🐛 Fix prop creation on variants move layer 2025-09-02 10:01:30 +02:00
Alejandro Alonso
1da69cfa38 📎 Add next release entries to the changelog 2025-09-01 11:10:09 +02:00
601 changed files with 45284 additions and 11897 deletions

View File

@@ -226,14 +226,29 @@ jobs:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
# Build frontend
- run:
name: "integration tests"
name: "frontend build"
working_directory: "./frontend"
command: |
yarn install
yarn run build:app:assets
yarn run build:app
yarn run build:app:libs
# Build the wasm bundle
- run:
name: "wasm build"
working_directory: "./render-wasm"
command: |
EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh
./build release
# Run integration tests
- run:
name: "integration tests"
working_directory: "./frontend"
command: |
yarn run playwright install chromium
yarn run test:e2e -x --workers=4

View File

@@ -1,11 +1,11 @@
name: Build and Upload Penpot Bundle
name: Bundles Builder
on:
# Create bundle from manual action
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
@@ -22,7 +22,7 @@ on:
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch'
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
@@ -56,10 +56,9 @@ jobs:
- name: Extract some useful variables
id: vars
run: |
echo "commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Run manage.sh build-bundle from host
- name: Build bundle
env:
BUILD_WASM: ${{ inputs.build_wasm }}
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
@@ -76,13 +75,6 @@ jobs:
zip -r zips/penpot.zip penpot
- name: Upload Penpot bundle to S3
if: github.ref_type == 'branch'
run: |
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}-latest.zip
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.commit_hash }}.zip
- name: Upload Penpot bundle to S3
if: github.ref_type == 'tag'
run: |
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip

View File

@@ -1,14 +1,21 @@
name: DEVELOP - Build and Upload Penpot Bundle
name: _DEVELOP
on:
schedule:
- cron: '16 5-20 * * 1-5'
jobs:
build-develop-bundle:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "develop"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: "develop"

View File

@@ -0,0 +1,36 @@
name: DevEnv Docker Image Builder
on:
workflow_dispatch:
jobs:
build-and-push:
name: Build and push DevEnv Docker image
environment: release-admins
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Build and push DevEnv Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'penpotapp/devenv'
with:
context: ./docker/devenv/
file: ./docker/devenv/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.DOCKER_IMAGE }}:latest
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max

101
.github/workflows/build-docker.yml vendored Normal file
View File

@@ -0,0 +1,101 @@
name: Docker Images Builder
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch or ref'
type: string
required: true
default: 'develop'
jobs:
build-and-push:
name: Build and Push Penpot Docker Images
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Download Penpot Bundles
env:
FILE_NAME: penpot-${{ steps.vars.outputs.gh_ref }}.zip
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
pushd docker/images
aws s3 cp s3://${{ secrets.S3_BUCKET }}/$FILE_NAME .
unzip $FILE_NAME > /dev/null
mv penpot/backend bundle-backend
mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter
popd
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Backend Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'backend'
BUNDLE_PATH: './bundle-backend'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.backend
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Frontend Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'frontend'
BUNDLE_PATH: './bundle-frontend'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.frontend
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Exporter Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'exporter'
BUNDLE_PATH: './bundle-exporter'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.exporter
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max

View File

@@ -1,14 +1,21 @@
name: STAGING - Build and Upload Penpot Bundle
name: _STAGING
on:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-staging-bundle:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: "staging"

View File

@@ -1,4 +1,4 @@
name: TAG - Build and Upload Penpot Bundle
name: _TAG
on:
push:
@@ -6,10 +6,25 @@ on:
- '*'
jobs:
build-tag-bundle:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
build_wasm: "no"
build_storybook: "yes"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
# publish-final-tag:
# if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
# needs: build-docker
# uses: ./.github/workflows/release.yml
# secrets: inherit
# with:
# gh_ref: ${{ github.ref_name }}

View File

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

95
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,95 @@
name: Release Publisher
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Tag to release'
type: string
required: true
workflow_call:
inputs:
gh_ref:
description: 'Tag to release'
type: string
required: true
permissions:
contents: write
jobs:
release:
environment: release-admins
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.vars.outputs.gh_ref }}
release_notes: ${{ steps.extract_release_notes.outputs.release_notes }}
steps:
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.vars.outputs.gh_ref }}
# # --- Publicly release the docker images ---
# - name: Login to private registry
# uses: docker/login-action@v3
# with:
# registry: ${{ secrets.DOCKER_REGISTRY }}
# username: ${{ secrets.DOCKER_USERNAME }}
# password: ${{ secrets.DOCKER_PASSWORD }}
# - name: Login to DockerHub
# uses: docker/login-action@v3
# with:
# username: ${{ secrets.PUB_DOCKER_USERNAME }}
# password: ${{ secrets.PUB_DOCKER_PASSWORD }}
# - name: Publish docker images to DockerHub
# env:
# TAG: ${{ steps.vars.outputs.gh_ref }}
# REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
# HUB: ${{ secrets.PUB_DOCKER_HUB }}
# run: |
# IMAGES=("frontend" "backend" "exporter")
# EXTRA_TAGS=("main" "latest")
# for image in "${IMAGES[@]}"; do
# docker pull "$REGISTRY/penpotapp/$image:$TAG"
# docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$TAG"
# docker push "penpotapp/$image:$TAG"
# for tag in "${EXTRA_TAGS[@]}"; do
# docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$tag"
# docker push "penpotapp/$image:$tag"
# done
# done
# --- Release notes extraction ---
- name: Extract release notes from CHANGES.md
id: extract_release_notes
env:
TAG: ${{ steps.vars.outputs.gh_ref }}
run: |
RELEASE_NOTES=$(awk "/^## $TAG$/{flag=1; next} /^## /{flag=0} flag" CHANGES.md | awk '{$1=$1};1')
if [ -z "$RELEASE_NOTES" ]; then
RELEASE_NOTES="No changes for $TAG according to CHANGES.md"
fi
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# --- Create GitHub release ---
- name: Create GitHub release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.vars.outputs.gh_ref }}
name: ${{ steps.vars.outputs.gh_ref }}
body: ${{ steps.extract_release_notes.outputs.release_notes }}

View File

@@ -1,6 +1,51 @@
# CHANGELOG
## 2.10.0 (Unreleased)
## 2.11.0 (Unreleased)
### :rocket: Epics and highlights
- Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be
removed in future versions:
- The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its
values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`)
- The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY`
- The `PENPOT_STORAGE_ASSETS_S3_BUCKET` becomes `PENPOT_OBJECTS_STORAGE_S3_BUCKET`
- The `PENPOT_STORAGE_ASSETS_S3_REGION` becomes `PENPOT_OBJECTS_STORAGE_S3_REGION`
- The `PENPOT_STORAGE_ASSETS_S3_ENDPOINT` becomes `PENPOT_OBJECTS_STORAGE_S3_ENDPOINT`
- The `PENPOT_STORAGE_ASSETS_S3_IO_THREADS` replaced (see below)
- Add `PENPOT_NETTY_IO_THREADS` and `PENPOT_EXECUTOR_THREADS` variables to provide the
control over concurrency of the shared resources used by netty. Penpot uses the netty IO
threads for AWS S3 SDK and Redis/Valkey communication, and the EXEC threads to perform
out of HTTP serving threads tasks such that cache invalidation, S3 response completion,
configuration reloading and many other auxiliar tasks. By default they use a half number
if available cpus with a minumum of 2 for both executors. You should not touch that
variables unless you are know what you are doing.
- Replace the `PENPOT_STORAGE_ASSETS_S3_IO_THREADS` with a more general configuration
`PENPOT_NETTY_IO_THREADS` used to configure a shared netty resources across different
services which use netty internally (redis connection, S3 SDK client). This
configuration is not very commonly used so don't expected real impact on any user.
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
- Show current Penpot version [Taiga #11603](https://tree.taiga.io/project/penpot/us/11603)
- Switch several variant copies at the same time [Taiga #11411](https://tree.taiga.io/project/penpot/us/11411)
- Invitations management improvements [Taiga #3479](https://tree.taiga.io/project/penpot/us/3479)
- Alternative ways of creating variants - Button Viewport [Taiga #11931](https://tree.taiga.io/project/penpot/us/11931)
### :bug: Bugs fixed
- Fix selection problems when devtools open [Taiga #11950](https://tree.taiga.io/project/penpot/issue/11950)
- Fix long font names overlap [Taiga #11844](https://tree.taiga.io/project/penpot/issue/11844)
- Fix paste behavior according to the selected element [Taiga #11979](https://tree.taiga.io/project/penpot/issue/11979)
- Fix problem with export size [Github #7160](https://github.com/penpot/penpot/issues/7160)
- Fix multi level library dependencies [Taiga #12155](https://tree.taiga.io/project/penpot/issue/12155)
- Fix component context menu options order in assets tab [Taiga #11941](https://tree.taiga.io/project/penpot/issue/11941)
## 2.10.0
### :rocket: Epics and highlights
@@ -35,6 +80,7 @@
- Retrieve variants with nested components [Taiga #10277](https://tree.taiga.io/project/penpot/us/10277)
- Create variants in bulk from existing components [Taiga #7926](https://tree.taiga.io/project/penpot/us/7926)
- Alternative ways of creating variants - Button Design Tab [Taiga #10316](https://tree.taiga.io/project/penpot/us/10316)
- Fix problem with component swapping panel [Taiga #12175](https://tree.taiga.io/project/penpot/issue/12175)
### :bug: Bugs fixed
@@ -48,7 +94,7 @@
- Fix issue where Alt + arrow keys shortcut interferes with letter-spacing when moving text layers [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11771)
- Fix consistency issues on how font variants are visualized [Taiga #11499](https://tree.taiga.io/project/penpot/us/11499)
- Fix parsing rx and ry SVG values for rect radius [Taiga #11861](https://tree.taiga.io/project/penpot/issue/11861)
- Misleading affordance in saved versions [Taiga #11887](https://tree.taiga.io/project/penpot/issue/11887)
- Fix misleading affordance in saved versions [Taiga #11887](https://tree.taiga.io/project/penpot/issue/11887)
- Fix pasting RTF text crashes penpot [Taiga #11717](https://tree.taiga.io/project/penpot/issue/11717)
- Fix navigation arrows in Libraries & Templates carousel [Taiga #10609](https://tree.taiga.io/project/penpot/issue/10609)
- Fix applying tokens with zero value to size [Taiga #11618](https://tree.taiga.io/project/penpot/issue/11618)
@@ -143,7 +189,7 @@
**Penpot Library**
The initial prototype is completly reworked for provide a more consistent API
The initial prototype is completly reworked to provide a more consistent API
and to have proper validation and params decoding. All the details can be found
on [its own changelog](library/CHANGES.md)

View File

@@ -77,17 +77,14 @@ Provide your team or organization with a completely owned collaborative design t
### Integrations ###
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
### Whats great for design ###
With Penpot you can design libraries to share and reuse; turn design elements into components and tokens to allow reusability and scalability; and build realistic user flows and interactions.
### Design Tokens ###
With Penpots standardized [design tokens](https://penpot.dev/collaboration/design-tokens) format, you can easily reuse and sync tokens across different platforms, workflows, and disciplines.
### Building Design Systems: design tokens, components and variants ###
Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms.
<br />
<p align="center">
<img src="https://img.plasmic.app/img-optimizer/v1/img?src=https%3A%2F%2Fimg.plasmic.app%2Fimg-optimizer%2Fv1%2Fimg%2F9dd677c36afb477e9666ccd1d3f009ad.png" alt="Open Source" style="width: 65%;">
<img src="https://github.com/user-attachments/assets/cce75ad6-f783-473f-8803-da9eb8255fef">
</p>
<br />

View File

@@ -6,7 +6,7 @@
org.clojure/clojure {:mvn/version "1.12.2"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-3"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.7.0.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.8.1.RELEASE"}
;; Minimal dependencies required by lettuce, we need to include them
;; explicitly because clojure dependency management does not support
;; yet the BOM format.
@@ -28,29 +28,30 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.4"
:git/sha "ce50d42"
{:git/tag "v11.6"
:git/sha "94dc017"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc
{:mvn/version "1.3.1002"}
{:mvn/version "1.3.1070"}
metosin/reitit-core {:mvn/version "0.9.1"}
nrepl/nrepl {:mvn/version "1.3.1"}
nrepl/nrepl {:mvn/version "1.4.0"}
org.postgresql/postgresql {:mvn/version "42.7.7"}
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
io.whitfin/siphash {:mvn/version "2.0.0"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.0"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.2"}
org.jsoup/jsoup {:mvn/version "1.20.1"}
org.jsoup/jsoup {:mvn/version "1.21.2"}
org.im4java/im4java
{:git/tag "1.4.0-penpot-2"
:git/sha "e2b3e16"
@@ -60,12 +61,12 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.12.3"}
dawran6/emoji {:mvn/version "0.2.0"}
markdown-clj/markdown-clj {:mvn/version "1.12.4"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.33.8"}}
software.amazon.awssdk/s3 {:mvn/version "2.33.10"}}
:paths ["src" "resources" "target/classes"]
:aliases
@@ -80,12 +81,14 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
:ns-default build}
:test
{:main-opts ["-m" "kaocha.runner"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
"--sun-misc-unsafe-memory-access=allow"
"--enable-native-access=ALL-UNNAMED"]
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
:outdated

View File

@@ -35,6 +35,7 @@
[app.util.blob :as blob]
[clj-async-profiler.core :as prof]
[clojure.contrib.humanize :as hum]
[clojure.datafy :refer [datafy]]
[clojure.java.io :as io]
[clojure.pprint :refer [pprint print-table]]
[clojure.repl :refer :all]

View File

@@ -1 +1 @@
Invitation to join {{team}}
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}

View File

@@ -31,8 +31,7 @@ export PENPOT_FLAGS="\
disable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation \
enable-subscriptions \
disable-subscriptions-old";
enable-subscriptions";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
@@ -78,10 +77,14 @@ export JAVA_OPTS="\
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
-Djdk.tracePinnedThreads=full \
-Dim4java.useV7=true \
-XX:+UseShenandoahGC \
-XX:+EnableDynamicAgentLoading \
-XX:-OmitStackTraceInFastThrow \
-XX:+UnlockExperimentalVMOptions \
-XX:+UnlockDiagnosticVMOptions \
-XX:+DebugNonSafepoints \
-XX:ShenandoahGCMode=generational \
-XX:+UseCompactObjectHeaders \
--sun-misc-unsafe-memory-access=allow \
--enable-preview \
--enable-native-access=ALL-UNNAMED";

View File

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

View File

@@ -34,8 +34,7 @@
[clojure.set :as set]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[datoteka.io :as io]
[promesa.exec :as px]))
[datoteka.io :as io]))
(set! *warn-on-reflection* true)
@@ -476,7 +475,7 @@
(vary-meta dissoc ::fmg/migrated))))
(defn encode-file
[{:keys [::wrk/executor] :as cfg} {:keys [id features] :as file}]
[cfg {:keys [id features] :as file}]
(let [file (if (and (contains? features "fdata/objects-map")
(:data file))
(fdata/enable-objects-map file)
@@ -493,7 +492,7 @@
(-> file
(d/update-when :features into-array)
(d/update-when :data (fn [data] (px/invoke! executor #(blob/encode data)))))))
(d/update-when :data blob/encode))))
(defn- file->params
[file]

View File

@@ -96,7 +96,7 @@
[:http-server-max-body-size {:optional true} ::sm/int]
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
[:http-server-io-threads {:optional true} ::sm/int]
[:http-server-worker-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int]
[:telemetry-uri {:optional true} :string]
[:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
@@ -214,20 +214,21 @@
[:media-uri {:optional true} :string]
[:assets-path {:optional true} :string]
;; Legacy, will be removed in 2.5
[:netty-io-threads {:optional true} ::sm/int]
[:executor-threads {:optional true} ::sm/int]
;; DEPRECATED
[:assets-storage-backend {:optional true} :keyword]
[:storage-assets-fs-directory {:optional true} :string]
[:storage-assets-s3-bucket {:optional true} :string]
[:storage-assets-s3-region {:optional true} :keyword]
[:storage-assets-s3-endpoint {:optional true} ::sm/uri]
[:storage-assets-s3-io-threads {:optional true} ::sm/int]
[:objects-storage-backend {:optional true} :keyword]
[:objects-storage-fs-directory {:optional true} :string]
[:objects-storage-s3-bucket {:optional true} :string]
[:objects-storage-s3-region {:optional true} :keyword]
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]
[:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]]))
(defn- parse-flags
[config]

View File

@@ -12,15 +12,14 @@
[app.common.files.helpers :as cfh]
[app.common.files.migrations :as fmg]
[app.common.logging :as l]
[app.common.types.objects-map :as omap]
[app.common.types.path :as path]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.storage :as sto]
[app.util.blob :as blob]
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.worker :as wrk]
[promesa.exec :as px]))
[app.util.objects-map :as omap.legacy]
[app.util.pointer-map :as pmap]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OFFLOAD
@@ -38,10 +37,7 @@
[file & _opts]
(let [update-page
(fn [page]
(if (and (pmap/pointer-map? page)
(not (pmap/loaded? page)))
page
(update page :objects omap/wrap)))
(update page :objects omap/wrap))
update-data
(fn [fdata]
@@ -51,6 +47,20 @@
(update :data update-data)
(update :features conj "fdata/objects-map"))))
(defn disable-objects-map
[file & _opts]
(let [update-page
(fn [page]
(update page :objects #(into {} %)))
update-data
(fn [fdata]
(update fdata :pages-index d/update-vals update-page))]
(-> file
(update :data update-data)
(update :features disj "fdata/objects-map"))))
(defn process-objects
"Apply a function to all objects-map on the file. Usualy used for convert
the objects-map instances to plain maps"
@@ -60,7 +70,8 @@
(fn [page]
(update page :objects
(fn [objects]
(if (omap/objects-map? objects)
(if (or (omap/objects-map? objects)
(omap.legacy/objects-map? objects))
(update-fn objects)
objects)))))
fdata))
@@ -84,10 +95,10 @@
(assoc file :data data)))
(defn decode-file-data
[{:keys [::wrk/executor]} {:keys [data] :as file}]
[_system {:keys [data] :as file}]
(cond-> file
(bytes? data)
(assoc :data (px/invoke! executor #(blob/decode data)))))
(assoc :data (blob/decode data))))
(defn load-pointer
"A database loader pointer helper"

View File

@@ -26,9 +26,7 @@
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc]
[app.setup :as-alias setup]
[app.worker :as wrk]
[integrant.core :as ig]
[promesa.exec :as px]
[reitit.core :as r]
[reitit.middleware :as rr]
[yetti.adapter :as yt]
@@ -55,6 +53,8 @@
[:map
[::port ::sm/int]
[::host ::sm/text]
[::io-threads {:optional true} ::sm/int]
[::max-worker-threads {:optional true} ::sm/int]
[::max-body-size {:optional true} ::sm/int]
[::max-multipart-body-size {:optional true} ::sm/int]
[::router {:optional true} [:fn r/router?]]
@@ -65,31 +65,41 @@
(assert (sm/check schema:server-params params)))
(defmethod ig/init-key ::server
[_ {:keys [::handler ::router ::host ::port ::wrk/executor] :as cfg}]
[_ {:keys [::handler ::router ::host ::port ::mtx/metrics] :as cfg}]
(l/info :hint "starting http server" :port port :host host)
(let [options {:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (or (::io-threads cfg)
(max 3 (px/get-available-processors)))
:xnio/dispatch executor
:ring/compat :ring2
:socket/backlog 4069}
(let [on-dispatch
(fn [_ start-at-ns]
(let [timing (- (System/nanoTime) start-at-ns)
timing (int (/ timing 1000000))]
(mtx/run! metrics
:id :http-server-dispatch-timing
:val timing)))
handler (cond
(some? router)
(router-handler router)
options
{:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (::io-threads cfg)
:xnio/max-worker-threads (::max-worker-threads cfg)
:ring/compat :ring2
:events/on-dispatch on-dispatch
:socket/backlog 4069}
(some? handler)
handler
handler
(cond
(some? router)
(router-handler router)
:else
(throw (UnsupportedOperationException. "handler or router are required")))
(some? handler)
handler
options (d/without-nils options)
server (yt/server handler options)]
:else
(throw (UnsupportedOperationException. "handler or router are required")))
server
(yt/server handler (d/without-nils options))]
(assoc cfg ::server (yt/start! server))))

View File

@@ -17,11 +17,9 @@
[app.main :as-alias main]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.worker :as-alias wrk]
[clojure.data.json :as j]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.exec :as px]
[yetti.request :as yreq]
[yetti.response :as-alias yres]))
@@ -40,8 +38,8 @@
[_ cfg]
(letfn [(handler [request]
(let [data (-> request yreq/body slurp)]
(px/run! :vthread (partial handle-request cfg data)))
{::yres/status 200})]
(handle-request cfg data)
{::yres/status 200}))]
["/sns" {:handler handler
:allowed-methods #{:post}}]))

View File

@@ -54,7 +54,7 @@
::yres/body (yres/stream-body
(fn [_ output]
(let [channel (sp/chan :buf buf :xf (keep encode))
listener (events/start-listener
listener (events/spawn-listener
channel
(partial write! output)
(partial pu/close! output))]

View File

@@ -42,6 +42,7 @@
[app.svgo :as-alias svgo]
[app.util.cron]
[app.worker :as-alias wrk]
[app.worker.executor]
[clojure.test :as test]
[clojure.tools.namespace.repl :as repl]
[cuerdas.core :as str]
@@ -148,23 +149,11 @@
::mdef/labels []
::mdef/type :histogram}
:executors-active-threads
{::mdef/name "penpot_executors_active_threads"
::mdef/help "Current number of threads available in the executor service."
::mdef/labels ["name"]
::mdef/type :gauge}
:executors-completed-tasks
{::mdef/name "penpot_executors_completed_tasks_total"
::mdef/help "Approximate number of completed tasks by the executor."
::mdef/labels ["name"]
::mdef/type :counter}
:executors-running-threads
{::mdef/name "penpot_executors_running_threads"
::mdef/help "Current number of threads with state RUNNING."
::mdef/labels ["name"]
::mdef/type :gauge}})
:http-server-dispatch-timing
{::mdef/name "penpot_http_server_dispatch_timing"
::mdef/help "Histogram of dispatch handler"
::mdef/labels []
::mdef/type :histogram}})
(def system-config
{::db/pool
@@ -176,14 +165,12 @@
::db/max-size (cf/get :database-max-pool-size 60)
::mtx/metrics (ig/ref ::mtx/metrics)}
;; Default thread pool for IO operations
::wrk/executor
{}
;; Default netty IO pool (shared between several services)
::wrk/netty-io-executor
{:threads (cf/get :netty-io-threads)}
::wrk/monitor
{::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/executor)
::wrk/name "default"}
::wrk/netty-executor
{:threads (cf/get :executor-threads)}
:app.migrations/migrations
{::db/pool (ig/ref ::db/pool)}
@@ -197,14 +184,19 @@
::rds/redis
{::rds/uri (cf/get :redis-uri)
::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/executor)}
::wrk/netty-executor
(ig/ref ::wrk/netty-executor)
::wrk/netty-io-executor
(ig/ref ::wrk/netty-io-executor)}
::mbus/msgbus
{::wrk/executor (ig/ref ::wrk/executor)
{::wrk/executor (ig/ref ::wrk/netty-executor)
::rds/redis (ig/ref ::rds/redis)}
:app.storage.tmp/cleaner
{::wrk/executor (ig/ref ::wrk/executor)}
{::wrk/executor (ig/ref ::wrk/netty-executor)}
::sto.gc-deleted/handler
{::db/pool (ig/ref ::db/pool)
@@ -232,9 +224,10 @@
::http/host (cf/get :http-server-host)
::http/router (ig/ref ::http/router)
::http/io-threads (cf/get :http-server-io-threads)
::http/max-worker-threads (cf/get :http-server-max-worker-threads)
::http/max-body-size (cf/get :http-server-max-body-size)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
::wrk/executor (ig/ref ::wrk/executor)}
::mtx/metrics (ig/ref ::mtx/metrics)}
::ldap/provider
{:host (cf/get :ldap-host)
@@ -312,17 +305,17 @@
::rpc/climit
{::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/executor)
::wrk/executor (ig/ref ::wrk/netty-executor)
::climit/config (cf/get :rpc-climit-config)
::climit/enabled (contains? cf/flags :rpc-climit)}
:app.rpc/rlimit
{::wrk/executor (ig/ref ::wrk/executor)}
{::wrk/executor (ig/ref ::wrk/netty-executor)}
:app.rpc/methods
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::wrk/executor (ig/ref ::wrk/netty-executor)
::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider)
::sto/storage (ig/ref ::sto/storage)
@@ -476,13 +469,14 @@
(cf/get :objects-storage-s3-bucket))
::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads)
(cf/get :objects-storage-s3-io-threads))
::wrk/executor (ig/ref ::wrk/executor)}
::wrk/netty-io-executor
(ig/ref ::wrk/netty-io-executor)}
:app.storage.fs/backend
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
(cf/get :objects-storage-fs-directory))}})
(def worker-config
{::wrk/cron
{::wrk/registry (ig/ref ::wrk/registry)

View File

@@ -216,8 +216,7 @@
(rds/add-listener sconn (create-listener rcv-ch))
(px/thread
{:name "penpot/msgbus/io-loop"
:virtual true}
{:name "penpot/msgbus"}
(try
(loop []
(let [timeout-ch (sp/timeout-chan 1000)

View File

@@ -21,8 +21,7 @@
[clojure.java.io :as io]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px])
[promesa.core :as p])
(:import
clojure.lang.MapEntry
io.lettuce.core.KeyValue
@@ -45,8 +44,10 @@
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
io.lettuce.core.resource.ClientResources
io.lettuce.core.resource.DefaultClientResources
io.netty.channel.nio.NioEventLoopGroup
io.netty.util.HashedWheelTimer
io.netty.util.Timer
io.netty.util.concurrent.EventExecutorGroup
java.lang.AutoCloseable
java.time.Duration))
@@ -111,20 +112,15 @@
(defmethod ig/expand-key ::redis
[k v]
(let [cpus (px/get-available-processors)
threads (max 1 (int (* cpus 0.2)))]
{k (-> (d/without-nils v)
(assoc ::timeout (ct/duration "10s"))
(assoc ::io-threads (max 3 threads))
(assoc ::worker-threads (max 3 threads)))}))
{k (-> (d/without-nils v)
(assoc ::timeout (ct/duration "10s")))})
(def ^:private schema:redis-params
[:map {:title "redis-params"}
::wrk/executor
::wrk/netty-io-executor
::wrk/netty-executor
::mtx/metrics
[::uri ::sm/uri]
[::worker-threads ::sm/int]
[::io-threads ::sm/int]
[::timeout ::ct/duration]])
(defmethod ig/assert-key ::redis
@@ -141,17 +137,30 @@
(defn- initialize-resources
"Initialize redis connection resources"
[{:keys [::uri ::io-threads ::worker-threads ::wrk/executor ::mtx/metrics] :as params}]
[{:keys [::uri ::mtx/metrics ::wrk/netty-io-executor ::wrk/netty-executor] :as params}]
(l/inf :hint "initialize redis resources"
:uri (str uri)
:io-threads io-threads
:worker-threads worker-threads)
:uri (str uri))
(let [timer (HashedWheelTimer.)
resources (.. (DefaultClientResources/builder)
(ioThreadPoolSize ^long io-threads)
(computationThreadPoolSize ^long worker-threads)
(eventExecutorGroup ^EventExecutorGroup netty-executor)
;; We provide lettuce with a shared event loop
;; group instance instead of letting lettuce to
;; create its own
(eventLoopGroupProvider
(reify io.lettuce.core.resource.EventLoopGroupProvider
(allocate [_ _] netty-io-executor)
(threadPoolSize [_]
(.executorCount ^NioEventLoopGroup netty-io-executor))
(release [_ _ _ _ _]
;; Do nothing
)
(shutdown [_ _ _ _]
;; Do nothing
)))
(timer ^Timer timer)
(build))
@@ -166,7 +175,7 @@
(l/trace :hint "evict connection (cache)" :key key :reason cause)
(some-> val d/close!))
cache (cache/create :executor executor
cache (cache/create :executor netty-executor
:on-remove on-remove
:keepalive "5m")]
(reify

View File

@@ -21,7 +21,6 @@
[clojure.set :as set]
[datoteka.fs :as fs]
[integrant.core :as ig]
[promesa.exec :as px]
[promesa.exec.bulkhead :as pbh])
(:import
clojure.lang.ExceptionInfo
@@ -289,13 +288,9 @@
(get-limits cfg)))
(defn invoke!
"Run a function in context of climit.
Intended to be used in virtual threads."
[{:keys [::executor ::rpc/climit] :as cfg} f params]
"Run a function in context of climit."
[{:keys [::rpc/climit] :as cfg} f params]
(let [f (if climit
(let [f (if (some? executor)
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
f)]
(build-exec-chain cfg f))
(build-exec-chain cfg f)
f)]
(f cfg params)))

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.auth
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
@@ -62,7 +63,7 @@
(ex/raise :type :validation
:code :account-without-password
:hint "the current account does not have password")
(let [result (profile/verify-password cfg password (:password profile))]
(let [result (auth/verify-password password (:password profile))]
(when (:update result)
(l/trc :hint "updating profile password"
:id (str (:id profile))
@@ -156,7 +157,7 @@
(:profile-id tdata)))
(update-password [conn profile-id]
(let [pwd (profile/derive-password cfg password)]
(let [pwd (auth/derive-password password)]
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
nil))]
@@ -378,7 +379,7 @@
(not (contains? cf/flags :email-verification)))
params (-> params
(assoc :is-active is-active)
(update :password #(profile/derive-password cfg %)))
(update :password auth/derive-password))
profile (->> (create-profile! conn params)
(create-profile-rels! conn))]
(vary-meta profile assoc :created true))))

View File

@@ -28,7 +28,6 @@
[app.tasks.file-gc]
[app.util.services :as sv]
[app.worker :as-alias wrk]
[promesa.exec :as px]
[yetti.response :as yres]))
(set! *warn-on-reflection* true)
@@ -94,7 +93,7 @@
;; --- Command: import-binfile
(defn- import-binfile
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id project-id version name file]}]
[{:keys [::db/pool] :as cfg} {:keys [profile-id project-id version name file]}]
(let [team (teams/get-team pool
:profile-id profile-id
:project-id project-id)
@@ -105,13 +104,9 @@
(assoc ::bfc/name name)
(assoc ::bfc/input (:path file)))
;; NOTE: the importation process performs some operations that are
;; not very friendly with virtual threads, and for avoid
;; unexpected blocking of other concurrent operations we dispatch
;; that operation to a dedicated executor.
result (case (int version)
1 (px/invoke! executor (partial bf.v1/import-files! cfg))
3 (px/invoke! executor (partial bf.v3/import-files! cfg)))]
1 (bf.v1/import-files! cfg)
3 (bf.v3/import-files! cfg))]
(db/update! pool :project
{:modified-at (ct/now)}

View File

@@ -7,6 +7,7 @@
(ns app.rpc.commands.demo
"A demo specific mutations."
(:require
[app.auth :refer [derive-password]]
[app.common.exceptions :as ex]
[app.common.time :as ct]
[app.config :as cf]
@@ -14,7 +15,6 @@
[app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[buddy.core.codecs :as bc]
@@ -46,7 +46,7 @@
:fullname fullname
:is-active true
:deleted-at (ct/in-future (cf/get-deletion-delay))
:password (profile/derive-password cfg password)
:password (derive-password password)
:props {}}
profile (db/tx-run! cfg (fn [{:keys [::db/conn]}]
(->> (auth/create-profile! conn params)

View File

@@ -39,8 +39,7 @@
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.worker :as wrk]
[cuerdas.core :as str]
[promesa.exec :as px]))
[cuerdas.core :as str]))
;; --- FEATURES
@@ -251,7 +250,7 @@
(feat.fmigr/resolve-applied-migrations cfg file))))))
(defn get-file
[{:keys [::db/conn ::wrk/executor] :as cfg} id
[{:keys [::db/conn] :as cfg} id
& {:keys [project-id
migrate?
include-deleted?
@@ -273,13 +272,8 @@
::db/remove-deleted (not include-deleted?)
::sql/for-update lock-for-update?})
(feat.fmigr/resolve-applied-migrations cfg)
(feat.fdata/resolve-file-data cfg))
;; NOTE: we perform the file decoding in a separate thread
;; because it has heavy and synchronous operations for
;; decoding file body that are not very friendly with virtual
;; threads.
file (px/invoke! executor #(decode-row file))
(feat.fdata/resolve-file-data cfg)
(decode-row))
file (if (and migrate? (fmg/need-migration? file))
(migrate-file cfg file options)
@@ -342,14 +336,24 @@
(cfeat/check-client-features! (: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
;; pointers on backend and return a complete file.
(if (and (contains? (:features file) "fdata/pointer-map")
(not (contains? (:features params) "fdata/pointer-map")))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(update file :data feat.fdata/process-pointers deref))
file))))
(as-> file file
;; This operation is needed for backward comapatibility with
;; frontends that does not support pointer-map resolution
;; mechanism; this just resolves the pointers on backend and
;; return a complete file
(if (and (contains? (:features file) "fdata/pointer-map")
(not (contains? (:features params) "fdata/pointer-map")))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(update file :data feat.fdata/process-pointers deref))
file)
;; This operation is needed for backward comapatibility with
;; frontends that does not support objects-map mechanism; this
;; just converts all objects map instaces to plain maps
(if (and (contains? (:features file) "fdata/objects-map")
(not (contains? (:features params) "fdata/objects-map")))
(update file :data feat.fdata/process-objects (partial into {}))
file)))))
;; --- COMMAND QUERY: get-file-fragment (by id)
@@ -604,8 +608,16 @@
{:components components
:variant-ids variant-ids}))
;;coalesce(string_agg(flr.library_file_id::text, ','), '') as library_file_ids
(def ^:private sql:team-shared-files
"select f.id,
"with file_library_agg as (
select flr.file_id,
coalesce(array_agg(flr.library_file_id) filter (where flr.library_file_id is not null), '{}') as library_file_ids
from file_library_rel flr
group by flr.file_id
)
select f.id,
f.revn,
f.vern,
f.data,
@@ -618,10 +630,12 @@
f.version,
f.is_shared,
ft.media_id,
p.team_id
p.team_id,
fla.library_file_ids
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn and ft.deleted_at is null)
left join file_library_agg as fla on fla.file_id = f.id
where f.is_shared = true
and f.deleted_at is null
and p.deleted_at is null
@@ -665,6 +679,8 @@
(dissoc :media-id)
(assoc :thumbnail-id media-id))
(dissoc row :media-id))))
(map (fn [row]
(update row :library-file-ids db/decode-pgarray #{})))
(map #(assoc % :library-summary (get-library-summary cfg %)))
(map #(dissoc % :data))))))
@@ -1061,6 +1077,7 @@
[:library-id ::sm/uuid]])
(sv/defmethod ::link-file-to-library
"Link a file to a library. Returns the recursive list of libraries used by that library"
{::doc/added "1.17"
::webhooks/event? true
::sm/params schema:link-file-to-library}
@@ -1074,7 +1091,8 @@
(fn [{:keys [::db/conn]}]
(check-edition-permissions! conn profile-id file-id)
(check-edition-permissions! conn profile-id library-id)
(link-file-to-library conn params))))
(link-file-to-library conn params)
(bfc/get-libraries cfg [library-id]))))
;; --- MUTATION COMMAND: unlink-file-from-library

View File

@@ -112,14 +112,15 @@
;; FIXME: IMPORTANT: this code can have race conditions, because
;; we have no locks for updating team so, creating two files
;; concurrently can lead to lost team features updating
(when-let [features (-> features
(set/difference (:features team))
(set/difference cfeat/no-team-inheritable-features)
(not-empty))]
(let [features (->> features
(set/union (:features team))
(db/create-array conn "text"))]
(let [features (-> features
(set/union (:features team))
(set/difference cfeat/no-team-inheritable-features)
(into-array))]
(db/update! conn :team
{:features features}
{:id (:id team)}

View File

@@ -37,9 +37,7 @@
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.worker :as wrk]
[clojure.set :as set]
[promesa.exec :as px]))
[clojure.set :as set]))
(declare ^:private get-lagged-changes)
(declare ^:private send-notifications!)
@@ -160,7 +158,6 @@
tpoint (ct/tpoint)]
(when (not= (:vern params)
(:vern file))
(ex/raise :type :validation
@@ -183,15 +180,15 @@
(set/difference (:features team))
(set/difference cfeat/no-team-inheritable-features)
(not-empty))]
(let [features (->> features
(set/union (:features team))
(db/create-array conn "text"))]
(let [features (-> features
(set/union (:features team))
(set/difference cfeat/no-team-inheritable-features)
(into-array))]
(db/update! conn :team
{:features features}
{:id (:id team)}
{::db/return-keys false})))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
(binding [l/*context* (some-> (meta params)
@@ -209,7 +206,7 @@
Follow the inner implementation to `update-file-data!` function.
Only intended for internal use on this module."
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
[{:keys [::db/conn ::timestamp] :as cfg}
{:keys [profile-id file team features changes session-id skip-validate] :as params}]
(let [;; Retrieve the file data
@@ -222,15 +219,11 @@
;; We create a new lexycal scope for clearly delimit the result of
;; executing this update file operation and all its side effects
(let [file (px/invoke! executor
(fn []
;; Process the file data on separated thread for avoid to do
;; the CPU intensive operation on vthread.
(binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))))]
(let [file (binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))]
(feat.fmigr/upsert-migrations! conn file)
(persist-file! cfg file)

View File

@@ -26,9 +26,7 @@
[app.rpc.helpers :as rph]
[app.rpc.quotes :as quotes]
[app.storage :as sto]
[app.util.services :as sv]
[app.worker :as-alias wrk]
[promesa.exec :as px]))
[app.util.services :as sv]))
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"})
@@ -105,7 +103,7 @@
(create-font-variant cfg (assoc params :profile-id profile-id)))))
(defn create-font-variant
[{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}]
[{:keys [::sto/storage ::db/conn]} {:keys [data] :as params}]
(letfn [(generate-missing! [data]
(let [data (media/run {:cmd :generate-fonts :input data})]
(when (and (not (contains? data "font/otf"))
@@ -157,7 +155,7 @@
:otf-file-id (:id otf)
:ttf-file-id (:id ttf)}))]
(let [data (px/invoke! executor (partial generate-missing! data))
(let [data (generate-missing! data)
assets (persist-fonts-files! data)
result (insert-font-variant! assets)]
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))

View File

@@ -28,9 +28,7 @@
[app.setup :as-alias setup]
[app.setup.templates :as tmpl]
[app.storage.tmp :as tmp]
[app.util.services :as sv]
[app.worker :as-alias wrk]
[promesa.exec :as px]))
[app.util.services :as sv]))
;; --- COMMAND: Duplicate File
@@ -313,15 +311,14 @@
;; Update the modification date of the all affected projects
;; ensuring that the destination project is the most recent one.
(doseq [project-id (into (list project-id) source)]
;; NOTE: as this is executed on virtual thread, sleeping does
;; not causes major issues, and allows an easy way to set a
;; trully different modification date to each file.
(px/sleep 10)
(db/update! conn :project
{:modified-at (ct/now)}
{:id project-id}))
(loop [project-ids (into (list project-id) source)
modified-at (ct/now)]
(when-let [project-id (first project-ids)]
(db/update! conn :project
{:modified-at modified-at}
{:id project-id})
(recur (rest project-ids)
(ct/plus modified-at 10))))
nil))
@@ -396,12 +393,7 @@
;; --- COMMAND: Clone Template
(defn clone-template
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [project-id profile-id] :as params} template]
;; NOTE: the importation process performs some operations
;; that are not very friendly with virtual threads, and for
;; avoid unexpected blocking of other concurrent operations
;; we dispatch that operation to a dedicated executor.
[{:keys [::db/pool] :as cfg} {:keys [project-id profile-id] :as params} template]
(let [template (tmp/tempfile-from template
:prefix "penpot.template."
:suffix ""
@@ -419,8 +411,8 @@
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
result (if (= format :binfile-v3)
(px/invoke! executor (partial bf.v3/import-files! cfg))
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
(bf.v3/import-files! cfg)
(bf.v1/import-files! cfg))]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]

View File

@@ -24,10 +24,8 @@
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.util.services :as sv]
[app.worker :as-alias wrk]
[cuerdas.core :as str]
[datoteka.io :as io]
[promesa.exec :as px]))
[datoteka.io :as io]))
(def default-max-file-size
(* 1024 1024 10)) ; 10 MiB
@@ -153,9 +151,9 @@
(assoc ::image (process-main-image info)))))
(defn- create-file-media-object
[{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
[{:keys [::sto/storage ::db/conn] :as cfg}
{:keys [id file-id is-local name content]}]
(let [result (px/invoke! executor (partial process-image content))
(let [result (process-image content)
image (sto/put-object! storage (::image result))
thumb (when-let [params (::thumb result)]
(sto/put-object! storage params))]

View File

@@ -30,16 +30,13 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.worker :as wrk]
[cuerdas.core :as str]
[promesa.exec :as px]))
[cuerdas.core :as str]))
(declare check-profile-existence!)
(declare decode-row)
(declare derive-password)
(declare filter-props)
(declare get-profile)
(declare strip-private-attrs)
(declare verify-password)
(def schema:props-notifications
[:map {:title "props-notifications"}
@@ -192,7 +189,7 @@
[{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
(let [profile (db/get-by-id conn :profile profile-id ::sql/for-update true)]
(when (and (not= (:password profile) "!")
(not (:valid (verify-password cfg old-password (:password profile)))))
(not (:valid (auth/verify-password old-password (:password profile)))))
(ex/raise :type :validation
:code :old-password-not-match))
profile))
@@ -201,7 +198,7 @@
[{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}]
(when-not (db/read-only? conn)
(db/update! conn :profile
{:password (derive-password cfg password)}
{:password (auth/derive-password password)}
{:id id})
nil))
@@ -303,12 +300,11 @@
:content-type (:mtype thumb)}))
(defn upload-photo
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file] :as params}]
[{:keys [::sto/storage] :as cfg} {:keys [file] :as params}]
(let [params (-> cfg
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
[:process-image/global]])
(assoc ::climit/label "upload-photo")
(assoc ::climit/executor executor)
(climit/invoke! generate-thumbnail! file))]
(sto/put-object! storage params)))
@@ -548,15 +544,6 @@
[props]
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
(defn derive-password
[{:keys [::wrk/executor]} password]
(when password
(px/invoke! executor (partial auth/derive-password password))))
(defn verify-password
[{:keys [::wrk/executor]} password password-data]
(px/invoke! executor (partial auth/verify-password password password-data)))
(defn decode-row
[{:keys [props] :as row}]
(cond-> row

View File

@@ -503,7 +503,7 @@
(let [features (-> (cfeat/get-enabled-features cf/flags)
(set/difference cfeat/frontend-only-features)
(cfeat/check-client-features! (:features params)))
(set/difference cfeat/no-team-inheritable-features))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))

View File

@@ -224,62 +224,112 @@
(def ^:private xf:map-email (map :email))
(defn- create-team-invitations
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
(let [emails (set emails)
"Unified function to handle both create and resend team invitations.
Accepts either:
- emails (set) + role (single role for all emails)
- invitations (vector of {:email :role} maps)"
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails invitations] :as params}]
(let [;; Normalize input to a consistent format: [{:email :role}]
invitation-data (cond
;; Case 1: emails + single role (create invitations style)
(and emails role)
(map (fn [email] {:email email :role role}) emails)
join-requests (->> (get-valid-access-request-profiles conn (:id team))
(d/index-by :email))
;; Case 2: invitations with individual roles (resend invitations style)
(some? invitations)
invitations
team-members (into #{} xf:map-email
(teams/get-team-members conn (:id team)))
:else
(throw (ex-info "Invalid parameters: must provide either emails+role or invitations" {})))
invitations (into #{}
(comp
;; We don't re-send inviation to
;; already existing members
(remove team-members)
invitation-emails (into #{} (map :email) invitation-data)
join-requests (->> (get-valid-access-request-profiles conn (:id team))
(d/index-by :email))
team-members (into #{} xf:map-email
(teams/get-team-members conn (:id team)))
invitations (into #{}
(comp
;; We don't re-send invitations to
;; already existing members
(remove #(contains? team-members (:email %)))
;; We don't send invitations to
;; join-requested members
(remove join-requests)
(map (fn [email] (assoc params :email email)))
(keep (partial create-invitation cfg)))
emails)]
(remove #(contains? join-requests (:email %)))
(map (fn [{:keys [email role]}]
(create-invitation cfg
(-> params
(assoc :email email)
(assoc :role role)))))
(remove nil?))
invitation-data)]
;; For requested invitations, do not send invitation emails, add
;; the user directly to the team
(->> join-requests
(filter #(contains? emails (key %)))
(map val)
(run! (partial add-member-to-team conn profile team role)))
(filter #(contains? invitation-emails (key %)))
(map (fn [[email member]]
(let [role (:role (first (filter #(= (:email %) email) invitation-data)))]
(add-member-to-team conn profile team role member))))
(doall))
invitations))
(def ^:private schema:create-team-invitations
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
[:role types.team/schema:role]
[:emails [::sm/set ::sm/email]]])
[:and
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
;; Support both formats:
;; 1. emails (set) + role (single role for all)
;; 2. invitations (vector of {:email :role} maps)
[:emails {:optional true} [::sm/set ::sm/email]]
[:role {:optional true} types.team/schema:role]
[:invitations {:optional true} [:vector [:map
[:email ::sm/email]
[:role types.team/schema:role]]]]]
;; Ensure exactly one format is provided
[:fn (fn [params]
(let [has-emails-role (and (contains? params :emails)
(contains? params :role))
has-invitations (contains? params :invitations)]
(and (or has-emails-role has-invitations)
(not (and has-emails-role has-invitations)))))]])
(def ^:private max-invitations-by-request-threshold
"The number of invitations can be sent in a single rpc request"
25)
(sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to
join the team."
"A rpc call that allows to send single or multiple invitations to join the team.
Supports two parameter formats:
1. emails (set) + role (single role for all emails)
2. invitations (vector of {:email :role} maps for individual roles)"
{::doc/added "1.17"
::doc/module :teams
::sm/params schema:create-team-invitations}
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
[cfg {:keys [::rpc/profile-id team-id role emails] :as params}]
(let [perms (teams/get-permissions cfg profile-id team-id)
profile (db/get-by-id cfg :profile profile-id)
emails (into #{} (map profile/clean-email) emails)]
;; Determine which format is being used
using-emails-format? (and emails role)
;; Handle both parameter formats
emails (if using-emails-format?
(into #{} (map profile/clean-email) emails)
#{})
;; Calculate total invitation count for both formats
invitation-count (if using-emails-format?
(count emails)
(count (:invitations params)))]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(when (> (count emails) max-invitations-by-request-threshold)
(when (> invitation-count max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
@@ -288,7 +338,7 @@
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id team-id)
(assoc ::quotes/incr (count emails))
(assoc ::quotes/incr invitation-count)
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
@@ -304,7 +354,12 @@
(-> params
(assoc :profile profile)
(assoc :team team)
(assoc :emails emails)))]
;; Pass parameters in the correct format for the unified function
(cond-> using-emails-format?
;; If using emails+role format, ensure both are present
(assoc :emails emails :role role)
;; If using invitations format, the :invitations key is already in params
(not using-emails-format?) identity)))]
(with-meta {:total (count invitations)
:invitations invitations}

View File

@@ -7,7 +7,7 @@
(ns app.srepl.cli
"PREPL API for external usage (CLI or ADMIN)"
(:require
[app.auth :as auth]
[app.auth :refer [derive-password]]
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
@@ -54,7 +54,7 @@
(some-> (get-current-system)
(db/tx-run!
(fn [{:keys [::db/conn] :as system}]
(let [password (cmd.profile/derive-password system password)
(let [password (derive-password password)
params {:id (uuid/next)
:email email
:fullname fullname
@@ -74,7 +74,7 @@
(assoc :fullname fullname)
(some? password)
(assoc :password (auth/derive-password password))
(assoc :password (derive-password password))
(some? is-active)
(assoc :is-active is-active))]
@@ -124,7 +124,7 @@
(defmethod exec-command "derive-password"
[{:keys [password]}]
(auth/derive-password password))
(derive-password password))
(defmethod exec-command "authenticate"
[{:keys [token]}]

View File

@@ -40,6 +40,7 @@
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.worker :as wrk]
[clojure.datafy :refer [datafy]]
[clojure.java.io :as io]
[clojure.pprint :refer [print-table]]
[clojure.stacktrace :as strace]

View File

@@ -27,7 +27,9 @@
(defn get-legacy-backend
[]
(let [name (cf/get :assets-storage-backend)]
(when-let [name (cf/get :assets-storage-backend)]
(l/wrn :hint "using deprecated configuration, please read 2.11 release notes"
:href "https://github.com/penpot/penpot/releases/tag/2.11.0")
(case name
:assets-fs :fs
:assets-s3 :s3

View File

@@ -31,13 +31,13 @@
java.time.Duration
java.util.Collection
java.util.Optional
java.util.concurrent.atomic.AtomicLong
org.reactivestreams.Subscriber
software.amazon.awssdk.core.ResponseBytes
software.amazon.awssdk.core.async.AsyncRequestBody
software.amazon.awssdk.core.async.AsyncResponseTransformer
software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
software.amazon.awssdk.regions.Region
@@ -87,12 +87,11 @@
(def ^:private schema:config
[:map {:title "s3-backend-config"}
::wrk/executor
::wrk/netty-io-executor
[::region {:optional true} :keyword]
[::bucket {:optional true} ::sm/text]
[::prefix {:optional true} ::sm/text]
[::endpoint {:optional true} ::sm/uri]
[::io-threads {:optional true} ::sm/int]])
[::endpoint {:optional true} ::sm/uri]])
(defmethod ig/expand-key ::backend
[k v]
@@ -110,6 +109,7 @@
presigner (build-s3-presigner params)]
(assoc params
::sto/type :s3
::counter (AtomicLong. 0)
::client @client
::presigner presigner
::close-fn #(.close ^java.lang.AutoCloseable client)))))
@@ -121,7 +121,7 @@
(defmethod ig/halt-key! ::backend
[_ {:keys [::close-fn]}]
(when (fn? close-fn)
(px/run! close-fn)))
(close-fn)))
(def ^:private schema:backend
[:map {:title "s3-backend"}
@@ -198,19 +198,16 @@
(Region/of (name region)))
(defn- build-s3-client
[{:keys [::region ::endpoint ::io-threads ::wrk/executor]}]
[{:keys [::region ::endpoint ::wrk/netty-io-executor]}]
(let [aconfig (-> (ClientAsyncConfiguration/builder)
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
(.build))
sconfig (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))
thr-num (or io-threads (min 16 (px/get-available-processors)))
hclient (-> (NettyNioAsyncHttpClient/builder)
(.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder)
(.numberOfThreads (int thr-num))))
(.eventLoopGroup (SdkEventLoopGroup/create netty-io-executor))
(.connectionAcquisitionTimeout default-timeout)
(.connectionTimeout default-timeout)
(.readTimeout default-timeout)
@@ -262,7 +259,7 @@
(.close ^InputStream input))))
(defn- make-request-body
[executor content]
[counter content]
(let [size (impl/get-size content)]
(reify
AsyncRequestBody
@@ -272,16 +269,19 @@
(^void subscribe [_ ^Subscriber subscriber]
(let [delegate (AsyncRequestBody/forBlockingInputStream (long size))
input (io/input-stream content)]
(px/run! executor (partial write-input-stream delegate input))
(px/thread-call (partial write-input-stream delegate input)
{:name (str "penpot/storage/" (.getAndIncrement ^AtomicLong counter))})
(.subscribe ^BlockingInputStreamAsyncRequestBody delegate
^Subscriber subscriber))))))
(defn- put-object
[{:keys [::client ::bucket ::prefix ::wrk/executor]} {:keys [id] :as object} content]
[{:keys [::client ::bucket ::prefix ::counter]} {:keys [id] :as object} content]
(let [path (dm/str prefix (impl/id->path id))
mdata (meta object)
mtype (:content-type mdata "application/octet-stream")
rbody (make-request-body executor content)
rbody (make-request-body counter content)
request (.. (PutObjectRequest/builder)
(bucket bucket)
(contentType mtype)

View File

@@ -44,7 +44,7 @@
[_ cfg]
(fs/create-dir default-tmp-dir)
(px/fn->thread (partial io-loop cfg)
{:name "penpot/storage/tmp-cleaner" :virtual true}))
{:name "penpot/storage/tmp-cleaner"}))
(defmethod ig/halt-key! ::cleaner
[_ thread]

View File

@@ -27,7 +27,7 @@
(sp/put! channel [type data])
nil)))
(defn start-listener
(defn spawn-listener
[channel on-event on-close]
(assert (sp/chan? channel) "expected active events channel")
@@ -51,7 +51,7 @@
[f on-event]
(binding [*channel* (sp/chan :buf 32)]
(let [listener (start-listener *channel* on-event (constantly nil))]
(let [listener (spawn-listener *channel* on-event (constantly nil))]
(try
(f)
(finally

View File

@@ -112,7 +112,7 @@
(if (db/read-only? pool)
(l/wrn :hint "not started (db is read-only)")
(px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual false))))
(px/fn->thread dispatcher :name "penpot/worker-dispatcher"))))
(defmethod ig/halt-key! ::wrk/dispatcher
[_ thread]

View File

@@ -7,97 +7,79 @@
(ns app.worker.executor
"Async tasks abstraction (impl)."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.math :as mth]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.metrics :as mtx]
[app.worker :as-alias wrk]
[integrant.core :as ig]
[promesa.exec :as px])
(:import
java.util.concurrent.ThreadPoolExecutor))
io.netty.channel.nio.NioEventLoopGroup
io.netty.util.concurrent.DefaultEventExecutorGroup
java.util.concurrent.ExecutorService
java.util.concurrent.ThreadFactory))
(set! *warn-on-reflection* true)
(sm/register!
{:type ::wrk/executor
:pred #(instance? ThreadPoolExecutor %)
:pred #(instance? ExecutorService %)
:type-properties
{:title "executor"
:description "Instance of ThreadPoolExecutor"}})
:description "Instance of ExecutorService"}})
(sm/register!
{:type ::wrk/netty-io-executor
:pred #(instance? NioEventLoopGroup %)
:type-properties
{:title "executor"
:description "Instance of NioEventLoopGroup"}})
(sm/register!
{:type ::wrk/netty-executor
:pred #(instance? DefaultEventExecutorGroup %)
:type-properties
{:title "executor"
:description "Instance of DefaultEventExecutorGroup"}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EXECUTOR
;; IO Executor
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/init-key ::wrk/executor
[_ _]
(let [factory (px/thread-factory :prefix "penpot/default/")
executor (px/cached-executor :factory factory :keepalive 60000)]
(l/inf :hint "executor started")
executor))
(defmethod ig/assert-key ::wrk/netty-io-executor
[_ {:keys [threads]}]
(assert (or (nil? threads) (int? threads))
"expected valid threads value, revisit PENPOT_NETTY_IO_THREADS environment variable"))
(defmethod ig/halt-key! ::wrk/executor
(defmethod ig/init-key ::wrk/netty-io-executor
[_ {:keys [threads]}]
(let [factory (px/thread-factory :prefix "penpot/netty-io/")
nthreads (or threads (mth/round (/ (px/get-available-processors) 2)))
nthreads (max 2 nthreads)]
(l/inf :hint "start netty io executor" :threads nthreads)
(NioEventLoopGroup. (int nthreads) ^ThreadFactory factory)))
(defmethod ig/halt-key! ::wrk/netty-io-executor
[_ instance]
(px/shutdown! instance))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MONITOR
;; IO Offload Executor
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- get-stats
[^ThreadPoolExecutor executor]
{:active (.getPoolSize ^ThreadPoolExecutor executor)
:running (.getActiveCount ^ThreadPoolExecutor executor)
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
(defmethod ig/assert-key ::wrk/netty-executor
[_ {:keys [threads]}]
(assert (or (nil? threads) (int? threads))
"expected valid threads value, revisit PENPOT_EXEC_THREADS environment variable"))
(defmethod ig/expand-key ::wrk/monitor
[k v]
{k (-> (d/without-nils v)
(assoc ::interval (ct/duration "2s")))})
(defmethod ig/init-key ::wrk/netty-executor
[_ {:keys [threads]}]
(let [factory (px/thread-factory :prefix "penpot/exec/")
nthreads (or threads (mth/round (/ (px/get-available-processors) 2)))
nthreads (max 2 nthreads)]
(l/inf :hint "start default executor" :threads nthreads)
(DefaultEventExecutorGroup. (int nthreads) ^ThreadFactory factory)))
(defmethod ig/init-key ::wrk/monitor
[_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}]
(letfn [(monitor! [executor prev-completed]
(let [labels (into-array String [(d/name name)])
stats (get-stats executor)
completed (:completed stats)
completed-inc (- completed prev-completed)
completed-inc (if (neg? completed-inc) 0 completed-inc)]
(mtx/run! metrics
:id :executor-active-threads
:labels labels
:val (:active stats))
(mtx/run! metrics
:id :executor-running-threads
:labels labels
:val (:running stats))
(mtx/run! metrics
:id :executors-completed-tasks
:labels labels
:inc completed-inc)
completed-inc))]
(px/thread
{:name "penpot/executors-monitor" :virtual true}
(l/inf :hint "monitor started" :name name)
(try
(loop [completed 0]
(px/sleep interval)
(recur (long (monitor! executor completed))))
(catch InterruptedException _cause
(l/trc :hint "monitor: interrupted" :name name))
(catch Throwable cause
(l/err :hint "monitor: unexpected error" :name name :cause cause))
(finally
(l/inf :hint "monitor: terminated" :name name))))))
(defmethod ig/halt-key! ::wrk/monitor
[_ thread]
(px/interrupt! thread))
(defmethod ig/halt-key! ::wrk/netty-executor
[_ instance]
(px/shutdown! instance))

View File

@@ -248,7 +248,7 @@
(defn- start-thread!
[{:keys [::rds/redis ::id ::queue ::wrk/tenant] :as cfg}]
(px/thread
{:name (format "penpot/worker/runner:%s" id)}
{:name (str "penpot/worker-runner/" id)}
(l/inf :hint "started" :id id :queue queue)
(try
(dm/with-open [rconn (rds/connect redis)]
@@ -303,7 +303,7 @@
(l/wrn :hint "not started (db is read-only)" :queue queue :parallelism parallelism)
(doall
(->> (range parallelism)
(map #(assoc cfg ::id %))
(map #(assoc cfg ::id (str queue "/" %)))
(map start-thread!))))))
(defmethod ig/halt-key! ::wrk/runner

View File

@@ -113,7 +113,6 @@
:app.auth.oidc.providers/generic
:app.setup/templates
:app.auth.oidc/routes
:app.worker/monitor
:app.http.oauth/handler
:app.notifications/handler
:app.loggers.mattermost/reporter

View File

@@ -7,25 +7,25 @@
org.clojure/clojurescript {:mvn/version "1.12.42"}
;; Logging
org.apache.logging.log4j/log4j-api {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}
org.apache.logging.log4j/log4j-api {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.25.1"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.25.1"}
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.40"}
selmer/selmer {:mvn/version "1.12.62"}
criterium/criterium {:mvn/version "0.4.6"}
metosin/jsonista {:mvn/version "0.3.13"}
metosin/malli {:mvn/version "0.18.0"}
metosin/malli {:mvn/version "0.19.1"}
expound/expound {:mvn/version "0.9.0"}
com.cognitect/transit-clj {:mvn/version "1.0.333"}
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
integrant/integrant {:mvn/version "0.13.1"}
integrant/integrant {:mvn/version "1.0.0"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2025.06.16-414"}
@@ -47,7 +47,7 @@
org.la4j/la4j {:mvn/version "0.6.0"}
;; exception printing
fipp/fipp {:mvn/version "0.6.27"}
fipp/fipp {:mvn/version "0.6.29"}
me.flowthing/pp {:mvn/version "2024-11-13.77"}
@@ -59,7 +59,7 @@
{:dev
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "3.1.5"}
thheller/shadow-cljs {:mvn/version "3.2.0"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
@@ -68,7 +68,7 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
:ns-default build}
:test

View File

@@ -50,6 +50,13 @@
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(long (.getInt ~target (unchecked-int ~offset))))))
(defmacro read-long
[target offset]
(if (:ns &env)
`(.getInt64 ~target ~offset true)
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
`(.getLong ~target (unchecked-int ~offset)))))
(defmacro read-float
[target offset]
(if (:ns &env)
@@ -75,6 +82,40 @@
(finally
(.order ~target ByteOrder/LITTLE_ENDIAN))))))
(defmacro read-bytes
"Get a byte array from buffer. It is potentially unsafe because on
JS/CLJS it returns a subarray without doing any copy of data."
[target offset size]
(if (:ns &env)
`(new js/Uint8Array
(.-buffer ~target)
(+ (.-byteOffset ~target) ~offset)
~size)
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})
bbuf (with-meta (gensym "bbuf") {:tag bytes})]
`(let [~bbuf (byte-array ~size)]
(.get ~target
(unchecked-int ~offset)
~bbuf
0
~size)
~bbuf))))
;; FIXME: implement in cljs
(defmacro write-bytes
([target offset src size]
`(write-bytes ~target ~offset ~src 0 ~size))
([target offset src src-offset size]
(if (:ns &env)
(throw (ex-info "not implemented" {}))
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})
src (with-meta src {:tag 'bytes})]
`(.put ~target
(unchecked-int ~offset)
~src
(unchecked-int ~src-offset)
(unchecked-int ~size))))))
(defmacro write-byte
[target offset value]
(if (:ns &env)
@@ -144,13 +185,15 @@
(.setUint32 ~target (+ ~offset 12) (aget barray# 3) true))
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})
value (with-meta value {:tag 'java.util.UUID})]
`(try
(.order ~target ByteOrder/BIG_ENDIAN)
(.putLong ~target (unchecked-int (+ ~offset 0)) (.getMostSignificantBits ~value))
(.putLong ~target (unchecked-int (+ ~offset 8)) (.getLeastSignificantBits ~value))
(finally
(.order ~target ByteOrder/LITTLE_ENDIAN))))))
value (with-meta value {:tag 'java.util.UUID})
prev (with-meta (gensym "prev-") {:tag 'java.nio.ByteOrder})]
`(let [~prev (.order ~target)]
(try
(.order ~target ByteOrder/BIG_ENDIAN)
(.putLong ~target (unchecked-int (+ ~offset 0)) (.getMostSignificantBits ~value))
(.putLong ~target (unchecked-int (+ ~offset 8)) (.getLeastSignificantBits ~value))
(finally
(.order ~target ~prev)))))))
(defn wrap
[data]
@@ -160,7 +203,7 @@
(defn allocate
[size]
#?(:clj (let [buffer (ByteBuffer/allocate (int size))]
#?(:clj (let [buffer (ByteBuffer/allocate (unchecked-int size))]
(.order buffer ByteOrder/LITTLE_ENDIAN))
:cljs (new js/DataView (new js/ArrayBuffer size))))
@@ -181,6 +224,14 @@
(.set dst-view src-view)
(js/DataView. dst-buff))))
;; FIXME: cljs impl
#?(:clj
(defn copy-bytes
[src src-offset size dst dst-offset]
(let [tmp (byte-array size)]
(.get ^ByteBuffer src src-offset tmp 0 size)
(.put ^ByteBuffer dst dst-offset tmp 0 size))))
(defn equals?
[buffer-a buffer-b]
#?(:clj
@@ -208,3 +259,18 @@
[o]
#?(:clj (instance? ByteBuffer o)
:cljs (instance? js/DataView o)))
(defn slice
[buffer offset size]
#?(:cljs
(let [offset (+ (.-byteOffset buffer) offset)]
(new js/DataView (.-buffer buffer) offset size))
:clj
(-> (.slice ^ByteBuffer buffer (unchecked-int offset) (unchecked-int size))
(.order ByteOrder/LITTLE_ENDIAN))))
(defn size
[o]
#?(:cljs (.-byteLength ^js o)
:clj (.capacity ^ByteBuffer o)))

View File

@@ -51,6 +51,7 @@
"styles/v2"
"layout/grid"
"plugins/runtime"
"tokens/numeric-input"
"design-tokens/v1"
"text-editor/v2"
"render-wasm/v1"
@@ -67,11 +68,6 @@
"design-tokens/v1"
"variants/v1"})
;; A set of features that should not be propagated to team on creating
;; or modifying a file
(def no-team-inheritable-features
#{"fdata/path-data"})
;; 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
;; persist on file features field but can be permanently enabled on
@@ -80,13 +76,20 @@
#{"styles/v2"
"plugins/runtime"
"text-editor/v2"
"tokens/numeric-input"
"render-wasm/v1"})
;; Features that are mainly backend only or there are a proper
;; fallback when frontend reports no support for it
(def backend-only-features
#{"fdata/objects-map"
"fdata/pointer-map"})
#{"fdata/pointer-map"
"fdata/objects-map"})
;; A set of features that should not be propagated to team on creating
;; or modifying a file or creating or modifying a team
(def no-team-inheritable-features
#{"fdata/path-data"
"fdata/shape-data-type"})
;; This is a set of features that does not require an explicit
;; migration like components/v2 or the migration is not mandatory to
@@ -97,6 +100,7 @@
"design-tokens/v1"
"fdata/shape-data-type"
"fdata/path-data"
"tokens/numeric-input"
"variants/v1"}
(into frontend-only-features)
(into backend-only-features)))
@@ -121,6 +125,7 @@
:feature-text-editor-v2 "text-editor/v2"
:feature-render-wasm "render-wasm/v1"
:feature-variants "variants/v1"
:feature-token-input "tokens/numeric-input"
nil))
(defn migrate-legacy-features
@@ -222,8 +227,6 @@
:hint (str/ffmt "enabled feature '%' not present in file (missing migration)"
not-supported)))
(check-supported-features! file-features)
;; Components v1 is deprecated
(when-not (contains? file-features "components/v2")
(ex/raise :type :restriction

View File

@@ -323,7 +323,7 @@
[:main-instance-page ::sm/uuid]]]
[:mod-component
[:map {:title "ModCompoenentChange"}
[:map {:title "ModComponentChange"}
[:type [:= :mod-component]]
[:id ::sm/uuid]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
@@ -366,9 +366,33 @@
[:type [:= :del-typography]]
[:id ::sm/uuid]]]
[:update-active-token-themes
[:map {:title "UpdateActiveTokenThemes"}
[:type [:= :update-active-token-themes]]
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib ::sm/any]]] ;; TODO: we should define a plain object schema for tokens-lib
[:set-token
[:map {:title "SetTokenChange"}
[:type [:= :set-token]]
[:set-id ::sm/uuid]
[:token-id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-attrs]]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}
[:type [:= :set-token-set]]
[:id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-set-attrs]]]]
[:set-token-theme
[:map {:title "SetTokenThemeChange"}
[:type [:= :set-token-theme]]
[:id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-theme-attrs]]]]
[:set-active-token-themes
[:map {:title "SetActiveTokenThemes"}
[:type [:= :set-active-token-themes]]
[:theme-paths [:set :string]]]]
[:rename-token-set-group
@@ -393,39 +417,6 @@
[:before-path [:maybe [:vector :string]]]
[:before-group [:maybe :boolean]]]]
[:set-token-theme
[:map {:title "SetTokenThemeChange"}
[:type [:= :set-token-theme]]
[:theme-name :string]
[:group :string]
[:theme [:maybe ctob/schema:token-theme-attrs]]]]
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib ::sm/any]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}
[:type [:= :set-token-set]]
[:set-name :string]
[:group? :boolean]
;; FIXME: we should not pass private types as part of changes
;; protocol, the changes protocol should reflect a
;; method/protocol for perform surgical operations on file data,
;; this has nothing todo with internal types of a file data
;; structure.
[:token-set {:gen/gen (sg/generator ctob/schema:token-set)}
[:maybe [:fn ctob/token-set?]]]]]
[:set-token
[:map {:title "SetTokenChange"}
[:type [:= :set-token]]
[:set-name :string]
[:token-id ::sm/uuid]
[:token [:maybe ctob/schema:token-attrs]]]]
[:set-base-font-size
[:map {:title "ModBaseFontSize"}
[:type [:= :set-base-font-size]]
@@ -978,64 +969,63 @@
[data {:keys [id]}]
(ctyl/delete-typography data id))
;; -- Tokens
;; -- Design Tokens
(defmethod process-change :set-tokens-lib
[data {:keys [tokens-lib]}]
(assoc data :tokens-lib tokens-lib))
(defmethod process-change :set-token
[data {:keys [set-name token-id token]}]
[data {:keys [set-id token-id attrs]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not token)
(ctob/delete-token-from-set lib' set-name token-id)
(not attrs)
(ctob/delete-token lib' set-id token-id)
(not (ctob/get-token-in-set lib' set-name token-id))
(ctob/add-token-in-set lib' set-name (ctob/make-token token))
(not (ctob/get-token lib' set-id token-id))
(ctob/add-token lib' set-id (ctob/make-token attrs))
:else
(ctob/update-token-in-set lib' set-name token-id (fn [prev-token]
(ctob/make-token (merge prev-token token)))))))))
(ctob/update-token lib' set-id token-id
(fn [prev-token]
(ctob/make-token (merge prev-token attrs)))))))))
(defmethod process-change :set-token-set
[data {:keys [set-name group? token-set]}]
[data {:keys [id attrs]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not token-set)
(if group?
(ctob/delete-set-group lib' set-name)
(ctob/delete-set lib' set-name))
(not attrs)
(ctob/delete-set lib' id)
(not (ctob/get-set lib' set-name))
(ctob/add-set lib' token-set)
(not (ctob/get-set lib' id))
(ctob/add-set lib' (ctob/make-token-set attrs))
:else
(ctob/update-set lib' set-name (fn [_] token-set)))))))
(ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs))))))))
(defmethod process-change :set-token-theme
[data {:keys [group theme-name theme]}]
[data {:keys [id attrs]}]
(update data :tokens-lib
(fn [lib]
(let [lib' (ctob/ensure-tokens-lib lib)]
(cond
(not theme)
(ctob/delete-theme lib' group theme-name)
(not attrs)
(ctob/delete-theme lib' id)
(not (ctob/get-theme lib' group theme-name))
(ctob/add-theme lib' (ctob/make-token-theme theme))
(not (ctob/get-theme lib' id))
(ctob/add-theme lib' (ctob/make-token-theme attrs))
:else
(ctob/update-theme lib'
group theme-name
id
(fn [prev-token-theme]
(ctob/make-token-theme (merge prev-token-theme theme)))))))))
(ctob/make-token-theme (merge prev-token-theme attrs)))))))))
(defmethod process-change :update-active-token-themes
(defmethod process-change :set-active-token-themes
[data {:keys [theme-paths]}]
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
(ctob/set-active-themes theme-paths))))
@@ -1059,7 +1049,7 @@
(ctob/ensure-tokens-lib)
(ctob/move-set-group from-path to-path before-path before-group))))
;; === Base font size
;; === Design Tokens configuration
(defmethod process-change :set-base-font-size
[data {:keys [base-font-size]}]

View File

@@ -21,7 +21,8 @@
[app.common.types.path :as path]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]))
[app.common.uuid :as uuid]
[clojure.datafy :refer [datafy]]))
;; Auxiliary functions to help create a set of changes (undo + redo)
;; TODO: this is a duplicate schema
@@ -717,6 +718,7 @@
(reduce resize-parent changes all-parents)))
;; Library changes
(defn add-color
[changes color]
(-> changes
@@ -798,160 +800,6 @@
(update :undo-changes conj {:type :add-typography :typography prev-typography})
(apply-changes-local))))
(defn update-active-token-themes
[changes active-theme-paths prev-active-theme-paths]
(-> changes
(update :redo-changes conj {:type :update-active-token-themes :theme-paths active-theme-paths})
(update :undo-changes conj {:type :update-active-token-themes :theme-paths prev-active-theme-paths})
(apply-changes-local)))
(defn set-token-theme [changes group theme-name theme]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-theme (some-> (get library-data :tokens-lib)
(ctob/get-theme group theme-name))]
(-> changes
(update :redo-changes conj {:type :set-token-theme
:theme-name theme-name
:group group
:theme theme})
(update :undo-changes conj (if prev-theme
{:type :set-token-theme
:group group
:theme-name (or
;; Undo of edit
(:name theme)
;; Undo of delete
theme-name)
:theme prev-theme}
;; Undo of create
{:type :set-token-theme
:group group
:theme-name theme-name
:theme nil}))
(apply-changes-local))))
(defn rename-token-set-group
[changes set-group-path set-group-fname]
(let [undo-path (ctob/replace-last-path-name set-group-path set-group-fname)
undo-fname (last set-group-path)]
(-> changes
(update :redo-changes conj {:type :rename-token-set-group :set-group-path set-group-path :set-group-fname set-group-fname})
(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
[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
:from-path from-path
:to-path to-path
:before-path before-path
: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?})
(apply-changes-local)))
(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
: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
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn set-tokens-lib
[changes tokens-lib]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-tokens-lib (get library-data :tokens-lib)]
(-> changes
(update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib})
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
(apply-changes-local))))
(defn set-token [changes set-name token-id token]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token (some-> (get library-data :tokens-lib)
(ctob/get-set set-name)
(ctob/get-token token-id))]
(-> changes
(update :redo-changes conj {:type :set-token
:set-name set-name
:token-id token-id
:token token})
(update :undo-changes conj (if prev-token
{:type :set-token
:set-name set-name
:token-id (or
;; Undo of edit
(:id token)
;; Undo of delete
token-id)
:token prev-token}
;; Undo of create token
{:type :set-token
:set-name set-name
:token-id token-id
:token nil}))
(apply-changes-local))))
(defn rename-token-set
[changes name new-name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set name))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:set-name name
:token-set (ctob/rename prev-token-set new-name)
:group? false})
(update :undo-changes conj {:type :set-token-set
:set-name new-name
:token-set prev-token-set
:group? false})
(apply-changes-local))))
(defn set-token-set
[changes set-name group? token-set]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set set-name))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:set-name set-name
:token-set token-set
:group? group?})
(update :undo-changes conj (if prev-token-set
{:type :set-token-set
:set-name (if token-set
;; Undo of edit
(ctob/get-name token-set)
;; Undo of delete
set-name)
:token-set prev-token-set
:group? group?}
;; Undo of create
{:type :set-token-set
:set-name set-name
:token-set nil
:group? group?}))
(apply-changes-local))))
(defn add-component
([changes id path name updated-shapes main-instance-id main-instance-page]
(add-component changes id path name updated-shapes main-instance-id main-instance-page nil nil nil))
@@ -1081,6 +929,144 @@
:id id
:delta delta})))
;; Design Tokens changes
(defn set-tokens-lib
[changes tokens-lib]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-tokens-lib (get library-data :tokens-lib)]
(-> changes
(update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib})
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
(apply-changes-local))))
(defn set-token [changes set-id token-id token]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token (some-> (get library-data :tokens-lib)
(ctob/get-token set-id token-id))]
(-> changes
(update :redo-changes conj {:type :set-token
:set-id set-id
:token-id token-id
:attrs (datafy token)})
(update :undo-changes conj {:type :set-token
:set-id set-id
:token-id token-id
:attrs (datafy prev-token)})
(apply-changes-local))))
(defn set-token-set
[changes id token-set]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set id))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:id id
:attrs (datafy token-set)})
(update :undo-changes conj {:type :set-token-set
:id id
:attrs (datafy prev-token-set)})
(apply-changes-local))))
(defn rename-token-set
[changes id new-name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-set (some-> (get library-data :tokens-lib)
(ctob/get-set id))]
(-> changes
(update :redo-changes conj {:type :set-token-set
:id id
:attrs (datafy (ctob/rename prev-token-set new-name))})
(update :undo-changes conj {:type :set-token-set
:id id
:attrs (datafy prev-token-set)})
(apply-changes-local))))
(defn set-token-theme [changes id theme]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-theme (some-> (get library-data :tokens-lib)
(ctob/get-theme id))]
(-> changes
(update :redo-changes conj {:type :set-token-theme
:id id
:attrs (datafy theme)})
(update :undo-changes conj {:type :set-token-theme
:id id
:attrs (datafy prev-theme)})
(apply-changes-local))))
(defn set-active-token-themes
[changes active-theme-paths]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-active-theme-paths (d/nilv (some-> (get library-data :tokens-lib)
(ctob/get-active-theme-paths))
#{})]
(-> changes
(update :redo-changes conj {:type :set-active-token-themes :theme-paths active-theme-paths})
(update :undo-changes conj {:type :set-active-token-themes :theme-paths prev-active-theme-paths})
(apply-changes-local))))
(defn rename-token-set-group
[changes set-group-path set-group-fname]
(let [undo-path (ctob/replace-last-path-name set-group-path set-group-fname)
undo-fname (last set-group-path)]
(-> changes
(update :redo-changes conj {:type :rename-token-set-group :set-group-path set-group-path :set-group-fname set-group-fname})
(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
[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
:from-path from-path
:to-path to-path
:before-path before-path
: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?})
(apply-changes-local)))
(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
: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
:from-path to-path
:to-path from-path
:before-path prev-before-path
:before-group prev-before-group?})
(apply-changes-local)))
(defn set-base-font-size
[changes new-base-font-size]
(assert-file-data! changes)
(let [file-data (::file-data (meta changes))
previous-font-size (ctf/get-base-font-size file-data)]
(-> changes
(update :redo-changes conj {:type :set-base-font-size
:base-font-size new-base-font-size})
(update :undo-changes conj {:type :set-base-font-size
:base-font-size previous-font-size})
(apply-changes-local))))
;; Misc changes
(defn reorder-children
[changes id children]
(assert-page-id! changes)
@@ -1163,15 +1149,3 @@
[changes]
(::page-id (meta changes)))
(defn set-base-font-size
[changes new-base-font-size]
(assert-file-data! changes)
(let [file-data (::file-data (meta changes))
previous-font-size (ctf/get-base-font-size file-data)]
(-> changes
(update :redo-changes conj {:type :set-base-font-size
:base-font-size new-base-font-size})
(update :undo-changes conj {:type :set-base-font-size
:base-font-size previous-font-size})
(apply-changes-local))))

View File

@@ -692,129 +692,9 @@
(walk/postwalk process-form data)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHAPES ORGANIZATION (PATH MANAGEMENT)
;; SHAPES ORGANIZATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-path
"Decompose a string in the form 'one / two / three' into
a vector of strings, normalizing spaces."
[path]
(let [xf (comp (map str/trim)
(remove str/empty?))]
(->> (str/split path "/")
(into [] xf))))
(defn join-path
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join " / " path-vec))
(defn join-path-with-dot
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join "\u00A0\u2022\u00A0" path-vec))
(defn clean-path
"Remove empty items from the path."
[path]
(->> (split-path path)
(join-path)))
(defn parse-path-name
"Parse a string in the form 'group / subgroup / name'.
Retrieve the path and the name in separated values, normalizing spaces."
[path-name]
(let [path-name-split (split-path path-name)
path (str/join " / " (butlast path-name-split))
name (or (last path-name-split) "")]
[path name]))
(defn merge-path-item
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path " / " name)
path)
name))
(defn merge-path-item-with-dot
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path "\u00A0\u2022\u00A0" name)
path)
name))
(defn compact-path
"Separate last item of the path, and truncate the others if too long:
'one' -> ['' 'one' false]
'one / two / three' -> ['one / two' 'three' false]
'one / two / three / four' -> ['one / two / ...' 'four' true]
'one-item-but-very-long / two' -> ['...' 'two' true] "
[path max-length dot?]
(let [path-split (split-path path)
last-item (last path-split)
merge-path (if dot?
merge-path-item-with-dot
merge-path-item)]
(loop [other-items (seq (butlast path-split))
other-path ""]
(if-let [item (first other-items)]
(let [full-path (-> other-path
(merge-path item)
(merge-path last-item))]
(if (> (count full-path) max-length)
[(merge-path other-path "...") last-item true]
(recur (next other-items)
(merge-path other-path item))))
[other-path last-item false]))))
(defn butlast-path
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path (butlast split)))))
(defn butlast-path-with-dots
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path-with-dot (butlast split)))))
(defn last-path
"Returns the last item of the path."
[path]
(last (split-path path)))
(defn compact-name
"Append the first item of the path and the name."
[path name]
(let [path-split (split-path path)]
(merge-path-item (first path-split) name)))
(defn inside-path? [child parent]
(let [child-path (split-path child)
parent-path (split-path parent)]
(and (<= (count parent-path) (count child-path))
(= parent-path (take (count parent-path) child-path)))))
(defn split-by-last-period
"Splits a string into two parts:
the text before and including the last period,
and the text after the last period."
[s]
(if-let [last-period (str/last-index-of s ".")]
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
[s ""]))
(defn get-frame-objects
"Retrieves a new objects map only with the objects under frame-id (with frame-id)"
[objects frame-id]

View File

@@ -6,14 +6,29 @@
(ns app.common.files.indices
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.uuid :as uuid]))
(defn- generate-index
"An optimized algorithm for calculate parents index that walk from top
to down starting from a provided shape-id. Usefull when you want to
create an index for the whole objects or subpart of the tree."
[index objects shape-id parents]
(let [shape (get objects shape-id)
index (assoc index shape-id parents)
parents (cons shape-id parents)]
(reduce (fn [index shape-id]
(generate-index index objects shape-id parents))
index
(:shapes shape))))
(defn generate-child-all-parents-index
"Creates an index where the key is the shape id and the value is a set
with all the parents"
([objects]
(generate-child-all-parents-index objects (vals objects)))
(generate-index {} objects uuid/zero []))
([objects shapes]
(let [shape->entry
@@ -24,24 +39,25 @@
(defn create-clip-index
"Retrieves the mask information for an object"
[objects parents-index]
(let [retrieve-clips
(let [get-clip-parents
(fn [shape]
(let [shape-id (dm/get-prop shape :id)]
(cond-> []
(or (and (cfh/frame-shape? shape)
(not (:show-content shape))
(not= uuid/zero shape-id))
(cfh/bool-shape? shape))
(conj shape)
(:masked-group shape)
(conj (get objects (->> shape :shapes first))))))
xform
(comp (map (d/getf objects))
(mapcat get-clip-parents))
populate-with-clips
(fn [parents]
(let [lookup-object (fn [id] (get objects id))
get-clip-parents
(fn [shape]
(cond-> []
(or (and (= :frame (:type shape))
(not (:show-content shape))
(not= uuid/zero (:id shape)))
(cfh/bool-shape? shape))
(conj shape)
(into [] xform parents))]
(:masked-group shape)
(conj (get objects (->> shape :shapes first)))))]
(into []
(comp (map lookup-object)
(mapcat get-clip-parents))
parents)))]
(-> parents-index
(update-vals retrieve-clips))))
(d/update-vals parents-index populate-with-clips)))

View File

@@ -320,6 +320,31 @@
(pcb/with-file-data file-data)
(pcb/update-shapes shape-ids detach-shape))))))
(defmethod repair-error :ref-shape-is-not-head
[_ {:keys [shape page-id] :as error} file-data _]
(let [repair-shape
(fn [shape]
; Convert shape in a normal copy, removing nested copy status
(log/debug :hint " -> unhead shape")
(ctk/unhead-shape shape))]
(log/dbg :hint "repairing shape :shape-ref-is-not-head" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :ref-shape-is-head
[_ {:keys [shape page-id args] :as error} file-data _]
(let [repair-shape
(fn [shape]
; Convert shape in a nested head, adding component info
(log/debug :hint " -> reroot shape")
(ctk/rehead-shape shape (:component-file args) (:component-id args)))]
(log/dbg :hint "repairing shape :shape-ref-is-head" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :shape-ref-cycle
[_ {:keys [shape args] :as error} file-data _]

View File

@@ -11,6 +11,7 @@
[app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.path-names :as cpn]
[app.common.schema :as sm]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
@@ -47,6 +48,8 @@
:should-be-component-root
:should-not-be-component-root
:ref-shape-not-found
:ref-shape-is-head
:ref-shape-is-not-head
:shape-ref-in-main
:root-main-not-allowed
:nested-main-not-allowed
@@ -305,6 +308,28 @@
"Shape inside main instance should not have shape-ref"
shape file page)))
(defn- check-ref-is-not-head
"Validate that the referenced shape is not a nested copy root."
[shape file page libraries]
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)]
(when (and (some? ref-shape)
(ctk/instance-head? ref-shape))
(report-error :ref-shape-is-head
(str/ffmt "Referenced shape % is a component, so the copy must also be" (:shape-ref shape))
shape file page))))
(defn- check-ref-is-head
"Validate that the referenced shape is a nested copy root."
[shape file page libraries]
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)]
(when (and (some? ref-shape)
(not (ctk/instance-head? ref-shape)))
(report-error :ref-shape-is-not-head
(str/ffmt "Referenced shape % of a head copy must also be a head" (:shape-ref shape))
shape file page
:component-file (:component-file ref-shape)
:component-id (:component-id ref-shape)))))
(defn- check-empty-swap-slot
"Validate that this shape does not have any swap slot."
[shape file page]
@@ -382,6 +407,7 @@
(check-component-not-main-head shape file page libraries)
(check-component-root shape file page)
(check-component-ref shape file page libraries)
(check-ref-is-head shape file page libraries)
(check-empty-swap-slot shape file page)
(check-duplicate-swap-slot shape file page)
(check-valid-touched shape file page)
@@ -399,7 +425,8 @@
;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached
;; so we only validate the shape-ref if the ancestor is from a valid library
(when library-exists
(check-component-ref shape file page libraries))
(check-component-ref shape file page libraries)
(check-ref-is-head shape file page libraries))
(run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape)))
(defn- check-shape-main-not-root
@@ -417,6 +444,7 @@
(check-component-not-main-not-head shape file page)
(check-component-not-root shape file page)
(check-component-ref shape file page libraries)
(check-ref-is-not-head shape file page libraries)
(check-empty-swap-slot shape file page)
(check-valid-touched shape file page)
(run! #(check-shape % file page libraries :context :copy-any) (:shapes shape)))
@@ -484,7 +512,7 @@
(report-error :variant-bad-name
(str/ffmt "Variant % has an invalid name" (:id shape))
shape file page))
(when-not (= (:name parent) (cfh/merge-path-item (:path component) (:name component)))
(when-not (= (:name parent) (cpn/merge-path-item (:path component) (:name component)))
(report-error :variant-component-bad-name
(str/ffmt "Component % has an invalid name" (:id shape))
shape file page))
@@ -543,7 +571,7 @@
;; mains can't be nested into mains
(if (or (= context :not-component) (= context :main-top))
(report-error :nested-main-not-allowed
"Nested main component only allowed inside other component"
"Component main not allowed inside other component"
shape file page)
(check-shape-main-root-nested shape file page libraries))
@@ -608,6 +636,20 @@
(str/ffmt "Shape % should be a variant" (:id main-component))
main-component file component-page))))
(defn- check-main-inside-main
[component file]
(let [component-page (ctf/get-component-page (:data file) component)
main-instance (ctst/get-shape component-page (:main-instance-id component))
main-parents? (->> main-instance
:id
(cfh/get-parents (:objects component-page))
(some ctk/main-instance?)
boolean)]
(when main-parents?
(report-error :nested-main-not-allowed
"Component main not allowed inside other component"
main-instance file component-page))))
(defn- check-component
"Validate semantic coherence of a component. Report all errors found."
[component file]
@@ -615,6 +657,8 @@
(report-error :component-nil-objects-not-allowed
"Objects list cannot be nil"
component file nil))
(when-not (:deleted component)
(check-main-inside-main component file))
(when (:deleted component)
(check-component-duplicate-swap-slot component file)
(check-ref-cycles component file))

View File

@@ -120,6 +120,7 @@
:tiered-file-data-storage
:token-units
:token-base-font-size
:token-color
:token-typography-types
:token-typography-composite
:transit-readable-response

View File

@@ -88,8 +88,11 @@
([shape]
(get-shape-filter-bounds shape false))
([shape ignore-shadow-margin?]
(if (and (cfh/svg-raw-shape? shape)
(not= :svg (dm/get-in shape [:content :tag])))
(if (or (and (cfh/svg-raw-shape? shape)
(not= :svg (dm/get-in shape [:content :tag])))
;; If no shadows or blur, we return the selrect as is
(and (empty? (-> shape :shadow))
(zero? (-> shape :blur :value (or 0)))))
(dm/get-prop shape :selrect)
(let [filters (shape->filters shape)
blur-value (or (-> shape :blur :value) 0)

View File

@@ -49,6 +49,7 @@
[app.common.exceptions :as ex]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[cuerdas.core :as str]
[promesa.exec :as px]
@@ -221,36 +222,42 @@
#?(:clj (inst-ms (java.time.Instant/now))
:cljs (js/Date.now)))
(defn emit-log
[props cause context logger level sync?]
(let [props (cond-> props sync? deref)
ts (current-timestamp)
gcontext *context*
logfn (fn []
(let [props (if sync? props (deref props))
props (into (d/ordered-map) props)
context (if (and (empty? gcontext)
(empty? context))
{}
(d/without-nils (merge gcontext context)))
lrecord {::id (uuid/next)
::timestamp ts
::message (delay (build-message props))
::props props
::context context
::level level
::logger logger}
lrecord (cond-> lrecord
(some? cause)
(assoc ::cause cause
::trace (delay (build-stack-trace cause))))]
(swap! log-record (constantly lrecord))))]
(if sync?
(logfn)
(px/exec! *default-executor* logfn))))
(defmacro log!
"Emit a new log record to the global log-record state (asynchronously). "
[& props]
(let [{:keys [::level ::logger ::context ::sync? cause] :or {sync? false}} props
props (into [] msg-props-xf props)]
`(when (enabled? ~logger ~level)
(let [props# (cond-> (delay ~props) ~sync? deref)
ts# (current-timestamp)
context# *context*
logfn# (fn []
(let [props# (if ~sync? props# (deref props#))
props# (into (d/ordered-map) props#)
cause# ~cause
context# (d/without-nils
(merge context# ~context))
lrecord# {::id (uuid/next)
::timestamp ts#
::message (delay (build-message props#))
::props props#
::context context#
::level ~level
::logger ~logger}
lrecord# (cond-> lrecord#
(some? cause#)
(assoc ::cause cause#
::trace (delay (build-stack-trace cause#))))]
(swap! log-record (constantly lrecord#))))]
(if ~sync?
(logfn#)
(px/exec! *default-executor* logfn#))))))
(emit-log (delay ~props) ~cause ~context ~logger ~level ~sync?))))
#?(:clj
(defn slf4j-log-handler
@@ -276,7 +283,8 @@
(when (enabled? logger level)
(let [hstyles (str/ffmt "font-weight: 600; color: %" (level->color level))
mstyles (str/ffmt "font-weight: 300; color: %" (level->color level))
header (str/concat "%c" (level->name level) " [" logger "] ")
ts (ct/format-inst (ct/now) "kk:mm:ss.SSSS")
header (str/concat "%c" (level->name level) " " ts " [" logger "] ")
message (str/concat header "%c" @message)]
(js/console.group message hstyles mstyles)

View File

@@ -11,11 +11,13 @@
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.logging :as log]
[app.common.logic.shapes :as cls]
[app.common.logic.variant-properties :as clvp]
[app.common.path-names :as cpn]
[app.common.spec :as us]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
@@ -985,7 +987,7 @@
(defn generate-rename-component
"Generate the changes for rename the component with the given id, in the current file library."
[changes id new-name library-data]
(let [[path name] (cfh/parse-path-name new-name)]
(let [[path name] (cpn/split-group-name new-name)]
(-> changes
(pcb/with-library-data library-data)
(pcb/update-component id #(assoc % :path path :name name)))))
@@ -1733,6 +1735,17 @@
[(conj roperations roperation)
(conj uoperations uoperation)]))
(defn- check-detached-main
[changes dest-shape origin-shape]
;; Only for direct updates (from main to copy). Check if the main shape
;; has been detached. If so, the copy shape must be unheaded (i.e. converted
;; into a normal copy and not a nested instance).
(if (and (= (:shape-ref dest-shape) (:id origin-shape))
(ctk/subcopy-head? dest-shape)
(not (ctk/instance-head? origin-shape)))
(pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true})
changes))
(defn- update-attrs
"The main function that implements the attribute sync algorithm. Copy
attributes that have changed in the origin shape to the dest shape.
@@ -1773,6 +1786,8 @@
(seq roperations)
(add-update-attr-changes dest-shape container roperations uoperations)
:always
(check-detached-main dest-shape origin-shape)
:always
(generate-update-tokens container dest-shape origin-shape touched omit-touched?))
(let [attr-group (get ctk/sync-attrs attr)
@@ -1796,7 +1811,6 @@
(= :content attr)
(touched attr-group))
skip-operations?
(or (= (get origin-shape attr) (get dest-shape attr))
(and (touched attr-group)
@@ -2222,7 +2236,7 @@
variant-id (when (ctk/is-variant? root) (:parent-id root))
props (when (ctk/is-variant? root) (get variant-props (:component-id root)))
[path name] (cfh/parse-path-name name)
[path name] (cpn/split-group-name name)
[root-shape updated-shapes]
(ctn/convert-shape-in-component root objects file-id)
@@ -2514,9 +2528,10 @@
frames)))
(defn- duplicate-variant
[changes library component base-pos parent-id page-id]
[changes library component base-pos parent page-id into-new-variant?]
(let [component-page (ctpl/get-page (:data library) (:main-instance-page component))
component-shape (dm/get-in component-page [:objects (:main-instance-id component)])
objects (:objects component-page)
component-shape (get objects (:main-instance-id component))
orig-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract base-pos orig-pos)
new-component-id (uuid/next)
@@ -2526,11 +2541,27 @@
new-component-id
{:apply-changes-local-library? true
:delta delta
:new-variant-id parent-id
:page-id page-id})]
:new-variant-id (if into-new-variant? nil (:id parent))
:page-id page-id})
value (when into-new-variant?
(str ctv/value-prefix
(-> (cfv/extract-properties-values (:data library) objects (:id parent))
last
:value
count
inc)))]
[shape
(-> changes
(pcb/change-parent parent-id [shape]))]))
(cond-> changes
into-new-variant?
(clvp/generate-make-shapes-variant [shape] parent)
;; If it has the same parent, update the value of the last property
(and into-new-variant? (= (:variant-id component) (:id parent)))
(clvp/generate-update-property-value new-component-id (-> component :variant-properties count dec) value)
:always
(pcb/change-parent (:id parent) [shape] 0))]))
(defn generate-duplicate-component-change
@@ -2542,11 +2573,13 @@
pos (as-> (gsh/move main delta) $
(gpt/point (:x $) (:y $)))
parent (get objects parent-id)
;; When we duplicate a variant alone, we will instanciate it
;; When we duplicate a variant along with its variant-container, we will duplicate it
in-variant-container? (contains? ids-map (:variant-id main))
restore-component
#(let [{:keys [shape changes]}
(prepare-restore-component changes
@@ -2559,29 +2592,42 @@
frame-id)]
[shape changes])
[_shape changes]
(if (nil? component)
(restore-component)
(if (and (ctk/is-variant? main) in-variant-container?)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent-id
(:id page))
(generate-instantiate-component changes
objects
file-id
component-id
pos
page
libraries
main-id
parent-id
frame-id
ids-map
{})))]
[_shape changes]
(cond
(nil? component)
(restore-component)
(and (ctk/is-variant? main) in-variant-container?)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent
(:id page)
false)
(ctk/is-variant-container? parent)
(duplicate-variant changes
(get libraries file-id)
component
pos
parent
(:id page)
true)
:else
(generate-instantiate-component changes
objects
file-id
component-id
pos
page
libraries
main-id
parent-id
frame-id
ids-map
{}))]
changes))
(defn generate-duplicate-shape-change
@@ -2740,7 +2786,8 @@
changes (-> changes
(pcb/with-page page)
(pcb/with-objects all-objects))
(pcb/with-objects all-objects)
(pcb/with-library-data library-data))
changes
(->> shapes
(reduce #(generate-duplicate-shape-change %1

View File

@@ -185,15 +185,17 @@
interactions)))
(vals objects))
id-to-delete? (set ids-to-delete)
changes
(reduce (fn [changes {:keys [id] :as flow}]
(if (contains? ids-to-delete (:starting-frame flow))
(-> changes
(pcb/with-page page)
(pcb/set-flow id nil))
changes))
changes
(:flows page))
(->> (:flows page)
(reduce
(fn [changes [id flow]]
(if (id-to-delete? (:starting-frame flow))
(-> changes
(pcb/with-page page)
(pcb/set-flow id nil))
changes))
changes))
all-parents

View File

@@ -17,18 +17,16 @@
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)
(let [active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
prev-hidden-theme (ctob/get-hidden-theme tokens-lib)
hidden-theme (-> (some-> prev-hidden-theme (ctob/set-sets active-token-set-names))
(update-theme-fn))]
hidden-theme (ctob/get-hidden-theme tokens-lib)
hidden-theme' (-> (some-> hidden-theme
(ctob/set-sets active-token-set-names))
(update-theme-fn))]
(-> changes
(pcb/update-active-token-themes #{(ctob/theme-path hidden-theme)} prev-active-token-themes)
(pcb/set-token-theme (:group prev-hidden-theme)
(:name prev-hidden-theme)
hidden-theme))))
(pcb/set-active-token-themes #{(ctob/get-theme-path hidden-theme')})
(pcb/set-token-theme (ctob/get-id hidden-theme)
hidden-theme'))))
(defn generate-toggle-token-set
"Toggle a token set at `set-name` in `tokens-lib` without modifying a
@@ -139,3 +137,12 @@
(if-let [params (calculate-move-token-set-or-set-group tokens-lib params)]
(pcb/move-token-set-group changes params)
changes))
(defn generate-delete-token-set-group
"Create changes for deleting a token set group."
[changes tokens-lib path]
(let [sets (ctob/get-sets-at-path tokens-lib path)]
(reduce (fn [changes set]
(pcb/set-token-set changes (ctob/get-id set) nil))
changes
sets)))

View File

@@ -7,8 +7,8 @@
(:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.path-names :as cpn]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctcl]
[app.common.types.variant :as ctv]
@@ -127,7 +127,7 @@
(defn- generate-make-shape-no-variant
[changes shape]
(let [new-name (ctv/variant-name-to-name shape)
[cpath cname] (cfh/parse-path-name new-name)]
[cpath cname] (cpn/split-group-name new-name)]
(-> changes
(pcb/update-component (:component-id shape)
#(-> (dissoc % :variant-id :variant-properties)
@@ -146,8 +146,8 @@
(defn- create-new-properties-from-variant
[shape min-props data container-name base-properties]
(let [component (ctcl/get-component data (:component-id shape) true)
add-name? (not= (:name component) container-name)
component-full-name (cpn/merge-path-item (:path component) (:name component))
add-name? (not= component-full-name container-name)
props (ctv/merge-properties base-properties
(:variant-properties component))
new-props (- min-props
@@ -188,7 +188,7 @@
(map #(assoc % :value "")))
num-base-props (count base-props)
[cpath cname] (cfh/parse-path-name (:name variant-container))
[cpath cname] (cpn/split-group-name (:name variant-container))
container-name (:name variant-container)
create-new-properties

View File

@@ -0,0 +1,134 @@
;; 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.path-names
(:require
[cuerdas.core :as str]))
"Functions to manipulate entity names that represent groups with paths,
e.g. 'Group / Subgroup / Name'.
Some naming conventions:
- Path string: the full string with groups and name, e.g. 'Group / Subgroup / Name'.
- Path: a vector of strings with the full path, e.g. ['Group' 'Subgroup' 'Name'].
- Group string: the group part of the path string, e.g. 'Group / Subgroup'.
- Group: a vector of strings with the group part of the path, e.g. ['Group' 'Subgroup'].
- Name: the final name part of the path, e.g. 'Name'."
(defn split-path
"Decompose a path string in the form 'one / two / three' into a vector
of strings, trimming spaces (e.g. ['one' 'two' 'three'])."
[path-str & {:keys [separator] :or {separator "/"}}]
(let [xf (comp (map str/trim)
(remove str/empty?))]
(->> (str/split path-str separator)
(into [] xf))))
(defn join-path
"Regenerate a path as a string, from a vector.
(e.g. ['one' 'two' 'three'] -> 'one / two / three')"
[path & {:keys [separator with-spaces?] :or {separator "/" with-spaces? true}}]
(if with-spaces?
(str/join (str " " separator " ") path)
(str/join separator path)))
(defn split-group-name
"Parse a path string. Retrieve the group and the name in separated values,
normalizing spaces (e.g. 'group / subgroup / name' -> ['group / subgroup' 'name'])."
[path-str & {:keys [separator with-spaces?] :or {separator "/" with-spaces? true}}]
(let [path (split-path path-str :separator separator)
group-str (join-path (butlast path) :separator separator :with-spaces? with-spaces?)
name (or (last path) "")]
[group-str name]))
(defn join-path-with-dot
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join "\u00A0\u2022\u00A0" path-vec))
(defn clean-path
"Remove empty items from the path."
[path]
(->> (split-path path)
(join-path)))
(defn merge-path-item
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path " / " name)
path)
name))
(defn merge-path-item-with-dot
"Put the item at the end of the path."
[path name]
(if-not (empty? path)
(if-not (empty? name)
(str path "\u00A0\u2022\u00A0" name)
path)
name))
(defn compact-path
"Separate last item of the path, and truncate the others if too long:
'one' -> ['' 'one' false]
'one / two / three' -> ['one / two' 'three' false]
'one / two / three / four' -> ['one / two / ...' 'four' true]
'one-item-but-very-long / two' -> ['...' 'two' true] "
[path max-length dot?]
(let [path-split (split-path path)
last-item (last path-split)
merge-path (if dot?
merge-path-item-with-dot
merge-path-item)]
(loop [other-items (seq (butlast path-split))
other-path ""]
(if-let [item (first other-items)]
(let [full-path (-> other-path
(merge-path item)
(merge-path last-item))]
(if (> (count full-path) max-length)
[(merge-path other-path "...") last-item true]
(recur (next other-items)
(merge-path other-path item))))
[other-path last-item false]))))
(defn butlast-path
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path (butlast split)))))
(defn butlast-path-with-dots
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path-with-dot (butlast split)))))
(defn last-path
"Returns the last item of the path."
[path]
(last (split-path path)))
(defn inside-path? [child parent]
(let [child-path (split-path child)
parent-path (split-path parent)]
(and (<= (count parent-path) (count child-path))
(= parent-path (take (count parent-path) child-path)))))
(defn split-by-last-period
"Splits a string into two parts:
the text before and including the last period,
and the text after the last period."
[s]
(if-let [last-period (str/last-index-of s ".")]
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
[s ""]))

View File

@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.schema.generators
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double])
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double not-empty])
#?(:cljs (:require-macros [app.common.schema.generators]))
(:require
[app.common.math :as mth]
@@ -146,3 +146,5 @@
(def any
(tg/one-of [text boolean double int keyword]))
(def not-empty tg/not-empty)

View File

@@ -12,6 +12,7 @@
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.logic.libraries :as cll]
[app.common.path-names :as cpn]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
@@ -36,7 +37,7 @@
updated-root (first updated-shapes) ; Can't use new-root because it has a new id
[path name] (cfh/parse-path-name (:name updated-root))]
[path name] (cpn/split-group-name (:name updated-root))]
(thi/set-id! label (:component-id updated-root))
(ctf/update-file-data
@@ -72,6 +73,10 @@
[file id]
(ctkl/get-component (:data file) id))
(defn get-components
[file]
(ctkl/components (:data file)))
(defn- set-children-labels!
[file shape-label children-labels]
(doseq [[label id]

View File

@@ -108,7 +108,8 @@
page (if (some? page-label)
(:id (get-page file page-label))
(current-page-id file))
libraries (or libraries {})]
libraries (or libraries
{(:id file) file})]
(ctf/dump-tree file page libraries params)))

View File

@@ -28,12 +28,10 @@
(ctf/update-file-data file #(update % :tokens-lib f)))
(defn get-token
[file set-name token-id]
[file set-id token-id]
(let [tokens-lib (:tokens-lib (:data file))]
(when tokens-lib
(-> tokens-lib
(ctob/get-set set-name)
(ctob/get-token token-id)))))
(ctob/get-token tokens-lib set-id token-id))))
(defn token-data-eq?
"Compare token data without comparing unstable fields."

View File

@@ -130,7 +130,6 @@
ms-or-obj
(integer? ms-or-obj)
(Duration/ofMillis ms-or-obj)
:else
@@ -433,4 +432,4 @@
#?(:cljs
(extend-protocol cljs.core/IEncodeJS
js/Date
(-clj->js [x] x)))
(-clj->js [x] x)))

View File

@@ -145,9 +145,12 @@
(defn component-attr?
"Check if some attribute is one that is involved in component syncrhonization.
Note that design tokens also are involved, although they go by an alternate
route and thus they are not part of :sync-attrs."
route and thus they are not part of :sync-attrs.
Also when detaching a nested copy it also needs to trigger a synchronization,
even though :shape-ref is not a synced attribute per se"
[attr]
(or (get sync-attrs attr)
(= :shape-ref attr)
(= :applied-tokens attr)))
(defn instance-root?
@@ -217,19 +220,16 @@
(and (= shape-id (:main-instance-id component))
(= page-id (:main-instance-page component))))
(defn is-variant?
"Check if this shape or component is a variant component"
[item]
(some? (:variant-id item)))
(defn is-variant-container?
"Check if this shape is a variant container"
[shape]
(:is-variant-container shape))
(defn set-touched-group
[touched group]
(when group
@@ -310,6 +310,22 @@
:shape-ref
:touched))
(defn unhead-shape
"Make the shape not be a component head, but keep its :shape-ref and :touched if it was a nested copy"
[shape]
(dissoc shape
:component-root
:component-file
:component-id
:main-instance))
(defn rehead-shape
"Make the shape a component head, by adding component info"
[shape component-file component-id]
(assoc shape
:component-file component-file
:component-id component-id))
(defn- extract-ids [shape]
(if (map? shape)
(let [current-id (:id shape)

View File

@@ -7,7 +7,6 @@
(ns app.common.types.container
(: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 :as gsh]
@@ -77,11 +76,8 @@
(defn get-shape
[container shape-id]
(assert (check-container container))
(assert (uuid? shape-id)
"expected valid uuid for `shape-id`")
(-> container
(get :objects)
(get shape-id)))
@@ -494,29 +490,40 @@
all-main?
(every? ctk/main-instance? top-children)
ascendants (cfh/get-parents-with-self objects parent-id)
any-main-ascendant (some ctk/main-instance? ascendants)
any-variant-container-ascendant (some ctk/is-variant-container? ascendants)
get-variant-id (fn [shape]
(when (:component-id shape)
(-> (get-component-from-shape shape libraries)
:variant-id)))
descendants (mapcat #(cfh/get-children-with-self objects %) children-ids)
any-variant-container-descendant (some ctk/is-variant-container? descendants)
descendants-variant-ids-set (->> descendants
(map get-variant-id)
set)
any-main-descendant
(some
(fn [shape]
(some ctk/main-instance? (cfh/get-children-with-self objects (:id shape))))
children)
children)]
;; Are all the top-children a main-instance of a cutted component?
all-comp-cut?
(when all-main?
(->> top-children
(map #(ctkl/get-component (dm/get-in libraries [(:component-file %) :data])
(:component-id %)
true))
(every? :deleted)))]
(if (or no-changes?
(and (not (invalid-structure-for-component? objects parent children pasting? libraries))
;; If we are moving into a main component, no descendant can be main
(or (nil? any-main-descendant) (not (ctk/main-instance? parent)))
;; If we are moving into a variant-container, all the items should be main
;; so if we are pasting, only allow main instances that are cut-and-pasted
(or (not (ctk/is-variant-container? parent))
(and (not pasting?) all-main?)
all-comp-cut?)))
;; If we are moving (not pasting) into a main component, no descendant can be main
(or pasting? (nil? any-main-descendant) (not (ctk/main-instance? parent)))
;; Don't allow variant-container inside variant container nor main
(or (not any-variant-container-descendant)
(and (not any-variant-container-ascendant) (not any-main-ascendant)))
;; If the parent is a variant-container, all the items should be main
(or (not (ctk/is-variant-container? parent)) all-main?)
;; If we are pasting, the parent can't be a "brother" of any of the pasted items,
;; so not have the same variant-id of any descendant
(or (not pasting?)
(not (ctk/is-variant? parent))
(not (contains? descendants-variant-ids-set (:variant-id parent))))))
[parent-id (get-frame parent-id)]
(recur (:parent-id parent) objects children pasting? libraries))))))

View File

@@ -0,0 +1,521 @@
;; 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.types.objects-map
"Implements a specialized map-like data structure for store an UUID =>
OBJECT mappings. The main purpose of this data structure is be able
to serialize it on fressian as byte-array and have the ability to
decode each field separatelly without the need to decode the whole
map from the byte-array.
It works transparently, so no aditional dynamic vars are needed. It
only works by reference equality and the hash-code is calculated
properly from each value."
(:require
#?(:clj [app.common.fressian :as fres])
#?(:clj [clojure.data.json :as json])
[app.common.transit :as t]
[clojure.core :as c]
[clojure.core.protocols :as cp])
#?(:clj
(:import
clojure.lang.Murmur3
clojure.lang.RT
java.util.Iterator)))
#?(:clj (set! *warn-on-reflection* true))
(declare create)
(declare ^:private do-compact)
(defprotocol IObjectsMap
(^:no-doc compact [this])
(^:no-doc get-data [this] "retrieve internal data")
(^:no-doc -hash-for-key [this key] "retrieve a hash for a key"))
#?(:cljs
(deftype ObjectsMapEntry [key omap]
c/IMapEntry
(-key [_] key)
(-val [_] (get omap key))
c/IHash
(-hash [_]
(-hash-for-key omap key))
c/IEquiv
(-equiv [this other]
(and (c/map-entry? other)
(= (key this)
(key other))
(= (val this)
(val other))))
c/ISequential
c/ISeqable
(-seq [this]
(cons key (lazy-seq (cons (c/-val this) nil))))
c/ICounted
(-count [_] 2)
c/IIndexed
(-nth [node n]
(cond (== n 0) key
(== n 1) (c/-val node)
:else (throw (js/Error. "Index out of bounds"))))
(-nth [node n not-found]
(cond (== n 0) key
(== n 1) (c/-val node)
:else not-found))
c/ILookup
(-lookup [node k]
(c/-nth node k nil))
(-lookup [node k not-found]
(c/-nth node k not-found))
c/IFn
(-invoke [node k]
(c/-nth node k))
(-invoke [node k not-found]
(c/-nth node k not-found))
c/IPrintWithWriter
(-pr-writer [this writer opts]
(c/pr-sequential-writer
writer
(fn [item w _]
(c/-write w (pr-str item)))
"[" ", " "]"
opts
this)))
:clj
(deftype ObjectsMapEntry [key omap]
clojure.lang.IMapEntry
(key [_] key)
(getKey [_] key)
(val [_]
(get omap key))
(getValue [_]
(get omap key))
clojure.lang.Indexed
(nth [node n]
(cond
(== n 0) key
(== n 1) (val node)
:else (throw (IllegalArgumentException. "Index out of bounds"))))
(nth [node n not-found]
(cond
(== n 0) key
(== n 1) (val node)
:else not-found))
clojure.lang.IPersistentCollection
(empty [_] [])
(count [_] 2)
(seq [this]
(cons key (lazy-seq (cons (val this) nil))))
(cons [this item]
(.cons ^clojure.lang.IPersistentCollection (vec this) item))
clojure.lang.IHashEq
(hasheq [_]
(-hash-for-key omap key))))
#?(:cljs
(deftype ObjectMapIterator [iterator omap]
Object
(hasNext [_]
(.hasNext ^js iterator))
(next [_]
(let [entry (.next iterator)]
(ObjectsMapEntry. (key entry) omap)))
(remove [_]
(js/Error. "Unsupported operation")))
:clj
(deftype ObjectsMapIterator [^Iterator iterator omap]
Iterator
(hasNext [_]
(.hasNext iterator))
(next [_]
(let [entry (.next iterator)]
(ObjectsMapEntry. (key entry) omap)))))
#?(:cljs
(deftype ObjectsMap [metadata cache
^:mutable data
^:mutable modified
^:mutable hash]
Object
(toString [this]
(pr-str* this))
(equiv [this other]
(c/-equiv this other))
(keys [this]
(c/es6-iterator (keys this)))
(entries [this]
(c/es6-entries-iterator (seq this)))
(values [this]
(es6-iterator (vals this)))
(has [this k]
(c/contains? this k))
(get [this k not-found]
(c/-lookup this k not-found))
(forEach [this f]
(run! (fn [[k v]] (f v k)) this))
cp/Datafiable
(datafy [_]
{:data data
:cache cache
:modified modified
:hash hash})
IObjectsMap
(compact [this]
(when modified
(do-compact data cache
(fn [data']
(set! (.-modified this) false)
(set! (.-data this) data'))))
this)
(get-data [this]
(compact this)
data)
(-hash-for-key [this key]
(if (c/-contains-key? cache key)
(c/-hash (c/-lookup cache key))
(c/-hash (c/-lookup this key))))
c/IWithMeta
(-with-meta [this new-meta]
(if (identical? new-meta meta)
this
(ObjectsMap. new-meta
cache
data
modified
hash)))
c/IMeta
(-meta [_] metadata)
c/ICloneable
(-clone [this]
(compact this)
(ObjectsMap. metadata {} data false nil))
c/IIterable
(-iterator [this]
(c/seq-iter this))
c/ICollection
(-conj [this entry]
(cond
(map-entry? entry)
(c/-assoc this (c/-key entry) (c/-val entry))
(vector? entry)
(c/-assoc this (c/-nth entry 0) (c/-nth entry 1))
:else
(loop [ret this es (seq entry)]
(if (nil? es)
ret
(let [e (first es)]
(if (vector? e)
(recur (c/-assoc ret (c/-nth e 0) (c/-nth e 1))
(next es))
(throw (js/Error. "conj on a map takes map entries or seqables of map entries"))))))))
c/IEmptyableCollection
(-empty [_]
(create))
c/IEquiv
(-equiv [this other]
(equiv-map this other))
c/IHash
(-hash [this]
(when-not hash
(set! hash (hash-unordered-coll this)))
hash)
c/ISeqable
(-seq [this]
(->> (keys data)
(map (fn [id] (new ObjectsMapEntry id this)))
(seq)))
c/ICounted
(-count [_]
(c/-count data))
c/ILookup
(-lookup [this k]
(or (c/-lookup cache k)
(if (c/-contains-key? data k)
(let [v (c/-lookup data k)
v (t/decode-str v)]
(set! (.-cache this) (c/-assoc cache k v))
v)
(do
(set! (.-cache this) (assoc cache key nil))
nil))))
(-lookup [this k not-found]
(if (c/-contains-key? data k)
(c/-lookup this k)
not-found))
c/IAssociative
(-assoc [_ k v]
(ObjectsMap. metadata
(c/-assoc cache k v)
(c/-assoc data k nil)
true
nil))
(-contains-key? [_ k]
(c/-contains-key? data k))
c/IFind
(-find [this k]
(when (c/-contains-key? data k)
(new ObjectsMapEntry k this)))
c/IMap
(-dissoc [_ k]
(ObjectsMap. metadata
(c/-dissoc cache k)
(c/-dissoc data k)
true
nil))
c/IKVReduce
(-kv-reduce [this f init]
(c/-kv-reduce data
(fn [init k _]
(f init k (c/-lookup this k)))
init))
c/IFn
(-invoke [this k]
(c/-lookup this k))
(-invoke [this k not-found]
(c/-lookup this k not-found))
c/IPrintWithWriter
(-pr-writer [this writer opts]
(c/pr-sequential-writer
writer
(fn [item w _]
(c/-write w (pr-str (c/-key item)))
(c/-write w \space)
(c/-write w (pr-str (c/-val item))))
"#penpot/objects-map {" ", " "}"
opts
(seq this))))
:clj
(deftype ObjectsMap [metadata cache
^:unsynchronized-mutable data
^:unsynchronized-mutable modified
^:unsynchronized-mutable hash]
Object
(hashCode [this]
(.hasheq ^clojure.lang.IHashEq this))
cp/Datafiable
(datafy [_]
{:data data
:cache cache
:modified modified
:hash hash})
IObjectsMap
(compact [this]
(locking this
(when modified
(do-compact data cache
(fn [data']
(set! (.-modified this) false)
(set! (.-data this) data')))))
this)
(get-data [this]
(compact this)
data)
(-hash-for-key [this key]
(if (contains? cache key)
(c/hash (get cache key))
(c/hash (get this key))))
json/JSONWriter
(-write [this writter options]
(json/-write (into {} this) writter options))
clojure.lang.IHashEq
(hasheq [this]
(when-not hash
(set! hash (Murmur3/hashUnordered this)))
hash)
clojure.lang.Seqable
(seq [this]
(RT/chunkIteratorSeq (.iterator ^Iterable this)))
java.lang.Iterable
(iterator [this]
(ObjectsMapIterator. (.iterator ^Iterable data) this))
clojure.lang.IPersistentCollection
(equiv [this other]
(and (instance? ObjectsMap other)
(= (count this) (count other))
(reduce-kv (fn [_ id _]
(let [this-val (get this id)
other-val (get other id)
result (= this-val other-val)]
(or result
(reduced false))))
true
data)))
clojure.lang.IPersistentMap
(cons [this o]
(if (map-entry? o)
(assoc this (key o) (val o))
(if (vector? o)
(assoc this (nth o 0) (nth o 1))
(throw (UnsupportedOperationException. "invalid arguments to cons")))))
(empty [_]
(create))
(containsKey [_ key]
(.containsKey ^clojure.lang.IPersistentMap data key))
(entryAt [this key]
(ObjectsMapEntry. this key))
(valAt [this key]
(or (get cache key)
(locking this
(if (contains? data key)
(let [value (get data key)
value (t/decode-str value)]
(set! (.-cache this) (assoc cache key value))
value)
(do
(set! (.-cache this) (assoc cache key nil))
nil)))))
(valAt [this key not-found]
(if (.containsKey ^clojure.lang.IPersistentMap data key)
(.valAt this key)
not-found))
(assoc [_ key val]
(ObjectsMap. metadata
(assoc cache key val)
(assoc data key nil)
true
nil))
(assocEx [_ _ _]
(throw (UnsupportedOperationException. "method not implemented")))
(without [_ key]
(ObjectsMap. metadata
(dissoc cache key)
(dissoc data key)
true
nil))
clojure.lang.Counted
(count [_]
(count data))))
#?(:cljs (es6-iterable ObjectsMap))
(defn- do-compact
[data cache update-fn]
(let [new-data
(persistent!
(reduce-kv (fn [data id obj]
(if (nil? obj)
(assoc! data id (t/encode-str (get cache id)))
data))
(transient data)
data))]
(update-fn new-data)
nil))
(defn from-data
[data]
(ObjectsMap. {} {}
data
false
nil))
(defn objects-map?
[o]
(instance? ObjectsMap o))
(defn create
([] (from-data {}))
([other]
(cond
(objects-map? other)
(-> other get-data from-data)
:else
(throw #?(:clj (UnsupportedOperationException. "invalid arguments")
:cljs (js/Error. "invalid arguments"))))))
(defn wrap
[objects]
(if (instance? ObjectsMap objects)
objects
(->> objects
(into (create))
(compact))))
#?(:clj
(fres/add-handlers!
{:name "penpot/objects-map/v2"
:class ObjectsMap
:wfn (fn [n w o]
(fres/write-tag! w n)
(fres/write-object! w (get-data o)))
:rfn (fn [r]
(-> r fres/read-object! from-data))}))
(t/add-handlers!
{:id "penpot/objects-map/v2"
:class ObjectsMap
:wfn get-data
:rfn from-data})

View File

@@ -26,6 +26,27 @@
(mu/keys)
(into #{})))
(defn find-token-value-references
"Returns set of token references found in `token-value`.
Used for checking if a token has a reference in the value.
Token references are strings delimited by curly braces.
E.g.: {foo.bar.baz} -> foo.bar.baz"
[token-value]
(if (string? token-value)
(some->> (re-seq #"\{([^}]*)\}" token-value)
(map second)
(into #{}))
#{}))
(defn token-value-self-reference?
"Check if the token is self referencing with its `token-name` in `token-value`.
Simple 1 level check, doesn't account for circular self refernces across multiple tokens."
[token-name token-value]
(let [token-references (find-token-value-references token-value)
self-reference? (get token-references token-name)]
self-reference?))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -52,7 +73,24 @@
:typography "typography"})
(def dtcg-token-type->token-type
(set/map-invert token-type->dtcg-token-type))
(-> (set/map-invert token-type->dtcg-token-type)
;; Allow these properties to be imported with singular key names for backwards compability
(assoc "fontWeight" :font-weight
"fontSize" :font-size
"fontFamily" :font-family)))
(def composite-token-type->dtcg-token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc token-type->dtcg-token-type
:line-height "lineHeights"))
(def composite-dtcg-token-type->token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height))
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
@@ -217,7 +255,8 @@
text-case-keys
text-decoration-keys
font-weight-keys
typography-token-keys))
typography-token-keys
#{:line-height}))
;; TODO: Created to extract the font-size feature from the typography feature flag.
;; Delete this once the typography feature flag is removed.
@@ -289,6 +328,7 @@
(font-size-keys shape-attr) #{shape-attr :typography}
(letter-spacing-keys shape-attr) #{shape-attr :typography}
(font-family-keys shape-attr) #{shape-attr :typography}
(= :line-height shape-attr) #{:line-height :typography}
(= :text-transform shape-attr) #{:text-case :typography}
(text-decoration-keys shape-attr) #{shape-attr :typography}
(font-weight-keys shape-attr) #{shape-attr :typography}
@@ -468,3 +508,30 @@
(when (font-weight-values weight)
(cond-> {:weight weight}
italic? (assoc :style "italic")))))
(defn typography-composite-token-reference?
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))
(def tokens-by-input
"A map from input name to applicable token for that input."
{:width #{:sizing :dimensions}
:height #{:sizing :dimensions}
:max-width #{:sizing :dimensions}
:max-height #{:sizing :dimensions}
:x #{:spacing :dimensions}
:y #{:spacing :dimensions}
:rotation #{:number :rotation}
:border-radius #{:border-radius :dimensions}
:row-gap #{:spacing :dimensions}
:column-gap #{:spacing :dimensions}
:horizontal-padding #{:spacing :dimensions}
:vertical-padding #{:spacing :dimensions}
:sided-paddings #{:spacing :dimensions}
:horizontal-margin #{:spacing :dimensions}
:vertical-margin #{:spacing :dimensions}
:sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number}
:font-size #{:font-size}
:letter-spacing #{:letter-spacing}})

View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@
(ns app.common.types.variant
(:require
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.math :as math]
[app.common.path-names :as cpn]
[app.common.schema :as sm]
[cuerdas.core :as str]))
@@ -50,7 +50,6 @@
(def property-max-length 60)
(def value-prefix "Value ")
(defn properties-to-name
"Transform the properties into a name, with the values separated by comma"
[properties]
@@ -59,7 +58,6 @@
(remove str/empty?)
(str/join ", ")))
(defn next-property-number
"Returns the next property number, to avoid duplicates on the property names"
[properties]
@@ -92,7 +90,7 @@
([path properties]
(path-to-properties path properties 0))
([path properties min-props]
(let [cpath (cfh/split-path path)
(let [cpath (cpn/split-path path)
total-props (max (count cpath) min-props)
assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range))
;; Add empty strings to the end of cpath to reach the minimum number of properties
@@ -100,7 +98,6 @@
remaining (drop (count properties) cpath)]
(add-new-props assigned remaining))))
(defn properties-map->formula
"Transforms a map of properties to a formula of properties omitting the empty ones"
[properties]
@@ -110,7 +107,6 @@
(str name "=" value))))
(str/join ", ")))
(defn properties-formula->map
"Transforms a formula of properties to a map of properties"
[s]
@@ -121,7 +117,6 @@
{:name (str/trim k)
:value (str/trim v)}))))
(defn valid-properties-formula?
"Checks if a formula is valid"
[s]
@@ -138,21 +133,18 @@
(let [upd-names (set (map :name upd-props))]
(filterv #(not (contains? upd-names (:name %))) prev-props)))
(defn find-properties-to-update
"Compares two property maps to find which properties should be updated"
[prev-props upd-props]
(filterv #(some (fn [prop] (and (= (:name %) (:name prop))
(not= (:value %) (:value prop)))) prev-props) upd-props))
(defn find-properties-to-add
"Compares two property maps to find which properties should be added"
[prev-props upd-props]
(let [prev-names (set (map :name prev-props))]
(filterv #(not (contains? prev-names (:name %))) upd-props)))
(defn- split-base-name-and-number
"Extract the number in parentheses from an item, if present, and return both the base name and the number"
[item]
@@ -192,7 +184,6 @@
:value (:value prop)}))
[])))
(defn find-index-for-property-name
"Finds the index of a name in a property map"
[props name]
@@ -318,4 +309,4 @@
"Transforms a variant-name (its properties values) into a standard name:
the real name of the shape joined by the properties values separated by '/'"
[variant]
(cfh/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))

View File

@@ -22,7 +22,7 @@
#?(:cljs
(defn weak-map
"Create a WeakMap like instance what uses clojure equality
"Create a WeakMap-like instance what uses clojure equality
semantics."
[]
(new wm/WeakEqMap #js {:hash hash :equals =})))

View File

@@ -14,7 +14,6 @@
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[clojure.pprint :refer [pprint]]
[clojure.test :as t]
[common-tests.types.shape-decode-encode-test :refer [json-roundtrip]]))

View File

@@ -446,3 +446,35 @@
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#fabada"))
(t/is (= (:fill-opacity fill') 1))))
(t/deftest test-detach-copy-in-main
(let [;; ==== Setup
file (-> (setup-file)
(thc/instantiate-component :c-big-board
:copy-big-board
:children-labels [:copy-h-board-with-ellipse
:copy-nested-h-ellipse
:copy-nested-ellipse]))
page (thf/current-page file)
;; ==== Action
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
(pcb/with-page page)
(pcb/with-objects (:objects page)))
page
{(:id file) file}
(thi/id :nested-h-ellipse))
file' (-> (thf/apply-changes file changes)
(tho/propagate-component-changes :c-board-with-ellipse)
(tho/propagate-component-changes :c-big-board))
;; ==== Get
nested2-h-ellipse (ths/get-shape file' :nested-h-ellipse)
copy-nested2-h-ellipse (ths/get-shape file' :copy-nested-h-ellipse)]
;; ==== Check
;; When the nested copy inside the main is detached, their copies are unheaded.
(t/is (not (ctk/subcopy-head? nested2-h-ellipse)))
(t/is (not (ctk/subcopy-head? copy-nested2-h-ellipse)))))

View File

@@ -144,20 +144,21 @@
file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib #(-> %
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set)
:name "test-token-set"))
(ctob/add-theme (ctob/make-token-theme :name "test-theme"
:sets #{"test-token-set"}))
(ctob/set-active-themes #{"/test-theme"})
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-sizing)
:name "token-sizing"
:type :sizing
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-spacing)
:name "token-spacing"
:type :spacing
:value 30))))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-sizing)
:name "token-sizing"
:type :sizing
:value 10))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-spacing)
:name "token-spacing"
:type :spacing
:value 30))))
(tho/add-frame :frame-1
:layout :flex ;; TODO: those values come from main.data.workspace.shape_layout/default-layout-params
:layout-flex-dir :row ;; it should be good to use it directly, but first it should be moved to common.logic

View File

@@ -27,65 +27,66 @@
(-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib #(-> %
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set)
:name "test-token-set"))
(ctob/add-theme (ctob/make-token-theme :name "test-theme"
:sets #{"test-token-set"}))
(ctob/set-active-themes #{"/test-theme"})
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-radius)
:name "token-radius"
:type :border-radius
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-rotation)
:name "token-rotation"
:type :rotation
:value 30))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-opacity)
:name "token-opacity"
:type :opacity
:value 0.7))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-stroke-width)
:name "token-stroke-width"
:type :stroke-width
:value 2))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-color)
:name "token-color"
:type :color
:value "#00ff00"))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-dimensions)
:name "token-dimensions"
:type :dimensions
:value 100))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-font-size)
:name "token-font-size"
:type :font-size
:value 24))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-letter-spacing)
:name "token-letter-spacing"
:type :letter-spacing
:value 2))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-font-family)
:name "token-font-family"
:type :font-family
:value ["Helvetica" "Arial" "sans-serif"]))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-sizing)
:name "token-sizing"
:type :sizing
:value 10))
(ctob/add-token-in-set "test-token-set"
(ctob/make-token :id (thi/new-id! :token-spacing)
:name "token-spacing"
:type :spacing
:value 30))))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-radius)
:name "token-radius"
:type :border-radius
:value 10))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-rotation)
:name "token-rotation"
:type :rotation
:value 30))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-opacity)
:name "token-opacity"
:type :opacity
:value 0.7))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-stroke-width)
:name "token-stroke-width"
:type :stroke-width
:value 2))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-color)
:name "token-color"
:type :color
:value "#00ff00"))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-dimensions)
:name "token-dimensions"
:type :dimensions
:value 100))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-font-size)
:name "token-font-size"
:type :font-size
:value 24))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-letter-spacing)
:name "token-letter-spacing"
:type :letter-spacing
:value 2))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-font-family)
:name "token-font-family"
:type :font-family
:value ["Helvetica" "Arial" "sans-serif"]))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-sizing)
:name "token-sizing"
:type :sizing
:value 10))
(ctob/add-token (thi/id :test-token-set)
(ctob/make-token :id (thi/new-id! :token-spacing)
:name "token-spacing"
:type :spacing
:value 30))))
(tho/add-frame :frame1
:layout :flex ;; TODO: those values come from main.data.workspace.shape_layout/default-layout-params
:layout-flex-dir :row ;; it should be good to use it directly, but first it should be moved to common.logic
@@ -131,17 +132,17 @@
frame1 (ths/get-shape file :frame1)
text1 (ths/get-shape file :text1)
circle1 (ths/get-shape file :circle1)
token-radius (tht/get-token file "test-token-set" (thi/id :token-radius))
token-rotation (tht/get-token file "test-token-set" (thi/id :token-rotation))
token-opacity (tht/get-token file "test-token-set" (thi/id :token-opacity))
token-stroke-width (tht/get-token file "test-token-set" (thi/id :token-stroke-width))
token-color (tht/get-token file "test-token-set" (thi/id :token-color))
token-dimensions (tht/get-token file "test-token-set" (thi/id :token-dimensions))
token-font-size (tht/get-token file "test-token-set" (thi/id :token-font-size))
token-letter-spacing (tht/get-token file "test-token-set" (thi/id :token-letter-spacing))
token-font-family (tht/get-token file "test-token-set" (thi/id :token-font-family))
token-sizing (tht/get-token file "test-token-set" (thi/id :token-sizing))
token-spacing (tht/get-token file "test-token-set" (thi/id :token-spacing))
token-radius (tht/get-token file (thi/id :test-token-set) (thi/id :token-radius))
token-rotation (tht/get-token file (thi/id :test-token-set) (thi/id :token-rotation))
token-opacity (tht/get-token file (thi/id :test-token-set) (thi/id :token-opacity))
token-stroke-width (tht/get-token file (thi/id :test-token-set) (thi/id :token-stroke-width))
token-color (tht/get-token file (thi/id :test-token-set) (thi/id :token-color))
token-dimensions (tht/get-token file (thi/id :test-token-set) (thi/id :token-dimensions))
token-font-size (tht/get-token file (thi/id :test-token-set) (thi/id :token-font-size))
token-letter-spacing (tht/get-token file (thi/id :test-token-set) (thi/id :token-letter-spacing))
token-font-family (tht/get-token file (thi/id :test-token-set) (thi/id :token-font-family))
token-sizing (tht/get-token file (thi/id :test-token-set) (thi/id :token-sizing))
token-spacing (tht/get-token file (thi/id :test-token-set) (thi/id :token-spacing))
;; ==== Action
changes (-> (-> (pcb/empty-changes nil)

View File

@@ -13,6 +13,7 @@
[app.common.test-helpers.tokens :as tht]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[clojure.datafy :refer [datafy]]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
@@ -33,6 +34,7 @@
(pcb/with-library-data (:data file))
(clt/generate-toggle-token-set (tht/get-tokens-lib file) "foo/bar"))
_ (prn "changes" changes)
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
@@ -83,127 +85,133 @@
(t/deftest set-token-theme-test
(t/testing "delete token theme"
(let [theme-name "foo"
group "main"
(let [theme-id (uuid/next)
file (setup-file #(-> %
(ctob/add-theme (ctob/make-token-theme :name theme-name
:group group))))
(ctob/add-theme (ctob/make-token-theme :id theme-id
:name "foo"
:group "main"))))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-theme group theme-name nil))
(pcb/set-token-theme theme-id nil))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
;; Redo
(t/is (nil? (ctob/get-theme redo-lib group theme-name)))
(t/is (nil? (ctob/get-theme redo-lib theme-id)))
;; Undo
(t/is (some? (ctob/get-theme undo-lib group theme-name)))))
(t/is (some? (ctob/get-theme undo-lib theme-id)))))
(t/testing "add token theme"
(let [theme-name "foo"
group "main"
theme (ctob/make-token-theme :name theme-name
:group group)
(let [theme-id (uuid/next)
theme (ctob/make-token-theme :id theme-id
:name "foo"
:group "main")
file (setup-file identity)
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-theme group theme-name theme))
(pcb/set-token-theme theme-id theme))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
;; Redo
(t/is (some? (ctob/get-theme redo-lib group theme-name)))
(t/is (some? (ctob/get-theme redo-lib theme-id)))
;; Undo
(t/is (nil? (ctob/get-theme undo-lib group theme-name)))))
(t/is (nil? (ctob/get-theme undo-lib theme-id)))))
(t/testing "update token theme"
(let [theme-name "foo"
group "main"
prev-theme (ctob/make-token-theme :name theme-name
:group group)
(let [theme-id (uuid/next)
prev-theme-name "foo"
prev-theme (ctob/make-token-theme :id theme-id
:name prev-theme-name
:group "main")
file (setup-file #(ctob/add-theme % prev-theme))
new-theme-name "foo1"
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-theme group new-theme-name prev-theme))
(pcb/set-token-theme theme-id (ctob/rename prev-theme new-theme-name)))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
redo-theme (ctob/get-theme redo-lib theme-id)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
undo-lib (tht/get-tokens-lib undo)
undo-theme (ctob/get-theme undo-lib theme-id)]
;; Redo
(t/is (some? (ctob/get-theme redo-lib group theme-name)))
(t/is (nil? (ctob/get-theme redo-lib group new-theme-name)))
(t/is (= new-theme-name (ctob/get-name redo-theme)))
;; Undo
(t/is (some? (ctob/get-theme undo-lib group theme-name)))
(t/is (nil? (ctob/get-theme undo-lib group new-theme-name)))))
(t/is (= prev-theme-name (ctob/get-name undo-theme)))))
(t/testing "toggling token theme updates using changes history"
(let [theme-name "foo-theme"
group "main"
(let [theme-id (uuid/next)
theme (ctob/make-token-theme :id theme-id
:name "foo-theme"
:group "main")
set-name "bar-set"
token-set (ctob/make-token-set :name set-name)
theme (ctob/make-token-theme :name theme-name
:group group)
file (setup-file #(-> %
(ctob/add-theme theme)
(ctob/add-set token-set)))
theme' (assoc theme :sets #{set-name})
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-theme group theme-name theme'))
(pcb/set-token-theme theme-id theme'))
changed-file (-> file
(thf/apply-changes changes)
(thf/apply-undo-changes changes)
(thf/apply-changes changes))
changed-lib (tht/get-tokens-lib changed-file)]
(t/is (= #{set-name}
(-> changed-lib (ctob/get-theme group theme-name) :sets))))))
(-> changed-lib (ctob/get-theme theme-id) :sets))))))
(t/deftest set-token-test
(t/testing "delete token"
(let [set-name "foo"
set-id (uuid/next)
token-id (uuid/next)
file (setup-file #(-> %
(ctob/add-set (ctob/make-token-set :name set-name))
(ctob/add-token-in-set set-name (ctob/make-token {:name "to.delete.color.red"
:id token-id
:value "red"
:type :color}))))
(ctob/add-set (ctob/make-token-set :id set-id
:name set-name))
(ctob/add-token set-id (ctob/make-token {:name "to.delete.color.red"
:id token-id
:value "red"
:type :color}))))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token set-name token-id nil))
(pcb/set-token set-id token-id nil))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
(t/is (nil? (ctob/get-token-in-set redo-lib set-name token-id)))
(t/is (nil? (ctob/get-token redo-lib set-id token-id)))
;; Undo
(t/is (some? (ctob/get-token-in-set undo-lib set-name token-id)))))
(t/is (some? (ctob/get-token undo-lib set-id token-id)))))
(t/testing "add token"
(let [set-name "foo"
set-id (uuid/next)
token (ctob/make-token {:name "to.add.color.red"
:value "red"
:type :color})
file (setup-file #(-> % (ctob/add-set (ctob/make-token-set :name set-name))))
file (setup-file #(-> % (ctob/add-set (ctob/make-token-set :id set-id
:name set-name))))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token set-name (:id token) token))
(pcb/set-token set-id (:id token) token))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
(t/is (= token (ctob/get-token-in-set redo-lib set-name (:id token))))
(t/is (= token (ctob/get-token redo-lib set-id (:id token))))
;; Undo
(t/is (nil? (ctob/get-token-in-set undo-lib set-name (:id token))))))
(t/is (nil? (ctob/get-token undo-lib set-id (:id token))))))
(t/testing "update token"
(let [set-name "foo"
set-id (uuid/next)
prev-token (ctob/make-token {:name "to.update.color.red"
:value "red"
:type :color})
@@ -211,27 +219,29 @@
(assoc :name "color.red.changed")
(assoc :value "blue"))
file (setup-file #(-> %
(ctob/add-set (ctob/make-token-set :name set-name))
(ctob/add-token-in-set set-name prev-token)))
(ctob/add-set (ctob/make-token-set :id set-id
:name set-name))
(ctob/add-token set-id prev-token)))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token set-name (:id prev-token) token))
(pcb/set-token set-id (:id prev-token) token))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
(t/is (tht/token-data-eq? token (ctob/get-token-in-set redo-lib set-name (:id token))))
(t/is (tht/token-data-eq? token (ctob/get-token redo-lib set-id (:id token))))
;; Undo
(t/is (tht/token-data-eq? prev-token (ctob/get-token-in-set undo-lib set-name (:id prev-token)))))))
(t/is (tht/token-data-eq? prev-token (ctob/get-token undo-lib set-id (:id prev-token)))))))
(t/deftest set-token-set-test
(t/testing "delete token set"
(let [set-name "foo"
file (setup-file #(ctob/add-set % (ctob/make-token-set :name set-name)))
set-id (uuid/next)
file (setup-file #(ctob/add-set % (ctob/make-token-set :id set-id :name set-name)))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-set set-name false nil))
(pcb/set-token-set set-id nil))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
@@ -243,11 +253,12 @@
(t/testing "add token set"
(let [set-name "foo"
token-set (ctob/make-token-set :name set-name)
set-id (uuid/next)
token-set (ctob/make-token-set :id set-id :name set-name)
file (setup-file identity)
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-set set-name false token-set))
(pcb/set-token-set set-id token-set))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
@@ -259,28 +270,26 @@
(t/testing "update token set"
(let [set-name "foo"
token-name "bar"
token (ctob/make-token {:name token-name
:value "red"
:type :color})
file (setup-file #(-> (ctob/add-set % (ctob/make-token-set :name set-name))
(ctob/add-token-in-set set-name token)))
prev-token-set (-> file tht/get-tokens-lib (ctob/get-set set-name))
set-id (uuid/next)
token-set (ctob/make-token-set :id set-id :name set-name)
file (setup-file #(-> (ctob/add-set % token-set)))
new-set-name "foo1"
changes (-> (pcb/empty-changes)
(pcb/with-library-data (:data file))
(pcb/set-token-set set-name false (ctob/rename prev-token-set new-set-name)))
(pcb/set-token-set set-id (ctob/rename token-set new-set-name)))
redo (thf/apply-changes file changes)
redo-lib (tht/get-tokens-lib redo)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)]
redo-token-set (ctob/get-set redo-lib set-id)
undo (thf/apply-undo-changes redo changes)
undo-lib (tht/get-tokens-lib undo)
undo-token-set (ctob/get-set undo-lib set-id)]
(t/is (= (ctob/get-name redo-token-set) new-set-name))
;; Undo
(t/is (some? (ctob/get-token-in-set undo-lib set-name (:id token))))
(t/is (nil? (ctob/get-token-in-set undo-lib new-set-name (:id token))))
;; Redo
(t/is (nil? (ctob/get-token-in-set redo-lib set-name (:id token))))
(t/is (some? (ctob/get-token-in-set redo-lib new-set-name (:id token)))))))
(t/is (= (ctob/get-name undo-token-set) set-name)))))
(t/deftest generate-toggle-token-set-group-test
(t/testing "toggling set group with no active sets inside will activate all child sets"
@@ -361,13 +370,13 @@
:position :top})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-ordered-set-names))
(ctob/get-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-ordered-set-names))]
(ctob/get-set-names))]
(t/is (= ["bar" "foo" "baz"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/testing "at bottom"
(let [file (setup-file #(-> %
@@ -380,13 +389,13 @@
:position :bot})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-ordered-set-names))
(ctob/get-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-ordered-set-names))]
(ctob/get-set-names))]
(t/is (= ["bar" "baz" "foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/testing "dropping out of set group"
(let [file (setup-file #(-> %
@@ -398,13 +407,13 @@
:position :top})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-ordered-set-names))
(ctob/get-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-ordered-set-names))]
(ctob/get-set-names))]
(t/is (= ["bar" "foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/testing "into set group"
(let [file (setup-file #(-> %
@@ -416,13 +425,13 @@
:position :bot})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-ordered-set-names))
(ctob/get-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-ordered-set-names))]
(ctob/get-set-names))]
(t/is (= ["foo/bar" "foo/foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/testing "edge-cases:"
(t/testing "prevent overriding set to identical path"
@@ -454,13 +463,13 @@
:collapsed-paths #{["foo"]}})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-ordered-set-names))
(ctob/get-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-ordered-set-names))]
(ctob/get-set-names))]
(t/is (= ["foo/bar" "foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-ordered-set-names lib) undo-sets))))))))
(t/is (= (ctob/get-set-names lib) undo-sets))))))))
(t/deftest generate-move-token-group-test
(t/testing "Ignore dropping set group to the same position"
@@ -496,14 +505,14 @@
:position :top})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-ordered-set-names))
(ctob/get-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-ordered-set-names))]
(ctob/get-set-names))]
(t/is (= ["bar/bar" "foo/foo" "baz/baz"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/testing "to bottom"
(let [file (setup-file #(-> %
@@ -515,14 +524,14 @@
:position :bot})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-ordered-set-names))
(ctob/get-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-ordered-set-names))]
(ctob/get-set-names))]
(t/is (= ["bar" "foo/foo"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-ordered-set-names lib) undo-sets)))))
(t/is (= (ctob/get-set-names lib) undo-sets)))))
(t/testing "into set group"
(let [file (setup-file #(-> %
@@ -534,13 +543,13 @@
:position :bot})
redo (thf/apply-changes file changes)
redo-sets (-> (tht/get-tokens-lib redo)
(ctob/get-ordered-set-names))
(ctob/get-set-names))
undo (thf/apply-undo-changes redo changes)
undo-sets (-> (tht/get-tokens-lib undo)
(ctob/get-ordered-set-names))]
(ctob/get-set-names))]
(t/is (= ["bar/foo/foo" "bar/bar"] (vec redo-sets)))
(t/testing "undo"
(t/is (= (ctob/get-ordered-set-names lib) undo-sets))))
(t/is (= (ctob/get-set-names lib) undo-sets))))
(t/testing "edge-cases:"
(t/testing "prevent overriding set to identical path"

View File

@@ -1,19 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.pages-helpers-test
(:require
[app.common.files.helpers :as cfh]
[clojure.pprint :refer [pprint]]
[clojure.test :as t]))
(t/deftest parse-path-name
(t/is (= ["foo" "bar"] (cfh/parse-path-name "foo/bar")))
(t/is (= ["" "foo"] (cfh/parse-path-name "foo")))
(t/is (= ["" "foo"] (cfh/parse-path-name "/foo")))
(t/is (= ["" ""] (cfh/parse-path-name "")))
(t/is (= ["" ""] (cfh/parse-path-name nil))))

View File

@@ -0,0 +1,33 @@
;; 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 common-tests.path-names-test
(:require
[app.common.path-names :as cpn]
[clojure.test :as t]))
(t/deftest split-group-name
(t/is (= ["foo" "bar"] (cpn/split-group-name "foo/bar")))
(t/is (= ["" "foo"] (cpn/split-group-name "foo")))
(t/is (= ["" "foo"] (cpn/split-group-name "/foo")))
(t/is (= ["" ""] (cpn/split-group-name "")))
(t/is (= ["" ""] (cpn/split-group-name nil))))
(t/deftest split-and-join-path
(let [name "group/subgroup/name"
path (cpn/split-path name :separator "/")
name' (cpn/join-path path :separator "/" :with-spaces? false)]
(t/is (= (first path) "group"))
(t/is (= (second path) "subgroup"))
(t/is (= (nth path 2) "name"))
(t/is (= name' name))))
(t/deftest split-and-join-path-with-spaces
(let [name "group / subgroup / name"
path (cpn/split-path name :separator "/")]
(t/is (= (first path) "group"))
(t/is (= (second path) "subgroup"))
(t/is (= (nth path 2) "name"))))

View File

@@ -30,7 +30,7 @@
[common-tests.logic.swap-as-override-test]
[common-tests.logic.token-test]
[common-tests.media-test]
[common-tests.pages-helpers-test]
[common-tests.path-names-test]
[common-tests.record-test]
[common-tests.schema-test]
[common-tests.svg-path-test]
@@ -41,6 +41,7 @@
[common-tests.types.components-test]
[common-tests.types.fill-test]
[common-tests.types.modifiers-test]
[common-tests.types.objects-map-test]
[common-tests.types.path-data-test]
[common-tests.types.shape-decode-encode-test]
[common-tests.types.shape-interactions-test]
@@ -81,7 +82,7 @@
'common-tests.logic.swap-as-override-test
'common-tests.logic.token-test
'common-tests.media-test
'common-tests.pages-helpers-test
'common-tests.path-names-test
'common-tests.record-test
'common-tests.schema-test
'common-tests.svg-path-test
@@ -90,9 +91,10 @@
'common-tests.time-test
'common-tests.types.absorb-assets-test
'common-tests.types.components-test
'common-tests.types.modifiers-test
'common-tests.types.path-data-test
'common-tests.types.fill-test
'common-tests.types.modifiers-test
'common-tests.types.objects-map-test
'common-tests.types.path-data-test
'common-tests.types.shape-decode-encode-test
'common-tests.types.shape-interactions-test
'common-tests.types.tokens-lib-test

View File

@@ -0,0 +1,26 @@
{
"fonts": {
"string-font-family": {
"$value": "Arial, Helvetica, sans-serif",
"$type": "fontFamilies",
"$description": "A font family defined as a string"
},
"array-font-family": {
"$value": ["Inter", "system-ui", "sans-serif"],
"$type": "fontFamilies",
"$description": "A font family defined as an array"
},
"single-font-family": {
"$value": "Georgia",
"$type": "fontFamilies"
},
"complex-font-family": {
"$value": "Times New Roman, serif",
"$type": "fontFamilies"
},
"font-with-spaces": {
"$value": "Source Sans Pro, Arial, sans-serif",
"$type": "fontFamilies"
}
}
}

View File

@@ -0,0 +1,53 @@
{
"test": {
"typo": {
"$value": {
"fontWeight": "100",
"fontSize": "16px",
"letterSpacing": "0.1em"
},
"$type": "typography"
},
"typo2": {
"$value": "{typo}",
"$type": "typography"
},
"font-weight": {
"$value": "200",
"$type": "fontWeights"
},
"typo-to-single": {
"$value": "{font-weight}",
"$type": "typography"
},
"test-empty": {
"$value": {},
"$type": "typography"
},
"font-size": {
"$value": "18px",
"$type": "fontSizes"
},
"typo-complex": {
"$value": {
"fontWeight": "bold",
"fontSize": "24px",
"letterSpacing": "0.05em",
"lineHeights": "100%",
"fontFamilies": ["Arial", "sans-serif"],
"textCase": "uppercase"
},
"$type": "typography",
"$description": "A complex typography token"
},
"typo-with-string-font-family": {
"$value": {
"fontWeight": "600",
"fontSize": "20px",
"fontFamilies": "Roboto, Helvetica, sans-serif"
},
"$type": "typography",
"$description": "Typography token with string font family"
}
}
}

View File

@@ -0,0 +1,133 @@
;; 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 common-tests.types.objects-map-test
(:require
#?(:clj [app.common.fressian :as fres])
[app.common.json :as json]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.schema.test :as smt]
[app.common.transit :as transit]
[app.common.types.objects-map :as omap]
[app.common.types.path :as path]
[app.common.types.plugins :refer [schema:plugin-data]]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[clojure.datafy :refer [datafy]]
[clojure.test :as t]))
(t/deftest basic-operations
(t/testing "assoc"
(let [id (uuid/custom 0 1)
id' (uuid/custom 0 2)
obj (-> (omap/create) (assoc id {:foo 1}))]
(t/is (not= id id'))
(t/is (not (contains? obj id')))
(t/is (contains? obj id))))
(t/testing "assoc-with-non-uuid-keys"
(let [obj (-> (omap/create)
(assoc :a {:foo 1})
(assoc :b {:bar 1}))]
(t/is (not (contains? obj :c)))
(t/is (contains? obj :a))
(t/is (contains? obj :b))))
(t/testing "dissoc"
(let [id (uuid/custom 0 1)
obj (-> (omap/create) (assoc id {:foo 1}))]
(t/is (contains? obj id))
(let [obj (dissoc obj id)]
(t/is (not (contains? obj id))))))
(t/testing "seq"
(let [id (uuid/custom 0 1)
obj (-> (omap/create) (assoc id 1))]
(t/is (contains? obj id))
(let [[entry] (seq obj)]
(t/is (map-entry? entry))
(t/is (= (key entry) id))
(t/is (= (val entry) 1)))))
(t/testing "cons & count"
(let [obj (into (omap/create) [[uuid/zero 1]])]
(t/is (contains? obj uuid/zero))
(t/is (= 1 (count obj)))
(t/is (omap/objects-map? obj))))
(t/testing "wrap"
(let [obj1 (omap/wrap {})
tmp (omap/create)
obj2 (omap/wrap tmp)]
(t/is (omap/objects-map? obj1))
(t/is (omap/objects-map? obj2))
(t/is (identical? tmp obj2))
(t/is (= 0 (count obj1)))
(t/is (= 0 (count obj2))))))
(t/deftest internal-state
(t/testing "modified & compact"
(let [obj (-> (omap/create)
(assoc :a 1)
(assoc :b 2))]
(t/is (= 2 (count obj)))
(t/is (-> obj datafy :modified))
(let [obj (omap/compact obj)]
(t/is (not (-> obj datafy :modified))))))
(t/testing "create from other"
(let [obj1 (-> (omap/create)
(assoc :a {:foo 1})
(assoc :b {:bar 2}))
obj2 (omap/create obj1)]
(t/is (not (identical? obj1 obj2)))
(t/is (= obj1 obj2))
(t/is (= (hash obj1) (hash obj2)))
(t/is (= (get obj1 :a) (get obj2 :a)))
(t/is (= (get obj1 :b) (get obj2 :b))))))
(t/deftest creation-and-duplication
(smt/check!
(smt/for [data (->> (sg/map-of (sg/uuid) (sg/generator cts/schema:shape))
(sg/not-empty))]
(let [obj1 (omap/wrap data)
obj2 (omap/create obj1)]
(and (= (hash obj1) (hash obj2))
(= obj1 obj2))))
{:num 100}))
#?(:clj
(t/deftest fressian-encode-decode
(smt/check!
(smt/for [data (->> (sg/map-of (sg/uuid) (sg/generator cts/schema:shape))
(sg/not-empty)
(sg/fmap omap/wrap)
(sg/fmap (fn [o] {:objects o})))]
(let [res (-> data fres/encode fres/decode)]
(and (contains? res :objects)
(omap/objects-map? (:objects res))
(= res data))))
{:num 100})))
(t/deftest transit-encode-decode
(smt/check!
(smt/for [data (->> (sg/map-of (sg/uuid) (sg/generator cts/schema:shape))
(sg/not-empty)
(sg/fmap omap/wrap)
(sg/fmap (fn [o] {:objects o})))]
(let [res (-> data transit/encode-str transit/decode-str)]
;; (app.common.pprint/pprint data)
;; (app.common.pprint/pprint res)
(and (every? (fn [[k v]]
(= v (get-in data [:objects k])))
(:objects res))
(omap/objects-map? (:objects data))
(omap/objects-map? (:objects res)))))
{:num 100}))

View File

File diff suppressed because it is too large Load Diff

View File

@@ -113,12 +113,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='6f8725d186d05c627176db9c46c732a6ef3ba41d9e9b3775c4727fc8ac642bb2'; \
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_aarch64_linux_hotspot_24.0.2_12.tar.gz'; \
ESUM='b60eb9d54c97ba4159547834a98cc5d016281dd2b3e60e7475cba4911324bcb4'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_aarch64.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='aea1cc55e51cf651c85f2f00ad021603fe269c4bb6493fa97a321ad770c9b096'; \
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_x64_linux_hotspot_24.0.2_12.tar.gz'; \
ESUM='164d901e5a240b8c18516f5ab55bc11fc9689ab6e829045aea8467356dcdb340'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_x64.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
@@ -183,8 +183,8 @@ RUN set -eux; \
FROM base AS setup-utils
ENV CLJKONDO_VERSION=2025.01.16 \
BABASHKA_VERSION=1.12.207 \
ENV CLJKONDO_VERSION=2025.07.28 \
BABASHKA_VERSION=1.12.208 \
CLJFMT_VERSION=0.13.1
RUN set -ex; \
@@ -310,6 +310,7 @@ RUN set -ex; \
fonts-wqy-zenhei \
fonts-tlwg-loma-otf \
fonts-freefont-ttf \
poppler-utils \
\
libasound2t64 \
libatk-bridge2.0-0t64 \

View File

@@ -39,7 +39,7 @@ tmux new-window -t penpot:3 -n 'exporter'
tmux select-window -t penpot:3
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter
tmux send-keys -t penpot 'yarn run watch' enter
tmux split-window -v
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l

View File

@@ -87,14 +87,9 @@ RUN set -ex; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
ca-certificates \
curl \
fontconfig \
fontforge \
python3 \
python3-tabulate \
tzdata \
woff-tools \
woff2 \
\
libfontconfig1 \
libfreetype6 \
libglib2.0-0 \
@@ -113,6 +108,11 @@ RUN set -ex; \
libxml2 \
libzip4t64 \
libzstd1 \
python3 \
python3-tabulate \
tzdata \
woff-tools \
woff2 \
; \
find tmp/usr/share/zoneinfo/* -type d ! -name 'Etc' |xargs rm -rf; \
rm -rf /var/lib /var/cache; \
@@ -126,7 +126,9 @@ RUN set -ex; \
COPY --from=build /opt/jre /opt/jre
COPY --from=build /opt/node /opt/node
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
ARG BUNDLE_PATH="./bundle-backend/"
ADD --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/backend/
USER penpot:penpot
WORKDIR /opt/penpot/backend

View File

@@ -89,7 +89,8 @@ RUN set -eux; \
mkdir -p /opt/penpot; \
chown -R penpot:penpot /opt/penpot;
ADD --chown=penpot:penpot ./bundle-exporter/ /opt/penpot/exporter
ARG BUNDLE_PATH="./bundle-exporter/"
ADD --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/exporter/
WORKDIR /opt/penpot/exporter
USER penpot:penpot

View File

@@ -6,14 +6,18 @@ USER root
RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
mkdir -p /opt/data/assets; \
chown -R penpot:penpot /opt/data;
chown -R penpot:penpot /opt/data; \
mkdir -p /etc/nginx/overrides/http.d/; \
mkdir -p /etc/nginx/overrides/server.d/; \
mkdir -p /etc/nginx/overrides/location.d/;
ADD ./bundle-frontend/ /var/www/app/
ARG BUNDLE_PATH="./bundle-frontend/"
ADD $BUNDLE_PATH /var/www/app/
ADD ./files/config.js /var/www/app/js/config.js
ADD ./files/nginx.conf /etc/nginx/nginx.conf.template
ADD ./files/nginx-proxies.conf /etc/nginx/nginx-proxies.conf
ADD ./files/resolvers.conf /etc/nginx/overrides.d/resolvers.conf.template
ADD ./files/nginx.conf.template /tmp/nginx.conf.template
ADD ./files/nginx-resolvers.conf.template /tmp/resolvers.conf.template
ADD ./files/nginx-mime.types /etc/nginx/mime.types
ADD ./files/nginx-external-locations.conf /etc/nginx/overrides/location.d/external-locations.conf
ADD ./files/nginx-entrypoint.sh /entrypoint.sh
RUN chown -R 1001:0 /var/cache/nginx; \

View File

@@ -5,10 +5,8 @@
#########################################
if [[ $PENPOT_FLAGS == *"enable-air-gapped-conf"* ]]; then
export INCLUDE_PROXIES=""
rm /etc/nginx/overrides/location.d/external-locations.conf;
export PENPOT_FLAGS="$PENPOT_FLAGS disable-google-fonts-provider disable-dashboard-templates-section"
else
export INCLUDE_PROXIES="include /etc/nginx/nginx-proxies.conf;"
fi
#########################################
@@ -33,14 +31,13 @@ update_flags /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-$PENPOT_DEFAULT_INTERNAL_RESOLVER}
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE,\$INCLUDE_PROXIES" \
< /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
envsubst "\$PENPOT_INTERNAL_RESOLVER" \
< /etc/nginx/overrides.d/resolvers.conf.template > /etc/nginx/overrides.d/resolvers.conf
< /tmp/resolvers.conf.template > /etc/nginx/overrides/http.d/resolvers.conf
exec "$@";

View File

@@ -1,10 +1,10 @@
worker_processes auto;
pid /tmp/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
include /etc/nginx/overrides/main.d/*.conf;
events {
worker_connections 2048;
# multi_accept on;
multi_accept on;
}
http {
@@ -33,6 +33,11 @@ http {
error_log /dev/stderr;
access_log /dev/stdout;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
send_timeout 300s;
gzip on;
gzip_vary on;
gzip_proxied any;
@@ -41,7 +46,7 @@ http {
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
@@ -57,7 +62,14 @@ http {
proxy_cache_valid any 48h;
proxy_cache_key "$host$request_uri";
include /etc/nginx/overrides.d/*.conf;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
include /etc/nginx/overrides/http.d/*.conf;
server {
listen 8080 default_server;
@@ -66,13 +78,6 @@ http {
client_max_body_size $PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE;
charset utf-8;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
etag off;
root /var/www/app/;
@@ -119,12 +124,10 @@ http {
location /api {
proxy_pass $PENPOT_BACKEND_URI/api;
proxy_buffering off;
}
location /readyz {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass $PENPOT_BACKEND_URI$request_uri;
}
@@ -134,8 +137,10 @@ http {
proxy_pass $PENPOT_BACKEND_URI/ws/notifications;
}
include /etc/nginx/overrides/server.d/*.conf;
location / {
$INCLUDE_PROXIES
include /etc/nginx/overrides/location.d/*.conf;
location ~ ^/js/config.js$ {
add_header Cache-Control "no-store, no-cache, max-age=0" always;

View File

@@ -234,7 +234,7 @@ Use variables from `frontend/src/app/main/ui/ds/spacing.scss`. These are predefi
For fixed dimensions (e.g., modals' widths) defined by design and not layout-driven, use or define variables in `frontend/src/app/main/ui/ds/_sizes.scss`. To use them:
```scss
@use "../_sizes.scss" as *;
@use "ds/_sizes.scss" as *;
```
Note: Since these values haven't been semantically defined yet, were temporarily using SASS variables instead of named CSS custom properties.
@@ -242,7 +242,7 @@ Note: Since these values haven't been semantically defined yet, were temporar
Use border thickness variables from `frontend/src/app/main/ui/ds/_borders.scss`. To import:
```scss
@use "../_borders.scss" as *;
@use "ds/_borders.scss" as *;
```
Avoid using sass variables defined on `frontend/resources/styles/common/refactor/spacing.scss` that are deprecated.
@@ -314,7 +314,7 @@ When applying typography in SCSS, use the proper mixin from the Design System.
**DO: Use the DS mixin**
```scss
@use "../ds/typography.scss" as t;
@use "ds/typography.scss" as t;
.class {
@include t.use-typography("body-small");

View File

@@ -122,7 +122,7 @@ desc: Discover Penpot's free user guide! Learn the interface, workspace basics,
</p>
<ol>
<li><strong>Teams:</strong> A team allows you to collaborate with other Penpot users. Team members are allowed to work with any project or file within the team depending on their permissions. Members with admin permissions can also invite other members. Create or join as many teams as you need with different groups of people.</li>
<li><strong>Teams:</strong> A team allows you to collaborate with other Penpot users. Team members are allowed to work with any project or file within the team depending on their permissions. Members with admin permissions can also invite other members. <a href="/user-guide/teams/#teams-management">Create or join as many teams as you need</a> with different groups of people.</li>
<li><strong>Search:</strong> If you are looking for a specific file just type its title at the search box.</li>
<li><strong>Projects:</strong> A project allows you to group design files. It works pretty much like a folder in a file system. You can create as many projects as you need. If you are going to work with more people in a project, you should create it inside a team.</li>
<li><strong>Drafts:</strong> The drafts section is where you can find the design files that are not inside any project.</li>
@@ -200,4 +200,4 @@ desc: Discover Penpot's free user guide! Learn the interface, workspace basics,
<img src="/img/interface/viewmode-light.webp" alt="Penpot's view mode" />
</a>
<figcaption>Penpot's view mode in light mode</figcaption>
</figure>
</figure>

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