Compare commits

...

158 Commits

Author SHA1 Message Date
Andrey Antukh
795f65632a 🐛 Fix wasm-playground on devenv 2026-01-08 10:42:37 +01:00
Andrey Antukh
952f622ce9 🔧 Add 'Reapply` prefix to valid commit checker prefixes 2026-01-07 11:56:38 +01:00
Andrey Antukh
a6c6f97f47 Reapply "💄 Group tokens by name path (#7775)"
This reverts commit eff572d3bb.
2026-01-07 11:55:56 +01:00
Andrey Antukh
88424eb54a Merge branch 'staging' into develop 2026-01-07 11:55:40 +01:00
Alejandro Alonso
919f78daeb Merge pull request #7965 from penpot/eva-fix-styles-on-viewer
🐛 Fix inspect tab styles on viewer
2026-01-07 11:54:33 +01:00
Eva Marco
b5c30f8c41 🐛 Fix inspect tab styles on viewer 2026-01-07 11:41:49 +01:00
Alejandro Alonso
60aa426753 Merge pull request #8022 from penpot/alotor-fix-drag-handlers
🐛 Fix problem with dragging handlers
2026-01-07 11:36:26 +01:00
Alejandro Alonso
86f7d6b26b Sanitizing error values (#8020) 2026-01-07 11:23:19 +01:00
Andrey Antukh
36732a4bd3 Make the devenv runtine initialization yarn independent (#8023) 2026-01-07 11:21:58 +01:00
Andrey Antukh
eff572d3bb Revert "💄 Group tokens by name path (#7775)"
This reverts commit 0956b66281.
2026-01-07 11:20:44 +01:00
alonso.torres
d470d96833 🐛 Fix problem with dragging handlers 2026-01-07 11:00:02 +01:00
Aitor Moreno
cab70773d2 Merge pull request #7667 from penpot/azazeln28-doc-add-more-info-text-editor-v2-readme
📚 Add more info about text editor v2
2026-01-07 09:40:06 +01:00
Alejandro Alonso
de9a21121a Merge remote-tracking branch 'origin/staging' into develop 2026-01-05 13:22:14 +01:00
Alejandro Alonso
32ca42a093 Merge remote-tracking branch 'origin/staging-render' into staging 2026-01-05 13:21:58 +01:00
Alejandro Alonso
523a97a4ec Merge pull request #8016 from penpot/alotor-fix-refresh-thumbnails
🐛 Fix problem with thumbnail regeneration
2026-01-05 13:21:34 +01:00
Alejandro Alonso
260f6861a3 Merge pull request #8015 from penpot/alotor-fix-grid-component-auto-sizing
🐛 Fix problem with grid layout components and auto sizing
2026-01-05 13:18:59 +01:00
alonso.torres
edd53b419a 🐛 Fix problem with thumbnail regeneration 2026-01-05 13:09:40 +01:00
Alejandro Alonso
cea10308b7 Merge remote-tracking branch 'origin/staging' into develop 2026-01-05 11:52:15 +01:00
alonso.torres
078a3d5a5c 🐛 Fix problem with grid layout components and auto sizing 2026-01-05 10:54:36 +01:00
Alejandro Alonso
c4e57427ac Merge branch 'staging-render' into staging 2026-01-05 10:30:06 +01:00
David Barragán Merino
5223c9c881 🔧 Fix a typo in an interpolation 2026-01-05 09:13:14 +01:00
Alejandro Alonso
be62fa10c4 📎 Bump new version on changelog 2026-01-05 08:42:57 +01:00
Alejandro Alonso
7a6405481c Merge remote-tracking branch 'origin/develop' into staging 2026-01-05 08:37:19 +01:00
Alejandro Alonso
218f34380a Merge pull request #8012 from penpot/azazeln28-refactor-minor-changes
♻️ Minor naming changes and event handling
2026-01-02 14:16:55 +01:00
Alejandro Alonso
47aaa2b5fa Merge pull request #8011 from penpot/alotor-fix-trash-bar
🐛 Fix problems with trash bar in dashboard
2026-01-02 13:46:53 +01:00
Aitor Moreno
6c6b3db87e ♻️ Minor naming changes and event handling 2026-01-02 13:41:48 +01:00
Alejandro Alonso
6eb32cfb79 Merge pull request #8008 from penpot/alotor-fix-path-editor
🐛 Fix problem with path editor and right click
2026-01-02 13:29:44 +01:00
alonso.torres
dbba3496af 🐛 Fix problems with trash bar in dashboard 2026-01-02 12:00:16 +01:00
alonso.torres
55752d361f 🐛 Fix problem with path editor and right click 2026-01-02 10:37:52 +01:00
Alejandro Alonso
fe94ee4526 Merge pull request #8009 from penpot/alotor-fix-style-font-input
🐛 Fix problem with style in fonts input
2026-01-02 10:23:02 +01:00
Aitor Moreno
e39f292499 📚 Add more info about text editor v2 2026-01-02 10:13:34 +01:00
Andrey Antukh
52b8560b70 Merge branch 'staging-render' into develop 2025-12-30 15:30:56 +01:00
Andrey Antukh
75860afe57 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-30 15:29:58 +01:00
Andrey Antukh
824ca1bbca 🔧 Make devenv init yarn indpendent 2025-12-30 15:28:19 +01:00
Andrey Antukh
5b6f9c1741 📎 Disable legacy cache on plugins nx config 2025-12-30 14:56:15 +01:00
Andrey Antukh
19853b832b 📚 Update documentation 2025-12-30 14:56:15 +01:00
Andrey Antukh
d20c011db2 Migrate plugins to pnpm 2025-12-30 14:56:15 +01:00
Andrey Antukh
9431ae6858 📎 Update docs 2025-12-30 14:56:15 +01:00
Andrey Antukh
96356c1b89 ⬆️ Update storybook and fix compatibility issues 2025-12-30 14:56:15 +01:00
Andrey Antukh
b7b68eeb47 🔥 Remove npx prefix on package.json scripts 2025-12-30 14:56:15 +01:00
Andrey Antukh
9bbeb657f8 🔧 Add plugins runtime ci job 2025-12-30 14:56:15 +01:00
Andrey Antukh
ec1af4ad96 🎉 Import penpot-plugins repository
As commit 819a549e4928d2b1fa98e52bee82d59aec0f70d8
2025-12-30 14:56:15 +01:00
alonso.torres
23e7116b24 🐛 Fix problem with style in fonts input 2025-12-30 14:28:10 +01:00
Alejandro Alonso
48e3f35bb3 🐛 Fix setting a portion of text as bold or underline messes things up 2025-12-30 11:34:24 +01:00
Andrey Antukh
6b794c9d12 Merge branch 'staging' into staging-render 2025-12-30 11:13:15 +01:00
Yamila Moreno
d3ee50daf5 🔧 Add ci for branch staging-render 2025-12-30 11:13:00 +01:00
Yamila Moreno
22a36d59d8 🔧 Add ci for branch staging-render 2025-12-30 10:57:51 +01:00
Alejandro Alonso
a948e49e51 🐛 Fix using cache on first zoom after pan 2025-12-30 10:03:24 +01:00
Alejandro Alonso
d635f5a8dc 🐛 Detecting situations where WebGL context is lost or no WebGL support 2025-12-30 10:03:24 +01:00
Alejandro Alonso
ab3a3ef43b 🎉 Resize cache only when required 2025-12-30 10:03:24 +01:00
Alejandro Alonso
9c21fd3359 🐛 Fix resize cache memory leak 2025-12-30 10:03:24 +01:00
Andrey Antukh
7b5817f407 ♻️ Make several adjustments to the dashboard deleted page (#7999)
* ♻️ Make several sustantial adjustments to the dashboard deleted page

* 📎 Add PR feedback changes
2025-12-30 09:52:29 +01:00
Yamila Moreno
e3405eacca 🔧 Improve mattermost notification 2025-12-29 19:06:26 +01:00
Alejandro Alonso
44b70cf1d4 Merge pull request #7998 from penpot/alotor-fix-problem-with-create-grid
🐛 Fix problem creating grid from elements
2025-12-29 14:31:15 +01:00
Alejandro Alonso
a8bd74b392 Merge pull request #8001 from penpot/alotor-fix-gfonts-references
🐛 Fix problem with some fonts
2025-12-29 14:25:54 +01:00
alonso.torres
3d3e3582d6 🐛 Fix problem with some fonts 2025-12-29 12:35:19 +01:00
Andrey Antukh
de052b5161 📎 Update changelog 2025-12-29 11:10:04 +01:00
Andrey Antukh
e01654ba43 Merge branch 'staging-render' into develop 2025-12-29 10:43:00 +01:00
Andrey Antukh
6ebd48b94c Merge branch 'staging' into staging-render 2025-12-29 10:41:08 +01:00
Andrey Antukh
8a3b33797f 🐛 Fix error handling on password change form
Fixes https://github.com/penpot/penpot/issues/7978
2025-12-29 10:27:27 +01:00
Andrey Antukh
13fd20f76f Backport form error management improvements from develop 2025-12-29 10:27:27 +01:00
alonso.torres
417cd80564 🐛 Fix problem creating grid from elements 2025-12-23 14:49:21 +01:00
Alejandro Alonso
a57011ec7b Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-23 13:35:27 +01:00
Andrey Antukh
69c880d00e 🐛 Fix importmap usage on firefox 2025-12-23 13:10:58 +01:00
Andrey Antukh
9eebc467ef Preload default translations 2025-12-23 13:10:58 +01:00
Andrey Antukh
b77712ce73 Move frontend/vendor to frontend/packages 2025-12-23 13:10:58 +01:00
Andrey Antukh
3d3e81f314 Replace tubax with more modern tooling 2025-12-23 13:10:58 +01:00
Andrey Antukh
fe6441bb24 Replace hightlight.js internal bundle with direct npm use 2025-12-23 13:10:58 +01:00
Andrey Antukh
e15f0baf30 Replace direct draft-js usage with internal module 2025-12-23 13:10:58 +01:00
Andrey Antukh
c040cbb784 🔥 Remove old gulp related dependencies 2025-12-23 13:10:58 +01:00
Andrey Antukh
7f674b78a9 📎 Move all deps to dev-dependencies on frontend package.json
All they only needed for build process.
2025-12-23 13:10:58 +01:00
Andrey Antukh
099b78affd 📎 Update frontend yarn.lock file 2025-12-23 13:10:58 +01:00
Andrey Antukh
78cc3f0aa4 📎 Add immutable dependency to vendor/draft-js 2025-12-23 13:10:58 +01:00
Andrey Antukh
76f5f12808 ⬆️ Update dependencies on exporter 2025-12-23 13:10:58 +01:00
Alejandro Alonso
cb325282ec Merge pull request #7994 from penpot/alotor-fix-font-style
🐛 Fix problem when changing colors with multiple fonts
2025-12-23 07:34:41 +01:00
Andrey Antukh
01ecde3bfa Add the ability to add relations on penpot sdk (#7987)
*  Add the ability to add relations on penpot sdk

* 📎 Remove debug console log
2025-12-22 20:55:31 +01:00
Andrey Antukh
047483a70a 🐛 Fix deleted files thumbnails generation 2025-12-22 20:20:43 +01:00
Alonso Torres
4000ec8762 🐛 Fix problem resizing auto size layouts (#7995) 2025-12-22 20:17:11 +01:00
Andrey Antukh
8cb2f27de8 ♻️ Move file permissions to binfile common ns 2025-12-22 20:16:41 +01:00
Andrey Antukh
0433336fc9 📎 Use correct criterium version on frontend deps 2025-12-22 20:16:41 +01:00
Andrey Antukh
ce234fbeda Allow get thumbnails for deleted files 2025-12-22 20:16:41 +01:00
Andrey Antukh
fc4d31eed7 Add minor efficiency improvements to deleted dashboard page 2025-12-22 20:16:41 +01:00
María Valderrama
c670aac339 🎉 Added deleted files to dashboard 2025-12-22 20:16:41 +01:00
Andrés Moya
1d3fb5434f Enable shadow tokens by default (#7996) 2025-12-22 18:17:29 +01:00
Andrey Antukh
f478399ae0 Merge remote-tracking branch 'origin/staging-render' into develop 2025-12-22 17:28:18 +01:00
Andrey Antukh
6a1854f180 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-22 17:28:01 +01:00
Andrés Moya
0858e297e5 🎉 Add composite tokens to plugins API (#7992) 2025-12-22 17:14:54 +01:00
alonso.torres
bd580ab159 🐛 Fix problem when changing colors with multiple fonts 2025-12-22 17:14:37 +01:00
Alejandro Alonso
5780a43fe0 🐛 Fix object added in different page (#7988) 2025-12-22 16:59:47 +01:00
Alejandro Alonso
737eceda3a 🐛 Fix unmasking shapes (#7989) 2025-12-22 16:59:04 +01:00
Alonso Torres
923c3c2dbd 🐛 Fix font weight token (#7991) 2025-12-22 16:58:26 +01:00
Alejandro Alonso
a14b4561e7 🐛 Fix comment bubbles (#7990) 2025-12-22 16:57:45 +01:00
Andrey Antukh
bb5568e15a 🎉 Enable hindi translations on the application 2025-12-22 16:57:00 +01:00
Pablo Alba
5cbcec3db6 🐛 Fix "maximum call stack size exceeded" crash on variant 2025-12-22 16:57:00 +01:00
Anonymous
105e1fe86c 🌐 Add translations for: Spanish
Currently translated at 97.1% (1940 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2025-12-22 16:34:43 +01:00
Yaron Shahrabani
3e0a916883 🌐 Add translations for: Hebrew
Currently translated at 99.3% (1985 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2025-12-22 16:34:42 +01:00
Ahmad HosseinBor
4f80238bc2 🌐 Add translations for: Persian
Currently translated at 39.2% (783 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2025-12-22 16:34:42 +01:00
Alejandro Alonso
5156cc5d9a 🌐 Add translations for: Yoruba
Currently translated at 58.8% (1176 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2025-12-22 16:34:42 +01:00
Yessenia Villarte Vaca
42c46b6cfc 🌐 Add translations for: Spanish (Latin America)
Currently translated at 6.5% (131 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es_419/
2025-12-22 16:34:41 +01:00
VKing9
8b3c40b35e 🌐 Add translations for: Hindi
Currently translated at 99.6% (1991 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2025-12-22 16:34:41 +01:00
Andy Li
d3996e5fb1 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 80.1% (1601 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2025-12-22 16:34:40 +01:00
Anonymous
0c42bca866 🌐 Add translations for: German
Currently translated at 95.5% (1908 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-22 16:34:40 +01:00
Marius
e5685c1f1c 🌐 Add translations for: German
Currently translated at 95.5% (1908 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-22 16:34:40 +01:00
Anonymous
2784209bde 🌐 Add translations for: Turkish
Currently translated at 99.5% (1989 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2025-12-22 16:34:40 +01:00
Alejandro Alonso
024f460e99 🌐 Add translations for: Igbo
Currently translated at 25.6% (512 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2025-12-22 16:34:39 +01:00
Anonymous
1d9b76b62a 🌐 Add translations for: Romanian
Currently translated at 97.2% (1942 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2025-12-22 16:34:39 +01:00
Shuaib Zahda
7e17a75b7d 🌐 Add translations for: Arabic
Currently translated at 56.4% (1127 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2025-12-22 16:34:39 +01:00
Anonymous
ca093d6fae 🌐 Add translations for: French
Currently translated at 96.8% (1934 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-22 16:34:38 +01:00
Alexandre Pawlak
0f0b7562b5 🌐 Add translations for: French
Currently translated at 96.8% (1934 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-22 16:34:38 +01:00
Anonymous
9cdc694697 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 90.3% (1804 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2025-12-22 16:34:37 +01:00
Dário
b972a4033b 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 90.3% (1804 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2025-12-22 16:34:37 +01:00
Anonymous
cbe9f4da51 🌐 Add translations for: Swedish
Currently translated at 99.3% (1985 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-12-22 16:34:37 +01:00
Anonymous
c583bde9e3 🌐 Add translations for: Russian
Currently translated at 76.8% (1534 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2025-12-22 16:34:37 +01:00
Vint Prox
3911ebdc4e 🌐 Add translations for: Russian
Currently translated at 76.8% (1534 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2025-12-22 16:34:36 +01:00
Anonymous
3e3b18667b 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 69.9% (1396 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-12-22 16:34:36 +01:00
Anonymous
ed81c9b8df 🌐 Add translations for: Dutch
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-12-22 16:34:36 +01:00
Sebastiaan Pasma
fbdf98d29c 🌐 Add translations for: Dutch
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-12-22 16:34:36 +01:00
Anonymous
e603825a55 🌐 Add translations for: Italian
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-12-22 16:34:35 +01:00
Valentina Chapellu
1d724783e6 🌐 Add translations for: Italian
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-12-22 16:34:35 +01:00
Hosted Weblate
e0abe7dcb5 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2025-12-22 16:33:05 +01:00
Andrey Antukh
5c1bbf5be8 Merge remote-tracking branch 'weblate/develop' into develop 2025-12-22 16:32:30 +01:00
Pablo Alba
bbb0d58190 🐛 Fix "maximum call stack size exceeded" crash on variant 2025-12-22 16:27:10 +01:00
Andrey Antukh
88dcf9d1fe 🐛 Mark rpc calls as authenticated when shared key is used (#7901) 2025-12-22 12:18:36 +01:00
Alejandro Alonso
fe44c14bac Merge pull request #7982 from penpot/niwinz-staging-import-bucket
🐛 Prefill storage object bucket if it comes nil on import binfile
2025-12-22 12:17:16 +01:00
Belén Albeza
20061067ad 🐛 Fix text editor not getting focus back after font variant change 2025-12-22 11:18:25 +01:00
Andrey Antukh
336173645e 🐛 Fix regression on export shape on plungins API 2025-12-22 10:41:42 +01:00
Andrey Antukh
2acf15958b Merge branch 'staging-render' into develop 2025-12-22 09:24:04 +01:00
Andrey Antukh
08267de242 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-22 09:23:48 +01:00
Pablo Alba
35fb376a78 Add proxypass to caddyfile on devenv (#7985) 2025-12-22 09:21:22 +01:00
Andrey Antukh
83bb4bf221 🐛 Prefill storage object bucket if it comes nil on import binfile 2025-12-19 09:32:51 +01:00
Henrik Steffens
dba6ae2820 🌐 Add translations for: German
Currently translated at 95.8% (1911 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-17 13:00:25 +01:00
Marius
ada101c236 🌐 Add translations for: German
Currently translated at 95.8% (1911 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-17 13:00:24 +01:00
Marius
ea48fb5825 🌐 Add translations for: German
Currently translated at 90.4% (1804 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-16 11:00:25 +01:00
Alejandro Alonso
15ed25ca79 Merge pull request #7966 from penpot/niwinz-staging-abrreviate
🐛 Fix incorrect string truncation with abbreviate template filter
2025-12-12 13:53:33 +01:00
Andrey Antukh
9aa387a473 🐛 Fix incorrect string truncation with abbreviate template filter 2025-12-12 13:50:46 +01:00
Alejandro Alonso
67ba91b4b9 Merge pull request #7971 from penpot/niwinz-staging-bugfix-6
🐛 Fix tokens-lib encoding when value is nilable
2025-12-12 13:46:06 +01:00
Alejandro Alonso
f67f1a6a0e Merge pull request #7972 from penpot/niwinz-staging-bugfix-7
🐛 Fix exception on assinging gradient to shadow on multiple selection
2025-12-12 13:42:39 +01:00
Alejandro Alonso
82d3e2024e Merge pull request #7973 from penpot/niwinz-staging-worker-scheduler
🐛 Fix incorrect redis connection error handling
2025-12-12 13:23:49 +01:00
Alejandro Alonso
4bd846c16d Merge pull request #7969 from penpot/niwinz-staging-fix-ratelimit
🐛 Fix issue on reading rlimit config
2025-12-12 13:22:53 +01:00
alonso.torres
8fde6b28ed 🐛 Fix problems with alignments and margins 2025-12-12 13:21:04 +01:00
alonso.torres
63325ec796 🐛 Fix problem with flex fill size distribution 2025-12-12 13:21:04 +01:00
alonso.torres
84415476d0 🐛 Fix problem with reflow layout 2025-12-12 13:21:04 +01:00
Andrey Antukh
94f95ca6b8 🐛 Fix incorrect redis connection error handling 2025-12-12 12:33:38 +01:00
Andrey Antukh
507bf7445b 🐛 Fix tokens-lib encoding when value is nilable 2025-12-12 11:42:15 +01:00
Andrey Antukh
81b72c5acd 🐛 Fix exception on assinging gradient to shadow on multiple selection 2025-12-12 11:24:53 +01:00
Andrey Antukh
974495e08f Reduce log level for profile picture download error
Because it is not blocking operation and does not provents user
to proceed.
2025-12-12 08:17:13 +01:00
Andrey Antukh
2ed39e43c3 🐛 Fix issue on reading rlimit config 2025-12-11 23:50:01 +01:00
Stephan Paternotte
b45bdd723f 🌐 Add translations for: Dutch
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-12-09 22:00:21 +00:00
Ingrid Pigueron
8696044620 🌐 Add translations for: French
Currently translated at 97.1% (1937 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-09 22:00:19 +00:00
Ingrid Pigueron
4f3ca6422c 🌐 Add translations for: French
Currently translated at 96.8% (1931 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-08 19:40:14 +01:00
Nicola Bortoletto
1c03457fda 🌐 Add translations for: Italian
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-12-06 07:00:19 +00:00
VKing9
74d4b9b045 🌐 Add translations for: Hindi
Currently translated at 100.0% (1994 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2025-12-04 11:00:32 +01:00
Yaron Shahrabani
60df56caa3 🌐 Add translations for: Hebrew
Currently translated at 99.6% (1988 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2025-12-02 21:00:32 +00:00
Nicola Bortoletto
34da754357 🌐 Add translations for: Italian
Currently translated at 98.9% (1973 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-11-28 05:00:28 +00:00
Oğuz Ersen
39eafae251 🌐 Add translations for: Turkish
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2025-11-25 16:51:25 +00:00
Edgars Andersons
e1e09b7f96 🌐 Add translations for: Latvian
Currently translated at 94.0% (1876 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-11-25 16:51:24 +00:00
Stephan Paternotte
3b39980f2f 🌐 Add translations for: Dutch
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-11-25 16:51:23 +00:00
Anton Palmqvist
223b12d2c7 🌐 Add translations for: Swedish
Currently translated at 99.6% (1987 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-11-25 16:51:21 +00:00
488 changed files with 82701 additions and 3454 deletions

View File

@@ -0,0 +1,14 @@
name: _STAGING RENDER
on:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging-render"
build_wasm: "yes"
build_storybook: "yes"

View File

@@ -33,7 +33,7 @@ jobs:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }} MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: | TEXT: |
🐳 *[PENPOT] Docker image available.* 🐳 *[PENPOT] Docker image available: ${{ github.ref_name }}*
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra @infra

View File

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

View File

@@ -51,6 +51,51 @@ jobs:
run: | run: |
./scripts/test ./scripts/test
test-plugins:
name: Plugins Runtime Linter & Tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Setup Node
id: setup-node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
pnpm install;
- name: Run Lint
working-directory: ./plugins
run: pnpm run lint
- name: Run Format Check
working-directory: ./plugins
run: pnpm run format:check
- name: Run Test
working-directory: ./plugins
run: pnpm run test
- name: Build runtime
working-directory: ./plugins
run: pnpm run build
- name: Build plugins
working-directory: ./plugins
run: pnpm run build:plugins
- name: Build styles
working-directory: ./plugins
run: pnpm run build:styles-example
test-frontend: test-frontend:
name: "Frontend Tests" name: "Frontend Tests"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -67,6 +112,8 @@ jobs:
- name: Component Tests - name: Component Tests
working-directory: ./frontend working-directory: ./frontend
env:
VITEST_BROWSER_TIMEOUT: 120000
run: | run: |
./scripts/test-components ./scripts/test-components

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
.pnpm-store
*-init.clj *-init.clj
*.css.json *.css.json
*.jar *.jar

2
.nvmrc
View File

@@ -1 +1 @@
v22.19.0 v22.21.1

View File

@@ -1,5 +1,18 @@
# CHANGELOG # CHANGELOG
## 2.14.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
### :bug: Bugs fixed
## 2.13.0 (Unreleased) ## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
@@ -12,16 +25,29 @@
### :sparkles: New features & Enhancements ### :sparkles: New features & Enhancements
- Add new Box Shadow Tokens [Taiga #10201](https://tree.taiga.io/project/penpot/us/10201)
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474) - Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
- Add deleted files to dashboard [Taiga #8149](https://tree.taiga.io/project/penpot/us/8149)
### :bug: Bugs fixed ### :bug: Bugs fixed
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565) - Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460) - Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339) - Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
- Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915)
## 2.12.0 (Unreleased) ## 2.12.1
### :bug: Bugs fixed
- Fix setting a portion of text as bold or underline messes things up [Github #7980](https://github.com/penpot/penpot/issues/7980)
- Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935)
- Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
## 2.12.0
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
@@ -83,6 +109,7 @@ example. It's still usable as before, we just removed the example.
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome) - Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887) - Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
- Enable Hindi translations on the application
### :sparkles: New features & Enhancements ### :sparkles: New features & Enhancements
@@ -116,6 +143,7 @@ example. It's still usable as before, we just removed the example.
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841) - Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
- Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492) - Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492)
- Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843) - Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843)
- Fix unicode handling on email template abbreviation filter [Github #7966](https://github.com/penpot/penpot/pull/7966)
## 2.11.1 ## 2.11.1

View File

@@ -240,4 +240,4 @@
</div> </div>
</body> </body>
</html> </html>

View File

@@ -36,17 +36,6 @@
[integrant.core :as ig] [integrant.core :as ig]
[yetti.response :as-alias yres])) [yetti.response :as-alias yres]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn obfuscate-string
[s]
(if (< (count s) 10)
(apply str (take (count s) (repeat "*")))
(str (subs s 0 5)
(apply str (take (- (count s) 5) (repeat "*"))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OIDC PROVIDER (GENERIC) ;; OIDC PROVIDER (GENERIC)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -177,7 +166,7 @@
(l/inf :hint "provider initialized" (l/inf :hint "provider initialized"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))) :client-secret (d/obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
@@ -222,7 +211,7 @@
(l/inf :hint "provider initialized" (l/inf :hint "provider initialized"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))) :client-secret (d/obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
@@ -299,7 +288,7 @@
(l/inf :hint "provider initialized" (l/inf :hint "provider initialized"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))) :client-secret (d/obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
@@ -341,7 +330,7 @@
:provider "gitlab" :provider "gitlab"
:base-uri (:base-uri provider) :base-uri (:base-uri provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))) :client-secret (d/obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
(ex/raise :type ::internal (ex/raise :type ::internal
@@ -361,7 +350,7 @@
(l/inf :hint "provider initialized" (l/inf :hint "provider initialized"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))) :client-secret (d/obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
@@ -459,7 +448,7 @@
(l/trc :hint "fetch access token" (l/trc :hint "fetch access token"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider)) :client-secret (d/obfuscate-string (:client-secret provider))
:grant-type (:grant_type params) :grant-type (:grant_type params)
:redirect-uri (:redirect_uri params)) :redirect-uri (:redirect_uri params))
@@ -512,7 +501,7 @@
[cfg provider tdata] [cfg provider tdata]
(l/trc :hint "fetch user info" (l/trc :hint "fetch user info"
:uri (:user-uri provider) :uri (:user-uri provider)
:token (obfuscate-string (:token/access tdata))) :token (d/obfuscate-string (:token/access tdata)))
(let [params {:uri (:user-uri provider) (let [params {:uri (:user-uri provider)
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))} :headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}

View File

@@ -331,6 +331,81 @@
(set/difference cfeat/backend-only-features)) (set/difference cfeat/backend-only-features))
#{})))) #{}))))
(defn check-file-exists
[cfg id & {:keys [include-deleted?]
:or {include-deleted? false}
:as options}]
(db/get-with-sql cfg [sql:get-minimal-file id]
{:db/remove-deleted (not include-deleted?)}))
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?")
(defn- get-file-permissions*
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-file-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions* conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-file-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(defn get-project (defn get-project
[cfg project-id] [cfg project-id]
(db/get cfg :project {:id project-id})) (db/get cfg :project {:id project-id}))

View File

@@ -821,9 +821,10 @@
entries (keep (match-storage-entry-fn) entries)] entries (keep (match-storage-entry-fn) entries)]
(doseq [{:keys [id entry]} entries] (doseq [{:keys [id entry]} entries]
(let [object (->> (read-entry input entry) (let [object (-> (read-entry input entry)
(decode-storage-object) (decode-storage-object)
(validate-storage-object)) (update :bucket d/nilv sto/default-bucket)
(validate-storage-object))
ext (cmedia/mtype->extension (:content-type object)) ext (cmedia/mtype->extension (:content-type object))
path (str "objects/" id ext) path (str "objects/" id ext)

View File

@@ -30,7 +30,7 @@
(defn- get-file-media-object (defn- get-file-media-object
[pool id] [pool id]
(db/get pool :file-media-object {:id id})) (db/get pool :file-media-object {:id id} {::db/remove-deleted false}))
(defn- serve-object-from-s3 (defn- serve-object-from-s3
[{:keys [::sto/storage] :as cfg} obj] [{:keys [::sto/storage] :as cfg} obj]

View File

@@ -309,7 +309,7 @@
(fn [request] (fn [request]
(let [key (yreq/get-header request "x-shared-key")] (let [key (yreq/get-header request "x-shared-key")]
(if (= key shared-key) (if (= key shared-key)
(handler request) (handler (assoc request ::http/auth-with-shared-key true))
{::yres/status 403})))) {::yres/status 403}))))
(fn [_ _] (fn [_ _]
{::yres/status 403}))) {::yres/status 403})))

View File

@@ -14,6 +14,7 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.uri :as u] [app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http :as-alias http] [app.http :as-alias http]
@@ -92,7 +93,11 @@
(let [handler-name (:type path-params) (let [handler-name (:type path-params)
etag (yreq/get-header request "if-none-match") etag (yreq/get-header request "if-none-match")
profile-id (or (::session/profile-id request) profile-id (or (::session/profile-id request)
(::actoken/profile-id request)) (::actoken/profile-id request)
(if (::http/auth-with-shared-key request)
uuid/zero
nil))
ip-addr (inet/parse-request request) ip-addr (inet/parse-request request)
data (-> params data (-> params

View File

@@ -307,7 +307,8 @@
:content-type (:mtype input)})] :content-type (:mtype input)})]
(:id sobject)) (:id sobject))
(catch Throwable cause (catch Throwable cause
(l/err :hint "unable to import profile picture" (l/wrn :hint "unable to import profile picture"
:uri uri
:cause cause) :cause cause)
nil))) nil)))

View File

@@ -79,85 +79,14 @@
;; --- FILE PERMISSIONS ;; --- FILE PERMISSIONS
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
and f.deleted_at is null
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
and f.deleted_at is null
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?
and f.deleted_at is null")
(defn get-file-permissions
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(def has-edit-permissions? (def has-edit-permissions?
(perms/make-edition-predicate-fn get-permissions)) (perms/make-edition-predicate-fn bfc/get-file-permissions))
(def has-read-permissions? (def has-read-permissions?
(perms/make-read-predicate-fn get-permissions)) (perms/make-read-predicate-fn bfc/get-file-permissions))
(def has-comment-permissions? (def has-comment-permissions?
(perms/make-comment-predicate-fn get-permissions)) (perms/make-comment-predicate-fn bfc/get-file-permissions))
(def check-edition-permissions! (def check-edition-permissions!
(perms/make-check-fn has-edit-permissions?)) (perms/make-check-fn has-edit-permissions?))
@@ -170,7 +99,7 @@
(defn check-comment-permissions! (defn check-comment-permissions!
[conn profile-id file-id share-id] [conn profile-id file-id share-id]
(let [perms (get-permissions conn profile-id file-id share-id) (let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
can-read (has-read-permissions? perms) can-read (has-read-permissions? perms)
can-comment (has-comment-permissions? perms)] can-comment (has-comment-permissions? perms)]
(when-not (or can-read can-comment) (when-not (or can-read can-comment)
@@ -222,7 +151,7 @@
(defn- get-minimal-file-with-perms (defn- get-minimal-file-with-perms
[cfg {:keys [:id ::rpc/profile-id]}] [cfg {:keys [:id ::rpc/profile-id]}]
(let [mfile (get-minimal-file cfg id) (let [mfile (get-minimal-file cfg id)
perms (get-permissions cfg profile-id id)] perms (bfc/get-file-permissions cfg profile-id id)]
(assoc mfile :permissions perms))) (assoc mfile :permissions perms)))
(defn get-file-etag (defn get-file-etag
@@ -248,7 +177,7 @@
;; will be already prefetched and we just reuse them instead ;; will be already prefetched and we just reuse them instead
;; of making an additional database queries. ;; of making an additional database queries.
(let [perms (or (:permissions (::cond/object params)) (let [perms (or (:permissions (::cond/object params))
(get-permissions conn profile-id id))] (bfc/get-file-permissions conn profile-id id))]
(check-read-permissions! perms) (check-read-permissions! perms)
(let [team (teams/get-team conn (let [team (teams/get-team conn
@@ -311,7 +240,7 @@
::sm/result schema:file-fragment} ::sm/result schema:file-fragment}
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}] [cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
(db/run! cfg (fn [cfg] (db/run! cfg (fn [cfg]
(let [perms (get-permissions cfg profile-id file-id share-id)] (let [perms (bfc/get-file-permissions cfg profile-id file-id share-id)]
(check-read-permissions! perms) (check-read-permissions! perms)
(-> (get-file-fragment cfg file-id fragment-id) (-> (get-file-fragment cfg file-id fragment-id)
(rph/with-http-cache long-cache-duration)))))) (rph/with-http-cache long-cache-duration))))))
@@ -456,8 +385,7 @@
:code :params-validation :code :params-validation
:hint "page-id is required when object-id is provided")) :hint "page-id is required when object-id is provided"))
(let [perms (get-permissions conn profile-id file-id share-id) (let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
file (bfc/get-file cfg file-id :read-only? true) file (bfc/get-file cfg file-id :read-only? true)
proj (db/get conn :project {:id (:project-id file)}) proj (db/get conn :project {:id (:project-id file)})
@@ -688,11 +616,10 @@
"Get libraries used by the specified file." "Get libraries used by the specified file."
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:get-file-libraries} ::sm/params schema:get-file-libraries}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] [cfg {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)] (bfc/check-file-exists cfg file-id)
(check-read-permissions! conn profile-id file-id) (check-read-permissions! cfg profile-id file-id)
(bfc/get-file-libraries conn file-id))) (bfc/get-file-libraries cfg file-id))
;; --- COMMAND QUERY: Files that use this File library ;; --- COMMAND QUERY: Files that use this File library
@@ -777,7 +704,6 @@
f.created_at, f.created_at,
f.modified_at, f.modified_at,
f.name, f.name,
f.is_shared,
f.deleted_at AS will_be_deleted_at, f.deleted_at AS will_be_deleted_at,
ft.media_id AS thumbnail_id, ft.media_id AS thumbnail_id,
row_number() OVER w AS row_num, row_number() OVER w AS row_num,
@@ -785,8 +711,7 @@
FROM file AS f FROM file AS f
INNER JOIN project AS p ON (p.id = f.project_id) INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id
AND ft.revn = f.revn AND ft.revn = f.revn)
AND ft.deleted_at is null)
WHERE p.team_id = ? WHERE p.team_id = ?
AND (p.deleted_at > ?::timestamptz OR AND (p.deleted_at > ?::timestamptz OR
f.deleted_at > ?::timestamptz) f.deleted_at > ?::timestamptz)
@@ -888,7 +813,7 @@
AND (f.deleted_at IS NULL OR f.deleted_at > now()) AND (f.deleted_at IS NULL OR f.deleted_at > now())
ORDER BY f.created_at ASC;") ORDER BY f.created_at ASC;")
(defn- absorb-library-by-file! (defn- absorb-library-by-file
[cfg ldata file-id] [cfg ldata file-id]
(assert (db/connection-map? cfg) (assert (db/connection-map? cfg)
@@ -912,7 +837,7 @@
:modified-at (ct/now) :modified-at (ct/now)
:has-media-trimmed false})))) :has-media-trimmed false}))))
(defn- absorb-library (defn- absorb-library*
"Find all files using a shared library, and absorb all library assets "Find all files using a shared library, and absorb all library assets
into the file local libraries" into the file local libraries"
[cfg {:keys [id data] :as library}] [cfg {:keys [id data] :as library}]
@@ -927,10 +852,10 @@
:library-id (str id) :library-id (str id)
:files (str/join "," (map str ids))) :files (str/join "," (map str ids)))
(run! (partial absorb-library-by-file! cfg data) ids) (run! (partial absorb-library-by-file cfg data) ids)
library)) library))
(defn absorb-library! (defn absorb-library
[{:keys [::db/conn] :as cfg} id] [{:keys [::db/conn] :as cfg} id]
(let [file (-> (bfc/get-file cfg id (let [file (-> (bfc/get-file cfg id
:realize? true :realize? true
@@ -947,7 +872,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file))) (cfeat/check-file-features! (:features file)))
(absorb-library cfg file))) (absorb-library* cfg file)))
(defn- set-file-shared (defn- set-file-shared
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
@@ -960,14 +885,14 @@
;; file, we need to perform more complex operation, ;; file, we need to perform more complex operation,
;; so in this case we retrieve the complete file and ;; so in this case we retrieve the complete file and
;; perform all required validations. ;; perform all required validations.
(let [file (-> (absorb-library! cfg id) (let [file (-> (absorb-library cfg id)
(assoc :is-shared false))] (assoc :is-shared false))]
(db/delete! conn :file-library-rel {:library-file-id id}) (db/delete! conn :file-library-rel {:library-file-id id})
(db/update! conn :file (db/update! conn :file
{:is-shared false {:is-shared false
:modified-at (ct/now)} :modified-at (ct/now)}
{:id id}) {:id id})
(select-keys file [:id :name :is-shared])) file)
(and (false? (:is-shared file)) (and (false? (:is-shared file))
(true? (:is-shared params))) (true? (:is-shared params)))
@@ -1014,6 +939,11 @@
{:id file-id} {:id file-id}
{::db/return-keys [:id :name :is-shared :deleted-at {::db/return-keys [:id :name :is-shared :deleted-at
:project-id :created-at :modified-at]})] :project-id :created-at :modified-at]})]
;; Remove all possible relations for that file
(db/delete! conn :file-library-rel
{:library-file-id file-id})
(wrk/submit! {::db/conn conn (wrk/submit! {::db/conn conn
::wrk/task :delete-object ::wrk/task :delete-object
::wrk/params {:object :file ::wrk/params {:object :file
@@ -1164,47 +1094,53 @@
;; --- MUTATION COMMAND: delete-files-immediatelly ;; --- MUTATION COMMAND: delete-files-immediatelly
(def ^:private sql:delete-team-files (def ^:private sql:get-delete-team-files-candidates
"UPDATE file AS uf SET deleted_at = ?::timestamptz "SELECT f.id
FROM ( FROM file AS f
SELECT f.id JOIN project AS p ON (p.id = f.project_id)
FROM file AS f JOIN team AS t ON (t.id = p.team_id)
JOIN project AS p ON (p.id = f.project_id) WHERE t.deleted_at IS NULL
JOIN team AS t ON (t.id = p.team_id) AND t.id = ?
WHERE t.deleted_at IS NULL AND f.id = ANY(?::uuid[])")
AND t.id = ?
AND f.id = ANY(?::uuid[])
) AS subquery
WHERE uf.id = subquery.id
RETURNING uf.id, uf.deleted_at;")
(def ^:private schema:permanently-delete-team-files (def ^:private schema:permanently-delete-team-files
[:map {:title "permanently-delete-team-files"} [:map {:title "permanently-delete-team-files"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:ids [::sm/set ::sm/uuid]]]) [:ids [::sm/set ::sm/uuid]]])
(defn- permanently-delete-team-files
[{:keys [::db/conn]} {:keys [::rpc/request-at team-id ids]}]
(let [ids (into #{}
d/xf:map-id
(db/exec! conn [sql:get-delete-team-files-candidates team-id
(db/create-array conn "uuid" ids)]))]
(reduce (fn [acc id]
(events/tap :progress {:file-id id :index (inc (count acc)) :total (count ids)})
(db/update! conn :file
{:deleted-at request-at}
{:id id}
{::db/return-keys false})
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at request-at
:id id}})
(conj acc id))
#{}
ids)))
(sv/defmethod ::permanently-delete-team-files (sv/defmethod ::permanently-delete-team-files
"Mark the specified files to be deleted immediatelly on the "Mark the specified files to be deleted immediatelly on the
specified team. The team-id on params will be used to filter and specified team. The team-id on params will be used to filter and
check writable permissons on team." check writable permissons on team."
{::doc/added "2.12" {::doc/added "2.13"
::sm/params schema:permanently-delete-team-files ::sm/params schema:permanently-delete-team-files}
::db/transaction true}
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at team-id ids]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! pool profile-id team-id)
(sse/response #(db/tx-run! cfg permanently-delete-team-files params)))
(reduce (fn [acc {:keys [id deleted-at]}]
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at deleted-at
:id id}})
(conj acc id))
#{}
(db/plan conn [sql:delete-team-files request-at team-id
(db/create-array conn "uuid" ids)])))
;; --- MUTATION COMMAND: restore-files-immediatelly ;; --- MUTATION COMMAND: restore-files-immediatelly
@@ -1268,7 +1204,7 @@
{:keys [files projects]} {:keys [files projects]}
(reduce (fn [result {:keys [id project-id]}] (reduce (fn [result {:keys [id project-id]}]
(let [index (-> result :files count)] (let [index (-> result :files count)]
(events/tap :progress {:file-id id :index index :total total-files}) (events/tap :progress {:file-id id :index (inc index) :total total-files})
(restore-file conn id) (restore-file conn id)
(-> result (-> result
@@ -1291,7 +1227,7 @@
(sv/defmethod ::restore-deleted-team-files (sv/defmethod ::restore-deleted-team-files
"Removes the deletion mark from the specified files (and respective "Removes the deletion mark from the specified files (and respective
projects) on the specified team." projects) on the specified team."
{::doc/added "2.12" {::doc/added "2.13"
::sse/stream? true ::sse/stream? true
::sm/params schema:restore-deleted-team-files} ::sm/params schema:restore-deleted-team-files}
[cfg params] [cfg params]

View File

@@ -199,15 +199,13 @@
[cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}] [cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
(let [team (teams/get-team conn (let [team (teams/get-team conn
:profile-id profile-id :profile-id profile-id
:file-id file-id) :file-id file-id)
file (bfc/get-file cfg file-id file (bfc/get-file cfg file-id
:include-deleted? true
:realize? true :realize? true
:read-only? true) :read-only? true)
strip-frames-with-thumbnails strip-frames-with-thumbnails
(or (nil? strip-frames-with-thumbnails) ;; if not present, default to true (or (nil? strip-frames-with-thumbnails) ;; if not present, default to true
(true? strip-frames-with-thumbnails))] (true? strip-frames-with-thumbnails))]
@@ -333,12 +331,16 @@
;; --- MUTATION COMMAND: create-file-thumbnail ;; --- MUTATION COMMAND: create-file-thumbnail
(defn- create-file-thumbnail! (defn- create-file-thumbnail
[{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}] [{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id revn props media] :as params}]
(media/validate-media-type! media) (media/validate-media-type! media)
(media/validate-media-size! media) (media/validate-media-size! media)
(let [props (db/tjson (or props {})) (let [file (bfc/get-file cfg file-id
:include-deleted? true
:load-data? false)
props (db/tjson (or props {}))
path (:path media) path (:path media)
mtype (:mtype media) mtype (:mtype media)
hash (sto/calculate-hash path) hash (sto/calculate-hash path)
@@ -367,7 +369,7 @@
(db/update! conn :file-thumbnail (db/update! conn :file-thumbnail
{:media-id (:id media) {:media-id (:id media)
:deleted-at nil :deleted-at (:deleted-at file)
:updated-at tnow :updated-at tnow
:props props} :props props}
{:file-id file-id {:file-id file-id
@@ -378,6 +380,7 @@
:revn revn :revn revn
:created-at tnow :created-at tnow
:updated-at tnow :updated-at tnow
:deleted-at (:deleted-at file)
:props props :props props
:media-id (:id media)})) :media-id (:id media)}))
@@ -402,6 +405,8 @@
::rtry/when rtry/conflict-exception? ::rtry/when rtry/conflict-exception?
::sm/params schema:create-file-thumbnail} ::sm/params schema:create-file-thumbnail}
;; FIXME: do not run the thumbnail upload inside a transaction
[cfg {:keys [::rpc/profile-id file-id] :as params}] [cfg {:keys [::rpc/profile-id file-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
;; TODO For now we check read permissions instead of write, ;; TODO For now we check read permissions instead of write,
@@ -409,6 +414,6 @@
;; review this approach on the future. ;; review this approach on the future.
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
(when-not (db/read-only? conn) (when-not (db/read-only? conn)
(let [media (create-file-thumbnail! cfg params)] (let [media (create-file-thumbnail cfg params)]
{:uri (files/resolve-public-uri (:id media)) {:uri (files/resolve-public-uri (:id media))
:id (:id media)}))))) :id (:id media)})))))

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.fonts (ns app.rpc.commands.fonts
(:require (:require
[app.binfile.common :as bfc]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.schema :as sm] [app.common.schema :as sm]
@@ -66,7 +67,7 @@
(uuid? file-id) (uuid? file-id)
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]}) (let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]}) project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})
perms (files/get-permissions conn profile-id file-id share-id)] perms (bfc/get-file-permissions conn profile-id file-id share-id)]
(files/check-read-permissions! perms) (files/check-read-permissions! perms)
(db/query conn :team-font-variant (db/query conn :team-font-variant
{:team-id (:team-id project) {:team-id (:team-id project)

View File

@@ -13,7 +13,6 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond] [app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -121,7 +120,7 @@
[system {:keys [::rpc/profile-id file-id share-id] :as params}] [system {:keys [::rpc/profile-id file-id share-id] :as params}]
(db/run! system (db/run! system
(fn [{:keys [::db/conn] :as system}] (fn [{:keys [::db/conn] :as system}]
(let [perms (files/get-permissions conn profile-id file-id share-id) (let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
params (-> params params (-> params
(assoc ::perms perms) (assoc ::perms perms)
(assoc :profile-id profile-id))] (assoc :profile-id profile-id))]

View File

@@ -104,28 +104,29 @@
(def ^:private schema:limit (def ^:private schema:limit
[:and [:and
[:map [:map
[::name :any] [::name :keyword]
[::strategy schema:strategy] [::strategy schema:strategy]
[::key :string] [::key :string]
[::opts :string]] [::opts :string]
[:or [::capacity {:optional true} ::sm/int]
[:map [::rate {:optional true} ::sm/int]
[::capacity ::sm/int] [::interval {:optional true} ::ct/duration]
[::rate ::sm/int] [::params {:optional true} [::sm/vec :any]]
[::internal ::ct/duration] [::permits {:optional true} ::sm/int]
[::params [::sm/vec :any]]] [::unit {:optional true} [:enum :days :hours :minutes :seconds :weeks]]]
[:map [:fn (fn [attrs]
[::nreq ::sm/int] (let [contains-fn (partial contains? attrs)]
[::unit [:enum :days :hours :minutes :seconds :weeks]]]]]) (or (every? contains-fn [::capacity ::rate ::interval])
(every? contains-fn [::permits ::unit]))))]])
(def ^:private schema:limits (def ^:private schema:limits
[:map-of :keyword [::sm/vec schema:limit]]) [:map-of :keyword [::sm/vec schema:limit]])
(def ^:private valid-limit-tuple? (def ^:private valid-limit-tuple?
(sm/lazy-validator schema:limit-tuple)) (sm/validator schema:limit-tuple))
(def ^:private valid-rlimit-instance? (def ^:private valid-rlimit-instance?
(sm/lazy-validator ::rpc/rlimit)) (sm/validator ::rpc/rlimit))
(defmethod parse-limit :window (defmethod parse-limit :window
[[name strategy opts :as vlimit]] [[name strategy opts :as vlimit]]
@@ -134,16 +135,16 @@
(merge (merge
{::name name {::name name
::strategy strategy} ::strategy strategy}
(if-let [[_ nreq unit] (re-find window-opts-re opts)] (if-let [[_ permits unit] (re-find window-opts-re opts)]
(let [nreq (parse-long nreq)] (let [permits (parse-long permits)]
{::nreq nreq {::permits permits
::unit (case unit ::unit (case unit
"d" :days "d" :days
"h" :hours "h" :hours
"m" :minutes "m" :minutes
"s" :seconds "s" :seconds
"w" :weeks) "w" :weeks)
::key (str "ratelimit.window." (d/name name)) ::key (str "penpot.rlimit." (cf/get :tenant) ".window." (d/name name))
::opts opts}) ::opts opts})
(ex/raise :type :validation (ex/raise :type :validation
:code :invalid-window-limit-opts :code :invalid-window-limit-opts
@@ -164,15 +165,15 @@
::interval interval ::interval interval
::opts opts ::opts opts
::params [(->seconds interval) rate capacity] ::params [(->seconds interval) rate capacity]
::key (str "ratelimit.bucket." (d/name name))}) ::key (str "penpot.rlimit." (cf/get :tenant) ".bucket." (d/name name))})
(ex/raise :type :validation (ex/raise :type :validation
:code :invalid-bucket-limit-opts :code :invalid-bucket-limit-opts
:hint (str/ffmt "looks like '%' does not have a valid format" opts)))) :hint (str/ffmt "looks like '%' does not have a valid format" opts))))
(defmethod process-limit :bucket (defmethod process-limit :bucket
[rconn user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}] [rconn profile-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
(let [script (-> bucket-rate-limit-script (let [script (-> bucket-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id)]) (assoc ::rscript/keys [(str key "." service "." profile-id)])
(assoc ::rscript/vals (conj params (->seconds now)))) (assoc ::rscript/vals (conj params (->seconds now))))
result (rds/eval rconn script) result (rds/eval rconn script)
allowed? (boolean (nth result 0)) allowed? (boolean (nth result 0))
@@ -192,18 +193,18 @@
(assoc ::lresult/remaining remaining)))) (assoc ::lresult/remaining remaining))))
(defmethod process-limit :window (defmethod process-limit :window
[rconn user-id now {:keys [::nreq ::unit ::key ::service] :as limit}] [rconn profile-id now {:keys [::permits ::unit ::key ::service] :as limit}]
(let [ts (ct/truncate now unit) (let [ts (ct/truncate now unit)
ttl (ct/diff now (ct/plus ts {unit 1})) ttl (ct/diff now (ct/plus ts {unit 1}))
script (-> window-rate-limit-script script (-> window-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id "." (ct/format-inst ts))]) (assoc ::rscript/keys [(str key "." service "." profile-id "." (ct/format-inst ts))])
(assoc ::rscript/vals [nreq (->seconds ttl)])) (assoc ::rscript/vals [permits (->seconds ttl)]))
result (rds/eval rconn script) result (rds/eval rconn script)
allowed? (boolean (nth result 0)) allowed? (boolean (nth result 0))
remaining (nth result 1)] remaining (nth result 1)]
(l/trace :hint "limit processed" (l/trace :hint "limit processed"
:service service :service service
:limit (name (::name limit)) :name (name (::name limit))
:strategy (name (::strategy limit)) :strategy (name (::strategy limit))
:opts (::opts limit) :opts (::opts limit)
:allowed allowed? :allowed allowed?
@@ -214,8 +215,8 @@
(assoc ::lresult/reset (ct/plus ts {unit 1}))))) (assoc ::lresult/reset (ct/plus ts {unit 1})))))
(defn- process-limits (defn- process-limits
[rconn user-id limits now] [rconn profile-id limits now]
(let [results (into [] (map (partial process-limit rconn user-id now)) limits) (let [results (into [] (map (partial process-limit rconn profile-id now)) limits)
remaining (->> results remaining (->> results
(d/index-by ::name ::lresult/remaining) (d/index-by ::name ::lresult/remaining)
(uri/map->query-string)) (uri/map->query-string))
@@ -227,7 +228,7 @@
(when rejected (when rejected
(l/warn :hint "rejected rate limit" (l/warn :hint "rejected rate limit"
:user-id (str user-id) :profile-id (str profile-id)
:limit-service (-> rejected ::service name) :limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name) :limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name))) :limit-strategy (-> rejected ::strategy name)))
@@ -371,12 +372,9 @@
(defn- on-refresh-error (defn- on-refresh-error
[_ cause] [_ cause]
(when-not (instance? java.util.concurrent.RejectedExecutionException cause) (when-not (instance? java.util.concurrent.RejectedExecutionException cause)
(if-let [explain (-> cause ex-data ex/explain)] (l/warn :hint "unexpected exception on loading config"
(l/warn ::l/raw (str "unable to refresh config, invalid format:\n" explain) :cause cause
::l/sync? true) ::l/sync? true)))
(l/warn :hint "unexpected exception on loading config"
:cause cause
::l/sync? true))))
(defn- get-config-path (defn- get-config-path
[] []

View File

@@ -25,9 +25,9 @@ local allowed = filled >= requested
local newTokens = filled local newTokens = filled
if allowed then if allowed then
newTokens = filled - requested newTokens = filled - requested
redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp)
end end
redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp)
redis.call("expire", tokensKey, ttl) redis.call("expire", tokensKey, ttl)
return { allowed, newTokens } return { allowed, newTokens }

View File

@@ -35,6 +35,9 @@
:assets-s3 :s3 :assets-s3 :s3
nil))) nil)))
(def default-bucket
"file-media-object")
(def valid-buckets (def valid-buckets
#{"file-media-object" #{"file-media-object"
"team-font-variant" "team-font-variant"

View File

@@ -25,7 +25,7 @@
[app.common.time :as ct] [app.common.time :as ct]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.storage :as-alias sto] [app.storage :as sto]
[app.storage.impl :as impl] [app.storage.impl :as impl]
[integrant.core :as ig])) [integrant.core :as ig]))
@@ -130,7 +130,7 @@
[{:keys [metadata]}] [{:keys [metadata]}]
(or (some-> metadata :bucket) (or (some-> metadata :bucket)
(some-> metadata :reference d/name) (some-> metadata :reference d/name)
"file-media-object")) sto/default-bucket))
(defn- process-objects! (defn- process-objects!
[conn has-refs? bucket objects] [conn has-refs? bucket objects]

View File

@@ -45,7 +45,8 @@
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
(db/update! conn :file (db/update! conn :file
{:deleted-at deleted-at} {:deleted-at deleted-at
:is-shared false}
{:id id} {:id id}
{::db/return-keys false}) {::db/return-keys false})
@@ -53,7 +54,7 @@
(not *team-deletion*)) (not *team-deletion*))
;; NOTE: we don't prevent file deletion on absorb operation failure ;; NOTE: we don't prevent file deletion on absorb operation failure
(try (try
(db/tx-run! cfg files/absorb-library! id) (db/tx-run! cfg files/absorb-library id)
(catch Throwable cause (catch Throwable cause
(l/warn :hint "error on absorbing library" (l/warn :hint "error on absorbing library"
:file-id id :file-id id

View File

@@ -7,10 +7,18 @@
(ns app.util.template (ns app.util.template
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[cuerdas.core :as str]
[selmer.filters :as sf]
[selmer.parser :as sp])) [selmer.parser :as sp]))
;; (sp/cache-off!) ;; (sp/cache-off!)
(sf/add-filter! :abbreviate
(fn [s n]
(let [n (parse-long n)]
(str/abbreviate s n))))
(defn render (defn render
[path context] [path context]
(try (try

View File

@@ -137,33 +137,34 @@ RETURNING task.id, task.queue")
::wait))) ::wait)))
(run-batch [] (run-batch []
(let [rconn (rds/connect cfg)] (try
(try (let [rconn (rds/connect cfg)]
(-> cfg (try
(assoc ::rds/conn rconn) (-> cfg
(db/tx-run! run-batch')) (assoc ::rds/conn rconn)
(db/tx-run! run-batch'))
(finally
(.close ^AutoCloseable rconn))))
(catch InterruptedException cause (catch InterruptedException cause
(throw cause)) (throw cause))
(catch Exception cause
(cond
(rds/exception? cause)
(do
(l/wrn :hint "redis exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
(db/sql-exception? cause) (catch Exception cause
(do (cond
(l/wrn :hint "database exception (will retry in an instant)" :cause cause) (rds/exception? cause)
(px/sleep timeout)) (do
(l/wrn :hint "redis exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
:else (db/sql-exception? cause)
(do (do
(l/err :hint "unhandled exception (will retry in an instant)" :cause cause) (l/wrn :hint "database exception (will retry in an instant)" :cause cause)
(px/sleep timeout)))) (px/sleep timeout))
(finally :else
(.close ^AutoCloseable rconn))))) (do
(l/err :hint "unhandled exception (will retry in an instant)" :cause cause)
(px/sleep timeout))))))
(dispatcher [] (dispatcher []
(l/inf :hint "started") (l/inf :hint "started")
@@ -176,7 +177,7 @@ RETURNING task.id, task.queue")
(catch InterruptedException _ (catch InterruptedException _
(l/trc :hint "interrupted")) (l/trc :hint "interrupted"))
(catch Throwable cause (catch Throwable cause
(l/err :hint " unexpected exception" :cause cause)) (l/err :hint "unexpected exception" :cause cause))
(finally (finally
(l/inf :hint "terminated"))))] (l/inf :hint "terminated"))))]

View File

@@ -595,8 +595,8 @@
(px/exec! :virtual #(rcp/write-body-to-stream body nil output)) (px/exec! :virtual #(rcp/write-body-to-stream body nil output))
(into [] (into []
(map (fn [{:keys [event data]}] (map (fn [{:keys [event data]}]
[(keyword event) (d/vec2 (keyword event)
(tr/decode-str data)])) (tr/decode-str data))))
(parse-sse (slurp' input))) (parse-sse (slurp' input)))
(finally (finally
(.close input))))) (.close input)))))

View File

@@ -1921,7 +1921,11 @@
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (= (:ids data) result))) (t/is (fn? result))
(let [[ev1 ev2 :as events] (th/consume-sse result)]
(t/is (= 2 (count events)))
(t/is (= (:ids data) (val ev2)))))
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])] (let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
(t/is (= (:deleted-at row) now))))))) (t/is (= (:deleted-at row) now)))))))

View File

@@ -29,8 +29,7 @@
java-http-clj/java-http-clj {:mvn/version "0.4.3"} java-http-clj/java-http-clj {:mvn/version "0.4.3"}
integrant/integrant {:mvn/version "1.0.0"} integrant/integrant {:mvn/version "1.0.0"}
funcool/tubax {:mvn/version "2021.05.20-0"} funcool/cuerdas {:mvn/version "2026.415"}
funcool/cuerdas {:mvn/version "2025.06.16-414"}
funcool/promesa funcool/promesa
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8" {:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"
:git/url "https://github.com/funcool/promesa"} :git/url "https://github.com/funcool/promesa"}

View File

@@ -1024,6 +1024,26 @@
:clj :clj
(sort comp-fn items)))) (sort comp-fn items))))
(defn obfuscate-string
"Obfuscates potentially sensitive values.
- One-arg arity:
* For strings shorter than 10 characters, all characters are replaced by `*`.
* For longer strings, the first 5 characters are preserved and the rest obfuscated.
- Two-arg arity accepts a boolean `full?` that, when true, replaces the whole value
by `*`, preserving only the length."
([v]
(obfuscate-string v false))
([v full?]
(let [s (str v)
n (count s)]
(cond
(zero? n) s
full? (apply str (repeat n "*"))
(< n 10) (apply str (repeat n "*"))
:else (str (subs s 0 5)
(apply str (repeat (- n 5) "*")))))))
(defn reorder (defn reorder
"Reorder a vector by moving one of their items from some position to some space between positions. "Reorder a vector by moving one of their items from some position to some space between positions.
It clamps the position numbers to a valid range." It clamps the position numbers to a valid range."

View File

@@ -10,6 +10,7 @@
(:refer-clojure :exclude [instance?]) (:refer-clojure :exclude [instance?])
(:require (:require
#?(:clj [clojure.stacktrace :as strace]) #?(:clj [clojure.stacktrace :as strace])
[app.common.data :refer [obfuscate-string]]
[app.common.pprint :as pp] [app.common.pprint :as pp]
[app.common.schema :as sm] [app.common.schema :as sm]
[clojure.core :as c] [clojure.core :as c]
@@ -19,6 +20,10 @@
(:import (:import
clojure.lang.IPersistentMap))) clojure.lang.IPersistentMap)))
(def ^:private sensitive-fields
"Keys whose values must be obfuscated in validation explains."
#{:password :old-password :token :invitation-token})
#?(:clj (set! *warn-on-reflection* true)) #?(:clj (set! *warn-on-reflection* true))
(def ^:dynamic *data-length* 8) (def ^:dynamic *data-length* 8)
@@ -110,7 +115,25 @@
(explain (:explain data) opts) (explain (:explain data) opts)
(contains? data ::sm/explain) (contains? data ::sm/explain)
(sm/humanize-explain (::sm/explain data) opts))) (let [exp (::sm/explain data)
sanitize-map (fn sanitize-map [m]
(reduce-kv
(fn [acc k v]
(let [k* (if (string? k) (keyword k) k)]
(cond
(contains? sensitive-fields k*)
(assoc acc k (if (map? v)
(sanitize-map v)
(obfuscate-string v true)))
(map? v) (assoc acc k (sanitize-map v))
:else (assoc acc k v))))
{}
m))
sanitize-explain (fn [exp]
(cond-> exp
(:value exp) (update :value sanitize-map)))]
(sm/humanize-explain (sanitize-explain exp) opts))))
#?(:clj #?(:clj
(defn format-throwable (defn format-throwable

View File

@@ -169,6 +169,7 @@
:enable-component-thumbnails :enable-component-thumbnails
:enable-render-wasm-dpr :enable-render-wasm-dpr
:enable-token-color :enable-token-color
:enable-token-shadow
:enable-inspect-styles :enable-inspect-styles
:enable-feature-fdata-objects-map]) :enable-feature-fdata-objects-map])

View File

@@ -340,7 +340,7 @@
(dfn-diff t2 t1))) (dfn-diff t2 t1)))
#?(:cljs #?(:cljs
(defn set-default-locale! (defn set-default-locale
[locale] [locale]
(when-let [locale (unchecked-get locales locale)] (when-let [locale (unchecked-get locales locale)]
(dfn-set-default-options #js {:locale locale})))) (dfn-set-default-options #js {:locale locale}))))

View File

@@ -269,8 +269,8 @@
"Remove flex children properties except the fit-content for flex layouts. These are properties "Remove flex children properties except the fit-content for flex layouts. These are properties
that we don't have to propagate to copies but will be respected when swapping components" that we don't have to propagate to copies but will be respected when swapping components"
[shape] [shape]
(let [layout-item-h-sizing (when (and (ctl/flex-layout? shape) (ctl/auto-width? shape)) :auto) (let [layout-item-h-sizing (when (and (ctl/any-layout? shape) (ctl/auto-width? shape)) :auto)
layout-item-v-sizing (when (and (ctl/flex-layout? shape) (ctl/auto-height? shape)) :auto)] layout-item-v-sizing (when (and (ctl/any-layout? shape) (ctl/auto-height? shape)) :auto)]
(-> shape (-> shape
(d/without-keys ctk/swap-keep-attrs) (d/without-keys ctk/swap-keep-attrs)
(cond-> (some? layout-item-h-sizing) (cond-> (some? layout-item-h-sizing)

View File

@@ -362,24 +362,24 @@
component (ctkl/get-component component-file (:component-id top-instance) true) component (ctkl/get-component component-file (:component-id top-instance) true)
remote-shape (get-ref-shape component-file component shape) remote-shape (get-ref-shape component-file component shape)
component-container (get-component-container component-file component) component-container (get-component-container component-file component)
[remote-shape component-container] [remote-shape component-container component-file]
(if (some? remote-shape) (if (some? remote-shape)
[remote-shape component-container] [remote-shape component-container component-file]
;; If not found, try the case of this being a fostered or swapped children ;; If not found, try the case of this being a fostered or swapped children
(let [head-instance (ctn/get-head-shape (:objects container) shape) (let [head-instance (ctn/get-head-shape (:objects container) shape)
component-file (get-in libraries [(:component-file head-instance) :data]) component-file (get-in libraries [(:component-file head-instance) :data])
head-component (ctkl/get-component component-file (:component-id head-instance) true) head-component (ctkl/get-component component-file (:component-id head-instance) true)
remote-shape' (get-ref-shape component-file head-component shape) remote-shape' (get-ref-shape component-file head-component shape)
component-container (get-component-container component-file component)] component-container' (get-component-container component-file head-component)]
[remote-shape' component-container]))] [remote-shape' component-container' component-file]))]
(if (nil? remote-shape) (if (nil? remote-shape)
nil nil
(if (nil? (:shape-ref remote-shape)) (if (nil? (:shape-ref remote-shape))
(cond-> remote-shape (cond-> remote-shape
(and remote-shape with-context?) (and remote-shape with-context?)
(with-meta {:file {:id (:id file-data) (with-meta {:file {:id (:id component-file)
:data file-data} :data component-file}
:container component-container})) :container component-container}))
(find-remote-shape component-container libraries remote-shape :with-context? with-context?))))) (find-remote-shape component-container libraries remote-shape :with-context? with-context?)))))

View File

@@ -112,8 +112,10 @@
(:c2y params) (update-in [index :params :c2y] + (:c2y params))) (:c2y params) (update-in [index :params :c2y] + (:c2y params)))
content))] content))]
(impl/path-data (if (some? modifiers)
(reduce apply-to-index (vec content) modifiers)))) (impl/path-data
(reduce apply-to-index (vec content) modifiers))
content)))
(defn transform-content (defn transform-content
"Applies a transformation matrix over content and returns a new "Applies a transformation matrix over content and returns a new

View File

@@ -0,0 +1,29 @@
;; 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.project
(:require
[app.common.schema :as sm]
[app.common.time :as cm]))
(def schema:project
[:map {:title "Profile"}
[:id ::sm/uuid]
[:created-at {:optional true} ::cm/inst]
[:modified-at {:optional true} ::cm/inst]
[:name :string]
[:is-default {:optional true} ::sm/boolean]
[:is-pinned {:optional true} ::sm/boolean]
[:count {:optional true} ::sm/int]
[:total-count {:optional true} ::sm/int]
[:team-id ::sm/uuid]])
(def valid-project?
(sm/lazy-validator schema:project))
(def check-project
(sm/check-fn schema:project))

View File

@@ -59,6 +59,7 @@
:dimensions "dimension" :dimensions "dimension"
:font-family "fontFamilies" :font-family "fontFamilies"
:font-size "fontSizes" :font-size "fontSizes"
:font-weight "fontWeights"
:letter-spacing "letterSpacing" :letter-spacing "letterSpacing"
:number "number" :number "number"
:opacity "opacity" :opacity "opacity"
@@ -70,7 +71,6 @@
:stroke-width "borderWidth" :stroke-width "borderWidth"
:text-case "textCase" :text-case "textCase"
:text-decoration "textDecoration" :text-decoration "textDecoration"
:font-weight "fontWeights"
:typography "typography"}) :typography "typography"})
(def dtcg-token-type->token-type (def dtcg-token-type->token-type

View File

@@ -1410,8 +1410,8 @@ Will return a value that matches this schema:
;; NOTE: we can't assign statically at eval time the value of a ;; NOTE: we can't assign statically at eval time the value of a
;; function that is declared but not defined; so we need to pass ;; function that is declared but not defined; so we need to pass
;; an anonymous function and delegate the resolution to runtime ;; an anonymous function and delegate the resolution to runtime
{:encode/json #(export-dtcg-json %) {:encode/json #(some-> % export-dtcg-json)
:decode/json #(read-multi-set-dtcg %) :decode/json #(some-> % read-multi-set-dtcg)
;; FIXME: add better, more reallistic generator ;; FIXME: add better, more reallistic generator
:gen/gen (->> (sg/small-int) :gen/gen (->> (sg/small-int)
(sg/fmap (fn [_] (sg/fmap (fn [_]
@@ -1545,7 +1545,7 @@ Will return a value that matches this schema:
(and (not (contains? decoded-json "$metadata")) (and (not (contains? decoded-json "$metadata"))
(not (contains? decoded-json "$themes")))) (not (contains? decoded-json "$themes"))))
(defn- convert-dtcg-font-family (defn convert-dtcg-font-family
"Convert font-family token value from DTCG format to internal format. "Convert font-family token value from DTCG format to internal format.
- If value is a string, split it into a collection of font families - If value is a string, split it into a collection of font families
- If value is already an array, keep it as is - If value is already an array, keep it as is
@@ -1556,7 +1556,7 @@ Will return a value that matches this schema:
(sequential? value) value (sequential? value) value
:else value)) :else value))
(defn- convert-dtcg-typography-composite (defn convert-dtcg-typography-composite
"Convert typography token value keys from DTCG format to internal format." "Convert typography token value keys from DTCG format to internal format."
[value] [value]
(if (map? value) (if (map? value)
@@ -1568,7 +1568,7 @@ Will return a value that matches this schema:
;; Reference value ;; Reference value
value)) value))
(defn- convert-dtcg-shadow-composite (defn convert-dtcg-shadow-composite
"Convert shadow token value from DTCG format to internal format." "Convert shadow token value from DTCG format to internal format."
[value] [value]
(let [process-shadow (fn [shadow] (let [process-shadow (fn [shadow]

View File

@@ -10,3 +10,7 @@ localhost:3449 {
http://localhost:3450 { http://localhost:3450 {
reverse_proxy localhost:4449 reverse_proxy localhost:4449
} }
http://penpot-devenv-main:3450 {
reverse_proxy localhost:4449
}

View File

@@ -145,8 +145,8 @@ http {
proxy_pass http://127.0.0.1:3000/; proxy_pass http://127.0.0.1:3000/;
} }
location /playground { location /wasm-playground {
alias /home/penpot/penpot/experiments/; alias /home/penpot/penpot/frontend/resources/public/wasm-playground/;
add_header Cache-Control "no-cache, max-age=0"; add_header Cache-Control "no-cache, max-age=0";
autoindex on; autoindex on;
} }

View File

@@ -8,14 +8,10 @@ source ~/.bashrc
echo "[start-tmux.sh] Installing node dependencies" echo "[start-tmux.sh] Installing node dependencies"
pushd ~/penpot/frontend/ pushd ~/penpot/frontend/
corepack install; ./scripts/setup;
yarn install;
yarn playwright install chromium
popd popd
pushd ~/penpot/exporter/ pushd ~/penpot/exporter/
corepack install; ./scripts/setup;
yarn install
yarn playwright install chromium
popd popd
tmux -2 new-session -d -s penpot tmux -2 new-session -d -s penpot
@@ -23,30 +19,25 @@ tmux -2 new-session -d -s penpot
tmux rename-window -t penpot:0 'frontend watch' tmux rename-window -t penpot:0 'frontend watch'
tmux select-window -t penpot:0 tmux select-window -t penpot:0
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t penpot 'yarn run watch' enter tmux send-keys -t penpot './scripts/watch app' enter
tmux new-window -t penpot:1 -n 'frontend shadow' tmux new-window -t penpot:1 -n 'frontend storybook'
tmux select-window -t penpot:1 tmux select-window -t penpot:1
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t penpot 'yarn run watch:app' enter tmux send-keys -t penpot './scripts/watch storybook' enter
tmux new-window -t penpot:2 -n 'frontend storybook' tmux new-window -t penpot:2 -n 'exporter'
tmux select-window -t penpot:2 tmux select-window -t penpot:2
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t penpot 'yarn run watch:storybook' enter
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 'cd penpot/exporter' enter C-l
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
tmux send-keys -t penpot 'yarn run watch' enter tmux send-keys -t penpot './scripts/watch' enter
tmux split-window -v tmux split-window -v
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t penpot './scripts/wait-and-start.sh' enter tmux send-keys -t penpot './scripts/wait-and-start.sh' enter
tmux new-window -t penpot:4 -n 'backend' tmux new-window -t penpot:3 -n 'backend'
tmux select-window -t penpot:4 tmux select-window -t penpot:3
tmux send-keys -t penpot 'cd penpot/backend' enter C-l tmux send-keys -t penpot 'cd penpot/backend' enter C-l
tmux send-keys -t penpot './scripts/start-dev' enter tmux send-keys -t penpot './scripts/start-dev' enter

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC", "author": "Kaleidos INC",
"private": true, "private": true,
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f", "packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "https://github.com/penpot/penpot"
@@ -16,9 +16,9 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"generic-pool": "^3.9.0", "generic-pool": "^3.9.0",
"inflation": "^2.1.0", "inflation": "^2.1.0",
"ioredis": "^5.8.1", "ioredis": "^5.8.2",
"playwright": "^1.55.1", "playwright": "^1.57.0",
"raw-body": "^3.0.1", "raw-body": "^3.0.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"svgo": "penpot/svgo#v3.1", "svgo": "penpot/svgo#v3.1",
"undici": "^7.16.0", "undici": "^7.16.0",
@@ -30,8 +30,8 @@
}, },
"scripts": { "scripts": {
"clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target", "clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target",
"watch:app": "clojure -M:dev:shadow-cljs watch main", "watch:app": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs watch main",
"watch": "yarn run clear:shadow-cache && yarn run watch:app", "watch": "yarn run watch:app",
"build:app": "clojure -M:dev:shadow-cljs release main", "build:app": "clojure -M:dev:shadow-cljs release main",
"build": "yarn run clear:shadow-cache && yarn run build:app", "build": "yarn run clear:shadow-cache && yarn run build:app",
"fmt:clj:check": "cljfmt check --parallel=false src/", "fmt:clj:check": "cljfmt check --parallel=false src/",

8
exporter/scripts/setup Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e;
corepack enable;
corepack install;
yarn install;
yarn playwright install chromium

7
exporter/scripts/watch Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
TARGET=${1:-app};
set -ex
exec yarn run watch:$TARGET

View File

@@ -243,7 +243,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"bytes@npm:3.1.2": "bytes@npm:~3.1.2":
version: 3.1.2 version: 3.1.2
resolution: "bytes@npm:3.1.2" resolution: "bytes@npm:3.1.2"
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
@@ -442,7 +442,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"depd@npm:2.0.0, depd@npm:~2.0.0": "depd@npm:~2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "depd@npm:2.0.0" resolution: "depd@npm:2.0.0"
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
@@ -577,9 +577,9 @@ __metadata:
date-fns: "npm:^4.1.0" date-fns: "npm:^4.1.0"
generic-pool: "npm:^3.9.0" generic-pool: "npm:^3.9.0"
inflation: "npm:^2.1.0" inflation: "npm:^2.1.0"
ioredis: "npm:^5.8.1" ioredis: "npm:^5.8.2"
playwright: "npm:^1.55.1" playwright: "npm:^1.57.0"
raw-body: "npm:^3.0.1" raw-body: "npm:^3.0.2"
source-map-support: "npm:^0.5.21" source-map-support: "npm:^0.5.21"
svgo: "penpot/svgo#v3.1" svgo: "penpot/svgo#v3.1"
undici: "npm:^7.16.0" undici: "npm:^7.16.0"
@@ -683,16 +683,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"http-errors@npm:2.0.0": "http-errors@npm:~2.0.1":
version: 2.0.0 version: 2.0.1
resolution: "http-errors@npm:2.0.0" resolution: "http-errors@npm:2.0.1"
dependencies: dependencies:
depd: "npm:2.0.0" depd: "npm:~2.0.0"
inherits: "npm:2.0.4" inherits: "npm:~2.0.4"
setprototypeof: "npm:1.2.0" setprototypeof: "npm:~1.2.0"
statuses: "npm:2.0.1" statuses: "npm:~2.0.2"
toidentifier: "npm:1.0.1" toidentifier: "npm:~1.0.1"
checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19 checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4
languageName: node languageName: node
linkType: hard linkType: hard
@@ -716,15 +716,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"iconv-lite@npm:0.7.0":
version: 0.7.0
resolution: "iconv-lite@npm:0.7.0"
dependencies:
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2": "iconv-lite@npm:^0.6.2":
version: 0.6.3 version: 0.6.3
resolution: "iconv-lite@npm:0.6.3" resolution: "iconv-lite@npm:0.6.3"
@@ -734,6 +725,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"iconv-lite@npm:~0.7.0":
version: 0.7.0
resolution: "iconv-lite@npm:0.7.0"
dependencies:
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f
languageName: node
linkType: hard
"ieee754@npm:^1.2.1": "ieee754@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "ieee754@npm:1.2.1" resolution: "ieee754@npm:1.2.1"
@@ -755,16 +755,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"inherits@npm:2.0.4, inherits@npm:~2.0.3": "inherits@npm:~2.0.3, inherits@npm:~2.0.4":
version: 2.0.4 version: 2.0.4
resolution: "inherits@npm:2.0.4" resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
languageName: node languageName: node
linkType: hard linkType: hard
"ioredis@npm:^5.8.1": "ioredis@npm:^5.8.2":
version: 5.8.1 version: 5.8.2
resolution: "ioredis@npm:5.8.1" resolution: "ioredis@npm:5.8.2"
dependencies: dependencies:
"@ioredis/commands": "npm:1.4.0" "@ioredis/commands": "npm:1.4.0"
cluster-key-slot: "npm:^1.1.0" cluster-key-slot: "npm:^1.1.0"
@@ -775,7 +775,7 @@ __metadata:
redis-errors: "npm:^1.2.0" redis-errors: "npm:^1.2.0"
redis-parser: "npm:^3.0.0" redis-parser: "npm:^3.0.0"
standard-as-callback: "npm:^2.1.0" standard-as-callback: "npm:^2.1.0"
checksum: 10c0/4ed66444017150da027bce940a24bf726994691e2a7b3aa11d52f8aeb37f258068cc171af4d9c61247acafc28eb086fa8a7c79420b8e8d2907d2f74f39584465 checksum: 10c0/305e385f811d49908899e32c2de69616cd059f909afd9e0a53e54f596b1a5835ee3449bfc6a3c49afbc5a2fd27990059e316cc78f449c94024957bd34c826d88
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1106,27 +1106,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"playwright-core@npm:1.55.1": "playwright-core@npm:1.57.0":
version: 1.55.1 version: 1.57.0
resolution: "playwright-core@npm:1.55.1" resolution: "playwright-core@npm:1.57.0"
bin: bin:
playwright-core: cli.js playwright-core: cli.js
checksum: 10c0/39837a8c1232ec27486eac8c3fcacc0b090acc64310f7f9004b06715370fc426f944e3610fe8c29f17cd3d68280ed72c75f660c02aa5b5cf0eb34bab0031308f checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9
languageName: node languageName: node
linkType: hard linkType: hard
"playwright@npm:^1.55.1": "playwright@npm:^1.57.0":
version: 1.55.1 version: 1.57.0
resolution: "playwright@npm:1.55.1" resolution: "playwright@npm:1.57.0"
dependencies: dependencies:
fsevents: "npm:2.3.2" fsevents: "npm:2.3.2"
playwright-core: "npm:1.55.1" playwright-core: "npm:1.57.0"
dependenciesMeta: dependenciesMeta:
fsevents: fsevents:
optional: true optional: true
bin: bin:
playwright: cli.js playwright: cli.js
checksum: 10c0/b84a97b0d764403df512f5bbb10c7343974e151a28202cc06f90883a13e8a45f4491a0597f0ae5fb03a026746cbc0d200f0f32195bfaa381aee5ca5770626771 checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1161,15 +1161,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"raw-body@npm:^3.0.1": "raw-body@npm:^3.0.2":
version: 3.0.1 version: 3.0.2
resolution: "raw-body@npm:3.0.1" resolution: "raw-body@npm:3.0.2"
dependencies: dependencies:
bytes: "npm:3.1.2" bytes: "npm:~3.1.2"
http-errors: "npm:2.0.0" http-errors: "npm:~2.0.1"
iconv-lite: "npm:0.7.0" iconv-lite: "npm:~0.7.0"
unpipe: "npm:1.0.0" unpipe: "npm:~1.0.0"
checksum: 10c0/892f4fbd21ecab7e2fed0f045f7af9e16df7e8050879639d4e482784a2f4640aaaa33d916a0e98013f23acb82e09c2e3c57f84ab97104449f728d22f65a7d79a checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1270,7 +1270,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"setprototypeof@npm:1.2.0": "setprototypeof@npm:~1.2.0":
version: 1.2.0 version: 1.2.0
resolution: "setprototypeof@npm:1.2.0" resolution: "setprototypeof@npm:1.2.0"
checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc
@@ -1368,10 +1368,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"statuses@npm:2.0.1": "statuses@npm:~2.0.2":
version: 2.0.1 version: 2.0.2
resolution: "statuses@npm:2.0.1" resolution: "statuses@npm:2.0.2"
checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0 checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1500,7 +1500,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"toidentifier@npm:1.0.1": "toidentifier@npm:~1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "toidentifier@npm:1.0.1" resolution: "toidentifier@npm:1.0.1"
checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1
@@ -1539,7 +1539,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"unpipe@npm:1.0.0": "unpipe@npm:~1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "unpipe@npm:1.0.0" resolution: "unpipe@npm:1.0.0"
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c

View File

@@ -1,3 +1,5 @@
import { defineConfig } from 'vite';
/** @type { import('@storybook/react-vite').StorybookConfig } */ /** @type { import('@storybook/react-vite').StorybookConfig } */
const config = { const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
@@ -5,18 +7,38 @@ const config = {
addons: [ addons: [
"@storybook/addon-themes", "@storybook/addon-themes",
"@storybook/addon-docs", "@storybook/addon-docs",
"@storybook/addon-vitest" "@storybook/addon-vitest",
], ],
core: {
builder: "@storybook/builder-vite",
options: {
viteConfigPath: "../vite.config.js",
},
},
framework: { framework: {
name: "@storybook/react-vite", name: "@storybook/react-vite",
options: {}, options: {
// fastRefresh: false,
}
}, },
docs: {}, docs: {},
async viteFinal(config) {
return defineConfig({
...config,
plugins: [
...(config.plugins ?? []),
{
name: 'force-full-reload-always',
apply: 'serve',
enforce: 'post',
handleHotUpdate(ctx) {
ctx.server.ws.send({
type: 'full-reload',
path: '*',
});
// returning [] tells Vite: “no modules handled”
return [];
},
}
]
});
}
}; };
export default config; export default config;

View File

@@ -1,6 +1,5 @@
import { withThemeByClassName } from "@storybook/addon-themes"; import { withThemeByClassName } from "@storybook/addon-themes";
import Components from "@target/components"; import Components from "@target/components";
import translations from "@public/translation.en.js"; import translations from "@public/translation.en.js";
Components.setDefaultTranslations(translations); Components.setDefaultTranslations(translations);

View File

@@ -8,6 +8,11 @@
metosin/reitit-core {:mvn/version "0.9.1"} metosin/reitit-core {:mvn/version "0.9.1"}
funcool/okulary {:mvn/version "2022.04.11-16"} funcool/okulary {:mvn/version "2022.04.11-16"}
funcool/tubax
{:git/tag "v2025.11.28"
:git/sha "2d9a986"
:git/url "https://github.com/funcool/tubax.git"}
funcool/potok2 funcool/potok2
{:git/tag "v2.2" {:git/tag "v2.2"
:git/sha "0f7e15a" :git/sha "0f7e15a"
@@ -45,7 +50,7 @@
{thheller/shadow-cljs {:mvn/version "3.2.2"} {thheller/shadow-cljs {:mvn/version "3.2.2"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"} com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"} org.clojure/tools.namespace {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"} criterium/criterium {:mvn/version "0.4.6"}
cider/cider-nrepl {:mvn/version "0.57.0"}}} cider/cider-nrepl {:mvn/version "0.57.0"}}}
:shadow-cljs :shadow-cljs

View File

@@ -47,89 +47,81 @@
"watch:app:libs": "node ./scripts/build-libs.js --watch", "watch:app:libs": "node ./scripts/build-libs.js --watch",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook", "watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
"clear:shadow-cache": "rm -rf .shadow-cljs", "clear:shadow-cache": "rm -rf .shadow-cljs",
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"", "watch": "exit 0",
"watch": "yarn run watch:app:assets", "watch:app": "yarn run clear:shadow-cache && concurrently --kill-others-on-fail \"yarn run watch:app:assets\" \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
"watch:storybook": "yarn run build:storybook:assets && concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"", "watch:storybook": "yarn run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\""
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.52.0", "@penpot/draft-js": "portal:./packages/draft-js",
"@storybook/addon-docs": "10.0.4", "@penpot/mousetrap": "portal:./packages/mousetrap",
"@storybook/addon-themes": "10.0.4", "@penpot/plugins-runtime": "1.3.2",
"@storybook/addon-vitest": "10.0.4", "@penpot/svgo": "penpot/svgo#v3.2",
"@storybook/react-vite": "10.0.4", "@penpot/text-editor": "portal:./text-editor",
"@types/node": "^22.15.21", "@playwright/test": "1.57.0",
"@vitest/browser": "3.2.4", "@storybook/addon-docs": "10.1.11",
"@vitest/coverage-v8": "3.2.4", "@storybook/addon-themes": "10.1.11",
"@storybook/addon-vitest": "10.1.11",
"@storybook/react-vite": "10.1.11",
"@tokens-studio/sd-transforms": "1.2.11",
"@types/node": "^22.19.3",
"@vitest/browser": "4.0.16",
"@vitest/browser-playwright": "^4.0.16",
"@vitest/coverage-v8": "4.0.16",
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"compression": "^1.8.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"date-fns": "^4.1.0",
"esbuild": "^0.25.9", "esbuild": "^0.25.9",
"eventsource-parser": "^3.0.6",
"express": "^5.1.0", "express": "^5.1.0",
"fancy-log": "^2.0.0", "fancy-log": "^2.0.0",
"getopts": "^2.3.0", "getopts": "^2.3.0",
"gettext-parser": "^8.0.0", "gettext-parser": "^8.0.0",
"gulp-concat": "^2.6.1", "highlight.js": "^11.10.0",
"gulp-gzip": "^1.4.2", "js-beautify": "^1.15.4",
"gulp-mustache": "^5.0.0", "jsdom": "^27.4.0",
"gulp-postcss": "^10.0.0", "lodash": "^4.17.21",
"gulp-rename": "^2.0.0", "lodash.debounce": "^4.0.8",
"gulp-sourcemaps": "^3.0.0",
"gulp-svg-sprite": "^2.0.3",
"jsdom": "^27.0.0",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"marked": "^15.0.12", "marked": "^15.0.12",
"mkdirp": "^3.0.1", "mkdirp": "^3.0.1",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"opentype.js": "^1.3.4",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
"playwright": "1.56.1", "playwright": "1.56.1",
"postcss": "^8.5.4", "postcss": "^8.5.4",
"postcss-clean": "^1.2.2", "postcss-clean": "^1.2.2",
"postcss-modules": "^6.0.1",
"prettier": "3.5.3", "prettier": "3.5.3",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"rimraf": "^6.0.1",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"storybook": "10.0.4",
"svg-sprite": "^2.0.4",
"typescript": "^5.9.2",
"vite": "^6.3.5",
"vitest": "^3.2.0",
"wasm-pack": "^0.13.1",
"watcher": "^2.3.1",
"workerpool": "^9.3.2"
},
"dependencies": {
"@penpot/draft-js": "portal:./vendor/draft-js",
"@penpot/hljs": "portal:./vendor/hljs",
"@penpot/mousetrap": "portal:./vendor/mousetrap",
"@penpot/plugins-runtime": "1.3.2",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "portal:./text-editor",
"@tokens-studio/sd-transforms": "1.2.11",
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"compression": "^1.8.1",
"date-fns": "^4.1.0",
"eventsource-parser": "^3.0.6",
"js-beautify": "^1.15.4",
"lodash": "^4.17.21",
"lodash.debounce": "^4.0.8",
"opentype.js": "^1.3.4",
"postcss-modules": "^6.0.1",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-virtualized": "^9.22.6", "react-virtualized": "^9.22.6",
"rimraf": "^6.0.1",
"rxjs": "8.0.0-alpha.14", "rxjs": "8.0.0-alpha.14",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"sax": "^1.4.1", "sax": "^1.4.1",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"storybook": "10.1.11",
"style-dictionary": "5.0.0-rc.1", "style-dictionary": "5.0.0-rc.1",
"svg-sprite": "^2.0.4",
"tdigest": "^0.1.2", "tdigest": "^0.1.2",
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"typescript": "^5.9.2",
"ua-parser-js": "2.0.5", "ua-parser-js": "2.0.5",
"vite": "^7.3.0",
"vitest": "^4.0.16",
"wait-on": "^9.0.3",
"wasm-pack": "^0.13.1",
"watcher": "^2.3.1",
"workerpool": "^9.3.2",
"xregexp": "^5.1.2" "xregexp": "^5.1.2"
} }
} }

View File

@@ -16,7 +16,9 @@ export const {
RichTextEditorUtil, RichTextEditorUtil,
SelectionState, SelectionState,
convertFromRaw, convertFromRaw,
convertToRaw convertToRaw,
EditorBlock,
Editor
} = pkg; } = pkg;
import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor.js'; import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor.js';

View File

@@ -8,7 +8,8 @@
"author": "Andrey Antukh", "author": "Andrey Antukh",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0" "draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0",
"immutable": "^5.1.4"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=0.17.0", "react": ">=0.17.0",

View File

@@ -173,12 +173,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@penpot/draft-js-wrapper@workspace:.": "@penpot/draft-js@workspace:.":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@penpot/draft-js-wrapper@workspace:." resolution: "@penpot/draft-js@workspace:."
dependencies: dependencies:
draft-js: "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0" draft-js: "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0"
esbuild: "npm:^0.24.0" esbuild: "npm:^0.24.0"
immutable: "npm:^5.1.4"
peerDependencies: peerDependencies:
react: ">=0.17.0" react: ">=0.17.0"
react-dom: ">=0.17.0" react-dom: ">=0.17.0"
@@ -320,6 +321,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"immutable@npm:^5.1.4":
version: 5.1.4
resolution: "immutable@npm:5.1.4"
checksum: 10c0/f1c98382e4cde14a0b218be3b9b2f8441888da8df3b8c064aa756071da55fbed6ad696e5959982508456332419be9fdeaf29b2e58d0eadc45483cc16963c0446
languageName: node
linkType: hard
"immutable@npm:~3.7.4": "immutable@npm:~3.7.4":
version: 3.7.6 version: 3.7.6
resolution: "immutable@npm:3.7.6" resolution: "immutable@npm:3.7.6"

View File

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

View File

@@ -0,0 +1,47 @@
[
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41234",
"~:revn": 1,
"~:vern": 1,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1705307400000",
"~:modified-at": "~m1732111500000",
"~:deleted-at": "~m1732111500000",
"~:name": "Deleted Design File 1",
"~:is-shared": false,
"~:will-be-deleted-at": "~m1732716300000",
"~:thumbnail-id": null,
"~:row-num": 1,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41235",
"~:revn": 2,
"~:vern": 2,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1704875700000",
"~:modified-at": "~m1732025400000",
"~:deleted-at": "~m1732025400000",
"~:name": "Deleted Design File 2",
"~:is-shared": true,
"~:will-be-deleted-at": "~m1732630200000",
"~:thumbnail-id": null,
"~:row-num": 2,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41236",
"~:revn": 3,
"~:vern": 3,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920c",
"~:created-at": "~m1706792400000",
"~:modified-at": "~m1731939600000",
"~:deleted-at": "~m1731939600000",
"~:name": "Old Project Design",
"~:is-shared": false,
"~:will-be-deleted-at": "~m1732544400000",
"~:thumbnail-id": null,
"~:row-num": 3,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
}
]

View File

@@ -106,6 +106,13 @@ export class DashboardPage extends BaseWebSocketPage {
); );
} }
async setupDeletedFiles() {
await this.mockRPC(
"get-team-deleted-files?team-id=*",
"dashboard/get-team-deleted-files.json",
);
}
async setupDrafts() { async setupDrafts() {
await this.mockRPC( await this.mockRPC(
"get-project-files?project-id=*", "get-project-files?project-id=*",
@@ -160,6 +167,10 @@ export class DashboardPage extends BaseWebSocketPage {
}); });
await this.mockRPC("search-files", "dashboard/search-files.json"); await this.mockRPC("search-files", "dashboard/search-files.json");
await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json"); await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json");
await this.mockRPC(
"get-team-deleted-files?team-id=*",
"dashboard/get-team-deleted-files.json",
);
} }
async setupAccessTokensEmpty() { async setupAccessTokensEmpty() {
@@ -289,6 +300,13 @@ export class DashboardPage extends BaseWebSocketPage {
await expect(this.mainHeading).toHaveText("Libraries"); await expect(this.mainHeading).toHaveText("Libraries");
} }
async goToDeleted() {
await this.page.goto(
`#/dashboard/deleted?team-id=${DashboardPage.anyTeamId}`,
);
await expect(this.mainHeading).toHaveText("Projects");
}
async openProfileMenu() { async openProfileMenu() {
await this.userAccount.click(); await this.userAccount.click();
} }

View File

@@ -0,0 +1,31 @@
import { test, expect } from "@playwright/test";
import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test.describe("Dashboard Deleted Page", () => {
test("User can navigate to deleted page", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
// Setup mock for deleted files API
await dashboardPage.setupDeletedFiles();
// Navigate directly to deleted page
await dashboardPage.goToDeleted();
// Check for the restore all and clear trash buttons
await expect(
page.getByRole("button", { name: "Restore All" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Clear trash" }),
).toBeVisible();
});
});

View File

@@ -305,7 +305,7 @@ test.describe("Inspect tab - Styles", () => {
); );
await openInspectTab(workspacePage); await openInspectTab(workspacePage);
const panel = await getPanelByTitle(workspacePage, "Size & position"); const panel = await getPanelByTitle(workspacePage, "Size and position");
await expect(panel).toBeVisible(); await expect(panel).toBeVisible();
const propertyRow = panel.getByTestId("property-row"); const propertyRow = panel.getByTestId("property-row");
@@ -335,7 +335,7 @@ test.describe("Inspect tab - Styles", () => {
); );
await openInspectTab(workspacePage); await openInspectTab(workspacePage);
const panel = await getPanelByTitle(workspacePage, "Size & position"); const panel = await getPanelByTitle(workspacePage, "Size and position");
await expect(panel).toBeVisible(); await expect(panel).toBeVisible();
const propertyRow = panel.getByTestId("property-row"); const propertyRow = panel.getByTestId("property-row");
@@ -375,7 +375,7 @@ test.describe("Inspect tab - Styles", () => {
); );
await openInspectTab(workspacePage); await openInspectTab(workspacePage);
const panel = await getPanelByTitle(workspacePage, "Size & position"); const panel = await getPanelByTitle(workspacePage, "Size and position");
await expect(panel).toBeVisible(); await expect(panel).toBeVisible();
const propertyRow = panel.getByTestId("property-row"); const propertyRow = panel.getByTestId("property-row");

View File

@@ -24,6 +24,8 @@
<link rel="icon" href="images/favicon.png" /> <link rel="icon" href="images/favicon.png" />
<script type="importmap">{{& manifest.importmap }}</script>
<script type="module"> <script type="module">
globalThis.penpotVersion = "{{& version}}"; globalThis.penpotVersion = "{{& version}}";
globalThis.penpotBuildDate = "{{& build_date}}"; globalThis.penpotBuildDate = "{{& build_date}}";
@@ -33,7 +35,6 @@
{{# manifest}} {{# manifest}}
<script src="{{& config}}"></script> <script src="{{& config}}"></script>
<script src="{{& polyfills}}"></script> <script src="{{& polyfills}}"></script>
<script type="importmap">{{& importmap }}</script>
{{/manifest}} {{/manifest}}
<!--cookie-consent--> <!--cookie-consent-->
@@ -49,7 +50,9 @@
<script type="module" src="{{& libs}}"></script> <script type="module" src="{{& libs}}"></script>
<script type="module"> <script type="module">
import { init } from "{{& app_main}}"; import { init } from "{{& app_main}}";
init(); import defaultTranslations from "{{& default_translations}}";
init({defaultTranslations});
</script> </script>
{{/manifest}} {{/manifest}}
</body> </body>

View File

@@ -74,7 +74,7 @@ export function isJsFile(path) {
export async function compileSass(worker, path, options) { export async function compileSass(worker, path, options) {
path = ph.resolve(path); path = ph.resolve(path);
log.info("compile:", path); // log.info("compile:", path);
return worker.exec("compileSass", [path, options]); return worker.exec("compileSass", [path, options]);
} }
@@ -187,7 +187,7 @@ async function readManifestFile(resource) {
return JSON.parse(content); return JSON.parse(content);
} }
async function readShadowManifest() { async function generateManifest() {
const index = { const index = {
app_main: "./js/main.js", app_main: "./js/main.js",
render_main: "./js/render.js", render_main: "./js/render.js",
@@ -197,6 +197,7 @@ async function readShadowManifest() {
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION, polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
libs: "./js/libs.js?version=" + CURRENT_VERSION, libs: "./js/libs.js?version=" + CURRENT_VERSION,
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION, worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
default_translations: "./js/translation.en.js?version=" + CURRENT_VERSION,
importmap: JSON.stringify({ importmap: JSON.stringify({
"imports": { "imports": {
@@ -276,6 +277,7 @@ export async function compileTranslations() {
"id", "id",
"ru", "ru",
"tr", "tr",
"hi",
"zh_CN", "zh_CN",
"zh_Hant", "zh_Hant",
"hr", "hr",
@@ -391,7 +393,7 @@ async function generateTemplates() {
const isDebug = process.env.NODE_ENV !== "production"; const isDebug = process.env.NODE_ENV !== "production";
await fs.mkdir("./resources/public/", { recursive: true }); await fs.mkdir("./resources/public/", { recursive: true });
const manifest = await readShadowManifest(); const manifest = await generateManifest();
let content; let content;
const iconsSprite = await fs.readFile( const iconsSprite = await fs.readFile(

6
frontend/scripts/setup Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
corepack enable;
corepack install;
yarn install;
yarn playwright install chromium;

View File

@@ -1,13 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
SCRIPT_DIR=$(dirname $0);
set -ex set -ex
corepack enable;
corepack install;
yarn install;
yarn run playwright install chromium --with-deps; $SCRIPT_DIR/setup;
yarn run build:storybook yarn run build:storybook
yarn run test:storybook
exec npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on tcp:6006 && yarn test:storybook"

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
SCRIPT_DIR=$(dirname $0);
set -ex set -ex
corepack enable;
corepack install; $SCRIPT_DIR/setup;
yarn install;
yarn run playwright install chromium --with-deps;
yarn run test:e2e -x --workers=2 --reporter=list "$@"; yarn run test:e2e -x --workers=2 --reporter=list "$@";

7
frontend/scripts/watch Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
TARGET=${1:-app};
set -ex
exec yarn run watch:$TARGET

View File

@@ -121,24 +121,22 @@
:storybook :storybook
{:target :esm {:target :esm
:output-dir "target/storybook/" :output-dir "target/storybook/"
:devtools {:enabled false} :devtools {:enabled false
:console-support false}
:js-options :js-options
{:js-provider :import {:js-provider :import
:entry-keys ["module" "browser" "main"] :entry-keys ["module" "browser" "main"]
:export-conditions ["module" "import", "browser" "require" "default"]} :export-conditions ["module" "import", "browser" "require" "default"]}
:modules :modules
{:base {:components
{:entries []}
:components
{:exports {default app.main.ui.ds/default {:exports {default app.main.ui.ds/default
helpers app.main.ui.ds.helpers/default} helpers app.main.ui.ds.helpers/default}
:prepend-js ";(globalThis.goog.provide = globalThis.goog.constructNamespace_);(globalThis.goog.require = globalThis.goog.module.get);" :prepend-js ";(globalThis.goog.provide = globalThis.goog.constructNamespace_);(globalThis.goog.require = globalThis.goog.module.get);"
:depends-on #{:base}}} :depends-on #{}}}
:compiler-options :compiler-options
{:output-feature-set :es2020 {:output-feature-set :es-next
:output-wrapper false :output-wrapper false
:warnings {:fn-deprecated false}}} :warnings {:fn-deprecated false}}}

View File

@@ -90,7 +90,10 @@
(rx/map #(ws/initialize))))))) (rx/map #(ws/initialize)))))))
(defn ^:export init (defn ^:export init
[] [options]
(some-> (unchecked-get options "defaultTranslations")
(i18n/set-default-translations))
(mw/init!) (mw/init!)
(i18n/init) (i18n/init)
(cur/init-styles) (cur/init-styles)

View File

@@ -302,3 +302,9 @@
:height 720}]) :height 720}])
(def max-input-length 255) (def max-input-length 255)
(def ^:const default-slow-progress-threshold
"A constant value that represents a threshold in milliseconds when a
normal progress becomes tagged as slow if no event received in the
specified amount of time"
1000)

View File

@@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.team :as ctt] [app.common.types.team :as ctt]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@@ -229,6 +230,91 @@
;; Delay so the navigation can finish ;; Delay so the navigation can finish
(rx/delay 250)))))))) (rx/delay 250))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PROGRESS EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def noop-fn
(constantly nil))
(def ^:private schema:progress-params
[:map {:title "Progress"}
[:key {:optional true} ::sm/text]
[:index {:optional true} ::sm/int]
[:total ::sm/int]
[:hints
[:map-of :keyword fn?]]
[:slow-progress-threshold {:optional true} ::sm/int]])
(def ^:private check-progress-params
(sm/check-fn schema:progress-params))
(defn initialize-progress
[& {:keys [key index total hints slow-progress-threshold] :as params}]
(assert (check-progress-params params))
(ptk/reify ::initialize-progress
ptk/UpdateEvent
(update [_ state]
(update state :progress
(fn [_]
(let [hint ((:normal hints noop-fn) params)]
{:threshold (or slow-progress-threshold 5000)
:key key
:last-update (ct/now)
:healthy true
:visible true
:hints hints
:progress (d/nilv index 0)
:total total
:hint hint}))))))
(defn update-progress
[{:keys [index total] :as params}]
(assert (check-progress-params params))
(ptk/reify ::update-progress
ptk/UpdateEvent
(update [_ state]
(update state :progress
(fn [state]
(let [last-update (get state :last-update)
hints (get state :hints)
threshold (get state :slow-progress-threshold)
time-diff (ct/diff-ms last-update (ct/now))
healthy? (< time-diff threshold)
hint (if healthy?
((:normal hints noop-fn) params)
((:slow hints noop-fn) params))]
(-> state
(assoc :progress index)
(assoc :total total)
(assoc :last-update (ct/now))
(assoc :healthy healthy?)
(assoc :hint hint))))))))
(defn toggle-progress-visibility
[]
(ptk/reify ::toggle-progress-visibility
ptk/UpdateEvent
(update [_ state]
(update state :progress
(fn [state]
(update state :visible not))))))
(defn clear-progress
[]
(ptk/reify ::clear-progress
ptk/UpdateEvent
(update [_ state]
(dissoc state :progress))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; NAVEGATION EVENTS ;; NAVEGATION EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -386,3 +472,21 @@
(rx/of ::dps/force-persist (rx/of ::dps/force-persist
(rt/nav :viewer params options)))))) (rt/nav :viewer params options))))))
(defn go-to-dashboard-deleted
[& {:keys [team-id] :as options}]
(ptk/reify ::go-to-dashboard-deleted
ptk/WatchEvent
(watch [_ state _]
(let [profile (get state :profile)
team-id (cond
(= :default team-id)
(:default-team-id profile)
(uuid? team-id)
team-id
:else
(:current-team-id state))
params {:team-id team-id}]
(rx/of (modal/hide)
(rt/nav :dashboard-deleted params options))))))

View File

@@ -13,14 +13,18 @@
[app.common.logging :as log] [app.common.logging :as log]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.types.project :refer [valid-project?]]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.constants :as mconst]
[app.main.data.common :as dcm] [app.main.data.common :as dcm]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.fonts :as df] [app.main.data.fonts :as df]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.websocket :as dws] [app.main.data.websocket :as dws]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.sse :as sse] [app.util.sse :as sse]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
@@ -76,7 +80,8 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(reduce (fn [state {:keys [id] :as project}] (reduce (fn [state {:keys [id] :as project}]
(update-in state [:projects id] merge project)) ;; Replace completely instead of merge to ensure deleted-at is removed
(assoc-in state [:projects id] project))
state state
projects)))) projects))))
@@ -152,6 +157,34 @@
(->> (rp/cmd! :get-builtin-templates) (->> (rp/cmd! :get-builtin-templates)
(rx/map builtin-templates-fetched))))) (rx/map builtin-templates-fetched)))))
;; --- EVENT: deleted-files
(defn- deleted-files-fetched
[files]
(ptk/reify ::deleted-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [now (ct/now)
filtered-files (filterv (fn [file]
(let [will-be-deleted-at (:will-be-deleted-at file)]
(or (nil? will-be-deleted-at)
(ct/is-after? will-be-deleted-at now))))
files)
files (d/index-by :id filtered-files)]
(-> state
(assoc :deleted-files files)
(update :files d/merge files))))))
(defn fetch-deleted-files
([] (fetch-deleted-files nil))
([team-id]
(ptk/reify ::fetch-deleted-files
ptk/WatchEvent
(watch [_ state _]
(when-let [team-id (or team-id (:current-team-id state))]
(->> (rp/cmd! :get-team-deleted-files {:team-id team-id})
(rx/map deleted-files-fetched)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection ;; Data Selection
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -460,6 +493,7 @@
(-> state (-> state
(d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id) (d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id) (d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:deleted-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-when :dashboard-search-result update-search-files)))))) (d/update-when :dashboard-search-result update-search-files))))))
;; --- EVENT: create-file ;; --- EVENT: create-file
@@ -656,3 +690,251 @@
:team-role-change (handle-change-team-role msg) :team-role-change (handle-change-team-role msg)
:team-membership-change (dcm/team-membership-change msg) :team-membership-change (dcm/team-membership-change msg)
nil)) nil))
;; --- Delete files immediately
(defn- delete-files
[{:keys [team-id ids on-success on-error]}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(assert (fn? on-success))
(assert (fn? on-error))
(ptk/reify ::delete-files
ptk/WatchEvent
(watch [_ _ _]
(let [progress-hint #(tr "dashboard.progress-notification.deleting-files")
slow-hint #(tr "dashboard.progress-notification.slow-delete")
stream (->> (rp/cmd! ::sse/permanently-delete-team-files {:team-id team-id :ids ids})
(rx/share))]
(rx/merge
(rx/of (dcm/initialize-progress
{:slow-progress-threshold
mconst/default-slow-progress-threshold
:total (count ids)
:hints {:progress progress-hint
:slow slow-hint}}))
(->> stream
(rx/filter sse/progress?)
(rx/mapcat (fn [event]
(if-let [payload (sse/get-payload event)]
(let [{:keys [index total]} payload]
(if (and index total)
(rx/of (dcm/update-progress {:index index :total total}))
(rx/empty)))
(rx/empty))))
(rx/catch rx/empty))
(->> stream
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/merge-map (fn [_]
(rx/concat
(rx/of (dcm/clear-progress)
(fetch-projects team-id)
(fetch-deleted-files team-id)
(fetch-projects team-id))
(on-success))))
(rx/catch (fn [error]
(rx/concat
(rx/of (dcm/clear-progress))
(on-error error))))))))))
(defn delete-files-immediately
[{:keys [team-id ids] :as params}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(ptk/reify ::delete-files-immediately
ptk/WatchEvent
(watch [_ state _]
(let [deleted-files
(get state :deleted-files)
on-success
(fn []
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/success (tr "dashboard.delete-success-notification" fname))))
(rx/of (ntf/success (tr "dashboard.delete-files-success-notification" (count ids))))))
on-error
#(rx/of (ntf/error (tr "dashboard.errors.error-on-delete-files")))]
(rx/of (ev/event
{::ev/name "delete-files"
::ev/origin "dashboard:trash"
:team-id team-id
:num-files (count ids)})
(delete-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
(defn delete-project-immediately
[{:keys [team-id id name] :as project}]
(assert (valid-project? project))
(ptk/reify ::delete-project-immediately
ptk/WatchEvent
(watch [_ state _]
(let [ids
(reduce-kv (fn [acc file-id file]
(if (= (:project-id file) id)
(conj acc file-id)
acc))
#{}
(get state :deleted-files))
on-success
#(rx/of (ntf/success (tr "dashboard.delete-success-notification" name)))
on-error
#(rx/of (ntf/error (tr "dashboard.errors.error-on-delete-project" name)))]
(rx/of (ev/event
{::ev/name "delete-files"
::ev/origin "dashboard:trash"
:team-id team-id
:project-id id
:num-files (count ids)})
(delete-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
;; --- Restore deleted files immediately
(defn- restore-files
[{:keys [team-id ids on-success on-error]}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(assert (fn? on-success))
(assert (fn? on-error))
(ptk/reify ::restore-files
ptk/WatchEvent
(watch [_ _ _]
(let [progress-hint #(tr "dashboard.progress-notification.restoring-files")
slow-hint #(tr "dashboard.progress-notification.slow-restore")]
(rx/merge
(rx/of (dcm/initialize-progress
{:slow-progress-threshold
mconst/default-slow-progress-threshold
:total (count ids)
:hints {:progress progress-hint
:slow slow-hint}}))
(let [stream (->> (rp/cmd! ::sse/restore-deleted-team-files {:team-id team-id :ids ids})
(rx/share))]
(rx/merge
(->> stream
(rx/filter sse/progress?)
(rx/mapcat (fn [event]
(if-let [payload (sse/get-payload event)]
(let [{:keys [index total]} payload]
(if (and index total)
(rx/of (dcm/update-progress {:index index :total total}))
(rx/empty)))
(rx/empty))))
(rx/catch rx/empty))
(->> stream
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/mapcat (fn [_]
(rx/concat
(rx/of (dcm/clear-progress)
;; (ntf/success (tr "dashboard.restore-success-notification"))
(fetch-projects team-id)
(fetch-deleted-files team-id)
(fetch-projects team-id))
(on-success))))
(rx/catch (fn [error]
(rx/concat
(rx/of (dcm/clear-progress))
(on-error error))))))))))))
(defn restore-files-immediately
[{:keys [team-id ids]}]
(assert (uuid? team-id))
(assert (set? ids))
(ptk/reify ::restore-files-immediately
ptk/WatchEvent
(watch [_ state _]
(let [deleted-files
(get state :deleted-files)
on-success
(fn []
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/success (tr "dashboard.restore-success-notification" fname))))
(rx/of (ntf/success (tr "dashboard.restore-files-success-notification" (count ids))))))
on-error
(fn [_cause]
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/error (tr "dashboard.errors.error-on-restore-file" fname))))
(rx/of (ntf/error (tr "dashboard.errors.error-on-restore-files")))))]
(rx/of (ev/event
{::ev/name "restore-files"
::ev/origin "dashboard:trash"
:team-id team-id
:num-files (count ids)})
(restore-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
(defn restore-project-immediately
[{:keys [team-id id name] :as project}]
(assert (valid-project? project))
(ptk/reify ::restore-project-immediately
ptk/WatchEvent
(watch [_ state _]
(let [ids
(reduce-kv (fn [acc file-id file]
(if (= (:project-id file) id)
(conj acc file-id)
acc))
#{}
(get state :deleted-files))
on-success
#(st/emit! (ntf/success (tr "dashboard.restore-success-notification" name)))
on-error
#(st/emit! (ntf/error (tr "dashboard.errors.error-on-restoring-project" name)))]
(rx/of (ev/event
{::ev/name "restore-files"
::ev/origin "dashboard:trash"
:team-id team-id
:project-id id
:num-files (count ids)})
(restore-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))

View File

@@ -98,9 +98,7 @@
(def context (def context
(atom (d/without-nils (collect-context)))) (atom (d/without-nils (collect-context))))
(add-watch i18n/state "events" (add-watch i18n/locale "events" #(swap! context assoc :locale %4))
(fn [_ _ _ v]
(swap! context assoc :locale (get v :locale))))
;; --- EVENT TRANSLATION ;; --- EVENT TRANSLATION

View File

@@ -270,8 +270,12 @@
(ptk/reify ::process-wasm-object (ptk/reify ::process-wasm-object
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]
(let [objects (dsh/lookup-page-objects state)] (let [objects (dsh/lookup-page-objects state)
(wasm.api/process-object (get objects id)))))) shape (get objects id)]
;; Only process objects that exist in the current page
;; This prevents errors when processing changes from other pages
(when shape
(wasm.api/process-object shape))))))
(defn initialize-workspace (defn initialize-workspace
[team-id file-id] [team-id file-id]
@@ -428,10 +432,12 @@
:val position-data :val position-data
:ignore-touched true :ignore-touched true
:ignore-geometry true}]})))] :ignore-geometry true}]})))]
(dch/commit-changes (when (d/not-empty? changes)
{:redo-changes changes :undo-changes [] (dch/commit-changes
:save-undo? false {:redo-changes changes :undo-changes []
:tags #{:position-data}}))))))) :save-undo? false
:tags #{:position-data}})))))
(rx/take-until stoper-s))))
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)

View File

@@ -14,7 +14,7 @@
[app.common.types.fills :as types.fills] [app.common.types.fills :as types.fills]
[app.common.types.library :as ctl] [app.common.types.library :as ctl]
[app.common.types.shape :as shp] [app.common.types.shape :as shp]
[app.common.types.shape.shadow :refer [check-shadow]] [app.common.types.shape.shadow :as types.shadow]
[app.common.types.text :as txt] [app.common.types.text :as txt]
[app.main.broadcast :as mbc] [app.main.broadcast :as mbc]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
@@ -406,30 +406,30 @@
(defn change-shadow (defn change-shadow
[ids attrs index] [ids attrs index]
(ptk/reify ::change-shadow (letfn [(update-shadow [shape]
ptk/WatchEvent (let [;; If we try to set a gradient to a shadow (for
(watch [_ _ _] ;; example using the color selection from
(rx/of (dwsh/update-shapes ;; multiple shapes) let's use the first stop
ids ;; color
(fn [shape] attrs (cond-> attrs
(let [;; If we try to set a gradient to a shadow (for (:gradient attrs)
;; example using the color selection from (-> (dm/get-in [:gradient :stops 0])
;; multiple shapes) let's use the first stop (select-keys types.shadow/color-attrs)))
;; color
attrs (cond-> attrs
(:gradient attrs)
(dm/get-in [:gradient :stops 0]))
attrs' (-> (dm/get-in shape [:shadow index :color]) attrs' (-> (dm/get-in shape [:shadow index :color])
(merge attrs) (merge attrs)
(d/without-nils))] (d/without-nils))]
(assoc-in shape [:shadow index :color] attrs')))))))) (assoc-in shape [:shadow index :color] attrs')))]
(ptk/reify ::change-shadow
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes ids update-shadow))))))
(defn add-shadow (defn add-shadow
[ids shadow] [ids shadow]
(assert (assert
(check-shadow shadow) (types.shadow/check-shadow shadow)
"expected a valid shadow struct") "expected a valid shadow struct")
(assert (assert
@@ -1146,16 +1146,16 @@
(defn- shadow->color-attr (defn- shadow->color-attr
"Given a stroke map enriched with :shape-id, :index, and optionally "Given a stroke map enriched with :shape-id, :index, and optionally
:has-token-applied / :token-name, returns a color attribute map. :has-token-applied / :token-name, returns a color attribute map.
If :has-token-applied is true, adds token metadata to :attrs: If :has-token-applied is true, adds token metadata to :attrs:
{:has-token-applied true {:has-token-applied true
:token-name <token-name>} :token-name <token-name>}
Args: Args:
- stroke: map with stroke info, including :shape-id and :index - stroke: map with stroke info, including :shape-id and :index
- file-id: current file UUID - file-id: current file UUID
- libraries: map of shared color libraries - libraries: map of shared color libraries
Returns: Returns:
A map like: A map like:
{:attrs {...color data...} {:attrs {...color data...}
@@ -1260,12 +1260,12 @@
will include extra attributes in its :attrs map: will include extra attributes in its :attrs map:
{:has-token-applied true {:has-token-applied true
:token-name <token-name>} :token-name <token-name>}
Args: Args:
- shapes: vector of shape maps - shapes: vector of shape maps
- file-id: current file UUID - file-id: current file UUID
- libraries: map of shared color libraries - libraries: map of shared color libraries
Returns: Returns:
A vector of color attribute maps with metadata for each shape." A vector of color attribute maps with metadata for each shape."
[shapes file-id libraries] [shapes file-id libraries]

View File

@@ -47,32 +47,31 @@
(ptk/reify ::apply-content-modifiers (ptk/reify ::apply-content-modifiers
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [page-id (get state :current-page-id state) (let [id (st/get-path-id state)
objects (dsh/lookup-page-objects state) shape (st/get-path state)
id (st/get-path-id state)
shape
(st/get-path state)
content-modifiers content-modifiers
(dm/get-in state [:workspace-local :edit-path id :content-modifiers]) (dm/get-in state [:workspace-local :edit-path id :content-modifiers])]
(if (or (nil? shape) (nil? content-modifiers))
(rx/of (dwe/clear-edition-mode))
(let [page-id (get state :current-page-id state)
objects (dsh/lookup-page-objects state)
content (get shape :content) content (get shape :content)
new-content (path/apply-content-modifiers content content-modifiers) new-content (path/apply-content-modifiers content content-modifiers)
old-points (path.segment/get-points content) old-points (path.segment/get-points content)
new-points (path.segment/get-points new-content) new-points (path.segment/get-points new-content)
point-change (->> (map hash-map old-points new-points) (reduce merge))] point-change (->> (map hash-map old-points new-points) (reduce merge))]
(when (and (some? new-content) (some? shape)) (when (and (some? new-content) (some? shape))
(let [changes (changes/generate-path-changes it objects page-id shape (:content shape) new-content)] (let [changes (changes/generate-path-changes it objects page-id shape (:content shape) new-content)]
(if (empty? new-content) (if (empty? new-content)
(rx/of (dch/commit-changes changes) (rx/of (dch/commit-changes changes)
(dwe/clear-edition-mode)) (dwe/clear-edition-mode))
(rx/of (dch/commit-changes changes) (rx/of (dch/commit-changes changes)
(selection/update-selection point-change) (selection/update-selection point-change)
(fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler)))))))))) (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler))))))))))))
(defn modify-content-point (defn modify-content-point
[content {dx :x dy :y} modifiers point] [content {dx :x dy :y} modifiers point]

View File

@@ -554,7 +554,7 @@
(when (features/active-feature? state "text-editor/v2") (when (features/active-feature? state "text-editor/v2")
(let [instance (:workspace-editor state) (let [instance (:workspace-editor state)
styles (some-> (editor.v2/getCurrentStyle instance) styles (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration) (styles/get-styles-from-style-declaration :removed-mixed true)
((comp update-node-fn migrate-node)) ((comp update-node-fn migrate-node))
(styles/attrs->styles))] (styles/attrs->styles))]
(editor.v2/applyStylesToSelection instance styles))))))) (editor.v2/applyStylesToSelection instance styles)))))))

View File

@@ -238,12 +238,12 @@
:always :always
(ctm/resize scalev resize-origin shape-transform shape-transform-inverse) (ctm/resize scalev resize-origin shape-transform shape-transform-inverse)
(and (ctl/any-layout-immediate-child? objects shape) (and (or (ctl/any-layout-immediate-child? objects shape) (ctl/any-layout? shape))
(not= (:layout-item-h-sizing shape) :fix) (not= (:layout-item-h-sizing shape) :fix)
^boolean change-width?) ^boolean change-width?)
(ctm/change-property :layout-item-h-sizing :fix) (ctm/change-property :layout-item-h-sizing :fix)
(and (ctl/any-layout-immediate-child? objects shape) (and (or (ctl/any-layout-immediate-child? objects shape) (ctl/any-layout? shape))
(not= (:layout-item-v-sizing shape) :fix) (not= (:layout-item-v-sizing shape) :fix)
^boolean change-height?) ^boolean change-height?)
(ctm/change-property :layout-item-v-sizing :fix) (ctm/change-property :layout-item-v-sizing :fix)

View File

@@ -636,3 +636,6 @@
(def persistence-state (def persistence-state
(l/derived (comp :status :persistence) st/state)) (l/derived (comp :status :persistence) st/state))
(def progress
(l/derived :progress st/state))

View File

@@ -87,6 +87,12 @@
{:stream? true {:stream? true
:form-data? true} :form-data? true}
::sse/permanently-delete-team-files
{:stream? true}
::sse/restore-deleted-team-files
{:stream? true}
:export-binfile {:response-type :blob} :export-binfile {:response-type :blob}
:retrieve-list-of-builtin-templates {:query-params :all}}) :retrieve-list-of-builtin-templates {:query-params :all}})

View File

@@ -224,7 +224,8 @@
:dashboard-members :dashboard-members
:dashboard-invitations :dashboard-invitations
:dashboard-webhooks :dashboard-webhooks
:dashboard-settings) :dashboard-settings
:dashboard-deleted)
(let [params (get params :query) (let [params (get params :query)
team-id (some-> params :team-id uuid/parse*) team-id (some-> params :team-id uuid/parse*)
project-id (some-> params :project-id uuid/parse*) project-id (some-> params :project-id uuid/parse*)

View File

@@ -50,7 +50,8 @@
touched? (and (contains? (:data @form) input-name) touched? (and (contains? (:data @form) input-name)
(get-in @form [:touched input-name])) (get-in @form [:touched input-name]))
error (get-in @form [:errors input-name]) error (or (get-in @form [:errors input-name])
(get-in @form [:extra-errors input-name]))
value (get-in @form [:data input-name] "") value (get-in @form [:data input-name] "")

View File

@@ -0,0 +1,103 @@
;; 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.main.ui.components.progress
"Assets exportation common components."
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.types.color :as clr]
[app.main.data.common :as dcm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as deprecated-icons]
[app.util.i18n :as i18n :refer [tr]]
[app.util.theme :as theme]
[rumext.v2 :as mf]))
(def ^:private neutral-icon
(deprecated-icons/icon-xref :msg-neutral (stl/css :icon)))
(def ^:private error-icon
(deprecated-icons/icon-xref :delete-text (stl/css :icon)))
(def ^:private close-icon
(deprecated-icons/icon-xref :close (stl/css :close-icon)))
(mf/defc progress-notification-widget*
[]
(let [state (mf/deref refs/progress)
profile (mf/deref refs/profile)
theme (get profile :theme theme/default)
default-theme? (= theme/default theme)
error? (:error state)
healthy? (:healthy state)
visible? (:visible state)
progress (:progress state)
hint (:hint state)
total (:total state)
pwidth
(if error?
280
(/ (* progress 280) total))
color
(cond
error? clr/new-danger
healthy? (if default-theme?
clr/new-primary
clr/new-primary-light)
(not healthy?) clr/new-warning)
background-clr
(if default-theme?
clr/background-quaternary
clr/background-quaternary-light)
toggle-detail-visibility
(mf/use-fn
(fn []
(st/emit! (dcm/toggle-progress-visibility))))]
[:*
(when visible?
[:div {:class (stl/css-case :progress-modal true
:has-error error?)}
(if error?
error-icon
neutral-icon)
[:div {:class (stl/css :title)}
[:div {:class (stl/css :title-text)} hint]
(if error?
[:button {:class (stl/css :retry-btn)
;; :on-click retry-last-operation
}
(tr "labels.retry")]
[:span {:class (stl/css :progress)}
(dm/str progress " / " total)])]
[:button {:class (stl/css :progress-close-button)
:on-click toggle-detail-visibility}
close-icon]
(when-not error?
[:svg {:class (stl/css :progress-bar)
:height 4
:width 280}
[:g
[:path {:d "M0 0 L280 0"
:stroke background-clr
:stroke-width 30}]
[:path {:d (dm/str "M0 0 L280 0")
:stroke color
:stroke-width 30
:fill "transparent"
:stroke-dasharray 280
:stroke-dashoffset (- 280 pwidth)
:style {:transition "stroke-dashoffset 1s ease-in-out"}}]]])])]))

View File

@@ -0,0 +1,101 @@
// 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
@use "refactor/common-refactor.scss" as deprecated;
// PROGRESS WIDGET
.progress-widget {
@include deprecated.flexCenter;
width: deprecated.$s-28;
height: deprecated.$s-28;
}
// PROGRESS MODAL
.progress-modal {
--export-modal-bg-color: var(--alert-background-color-default);
--export-modal-fg-color: var(--alert-text-foreground-color-default);
--export-modal-icon-color: var(--alert-icon-foreground-color-default);
--export-modal-border-color: var(--alert-border-color-default);
position: absolute;
right: deprecated.$s-16;
top: deprecated.$s-48;
display: grid;
grid-template-columns: deprecated.$s-24 1fr deprecated.$s-24;
grid-template-areas:
"icon text close"
"bar bar bar";
gap: deprecated.$s-4 deprecated.$s-8;
padding-block-start: deprecated.$s-8;
background-color: var(--export-modal-bg-color);
border: deprecated.$s-1 solid var(--export-modal-border-color);
border-radius: deprecated.$br-8;
z-index: deprecated.$z-index-modal;
overflow: hidden;
}
.has-error {
--export-modal-bg-color: var(--alert-background-color-error);
--export-modal-fg-color: var(--alert-text-foreground-color-error);
--export-modal-icon-color: var(--alert-icon-foreground-color-error);
--export-modal-border-color: var(--alert-border-color-error);
grid-template-areas: "icon text close";
gap: deprecated.$s-8;
padding-block: deprecated.$s-8;
}
.icon {
@extend .button-icon;
grid-area: icon;
align-self: center;
margin-inline-start: deprecated.$s-8;
stroke: var(--export-modal-icon-color);
}
.title {
@include deprecated.bodyMediumTypography;
display: grid;
grid-template-columns: auto 1fr;
gap: deprecated.$s-8;
grid-area: text;
align-self: center;
padding: 0;
margin: 0;
color: var(--export-modal-fg-color);
}
.progress {
@include deprecated.bodyMediumTypography;
padding-left: deprecated.$s-8;
margin: 0;
align-self: center;
color: var(--modal-text-foreground-color);
}
.retry-btn {
@include deprecated.buttonStyle;
@include deprecated.bodySmallTypography;
display: inline;
text-align: left;
color: var(--modal-link-foreground-color);
margin: 0;
padding: 0;
}
.progress-close-button {
@include deprecated.buttonStyle;
padding: 0;
margin-inline-end: deprecated.$s-8;
}
.close-icon {
@extend .button-icon;
stroke: var(--export-modal-icon-color);
}
.progress-bar {
margin-top: 0;
grid-area: bar;
}

View File

@@ -53,6 +53,6 @@
(mf/defc inspect-title-bar* (mf/defc inspect-title-bar*
[{:keys [class title]}] [{:keys [class title title-class]}]
[:div {:class [(stl/css :title-bar) class]} [:div {:class [(stl/css :title-bar) class]}
[:div {:class (stl/css :title-only :inspect-title)} title]]) [:div {:class [title-class (stl/css :title-only :inspect-title)]} title]])

View File

@@ -19,7 +19,9 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.progress :refer [progress-notification-widget*]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.dashboard.deleted :refer [deleted-section*]]
[app.main.ui.dashboard.files :refer [files-section*]] [app.main.ui.dashboard.files :refer [files-section*]]
[app.main.ui.dashboard.fonts :refer [fonts-page* font-providers-page*]] [app.main.ui.dashboard.fonts :refer [fonts-page* font-providers-page*]]
[app.main.ui.dashboard.import] [app.main.ui.dashboard.import]
@@ -73,7 +75,13 @@
show-templates? show-templates?
(and (contains? cf/flags :dashboard-templates-section) (and (contains? cf/flags :dashboard-templates-section)
(:can-edit permissions))] (:can-edit permissions))
show-deleted? (:can-edit permissions)
section (if (and (not show-deleted?) (= section :dashboard-deleted))
:dashboard-recent
section)]
(mf/with-effect [] (mf/with-effect []
(let [key1 (events/listen js/window "resize" on-resize)] (let [key1 (events/listen js/window "resize" on-resize)]
@@ -84,6 +92,9 @@
[:div {:class (stl/css :dashboard-content) [:div {:class (stl/css :dashboard-content)
:on-click clear-selected-fn :on-click clear-selected-fn
:ref container} :ref container}
[:> progress-notification-widget*]
(case section (case section
:dashboard-recent :dashboard-recent
(when (seq projects) (when (seq projects)
@@ -140,6 +151,11 @@
:dashboard-settings :dashboard-settings
[:> team-settings-page* {:team team :profile profile}] [:> team-settings-page* {:team team :profile profile}]
:dashboard-deleted
[:> deleted-section* {:team team
:projects projects
:profile profile}]
nil)])) nil)]))
(def ref:dashboard-initialized (def ref:dashboard-initialized

View File

@@ -0,0 +1,309 @@
;; 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.main.ui.dashboard.deleted
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.dashboard.grid :refer [grid*]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private ref:deleted-files
(l/derived :deleted-files st/state))
(def ^:private menu-icon
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
(mf/defc header*
{::mf/props :obj
::mf/private true}
[]
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
[:div#dashboard-deleted-title {:class (stl/css :dashboard-title)}
[:h1 (tr "dashboard.projects-title")]]])
(mf/defc deleted-project-menu*
[{:keys [project show on-close top left]}]
(let [top (d/nilv top 0)
left (d/nilv left 0)
on-restore-project
(mf/use-fn
(mf/deps project)
(fn []
(let [on-accept #(st/emit! (dd/restore-project-immediately project))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.restore-project-confirmation.title")
:message (tr "dashboard.restore-project-confirmation.description" (:name project))
:accept-style :primary
:accept-label (tr "labels.continue")
:on-accept on-accept})))))
on-delete-project
(mf/use-fn
(mf/deps project)
(fn []
(let [accept-fn #(st/emit! (dd/delete-project-immediately project))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "dashboard.delete-project-forever-confirmation.description" (:name project))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept accept-fn})))))
options
(mf/with-memo [on-restore-project on-delete-project]
[{:name (tr "dashboard.restore-project-button")
:id "project-restore"
:handler on-restore-project}
{:name (tr "dashboard.delete-project-button")
:id "project-delete"
:handler on-delete-project}])]
[:> context-menu*
{:on-close on-close
:show show
:fixed (or (not= top 0) (not= left 0))
:min-width true
:top top
:left left
:options options}]))
(mf/defc deleted-project-item*
{::mf/private true}
[{:keys [project files]}]
(let [project-files (filterv #(= (:project-id %) (:id project)) files)
empty? (empty? project-files)
selected-files (mf/deref refs/selected-files)
dstate (mf/deref refs/dashboard-local)
edit-id (:project-for-edit dstate)
local (mf/use-state
#(do {:menu-open false
:menu-pos nil
:edition (= (:id project) edit-id)}))
[rowref limit] (hooks/use-dynamic-grid-item-width)
on-menu-click
(mf/use-fn
(fn [event]
(dom/prevent-default event)
(let [client-position (dom/get-client-position event)
position (if (and (nil? (:y client-position)) (nil? (:x client-position)))
(let [target-element (dom/get-target event)
points (dom/get-bounding-rect target-element)
y (:top points)
x (:left points)]
(gpt/point x y))
client-position)]
(swap! local assoc
:menu-open true
:menu-pos position))))
on-menu-close
(mf/use-fn #(swap! local assoc :menu-open false))
handle-menu-click
(mf/use-callback
(mf/deps on-menu-click)
(fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(on-menu-click event))))]
[:article {:class (stl/css-case :dashboard-project-row true)}
[:header {:class (stl/css :project)}
[:div {:class (stl/css :project-name-wrapper)}
[:h2 {:class (stl/css :project-name)
:title (:name project)}
(:name project)]
(when (:deleted-at project)
[:div {:class (stl/css :info-wrapper)}
[:div {:class (stl/css-case :project-actions true)}
[:button {:class (stl/css :options-btn)
:on-click on-menu-click
:title (tr "dashboard.options")
:aria-label (tr "dashboard.options")
:data-testid "project-options"
:on-key-down handle-menu-click}
menu-icon]]
(when (:menu-open @local)
[:> deleted-project-menu*
{:project project
:show (:menu-open @local)
:left (+ 24 (:x (:menu-pos @local)))
:top (:y (:menu-pos @local))
:on-close on-menu-close}])])]]
[:div {:class (stl/css :grid-container) :ref rowref}
(if ^boolean empty?
[:> empty-placeholder* {:title (tr "dashboard.empty-placeholder-files-title")
:class (stl/css :placeholder-placement)
:type 1
:subtitle (tr "dashboard.empty-placeholder-files-subtitle")}]
[:> grid*
{:project project
:files project-files
:origin :deleted
:can-edit false
:can-restore true
:limit limit
:selected-files selected-files}])]]))
(mf/defc menu*
[{:keys [team-id section]}]
(let [on-recent-click
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id))))
on-deleted-click
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-deleted :team-id team-id))))]
[:div {:class (stl/css :nav)}
[:div {:class (stl/css :nav-inside)}
[:div {:class [(stl/css :nav-option)
(stl/css-case :selected (= section :dashboard-recent))]
:data-testid "recent-tab"
:on-click on-recent-click}
(tr "labels.recent")]
[:div {:class [(stl/css :nav-option)
(stl/css-case :selected (= section :dashboard-deleted))]
:variant "ghost"
:type "button"
:data-testid "deleted-tab"
:on-click on-deleted-click}
(tr "labels.deleted")]]]))
(mf/defc deleted-section*
[{:keys [team projects]}]
(let [deleted-map
(mf/deref ref:deleted-files)
projects
(mf/with-memo [projects deleted-map]
(->> projects
(filter (fn [project]
(or (:deleted-at project)
(when deleted-map
(some #(= (:id project) (:project-id %))
(vals deleted-map))))))
(filter (fn [project]
(when deleted-map
(some #(= (:id project) (:project-id %))
(vals deleted-map)))))
(sort-by :modified-at)
(reverse)))
team-id
(get team :id)
;; Calculate deletion days based on team subscription
deletion-days
(let [subscription (get team :subscription)
sub-type (get subscription :type)
sub-status (get subscription :status)
canceled? (contains? #{"canceled" "unpaid"} sub-status)]
(cond
(and (= "unlimited" sub-type) (not canceled?)) 30
(and (= "enterprise" sub-type) (not canceled?)) 90
:else 7))
on-delete-all
(mf/use-fn
(mf/deps team-id deleted-map)
(fn []
(when-let [ids (not-empty (into #{} (map key) deleted-map))]
(let [on-accept #(st/emit! (dd/delete-files-immediately
{:team-id team-id
:ids ids}))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "dashboard.delete-all-forever-confirmation.description" (count ids))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept on-accept}))))))
on-restore-all
(mf/use-fn
(mf/deps team-id deleted-map)
(fn []
(when-let [ids (not-empty (into #{} (map key) deleted-map))]
(let [on-accept #(st/emit! (dd/restore-files-immediately {:team-id team-id :ids ids}))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.restore-all-confirmation.title")
:message (tr "dashboard.restore-all-confirmation.description" (count ids))
:accept-label (tr "labels.continue")
:accept-style :primary
:on-accept on-accept}))))))]
(mf/with-effect [team-id]
(st/emit! (dd/fetch-projects team-id)
(dd/fetch-deleted-files team-id)
(dd/clear-selected-files)))
[:*
[:> header* {:team team}]
[:section {:class (stl/css :dashboard-container :no-bg)}
[:*
[:div {:class (stl/css :no-bg)}
[:> menu* {:team-id team-id :section :dashboard-deleted}]
[:div {:class (stl/css :deleted-info-content)}
[:p {:class (stl/css :deleted-info)}
(tr "dashboard.trash-info-text-part1")
[:span {:class (stl/css :info-text-highlight)}
(tr "dashboard.trash-info-text-part2" deletion-days)]
(tr "dashboard.trash-info-text-part3")
[:br]
(tr "dashboard.trash-info-text-part4")]
[:div {:class (stl/css :deleted-options)}
[:> button* {:variant "ghost"
:type "button"
:on-click on-restore-all}
(tr "dashboard.restore-all-deleted-button")]
[:> button* {:variant "destructive"
:type "button"
:icon "delete"
:on-click on-delete-all}
(tr "dashboard.clear-trash-button")]]]
(when (seq projects)
(for [{:keys [id] :as project} projects]
(let [files (when deleted-map
(->> (vals deleted-map)
(filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))]
[:> deleted-project-item* {:project project
:files files
:key id}])))]]]]))

View File

@@ -0,0 +1,139 @@
// 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
@use "refactor/common-refactor.scss" as deprecated;
@use "common/refactor/common-dashboard";
@use "../ds/typography.scss" as t;
@use "../ds/_borders.scss" as *;
@use "../ds/spacing.scss" as *;
@use "../ds/_sizes.scss" as *;
@use "../ds/z-index.scss" as *;
.dashboard-container {
flex: 1 0 0;
width: 100%;
margin-inline-end: var(--sp-l);
border-top: $b-1 solid var(--panel-border-color);
overflow-y: auto;
padding-block-end: var(--sp-xxxl);
}
.deleted-info-content {
display: flex;
justify-content: space-between;
padding: var(--sp-s) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
}
.deleted-info {
display: block;
height: fit-content;
color: var(--color-foreground-secondary);
@include t.use-typography("body-large");
line-height: 0.8;
height: var(--sp-xl);
}
.info-text-highlight {
color: var(--color-accent-primary);
}
.deleted-options {
display: flex;
gap: 5px;
flex-shrink: 0;
}
.nav {
background: var(--color-background-default);
padding: var(--sp-xxl) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
position: sticky;
top: 0;
// We need to use the the deprecated z-index so it won't clash with the dashboard
// onboarding modals
z-index: deprecated.$z-index-3;
}
.nav-inside {
border-bottom: $b-1 solid var(--panel-border-color);
display: flex;
gap: var(--sp-l);
justify-content: space-between;
}
.nav-option {
color: var(--color-foreground-secondary);
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border: $b-1 solid transparent;
cursor: pointer;
}
.selected {
color: var(--color-foreground-primary);
border-bottom: $b-1 solid var(--color-foreground-primary);
}
.project {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
width: 99%;
max-height: $sz-40;
padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-l);
margin-block-start: var(--sp-l);
border-radius: $br-4;
}
.project-name-wrapper {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
min-height: var(--sp-xxxl);
margin-inline-start: var(--sp-s);
}
.project-name {
@include t.use-typography("body-large");
width: fit-content;
margin-inline-end: var(--sp-m);
line-height: 0.8;
color: var(--title-foreground-color-hover);
height: var(--sp-l);
}
.project-actions {
display: flex;
opacity: var(--actions-opacity);
margin-inline-start: var(--sp-xxxl);
}
.add-file-btn,
.options-btn {
@extend .button-tertiary;
height: var(--sp-xxxl);
width: var(--sp-xxxl);
margin: 0 var(--sp-s);
padding: var(--sp-s);
}
.info-wrapper {
display: flex;
align-items: center;
gap: var(--sp-s);
}
.add-icon,
.menu-icon {
@extend .button-icon;
stroke: var(--icon-foreground);
}

View File

@@ -55,7 +55,7 @@
projects)) projects))
(mf/defc file-menu* (mf/defc file-menu*
[{:keys [files on-edit on-close top left navigate origin parent-id can-edit]}] [{:keys [files on-edit on-close top left navigate origin parent-id can-edit can-restore]}]
(assert (seq files) "missing `files` prop") (assert (seq files) "missing `files` prop")
(assert (fn? on-edit) "missing `on-edit` prop") (assert (fn? on-edit) "missing `on-edit` prop")
@@ -187,7 +187,39 @@
on-export-binary-files on-export-binary-files
(fn [] (fn []
(st/emit! (-> (fexp/open-export-dialog files) (st/emit! (-> (fexp/open-export-dialog files)
(with-meta {::ev/origin "dashboard"}))))] (with-meta {::ev/origin "dashboard"}))))
restore-fn
(fn [_]
(st/emit! (dd/restore-files-immediately
(with-meta {:team-id (:id current-team)
:ids #{(:id file)}}
{:on-success #(st/emit! (ntf/success (tr "dashboard.restore-success-notification" (:name file)))
(dd/fetch-projects (:id current-team))
(dd/fetch-deleted-files (:id current-team)))
:on-error #(st/emit! (ntf/error (tr "dashboard.errors.error-on-restore-file" (:name file))))}))))
on-restore-immediately
(fn []
(st/emit!
(modal/show {:type :confirm
:title (tr "dashboard-restore-file-confirmation.title")
:message (tr "dashboard-restore-file-confirmation.description" (:name file))
:accept-label (tr "labels.continue")
:accept-style :primary
:on-accept restore-fn})))
on-delete-immediately
(fn []
(let [accept-fn #(st/emit! (dd/delete-files-immediately
{:team-id (:id current-team)
:ids #{(:id file)}}))]
(st/emit!
(modal/show {:type :confirm
:title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "dashboard.delete-file-forever-confirmation.description" (:name file))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept accept-fn}))))]
(mf/with-effect [] (mf/with-effect []
(->> (rp/cmd! :get-all-projects) (->> (rp/cmd! :get-all-projects)
@@ -227,76 +259,85 @@
(:id sub-project))})})}])) (:id sub-project))})})}]))
options options
(if multi? (if can-restore
[(when can-edit [(when can-restore
{:name (tr "dashboard.duplicate-multi" file-count) {:name (tr "dashboard.restore-file-button")
:id "duplicate-multi" :id "restore-file"
:handler on-duplicate}) :handler on-restore-immediately})
(when can-restore
{:name (tr "dashboard.delete-file-button")
:id "delete-file"
:handler on-delete-immediately})]
(if multi?
[(when can-edit
{:name (tr "dashboard.duplicate-multi" file-count)
:id "duplicate-multi"
:handler on-duplicate})
(when (and (or (seq current-projects) (seq other-teams)) can-edit) (when (and (or (seq current-projects) (seq other-teams)) can-edit)
{:name (tr "dashboard.move-to-multi" file-count) {:name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi" :id "file-move-multi"
:options sub-options}) :options sub-options})
{:name (tr "dashboard.export-binary-multi" file-count) {:name (tr "dashboard.export-binary-multi" file-count)
:id "file-binary-export-multi" :id "file-binary-export-multi"
:handler on-export-binary-files} :handler on-export-binary-files}
(when (and (:is-shared file) can-edit) (when (and (:is-shared file) can-edit)
{:name (tr "labels.unpublish-multi-files" file-count) {:name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi" :id "file-unpublish-multi"
:handler on-del-shared}) :handler on-del-shared})
(when (and (not is-lib-page?) can-edit) (when (and (not is-lib-page?) can-edit)
{:name :separator} {:name :separator}
{:name (tr "labels.delete-multi-files" file-count) {:name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi" :id "file-delete-multi"
:handler on-delete})] :handler on-delete})]
[{:name (tr "dashboard.open-in-new-tab") [{:name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab" :id "file-open-new-tab"
:handler on-new-tab} :handler on-new-tab}
(when (and (not is-search-page?) can-edit) (when (and (not is-search-page?) can-edit)
{:name (tr "labels.rename") {:name (tr "labels.rename")
:id "file-rename" :id "file-rename"
:handler on-edit}) :handler on-edit})
(when (and (not is-search-page?) can-edit) (when (and (not is-search-page?) can-edit)
{:name (tr "dashboard.duplicate") {:name (tr "dashboard.duplicate")
:id "file-duplicate" :id "file-duplicate"
:handler on-duplicate}) :handler on-duplicate})
(when (and (not is-lib-page?) (when (and (not is-lib-page?)
(not is-search-page?) (not is-search-page?)
(or (seq current-projects) (seq other-teams)) (or (seq current-projects) (seq other-teams))
can-edit) can-edit)
{:name (tr "dashboard.move-to") {:name (tr "dashboard.move-to")
:id "file-move-to" :id "file-move-to"
:options sub-options}) :options sub-options})
(when (and (not is-search-page?) (when (and (not is-search-page?)
can-edit) can-edit)
(if (:is-shared file) (if (:is-shared file)
{:name (tr "dashboard.unpublish-shared") {:name (tr "dashboard.unpublish-shared")
:id "file-del-shared" :id "file-del-shared"
:handler on-del-shared} :handler on-del-shared}
{:name (tr "dashboard.add-shared") {:name (tr "dashboard.add-shared")
:id "file-add-shared" :id "file-add-shared"
:handler on-add-shared})) :handler on-add-shared}))
{:name :separator} {:name :separator}
{:name (tr "dashboard.download-binary-file") {:name (tr "dashboard.download-binary-file")
:id "download-binary-file" :id "download-binary-file"
:handler on-export-binary-files} :handler on-export-binary-files}
(when (and (not is-lib-page?) (not is-search-page?) can-edit) (when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name :separator}) {:name :separator})
(when (and (not is-lib-page?) (not is-search-page?) can-edit) (when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name (tr "labels.delete") {:name (tr "labels.delete")
:id "file-delete" :id "file-delete"
:handler on-delete})])] :handler on-delete})]))]
[:> context-menu* [:> context-menu*
{:on-close on-close {:on-close on-close

View File

@@ -360,7 +360,9 @@
(st/emit! (modal/show options)))))] (st/emit! (modal/show options)))))]
[:div {:class (stl/css :font-item :table-row)} [:div {:class (stl/css :font-item :table-row)}
[:div {:class (stl/css :table-field :family)} [:div {:class (stl/css-case :table-field true
:family true
:is-edition edition?)}
(if ^boolean edition? (if ^boolean edition?
[:input {:type "text" [:input {:type "text"
:auto-focus true :auto-focus true

View File

@@ -126,6 +126,9 @@
@include twoLineTextEllipsis; @include twoLineTextEllipsis;
min-width: $sz-200; min-width: $sz-200;
width: $sz-200; width: $sz-200;
&.is-edition {
overflow: visible;
}
} }
> .filenames { > .filenames {

View File

@@ -86,7 +86,7 @@
(mf/defc grid-item-thumbnail* (mf/defc grid-item-thumbnail*
{::mf/props :obj {::mf/props :obj
::mf/private true} ::mf/private true}
[{:keys [can-edit file]}] [{:keys [can-edit file can-restore]}]
(let [file-id (get file :id) (let [file-id (get file :id)
revn (get file :revn) revn (get file :revn)
thumbnail-id (get file :thumbnail-id) thumbnail-id (get file :thumbnail-id)
@@ -109,7 +109,8 @@
:message (ex-message cause)))))] :message (ex-message cause)))))]
(partial rx/dispose! subscription)))) (partial rx/dispose! subscription))))
[:div {:class (stl/css :grid-item-th) [:div {:class (stl/css-case :grid-item-th true
:deleted-item can-restore)
:style {:background-color bg-color} :style {:background-color bg-color}
:ref container} :ref container}
(when visible? (when visible?
@@ -131,13 +132,15 @@
(mf/defc grid-item-library* (mf/defc grid-item-library*
{::mf/props :obj} {::mf/props :obj}
[{:keys [file]}] [{:keys [file can-restore]}]
(mf/with-effect [file] (mf/with-effect [file]
(when file (when file
(let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))] (let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))]
(run! fonts/ensure-loaded! font-ids)))) (run! fonts/ensure-loaded! font-ids))))
[:div {:class (stl/css :grid-item-th :library)} [:div {:class (stl/css-case :grid-item-th true
:library true
:deleted-item can-restore)}
(if (nil? file) (if (nil? file)
[:> loader* {:class (stl/css :grid-loader) [:> loader* {:class (stl/css :grid-loader)
:overlay true :overlay true
@@ -237,10 +240,13 @@
;; --- Grid Item ;; --- Grid Item
(mf/defc grid-item-metadata (mf/defc grid-item-metadata*
[{:keys [modified-at]}] [{:keys [file]}]
(let [time (ct/timeago modified-at)] (let [time (ct/timeago (or (:will-be-deleted-at file)
[:span {:class (stl/css :date)} time])) (:modified-at file)))]
[:span {:class (stl/css :date)
:title (tr "dashboard.deleted.will-be-deleted-at" time)}
time]))
(defn create-counter-element (defn create-counter-element
[_element file-count] [_element file-count]
@@ -250,7 +256,7 @@
counter-el)) counter-el))
(mf/defc grid-item* (mf/defc grid-item*
[{:keys [file origin can-edit selected-files]}] [{:keys [file origin can-edit selected-files can-restore]}]
(let [file-id (get file :id) (let [file-id (get file :id)
state (mf/deref refs/dashboard-local) state (mf/deref refs/dashboard-local)
@@ -289,12 +295,13 @@
on-navigate on-navigate
(mf/use-fn (mf/use-fn
(mf/deps file-id) (mf/deps file-id can-restore)
(fn [event] (fn [event]
(let [menu-icon (mf/ref-val menu-ref) (when-not can-restore
target (dom/get-target event)] (let [menu-icon (mf/ref-val menu-ref)
(when-not (dom/child? target menu-icon) target (dom/get-target event)]
(st/emit! (dcm/go-to-workspace :file-id file-id)))))) (when-not (dom/child? target menu-icon)
(st/emit! (dcm/go-to-workspace :file-id file-id)))))))
on-drag-start on-drag-start
(mf/use-fn (mf/use-fn
@@ -412,8 +419,8 @@
[:div {:class (stl/css :overlay)}] [:div {:class (stl/css :overlay)}]
(if ^boolean is-library-view? (if ^boolean is-library-view?
[:> grid-item-library* {:file file}] [:> grid-item-library* {:file file :can-restore can-restore}]
[:> grid-item-thumbnail* {:file file :can-edit can-edit}]) [:> grid-item-thumbnail* {:file file :can-edit can-edit :can-restore can-restore}])
(when (and (:is-shared file) (not is-library-view?)) (when (and (:is-shared file) (not is-library-view?))
[:div {:class (stl/css :item-badge)} deprecated-icon/library]) [:div {:class (stl/css :item-badge)} deprecated-icon/library])
@@ -425,7 +432,7 @@
:on-end edit :on-end edit
:max-length 250}] :max-length 250}]
[:h3 (:name file)]) [:h3 (:name file)])
[:& grid-item-metadata {:modified-at (:modified-at file)}]] [:> grid-item-metadata* {:file file}]]
[:div {:class (stl/css-case :project-th-actions true :force-display menu-open?)} [:div {:class (stl/css-case :project-th-actions true :force-display menu-open?)}
[:div [:div
@@ -451,11 +458,12 @@
:on-edit on-edit :on-edit on-edit
:on-close on-menu-close :on-close on-menu-close
:origin origin :origin origin
:parent-id (dm/str file-id "-action-menu")}]])]]]]])) :parent-id (dm/str file-id "-action-menu")
:can-restore can-restore}]])]]]]]))
(mf/defc grid* (mf/defc grid*
{::mf/props :obj} {::mf/props :obj}
[{:keys [files project origin limit create-fn can-edit selected-files]}] [{:keys [files project origin limit create-fn can-edit selected-files can-restore]}]
(let [dragging? (mf/use-state false) (let [dragging? (mf/use-state false)
project-id (get project :id) project-id (get project :id)
team-id (get project :team-id) team-id (get project :team-id)
@@ -535,7 +543,8 @@
:key (dm/str (:id item)) :key (dm/str (:id item))
:origin origin :origin origin
:selected-files selected-files :selected-files selected-files
:can-edit can-edit}])]) :can-edit can-edit
:can-restore can-restore}])])
:else :else
[:> empty-grid-placeholder* [:> empty-grid-placeholder*
@@ -548,7 +557,7 @@
:on-finish-import on-finish-import}])])) :on-finish-import on-finish-import}])]))
(mf/defc line-grid-row (mf/defc line-grid-row
[{:keys [files selected-files dragging? limit can-edit] :as props}] [{:keys [files selected-files dragging? limit can-edit can-restore] :as props}]
(let [elements limit (let [elements limit
limit (if dragging? (dec limit) limit)] limit (if dragging? (dec limit) limit)]
[:ul {:class (stl/css :grid-row :no-wrap) [:ul {:class (stl/css :grid-row :no-wrap)
@@ -563,10 +572,11 @@
:file item :file item
:selected-files selected-files :selected-files selected-files
:can-edit can-edit :can-edit can-edit
:key (dm/str (:id item))}])])) :key (dm/str (:id item))
:can-restore can-restore}])]))
(mf/defc line-grid (mf/defc line-grid
[{:keys [project team files limit create-fn can-edit] :as props}] [{:keys [project team files limit create-fn can-edit can-restore] :as props}]
(let [dragging? (mf/use-state false) (let [dragging? (mf/use-state false)
project-id (:id project) project-id (:id project)
team-id (:id team) team-id (:id team)
@@ -664,7 +674,8 @@
:selected-files selected-files :selected-files selected-files
:dragging? @dragging? :dragging? @dragging?
:can-edit can-edit :can-edit can-edit
:limit limit}] :limit limit
:can-restore can-restore}]
:else :else
[:> empty-grid-placeholder* [:> empty-grid-placeholder*

View File

@@ -375,3 +375,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width
.grid-loader { .grid-loader {
--icon-width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25); --icon-width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25);
} }
.deleted-item {
opacity: 0.5;
}

View File

@@ -17,6 +17,7 @@
[app.main.data.project :as dpj] [app.main.data.project :as dpj]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.dashboard.deleted :as deleted]
[app.main.ui.dashboard.grid :refer [line-grid]] [app.main.ui.dashboard.grid :refer [line-grid]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.pin-button :refer [pin-button*]] [app.main.ui.dashboard.pin-button :refer [pin-button*]]
@@ -315,28 +316,30 @@
{::mf/props :obj} {::mf/props :obj}
[{:keys [team projects profile]}] [{:keys [team projects profile]}]
(let [projects (let [team-id (get team :id)
recent-map (mf/deref ref:recent-files)
permisions (:permissions team)
can-edit (:can-edit permisions)
can-invite (or (:is-owner permisions)
(:is-admin permisions))
show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
show-team-hero? (deref show-team-hero*)
my-penpot? (= (:default-team-id profile) team-id)
default-team? (:is-default team)
show-deleted? (:can-edit permisions)
projects
(mf/with-memo [projects] (mf/with-memo [projects]
(->> projects (->> projects
(remove :deleted-at) (remove :deleted-at)
(sort-by :modified-at) (sort-by :modified-at)
(reverse))) (reverse)))
team-id (get team :id)
recent-map (mf/deref ref:recent-files)
permisions (:permissions team)
can-edit (:can-edit permisions)
can-invite (or (:is-owner permisions)
(:is-admin permisions))
show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
show-team-hero? (deref show-team-hero*)
is-my-penpot (= (:default-team-id profile) team-id)
is-defalt-team? (:is-default team)
on-close on-close
(mf/use-fn (mf/use-fn
(fn [] (fn []
@@ -366,16 +369,20 @@
[:* [:*
(when (and show-team-hero? (when (and show-team-hero?
can-invite can-invite
(not is-defalt-team?)) (not default-team?))
[:> team-hero* {:team team :on-close on-close}]) [:> team-hero* {:team team :on-close on-close}])
[:div {:class (stl/css-case :dashboard-container true [:div {:class (stl/css-case :dashboard-container true
:no-bg true :no-bg true
:dashboard-projects true :dashboard-projects true
:with-team-hero (and (not is-my-penpot) :with-team-hero (and (not my-penpot?)
(not is-defalt-team?) (not default-team?)
show-team-hero? show-team-hero?
can-invite))} can-invite))}
(when show-deleted?
[:> deleted/menu* {:team-id team-id :section :dashboard-recent}])
(for [{:keys [id] :as project} projects] (for [{:keys [id] :as project} projects]
;; FIXME: refactor this, looks inneficient ;; FIXME: refactor this, looks inneficient
(let [files (when recent-map (let [files (when recent-map

View File

@@ -4,16 +4,21 @@
// //
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated; @use "common/refactor/common-refactor.scss" as deprecated;
@use "common/refactor/common-dashboard"; @use "common/refactor/common-dashboard";
@use "../ds/typography.scss" as t;
@use "../ds/_borders.scss" as *;
@use "../ds/spacing.scss" as *;
@use "../ds/_sizes.scss" as *;
@use "../ds/z-index.scss" as *;
.dashboard-container { .dashboard-container {
flex: 1 0 0; flex: 1 0 0;
width: 100%; width: 100%;
margin-right: deprecated.$s-16; margin-inline-end: var(--sp-l);
border-top: deprecated.$s-1 solid var(--panel-border-color); border-top: $b-1 solid var(--panel-border-color);
overflow-y: auto; overflow-y: auto;
padding-bottom: deprecated.$s-32; padding-bottom: var(--sp-xxxl);
} }
.dashboard-projects { .dashboard-projects {
@@ -27,16 +32,16 @@
.dashboard-shared { .dashboard-shared {
width: calc(100vw - deprecated.$s-320); width: calc(100vw - deprecated.$s-320);
margin-right: deprecated.$s-52; margin-inline-end: deprecated.$s-52;
} }
.search { .search {
margin-top: deprecated.$s-12; margin-block-start: var(--sp-m);
} }
.dashboard-project-row { .dashboard-project-row {
--actions-opacity: 0; --actions-opacity: 0;
margin-bottom: deprecated.$s-24; margin-block-end: var(--sp-xxl);
position: relative; position: relative;
&:hover, &:hover,
@@ -60,12 +65,12 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: deprecated.$s-8; gap: var(--sp-s);
width: 99%; width: 99%;
max-height: deprecated.$s-40; max-height: $sz-40;
padding: deprecated.$s-8 deprecated.$s-8 deprecated.$s-8 deprecated.$s-16; padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-l);
margin-top: deprecated.$s-16; margin-block-start: var(--sp-l);
border-radius: deprecated.$br-4; border-radius: $br-4;
} }
.project-name-wrapper { .project-name-wrapper {
@@ -73,30 +78,29 @@
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
width: 100%; width: 100%;
min-height: deprecated.$s-32; min-height: var(--sp-xxxl);
margin-left: deprecated.$s-8; margin-inline-start: var(--sp-s);
} }
.project-name { .project-name {
@include deprecated.bodyLargeTypography; @include t.use-typography("body-large");
@include deprecated.textEllipsis;
width: fit-content; width: fit-content;
margin-right: deprecated.$s-12; margin-inline-end: var(--sp-m);
line-height: 0.8; line-height: 0.8;
color: var(--title-foreground-color-hover); color: var(--title-foreground-color-hover);
cursor: pointer; cursor: pointer;
height: deprecated.$s-16; height: var(--sp-l);
} }
.info-wrapper { .info-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
gap: deprecated.$s-8; gap: var(--sp-s);
} }
.info, .info,
.recent-files-row-title-info { .recent-files-row-title-info {
@include deprecated.bodyMediumTypography; @include t.use-typography("body-medium");
color: var(--title-foreground-color); color: var(--title-foreground-color);
@media (max-width: 760px) { @media (max-width: 760px) {
display: none; display: none;
@@ -106,16 +110,16 @@
.project-actions { .project-actions {
display: flex; display: flex;
opacity: var(--actions-opacity); opacity: var(--actions-opacity);
margin-left: deprecated.$s-32; margin-inline-start: var(--sp-xxxl);
} }
.add-file-btn, .add-file-btn,
.options-btn { .options-btn {
@extend .button-tertiary; @extend .button-tertiary;
height: deprecated.$s-32; height: var(--sp-xxxl);
width: deprecated.$s-32; width: var(--sp-xxxl);
margin: 0 deprecated.$s-8; margin: 0 var(--sp-s);
padding: deprecated.$s-8; padding: var(--sp-s);
} }
.add-icon, .add-icon,
@@ -126,24 +130,24 @@
.grid-container { .grid-container {
width: 100%; width: 100%;
padding: 0 deprecated.$s-4; padding: 0 var(--sp-xs);
} }
.placeholder-placement { .placeholder-placement {
margin: deprecated.$s-16 deprecated.$s-32; margin: var(--sp-l) var(--sp-xxxl);
} }
.show-more { .show-more {
--show-more-color: var(--button-secondary-foreground-color-rest); --show-more-color: var(--button-secondary-foreground-color-rest);
@include deprecated.buttonStyle; @include deprecated.buttonStyle;
@include deprecated.bodyMediumTypography; @include t.use-typography("body-medium");
position: absolute; position: absolute;
top: deprecated.$s-8; top: var(--sp-s);
right: deprecated.$s-52; right: deprecated.$s-52;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
column-gap: deprecated.$s-12; column-gap: var(--sp-m);
color: var(--show-more-color); color: var(--show-more-color);
&:hover { &:hover {
@@ -152,8 +156,8 @@
} }
.show-more-icon { .show-more-icon {
height: deprecated.$s-16; height: var(--sp-l);
width: deprecated.$s-16; width: var(--sp-l);
fill: none; fill: none;
stroke: var(--show-more-color); stroke: var(--show-more-color);
} }
@@ -164,13 +168,13 @@
border-radius: deprecated.$br-8; border-radius: deprecated.$br-8;
border: none; border: none;
display: flex; display: flex;
margin: deprecated.$s-16; margin: var(--sp-l);
padding: deprecated.$s-8; padding: var(--sp-s);
position: relative; position: relative;
img { img {
border-radius: deprecated.$br-4; border-radius: $br-4;
height: deprecated.$s-200; height: var(--sp-xl) 0;
width: auto; width: auto;
@media (max-width: 1200px) { @media (max-width: 1200px) {
@@ -185,18 +189,18 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
flex-grow: 1; flex-grow: 1;
padding: deprecated.$s-20 deprecated.$s-20; padding: var(--sp-xl) var(--sp-xl);
} }
.title { .title {
font-size: deprecated.$fs-24; font-size: $sz-24;
color: var(--color-foreground-primary); color: var(--color-foreground-primary);
font-weight: deprecated.$fw400; font-weight: deprecated.$fw400;
} }
.info { .info {
flex: 1; flex: 1;
font-size: deprecated.$fs-16; font-size: $sz-16;
span { span {
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
display: block; display: block;
@@ -204,15 +208,15 @@
a { a {
color: var(--color-accent-primary); color: var(--color-accent-primary);
} }
padding: deprecated.$s-8 0; padding: var(--sp-s) 0;
} }
.close { .close {
--close-icon-foreground-color: var(--icon-foreground); --close-icon-foreground-color: var(--icon-foreground);
position: absolute; position: absolute;
top: deprecated.$s-20; top: var(--sp-xl);
right: deprecated.$s-24; right: var(--sp-xxl);
width: deprecated.$s-24; width: var(--sp-xxl);
background-color: transparent; background-color: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
@@ -227,7 +231,7 @@
} }
.invite { .invite {
height: deprecated.$s-32; height: var(--sp-xxxl);
width: deprecated.$s-180; width: deprecated.$s-180;
} }
@@ -235,8 +239,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: deprecated.$s-200; width: var(--sp-xl) 0;
height: deprecated.$s-200; height: var(--sp-xl) 0;
overflow: hidden; overflow: hidden;
border-radius: deprecated.$br-4; border-radius: deprecated.$br-4;
@media (max-width: 1200px) { @media (max-width: 1200px) {

View File

@@ -27,11 +27,11 @@
[app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.project-menu :refer [project-menu*]] [app.main.ui.dashboard.project-menu :refer [project-menu*]]
[app.main.ui.dashboard.subscription :refer [subscription-sidebar* [app.main.ui.dashboard.subscription :refer [dashboard-cta*
get-subscription-type
menu-team-icon* menu-team-icon*
dashboard-cta*
show-subscription-dashboard-banner? show-subscription-dashboard-banner?
get-subscription-type]] subscription-sidebar*]]
[app.main.ui.dashboard.team-form] [app.main.ui.dashboard.team-form]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.icons :as deprecated-icon] [app.main.ui.icons :as deprecated-icon]

View File

@@ -205,7 +205,10 @@
:cmd :export-frames :cmd :export-frames
:origin origin}])) :origin origin}]))
(mf/defc export-progress-widget ;; FIXME: deprecated, should be refactored in two components and use
;; the generic progress reporter
(mf/defc progress-widget
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[] []
(let [state (mf/deref refs/export) (let [state (mf/deref refs/export)
@@ -217,11 +220,11 @@
detail-visible? (:detail-visible state) detail-visible? (:detail-visible state)
widget-visible? (:widget-visible state) widget-visible? (:widget-visible state)
progress (:progress state) progress (:progress state)
exports (:exports state) items (:exports state)
total (count exports) total (or (:total state) (count items))
complete? (= progress total) complete? (= progress total)
circ (* 2 Math/PI 12) circ (* 2 Math/PI 12)
pct (- circ (* circ (/ progress total))) pct (if (zero? total) circ (- circ (* circ (/ progress total))))
pwidth pwidth
(if error? (if error?
@@ -243,16 +246,20 @@
title title
(cond (cond
error? (tr "workspace.options.exporting-object-error") error? (tr "workspace.options.exporting-object-error")
complete? (tr "workspace.options.exporting-complete") complete? (tr "workspace.options.exporting-complete")
healthy? (tr "workspace.options.exporting-object") healthy? (tr "workspace.options.exporting-object")
(not healthy?) (tr "workspace.options.exporting-object-slow")) (not healthy?) (tr "workspace.options.exporting-object-slow"))
retry-last-export retry-last-operation
(mf/use-fn #(st/emit! (de/retry-last-export))) (mf/use-fn
(fn []
(st/emit! (de/retry-last-export))))
toggle-detail-visibility toggle-detail-visibility
(mf/use-fn #(st/emit! (de/toggle-detail-visibililty)))] (mf/use-fn
(fn []
(st/emit! (de/toggle-detail-visibililty))))]
[:* [:*
(when widget-visible? (when widget-visible?
@@ -283,11 +290,11 @@
error-icon error-icon
neutral-icon) neutral-icon)
[:p {:class (stl/css :export-progress-title)} [:div {:class (stl/css :export-progress-title)}
title [:div {:class (stl/css :title-text)} title]
(if error? (if error?
[:button {:class (stl/css :retry-btn) [:button {:class (stl/css :retry-btn)
:on-click retry-last-export} :on-click retry-last-operation}
(tr "workspace.options.retry")] (tr "workspace.options.retry")]
[:span {:class (stl/css :progress)} [:span {:class (stl/css :progress)}

View File

@@ -12,14 +12,17 @@
flex-direction: column; flex-direction: column;
gap: var(--sp-l); gap: var(--sp-l);
width: 100%; width: 100%;
height: calc(100vh - px2rem(128)); // TODO: Fix this hardcoded value max-height: calc(100vh - px2rem(128)); // TODO: Fix this hardcoded value
padding-top: var(--sp-s); padding-top: var(--sp-s);
padding-inline: var(--sp-m);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-gutter: stable; scrollbar-gutter: stable;
background-color: var(--low-emphasis-background);
} }
.workspace-element-options { .workspace-element-options {
height: calc(100vh - px2rem(200)); // TODO: Fix this hardcoded value max-height: calc(100vh - px2rem(180)); // TODO: Fix this hardcoded value
padding-inline: var(--sp-m); padding-inline: var(--sp-m);
background-color: var(--low-emphasis-background);
} }

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