mirror of
https://github.com/penpot/penpot.git
synced 2026-01-25 14:50:32 -05:00
Compare commits
94 Commits
2.0.3
...
hiru-refac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41a46fe56a | ||
|
|
fd92437f7d | ||
|
|
81b52d7170 | ||
|
|
a969550aa4 | ||
|
|
7e20cf10c5 | ||
|
|
90022041e6 | ||
|
|
ce790d83fd | ||
|
|
606aeeb38f | ||
|
|
ef632bcae7 | ||
|
|
b9ed0e1146 | ||
|
|
6477a48a50 | ||
|
|
64ff6d05c0 | ||
|
|
ea42669ff6 | ||
|
|
b14b8f794a | ||
|
|
87927a3476 | ||
|
|
83f8218bf3 | ||
|
|
c9fbb21924 | ||
|
|
bc279977d5 | ||
|
|
ca640964d1 | ||
|
|
4d29bff9b5 | ||
|
|
ff04877d24 | ||
|
|
34d75957f0 | ||
|
|
5fccc59ad5 | ||
|
|
c1c0ec6f2f | ||
|
|
f47991fa9c | ||
|
|
656e910896 | ||
|
|
39c35d51d3 | ||
|
|
5b4fcd2c80 | ||
|
|
91ade7fe58 | ||
|
|
19878484a6 | ||
|
|
c27f884755 | ||
|
|
19fc5fa820 | ||
|
|
171320d1c0 | ||
|
|
3fa1d3b5fd | ||
|
|
fb24918fd9 | ||
|
|
984dacba2b | ||
|
|
93debeb272 | ||
|
|
e813fcb9b7 | ||
|
|
ad0aae375b | ||
|
|
d20a92ce50 | ||
|
|
4351c221ac | ||
|
|
1f5658ad1b | ||
|
|
0df97d4d7d | ||
|
|
e10c96fa8b | ||
|
|
4b846b17f0 | ||
|
|
6f2f2291c2 | ||
|
|
33c12117cc | ||
|
|
94f9551b92 | ||
|
|
259eae70c6 | ||
|
|
c0fa766b64 | ||
|
|
a79e4d7af3 | ||
|
|
589fb144db | ||
|
|
fd4470afde | ||
|
|
e1e05e6de8 | ||
|
|
d5dbe0b594 | ||
|
|
0606ef1c84 | ||
|
|
481058b8d4 | ||
|
|
a109673654 | ||
|
|
b17371d440 | ||
|
|
617edbebec | ||
|
|
e3f9bafb33 | ||
|
|
2a6589ab01 | ||
|
|
9e2e9f5b64 | ||
|
|
82dca0439a | ||
|
|
66f8ffb408 | ||
|
|
fd641e87c9 | ||
|
|
bec59ab3c2 | ||
|
|
b426db133d | ||
|
|
e232beeb59 | ||
|
|
dae277adb2 | ||
|
|
cdf8c5836a | ||
|
|
1743da7ebf | ||
|
|
cff44e34c3 | ||
|
|
740db82b6c | ||
|
|
4d7a572daa | ||
|
|
cb6db21e63 | ||
|
|
06033ea955 | ||
|
|
f15caf54dd | ||
|
|
94ee83a120 | ||
|
|
cb6e3a2e3c | ||
|
|
8abab982e7 | ||
|
|
6a412c75ce | ||
|
|
098e33bd98 | ||
|
|
a5d056f254 | ||
|
|
aa33bb1ebf | ||
|
|
8a81bc11e0 | ||
|
|
f0ea613d47 | ||
|
|
725501faf9 | ||
|
|
470cf08134 | ||
|
|
e48bfb5d94 | ||
|
|
326f18cb66 | ||
|
|
5983b9cd54 | ||
|
|
864881721f | ||
|
|
9aff12f3c6 |
16
CHANGES.md
16
CHANGES.md
@@ -1,13 +1,5 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.0.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix chrome scrollbar styling [Taiga Issue #7852](https://tree.taiga.io/project/penpot/issue/7852)
|
||||
- Fix incorrect password encoding on create-profile manage scritp [Github #3651](https://github.com/penpot/penpot/issues/3651)
|
||||
|
||||
|
||||
## 2.0.2
|
||||
|
||||
### :sparkles: Enhancements
|
||||
@@ -15,11 +7,6 @@
|
||||
- Fix locking contention on cron subsystem (causes backend start blocking)
|
||||
- Fix locking contention on file object thumbails backend RPC calls
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix color palette sorting [Taiga Issue #7458](https://tree.taiga.io/project/penpot/issue/7458)
|
||||
- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671)
|
||||
|
||||
|
||||
## 2.0.1
|
||||
|
||||
@@ -27,7 +14,6 @@
|
||||
|
||||
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
|
||||
|
||||
|
||||
## 2.0.0 - I Just Can't Get Enough
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -42,8 +28,6 @@
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- New strokes default to inside border [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
|
||||
- Change default z ordering on layers in flex layout. The previous behavior was inconsistent with how HTML works and we changed it to be more consistent. Previous layers that overlapped could be hidden, the fastest way to fix this is changing the z-index property but a better way is to change the order of your layers.
|
||||
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
|
||||
|
||||
124
README.md
124
README.md
@@ -2,10 +2,11 @@
|
||||
[uri_license]: https://www.mozilla.org/en-US/MPL/2.0
|
||||
[uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg
|
||||
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<img src="https://penpot.app/images/readme/git-readme-header.png" alt="PENPOT">
|
||||
</h1>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://penpot.app/images/readme/github-dark-mode.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://penpot.app/images/readme/github-light-mode.png">
|
||||
<img alt="penpot header image" src="https://penpot.app/images/readme/github-light-mode.png">
|
||||
</picture>
|
||||
|
||||
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitter.im/penpot/community" rel="nofollow"><img src="https://camo.githubusercontent.com/5b0aecb33434f82a7b158eab7247544235ada0cf7eeb9ce8e52562dd67f614b7/68747470733a2f2f6261646765732e6769747465722e696d2f736572656e6f2d78797a2f636f6d6d756e6974792e737667" alt="Gitter" data-canonical-src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
|
||||
@@ -13,22 +14,37 @@
|
||||
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
||||
<a href="https://help.penpot.app/technical-guide/getting-started/"><b>Getting Started</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/introduction/info/"><b>Tutorials & Info</b></a> •
|
||||
<a href="https://community.penpot.app/"><b>Community</b></a> •
|
||||
<a href="https://twitter.com/penpotapp"><b>Twitter</b></a> •
|
||||
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
|
||||
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
||||
<a href="https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g"><b>Youtube</b></a>
|
||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
||||
<a href="https://help.penpot.app/technical-guide/getting-started/"><b>Getting Started</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a> •
|
||||
<a href="https://help.penpot.app/user-guide/introduction/info/"><b>Tutorials & Info</b></a> •
|
||||
<a href="https://community.penpot.app/"><b>Community</b></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/@Penpot"><b>Youtube</b></a> •
|
||||
<a href="https://peertube.kaleidos.net/a/penpot_app/video-channels"><b>Peertube</b></a> •
|
||||
<a href="https://www.linkedin.com/company/penpot/"><b>Linkedin</b></a> •
|
||||
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
|
||||
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
||||
<a href="https://twitter.com/penpotapp"><b>X</b></a>
|
||||
|
||||
</p>
|
||||
|
||||

|
||||
<br />
|
||||
|
||||
🎇 **Penpot Fest exceeded all expectations - it was a complete success!** 🎇 Penpot Fest is our first Design event that brought designers and developers from the Open Source communities and beyond. Watch the replay of the talks on our [Youtube channel](https://www.youtube.com/playlist?list=PLgcCPfOv5v56-fghJo2dHNBqL9zlDTslh) or [Peertube channel](https://peertube.kaleidos.net/w/p/1tWgyJTt8sKbWwCEcBimZW)
|
||||
[Penpot video](https://github.com/penpot/penpot/assets/5446186/b8ad0764-585e-4ddc-b098-9b4090d337cc)
|
||||
|
||||
Penpot is the first **Open Source** design and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open standards (SVG). Penpot invites designers all over the world to fall in love with open source while getting developers excited about the design process in return.
|
||||
<br />
|
||||
|
||||
Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.
|
||||
|
||||
Penpot is available on browser and [self host](https://penpot.app/self-host). It’s web-based and works with open standards (SVG, CSS and HTML). And last but not least, it’s free!
|
||||
|
||||
Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more. Plus, it's faster and more accessible.
|
||||
|
||||
|
||||
🎇 **Penpot Fest is back!** Our design, code & Open Source event is happening in Barcelona | June 5-7th. [Get your tickets](https://www.eventbrite.es/e/penpot-fest-2024-tickets-859331883797) to join other designers and developers from open-source communities and beyond.
|
||||
Check out the highlights from [Penpot Fest 2023 edition](https://www.youtube.com/watch?v=sOpLZaK5mDc)!
|
||||
|
||||
## Table of contents ##
|
||||
|
||||
@@ -40,48 +56,47 @@ Penpot is the first **Open Source** design and prototyping platform meant for cr
|
||||
|
||||
## Why Penpot ##
|
||||
|
||||
Penpot makes design and prototyping accessible to every team in the world.
|
||||
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
|
||||
|
||||
### For cross-domain teams ###
|
||||
We have a clear focus on design and code teams and our capabilities reflect exactly that. The less hand-off mindset, the more fun for everyone.
|
||||
### Designed for developers ###
|
||||
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
|
||||
|
||||
### Multiplatform ###
|
||||
Being web based, Penpot is not dependent on operating systems or local installations, you will only need to run a modern browser.
|
||||
### Inspect mode ###
|
||||
Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code.
|
||||
|
||||
### Open Standards ###
|
||||
Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work.
|
||||
### Self host your own instance ###
|
||||
Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server.
|
||||
|
||||
### Integrations ###
|
||||
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
|
||||
|
||||
### What’s great for design ###
|
||||
With Penpot you can design libraries to share and reuse; turn design elements into components and tokens to allow reusability and scalability; and build realistic user flows and interactions.
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<img src="https://penpot.app/images/readme/git-open.png" alt="Open Source" style="width: 65%;">
|
||||
<img src="https://img.plasmic.app/img-optimizer/v1/img?src=https%3A%2F%2Fimg.plasmic.app%2Fimg-optimizer%2Fv1%2Fimg%2F9dd677c36afb477e9666ccd1d3f009ad.png" alt="Open Source" style="width: 65%;">
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
## Getting started ##
|
||||
|
||||
### Install with Elestio ###
|
||||
[Elestio](https://elest.io/) offers a fully managed service for on-premise instances of a selection of open-source software! This means you can deploy a dedicated instance of Penpot in just 3 minutes with no technical knowledge needed.
|
||||
Penpot is the only design & prototype platform that is deployment agnostic. You can use it or deploy it anywhere.
|
||||
|
||||
You don’t need to worry about DNS configuration, SMTP, backups, SSL certificates, OS & Penpot upgrades, and much more.
|
||||
|
||||
[Get started with Elestio.](https://help.penpot.app/technical-guide/getting-started/#install-with-elestio)
|
||||
|
||||
### Install with Docker ###
|
||||
|
||||
You can also get started with Penpot locally or self-host it with **docker** and **docker-compose**.
|
||||
|
||||
Here’s a step-by-step guide on [getting started with Docker.](https://help.penpot.app/technical-guide/getting-started/#install-with-docker)
|
||||
|
||||
### Penpot cloud app ###
|
||||
|
||||
If you prefer not to install Penpot in a local environment, [login or register on our Penpot cloud app](https://design.penpot.app). Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** on your own.
|
||||
Learn how to install it with Elestio and Docker, or other options on [our website](https://penpot.app/self-host).
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<img src="https://penpot.app/images/readme/git-self-host.png" alt="Getting started" style="width: 65%;">
|
||||
<img src="https://site-assets.plasmic.app/2168cf524dd543caeff32384eb9ea0a1.svg" alt="Open Source" style="width: 65%;">
|
||||
</p>
|
||||
<br />
|
||||
|
||||
## Community ##
|
||||
|
||||
We love the open source software community. Contributing is our passion and if it’s yours too, [participate](https://community.penpot.app/) and [improve](https://community.penpot.app/c/help-us-improve-penpot/7) Penpot. All your ideas and code are welcome!
|
||||
We love the Open Source software community. Contributing is our passion and if it’s yours too, participate and [improve](https://community.penpot.app/c/help-us-improve-penpot/7) Penpot. All your designs, code and ideas are welcome!
|
||||
|
||||
If you need help or have any questions; if you’d like to share your experience using Penpot or get inspired; if you’d rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)!
|
||||
|
||||
@@ -93,30 +108,41 @@ You will find the following categories:
|
||||
- [Events and Announcements](https://community.penpot.app/c/announcements/5)
|
||||
- [Inside Penpot](https://community.penpot.app/c/inside-penpot/21)
|
||||
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
|
||||
- [Design and Code Essentials](https://community.penpot.app/c/design-and-code-essentials/22)
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<img src="https://penpot.app/images/readme/git-collaborate.png" alt="Communnity" style="width: 65%;">
|
||||
<img src="https://github.com/penpot/penpot/assets/5446186/6ac62220-a16c-46c9-ab21-d24ae357ed03" alt="Community" style="width: 65%;">
|
||||
</p>
|
||||
<br />
|
||||
|
||||
## Contributing ##
|
||||
|
||||
Every sort of contribution will be very helpful to enhance Penpot. How you’ll participate? All your ideas, designs and code are welcome:
|
||||
Any contribution will make a difference to improve Penpot. How can you get involved?
|
||||
|
||||
Choose your way:
|
||||
|
||||
- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community
|
||||
- Invite your [team to join](https://design.penpot.app/#/auth/register)
|
||||
- Star this repo and follow us on Social Media: [Twitter](https://twitter.com/penpotapp), [Instagram](https://instagram.com/penpot.app), [Youtube](https://www.youtube.com/c/Penpot) or [Mastodon](https://fosstodon.org/@penpot/).
|
||||
- Participate in the [Community](https://community.penpot.app/) asking and answering questions, reacting to others’ articles or opening your own conversations.
|
||||
- Star this repo and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app) and [X](https://twitter.com/penpotapp).
|
||||
- Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project.
|
||||
- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues)
|
||||
- Create and [share Libraries & templates](https://penpot.app/libraries-templates.html) that will be helpful for the community
|
||||
- Become a [translator](https://help.penpot.app/contributing-guide/translations)
|
||||
- Give feedback: [Mail us](mailto:support@penpot.app)
|
||||
- Give feedback: [Email us](mailto:support@penpot.app)
|
||||
- **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end
|
||||
|
||||
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing-guide](https://help.penpot.app/contributing-guide/).
|
||||
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing guide](https://help.penpot.app/contributing-guide/).
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<img src="https://penpot.app/images/readme/git-community.png" alt="Contributing" style="width: 65%;">
|
||||
<img src="https://github.com/penpot/penpot/assets/5446186/fea18923-dc06-49be-86ad-c3496a7956e6" alt="Libraries and templates" style="width: 65%;">
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
## Resources ##
|
||||
|
||||
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
||||
@@ -141,4 +167,4 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
Copyright (c) KALEIDOS INC
|
||||
```
|
||||
Penpot is a Kaleidos’ [open source project](https://kaleidos.net/products)
|
||||
Penpot is a Kaleidos’ [open source project](https://kaleidos.net/)
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
:deps
|
||||
{penpot/common {:local/root "../common"}
|
||||
org.clojure/clojure {:mvn/version "1.12.0-alpha5"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.4.4"}
|
||||
org.clojure/clojure {:mvn/version "1.12.0-alpha9"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.5-11"}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "6.3.0.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.3.2.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/yetti
|
||||
@@ -26,13 +26,13 @@
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.909"}
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.925"}
|
||||
metosin/reitit-core {:mvn/version "0.6.0"}
|
||||
nrepl/nrepl {:mvn/version "1.1.0"}
|
||||
cider/cider-nrepl {:mvn/version "0.44.0"}
|
||||
nrepl/nrepl {:mvn/version "1.1.1"}
|
||||
cider/cider-nrepl {:mvn/version "0.47.1"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.1"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.44.1.0"}
|
||||
org.postgresql/postgresql {:mvn/version "42.7.3"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.45.2.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "5.1.0"}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||
|
||||
dawran6/emoji {:mvn/version "0.1.5"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.11.7"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.1"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}}
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.0" :git/sha "3a2c484"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
[app.http.client :as http]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
@@ -283,12 +282,12 @@
|
||||
(into [(keyword (:name provider) fitem)] (map keyword) items)))
|
||||
|
||||
(defn- build-redirect-uri
|
||||
[{:keys [provider] :as cfg}]
|
||||
[{:keys [::provider] :as cfg}]
|
||||
(let [public (u/uri (cf/get :public-uri))]
|
||||
(str (assoc public :path (str "/api/auth/oauth/" (:name provider) "/callback")))))
|
||||
|
||||
(defn- build-auth-uri
|
||||
[{:keys [provider] :as cfg} state]
|
||||
[{:keys [::provider] :as cfg} state]
|
||||
(let [params {:client_id (:client-id provider)
|
||||
:redirect_uri (build-redirect-uri cfg)
|
||||
:response_type "code"
|
||||
@@ -299,15 +298,19 @@
|
||||
(assoc :query query)
|
||||
(str))))
|
||||
|
||||
(defn- qualify-prop-key
|
||||
[provider k]
|
||||
(keyword (:name provider) (name k)))
|
||||
|
||||
(defn- qualify-props
|
||||
[provider props]
|
||||
(reduce-kv (fn [result k v]
|
||||
(assoc result (keyword (:name provider) (name k)) v))
|
||||
(assoc result (qualify-prop-key provider k) v))
|
||||
{}
|
||||
props))
|
||||
|
||||
(defn fetch-access-token
|
||||
[{:keys [provider] :as cfg} code]
|
||||
(defn- fetch-access-token
|
||||
[{:keys [::provider] :as cfg} code]
|
||||
(let [params {:client_id (:client-id provider)
|
||||
:client_secret (:client-secret provider)
|
||||
:code code
|
||||
@@ -364,7 +367,7 @@
|
||||
:props props})))
|
||||
|
||||
(defn- fetch-user-info
|
||||
[{:keys [provider] :as cfg} tdata]
|
||||
[{:keys [::provider] :as cfg} tdata]
|
||||
(l/trace :hint "fetch user info"
|
||||
:uri (:user-uri provider)
|
||||
:token (obfuscate-string (:token/access tdata)))
|
||||
@@ -389,7 +392,7 @@
|
||||
(-> response :body json/decode)))
|
||||
|
||||
(defn- get-user-info
|
||||
[{:keys [provider]} tdata]
|
||||
[{:keys [::provider]} tdata]
|
||||
(try
|
||||
(when (:token/id tdata)
|
||||
(let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))]
|
||||
@@ -413,8 +416,8 @@
|
||||
::fullname
|
||||
::props]))
|
||||
|
||||
(defn get-info
|
||||
[{:keys [provider ::setup/props] :as cfg} {:keys [params] :as request}]
|
||||
(defn- get-info
|
||||
[{:keys [::provider ::setup/props] :as cfg} {:keys [params] :as request}]
|
||||
(when-let [error (get params :error)]
|
||||
(ex/raise :type :internal
|
||||
:code :error-on-retrieving-code
|
||||
@@ -472,83 +475,108 @@
|
||||
(update :props merge (:props state)))))
|
||||
|
||||
(defn- get-profile
|
||||
[{:keys [::db/pool] :as cfg} info]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(some->> (:email info)
|
||||
(profile/clean-email)
|
||||
(profile/get-profile-by-email conn))))
|
||||
[cfg info]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(some->> (:email info)
|
||||
(profile/clean-email)
|
||||
(profile/get-profile-by-email conn)))))
|
||||
|
||||
(defn- redirect-response
|
||||
[uri]
|
||||
{::rres/status 302
|
||||
::rres/headers {"location" (str uri)}})
|
||||
|
||||
(defn- generate-error-redirect
|
||||
[_ cause]
|
||||
(let [data (if (ex/error? cause) (ex-data cause) nil)
|
||||
code (or (:code data) :unexpected)
|
||||
type (or (:type data) :internal)
|
||||
hint (or (:hint data)
|
||||
(if (ex/exception? cause)
|
||||
(ex-message cause)
|
||||
(str cause)))
|
||||
(defn- redirect-with-error
|
||||
([error] (redirect-with-error error nil))
|
||||
([error hint]
|
||||
(let [params {:error error :hint hint}
|
||||
params (d/without-nils params)
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/login")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(redirect-response uri))))
|
||||
|
||||
params {:error "unable-to-auth"
|
||||
:hint hint
|
||||
:type type
|
||||
:code code}
|
||||
(defn- redirect-to-register
|
||||
[cfg info]
|
||||
(let [info (assoc info
|
||||
:iss :prepared-register
|
||||
:exp (dt/in-future {:hours 48}))
|
||||
|
||||
params {:token (tokens/generate (::setup/props cfg) info)
|
||||
:fullname (:fullname info)}
|
||||
params (d/without-nils params)]
|
||||
|
||||
(redirect-response
|
||||
(-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/register/validate")
|
||||
(assoc :query (u/map->query-string params))))))
|
||||
|
||||
(defn- redirect-to-verify-token
|
||||
[token]
|
||||
(let [params {:token token}
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/login")
|
||||
(assoc :path "/#/auth/verify-token")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
(redirect-response uri)))
|
||||
|
||||
(defn- generate-redirect
|
||||
(defn- provider-has-email-verified?
|
||||
[{:keys [::provider] :as cfg} {:keys [props] :as info}]
|
||||
(let [prop (qualify-prop-key provider :email_verified)]
|
||||
(true? (get props prop))))
|
||||
|
||||
(defn- profile-has-provider-props?
|
||||
[{:keys [::provider] :as cfg} profile]
|
||||
(let [prop (qualify-prop-key provider :email)]
|
||||
(contains? (:props profile) prop)))
|
||||
|
||||
(defn- provider-matches-profile?
|
||||
[{:keys [::provider] :as cfg} profile info]
|
||||
(or (= (:auth-backend profile) (:name provider))
|
||||
(profile-has-provider-props? cfg profile)
|
||||
(provider-has-email-verified? cfg info)))
|
||||
|
||||
(defn- process-callback
|
||||
[cfg request info profile]
|
||||
(if profile
|
||||
(let [sxf (session/create-fn cfg (:id profile))
|
||||
token (or (:invitation-token info)
|
||||
(tokens/generate (::setup/props cfg)
|
||||
{:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:profile-id (:id profile)}))
|
||||
params {:token token}
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/verify-token")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(cond
|
||||
(some? profile)
|
||||
(cond
|
||||
(:is-blocked profile)
|
||||
(redirect-with-error "profile-blocked")
|
||||
|
||||
(when (:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
(not (provider-matches-profile? cfg profile info))
|
||||
(redirect-with-error "auth-provider-not-allowed")
|
||||
|
||||
(audit/submit! cfg {::audit/type "command"
|
||||
::audit/name "login-with-oidc"
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/ip-addr (audit/parse-client-ip request)
|
||||
::audit/props (audit/profile->props profile)})
|
||||
(not (:is-active profile))
|
||||
(let [info (assoc info :profile-id (:id profile))]
|
||||
(redirect-to-register cfg info))
|
||||
|
||||
(->> (redirect-response uri)
|
||||
(sxf request)))
|
||||
:else
|
||||
(let [sxf (session/create-fn cfg (:id profile))
|
||||
token (or (:invitation-token info)
|
||||
(tokens/generate (::setup/props cfg)
|
||||
{:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:props (:props info)
|
||||
:profile-id (:id profile)}))]
|
||||
|
||||
(audit/submit! cfg {::audit/type "command"
|
||||
::audit/name "login-with-oidc"
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/ip-addr (audit/parse-client-ip request)
|
||||
::audit/props (audit/profile->props profile)})
|
||||
|
||||
(if (auth/email-domain-in-whitelist? (:email info))
|
||||
(let [info (assoc info
|
||||
:iss :prepared-register
|
||||
:is-active true
|
||||
:exp (dt/in-future {:hours 48}))
|
||||
token (tokens/generate (::setup/props cfg) info)
|
||||
params (d/without-nils
|
||||
{:token token
|
||||
:provider (:provider (:path-params request))
|
||||
:fullname (:fullname info)})
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/#/auth/register/validate")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(->> (redirect-to-verify-token token)
|
||||
(sxf request))))
|
||||
|
||||
(redirect-response uri))
|
||||
(generate-error-redirect cfg "email-domain-not-allowed"))))
|
||||
(not (auth/email-domain-in-whitelist? (:email info)))
|
||||
(redirect-with-error "email-domain-not-allowed")
|
||||
|
||||
:else
|
||||
(let [info (assoc info :is-active (provider-has-email-verified? cfg info))]
|
||||
(if (contains? cf/flags :registration)
|
||||
(redirect-to-register cfg info)
|
||||
(redirect-with-error "registration-disabled")))))
|
||||
|
||||
(defn- auth-handler
|
||||
[cfg {:keys [params] :as request}]
|
||||
@@ -567,10 +595,10 @@
|
||||
(try
|
||||
(let [info (get-info cfg request)
|
||||
profile (get-profile cfg info)]
|
||||
(generate-redirect cfg request info profile))
|
||||
(process-callback cfg request info profile))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "error on oauth process" :cause cause)
|
||||
(generate-error-redirect cfg cause))))
|
||||
(l/err :hint "error on oauth process" :cause cause)
|
||||
(redirect-with-error "unable-to-auth" (ex-message cause)))))
|
||||
|
||||
(def provider-lookup
|
||||
{:compile
|
||||
@@ -579,13 +607,12 @@
|
||||
(fn [request]
|
||||
(let [provider (some-> request :path-params :provider keyword)]
|
||||
(if-let [provider (get providers provider)]
|
||||
(handler (assoc cfg :provider provider) request)
|
||||
(handler (assoc cfg ::provider provider) request)
|
||||
(ex/raise :type :restriction
|
||||
:code :provider-not-configured
|
||||
:provider provider
|
||||
:hint "provider not configured"))))))})
|
||||
|
||||
|
||||
(s/def ::client-id ::cf/oidc-client-id)
|
||||
(s/def ::client-secret ::cf/oidc-client-secret)
|
||||
(s/def ::base-uri ::cf/oidc-base-uri)
|
||||
@@ -598,7 +625,6 @@
|
||||
(s/def ::email-attr ::cf/oidc-email-attr)
|
||||
(s/def ::name-attr ::cf/oidc-name-attr)
|
||||
|
||||
;; FIXME: migrate to qualified-keywords
|
||||
(s/def ::provider
|
||||
(s/keys :req-un [::client-id
|
||||
::client-secret]
|
||||
|
||||
@@ -113,7 +113,8 @@
|
||||
(s/def ::worker-default-parallelism ::us/integer)
|
||||
(s/def ::worker-webhook-parallelism ::us/integer)
|
||||
|
||||
(s/def ::auth-data-cookie-domain ::us/string)
|
||||
(s/def ::authenticated-cookie-domain ::us/string)
|
||||
(s/def ::authenticated-cookie-name ::us/string)
|
||||
(s/def ::auth-token-cookie-name ::us/string)
|
||||
(s/def ::auth-token-cookie-max-age ::dt/duration)
|
||||
|
||||
@@ -221,6 +222,7 @@
|
||||
::audit-log-http-handler-concurrency
|
||||
::auth-token-cookie-name
|
||||
::auth-token-cookie-max-age
|
||||
::authenticated-cookie-name
|
||||
::authenticated-cookie-domain
|
||||
::database-password
|
||||
::database-uri
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.session :as session]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.files-create :refer [create-file]]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
@@ -341,57 +340,57 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- resend-email-notification
|
||||
[{:keys [::db/pool ::setup/props] :as cfg} {:keys [params] :as request}]
|
||||
[cfg {:keys [params] :as request}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(when-not (contains? params :force)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-force
|
||||
:hint "missing force checkbox"))
|
||||
|
||||
(when-not (contains? params :force)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-force
|
||||
:hint "missing force checkbox"))
|
||||
(let [profile (some->> params
|
||||
:email
|
||||
(profile/clean-email)
|
||||
(profile/get-profile-by-email conn))]
|
||||
|
||||
(let [profile (some->> params
|
||||
:email
|
||||
(profile/clean-email)
|
||||
(profile/get-profile-by-email pool))]
|
||||
(when-not profile
|
||||
(ex/raise :type :validation
|
||||
:code :missing-profile
|
||||
:hint "unable to find profile by email"))
|
||||
|
||||
(when-not profile
|
||||
(ex/raise :type :validation
|
||||
:code :missing-profile
|
||||
:hint "unable to find profile by email"))
|
||||
(cond
|
||||
(contains? params :block)
|
||||
(do
|
||||
(db/update! conn :profile {:is-blocked true} {:id (:id profile)})
|
||||
(db/delete! conn :http-session {:profile-id (:id profile)})
|
||||
|
||||
(cond
|
||||
(contains? params :block)
|
||||
(do
|
||||
(db/update! pool :profile {:is-blocked true} {:id (:id profile)})
|
||||
(db/delete! pool :http-session {:profile-id (:id profile)})
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body (str/ffmt "PROFILE '%' BLOCKED" (:email profile))})
|
||||
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body (str/ffmt "PROFILE '%' BLOCKED" (:email profile))})
|
||||
(contains? params :unblock)
|
||||
(do
|
||||
(db/update! conn :profile {:is-blocked false} {:id (:id profile)})
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body (str/ffmt "PROFILE '%' UNBLOCKED" (:email profile))})
|
||||
|
||||
(contains? params :unblock)
|
||||
(do
|
||||
(db/update! pool :profile {:is-blocked false} {:id (:id profile)})
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body (str/ffmt "PROFILE '%' UNBLOCKED" (:email profile))})
|
||||
(contains? params :resend)
|
||||
(if (:is-blocked profile)
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body "PROFILE ALREADY BLOCKED"}
|
||||
(do
|
||||
(#'auth/send-email-verification! cfg profile)
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body (str/ffmt "RESENDED FOR '%'" (:email profile))}))
|
||||
|
||||
(contains? params :resend)
|
||||
(if (:is-blocked profile)
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body "PROFILE ALREADY BLOCKED"}
|
||||
(do
|
||||
(auth/send-email-verification! pool props profile)
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body (str/ffmt "RESENDED FOR '%'" (:email profile))}))
|
||||
|
||||
:else
|
||||
(do
|
||||
(db/update! pool :profile {:is-active true} {:id (:id profile)})
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))}))))
|
||||
:else
|
||||
(do
|
||||
(db/update! conn :profile {:is-active true} {:id (:id profile)})
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))}))))))
|
||||
|
||||
|
||||
(defn- reset-file-version
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
@@ -34,7 +33,7 @@
|
||||
|
||||
;; A cookie that we can use to check from other sites of the same
|
||||
;; domain if a user is authenticated.
|
||||
(def default-auth-data-cookie-name "auth-data")
|
||||
(def default-authenticated-cookie-name "authenticated")
|
||||
|
||||
;; Default value for cookie max-age
|
||||
(def default-cookie-max-age (dt/duration {:days 7}))
|
||||
@@ -134,9 +133,9 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare ^:private assign-auth-token-cookie)
|
||||
(declare ^:private assign-auth-data-cookie)
|
||||
(declare ^:private assign-authenticated-cookie)
|
||||
(declare ^:private clear-auth-token-cookie)
|
||||
(declare ^:private clear-auth-data-cookie)
|
||||
(declare ^:private clear-authenticated-cookie)
|
||||
(declare ^:private gen-token)
|
||||
|
||||
(defn create-fn
|
||||
@@ -154,7 +153,7 @@
|
||||
(l/trace :hint "create" :profile-id (str profile-id))
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)
|
||||
(assign-auth-data-cookie session)))))
|
||||
(assign-authenticated-cookie session)))))
|
||||
|
||||
(defn delete-fn
|
||||
[{:keys [::manager]}]
|
||||
@@ -168,7 +167,7 @@
|
||||
(assoc :status 204)
|
||||
(assoc :body nil)
|
||||
(clear-auth-token-cookie)
|
||||
(clear-auth-data-cookie)))))
|
||||
(clear-authenticated-cookie)))))
|
||||
|
||||
(defn- gen-token
|
||||
[props {:keys [profile-id created-at]}]
|
||||
@@ -230,7 +229,7 @@
|
||||
(let [session (update! manager session)]
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)
|
||||
(assign-auth-data-cookie session)))
|
||||
(assign-authenticated-cookie session)))
|
||||
response))))
|
||||
|
||||
(def soft-auth
|
||||
@@ -263,11 +262,11 @@
|
||||
:secure secure?}]
|
||||
(update response :cookies assoc name cookie)))
|
||||
|
||||
(defn- assign-auth-data-cookie
|
||||
[response {profile-id :profile-id updated-at :updated-at}]
|
||||
(defn- assign-authenticated-cookie
|
||||
[response {updated-at :updated-at}]
|
||||
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
|
||||
domain (cf/get :auth-data-cookie-domain)
|
||||
cname default-auth-data-cookie-name
|
||||
domain (cf/get :authenticated-cookie-domain)
|
||||
cname (cf/get :authenticated-cookie-name "authenticated")
|
||||
|
||||
created-at (or updated-at (dt/now))
|
||||
renewal (dt/plus created-at default-renewal-max-age)
|
||||
@@ -275,17 +274,14 @@
|
||||
|
||||
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
|
||||
secure? (contains? cf/flags :secure-session-cookies)
|
||||
strict? (contains? cf/flags :strict-session-cookies)
|
||||
cors? (contains? cf/flags :cors)
|
||||
|
||||
cookie {:domain domain
|
||||
:expires expires
|
||||
:path "/"
|
||||
:comment comment
|
||||
:value (u/map->query-string {:profile-id profile-id})
|
||||
:same-site (if cors? :none (if strict? :strict :lax))
|
||||
:value true
|
||||
:same-site :strict
|
||||
:secure secure?}]
|
||||
|
||||
(cond-> response
|
||||
(string? domain)
|
||||
(update :cookies assoc cname cookie))))
|
||||
@@ -295,10 +291,10 @@
|
||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
|
||||
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
|
||||
|
||||
(defn- clear-auth-data-cookie
|
||||
(defn- clear-authenticated-cookie
|
||||
[response]
|
||||
(let [cname default-auth-data-cookie-name
|
||||
domain (cf/get :auth-data-cookie-domain)]
|
||||
(let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name)
|
||||
domain (cf/get :authenticated-cookie-domain)]
|
||||
(cond-> response
|
||||
(string? domain)
|
||||
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0}))))
|
||||
|
||||
@@ -150,8 +150,9 @@
|
||||
(clean-props))
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (d/without-nils
|
||||
{:access-token-id (some-> token-id str)})]
|
||||
context (-> (::context resultm)
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
(::rpc/type cfg))
|
||||
|
||||
@@ -77,19 +77,10 @@
|
||||
(when (seq events)
|
||||
(db/insert-many! pool :audit-log event-columns events))))
|
||||
|
||||
(def valid-event-types
|
||||
#{"action" "identify"})
|
||||
|
||||
(def schema:event
|
||||
[:map {:title "Event"}
|
||||
[:name
|
||||
[:and {:gen/elements ["update-file", "get-profile"]}
|
||||
[:string {:max 250}]
|
||||
[:re #"[\d\w-]{1,50}"]]]
|
||||
[:type
|
||||
[:and {:gen/elements valid-event-types}
|
||||
[:string {:max 250}]
|
||||
[::sm/one-of {:format "string"} valid-event-types]]]
|
||||
[:name [:string {:max 250}]]
|
||||
[:type [:string {:max 250}]]
|
||||
[:props
|
||||
[:map-of :keyword :any]]
|
||||
[:context {:optional true}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
[app.email :as eml]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
@@ -38,6 +37,14 @@
|
||||
(def schema:token
|
||||
[::sm/word-string {:max 6000}])
|
||||
|
||||
(def ^:private default-verify-threshold
|
||||
(dt/duration "15m"))
|
||||
|
||||
(defn- elapsed-verify-threshold?
|
||||
[profile]
|
||||
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
|
||||
(pos? (compare elapsed default-verify-threshold))))
|
||||
|
||||
;; ---- COMMAND: login with password
|
||||
|
||||
(defn login-with-password
|
||||
@@ -139,7 +146,7 @@
|
||||
|
||||
(update-password [conn profile-id]
|
||||
(let [pwd (profile/derive-password cfg password)]
|
||||
(db/update! conn :profile {:password pwd} {:id profile-id})
|
||||
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
|
||||
nil))]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
@@ -162,8 +169,8 @@
|
||||
|
||||
;; ---- COMMAND: Prepare Register
|
||||
|
||||
(defn validate-register-attempt!
|
||||
[{:keys [::db/pool] :as cfg} params]
|
||||
(defn- validate-register-attempt!
|
||||
[cfg params]
|
||||
|
||||
(when-not (contains? cf/flags :registration)
|
||||
(when-not (contains? params :invitation-token)
|
||||
@@ -171,7 +178,9 @@
|
||||
:code :registration-disabled)))
|
||||
|
||||
(when (contains? params :invitation-token)
|
||||
(let [invitation (tokens/verify (::setup/props cfg) {:token (:invitation-token params) :iss :team-invitation})]
|
||||
(let [invitation (tokens/verify (::setup/props cfg)
|
||||
{:token (:invitation-token params)
|
||||
:iss :team-invitation})]
|
||||
(when-not (= (:email params) (:member-email invitation))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-does-not-match-invitation
|
||||
@@ -181,13 +190,6 @@
|
||||
(ex/raise :type :validation
|
||||
:code :email-domain-is-not-allowed))
|
||||
|
||||
;; Don't allow proceed in preparing registration if the profile is
|
||||
;; already reported as spammer.
|
||||
(when (eml/has-bounce-reports? pool (:email params))
|
||||
(ex/raise :type :validation
|
||||
:code :email-has-permanent-bounces
|
||||
:hint "looks like the email has one or many bounces reported"))
|
||||
|
||||
;; Perform a basic validation of email & password
|
||||
(when (= (str/lower (:email params))
|
||||
(str/lower (:password params)))
|
||||
@@ -195,35 +197,13 @@
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password")))
|
||||
|
||||
(def register-retry-threshold
|
||||
(dt/duration "15m"))
|
||||
|
||||
(defn- elapsed-register-retry-threshold?
|
||||
[profile]
|
||||
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
|
||||
(pos? (compare elapsed register-retry-threshold))))
|
||||
|
||||
(defn prepare-register
|
||||
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
|
||||
|
||||
(validate-register-attempt! cfg params)
|
||||
|
||||
(let [email (profile/clean-email email)
|
||||
profile (when-let [profile (profile/get-profile-by-email pool email)]
|
||||
(cond
|
||||
(:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked)
|
||||
|
||||
(and (not (:is-active profile))
|
||||
(elapsed-register-retry-threshold? profile))
|
||||
profile
|
||||
|
||||
:else
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists
|
||||
:hint "profile already exists")))
|
||||
|
||||
profile (profile/get-profile-by-email pool email)
|
||||
params {:email email
|
||||
:password (:password params)
|
||||
:invitation-token (:invitation-token params)
|
||||
@@ -233,7 +213,6 @@
|
||||
:exp (dt/in-future {:days 7})}
|
||||
|
||||
params (d/without-nils params)
|
||||
|
||||
token (tokens/generate (::setup/props cfg) params)]
|
||||
(with-meta {:token token}
|
||||
{::audit/profile-id uuid/zero})))
|
||||
@@ -317,17 +296,16 @@
|
||||
{::db/return-keys true})
|
||||
(profile/decode-row))))
|
||||
|
||||
|
||||
(defn send-email-verification!
|
||||
[conn props profile]
|
||||
(let [vtoken (tokens/generate props
|
||||
[{:keys [::db/conn] :as cfg} profile]
|
||||
(let [vtoken (tokens/generate (::setup/props cfg)
|
||||
{:iss :verify-email
|
||||
:exp (dt/in-future "72h")
|
||||
:profile-id (:id profile)
|
||||
:email (:email profile)})
|
||||
;; NOTE: this token is mainly used for possible complains
|
||||
;; identification on the sns webhook
|
||||
ptoken (tokens/generate props
|
||||
ptoken (tokens/generate (::setup/props cfg)
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 30})})]
|
||||
@@ -346,69 +324,94 @@
|
||||
(into params)
|
||||
(assoc :fullname fullname))
|
||||
|
||||
is-active (or (:is-active params)
|
||||
(not (contains? cf/flags :email-verification)))
|
||||
|
||||
profile (if-let [profile-id (:profile-id claims)]
|
||||
(profile/get-profile conn profile-id)
|
||||
(let [params (-> params
|
||||
(assoc :is-active is-active)
|
||||
(update :password #(profile/derive-password cfg %)))]
|
||||
(let [is-active (or (boolean (:is-active params))
|
||||
(not (contains? cf/flags :email-verification)))
|
||||
params (-> params
|
||||
(assoc :is-active is-active)
|
||||
(update :password #(profile/derive-password cfg %)))]
|
||||
(->> (create-profile! conn params)
|
||||
(create-profile-rels! conn))))
|
||||
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))]
|
||||
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
|
||||
|
||||
props (audit/profile->props profile)]
|
||||
|
||||
;; If profile is filled in claims, means it tries to register
|
||||
;; again, so we proceed to update the modified-at attr
|
||||
;; accordingly.
|
||||
(when-let [id (:profile-id claims)]
|
||||
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
|
||||
(audit/submit! cfg
|
||||
{::audit/type "fact"
|
||||
::audit/name "register-profile-retry"
|
||||
::audit/profile-id id}))
|
||||
(cond
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
;; regenerate token and send back to the user a new invitation
|
||||
;; token (and mark current session as logged). This happens
|
||||
;; only if the invitation email matches with the register
|
||||
;; email.
|
||||
(and (some? invitation) (= (:email profile) (:member-email invitation)))
|
||||
;; When profile is blocked, we just ignore it and return plain data
|
||||
(:is-blocked profile)
|
||||
(do
|
||||
(l/wrn :hint "register attempt for already blocked profile"
|
||||
:profile-id (str (:id profile))
|
||||
:profile-email (:email profile))
|
||||
(rph/with-meta {:email (:email profile)}
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "ignore-because-blocked"}
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/name "register-profile-retry"}))
|
||||
|
||||
;; If invitation token comes in params, this is because the user
|
||||
;; comes from team-invitation process; in this case, regenerate
|
||||
;; token and send back to the user a new invitation token (and
|
||||
;; mark current session as logged). This happens only if the
|
||||
;; invitation email matches with the register email.
|
||||
(and (some? invitation)
|
||||
(= (:email profile)
|
||||
(:member-email invitation)))
|
||||
(let [claims (assoc invitation :member-id (:id profile))
|
||||
token (tokens/generate (::setup/props cfg) claims)
|
||||
resp {:invitation-token token}]
|
||||
(-> resp
|
||||
token (tokens/generate (::setup/props cfg) claims)]
|
||||
(-> {:invitation-token token}
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
|
||||
(rph/with-meta {::audit/replace-props props
|
||||
::audit/context {:action "accept-invitation"}
|
||||
::audit/profile-id (:id profile)})))
|
||||
|
||||
;; If auth backend is different from "penpot" means user is
|
||||
;; registering using third party auth mechanism; in this case
|
||||
;; we need to mark this session as logged.
|
||||
(not= "penpot" (:auth-backend profile))
|
||||
(-> (profile/strip-private-attrs profile)
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))
|
||||
;; When a new user is created and it is already activated by
|
||||
;; configuration or specified by OIDC, we just mark the profile
|
||||
;; as logged-in
|
||||
(not (:profile-id claims))
|
||||
(if (:is-active claims)
|
||||
(-> (profile/strip-private-attrs profile)
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-meta
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "login"}
|
||||
::audit/profile-id (:id profile)}))
|
||||
|
||||
;; If the `:enable-insecure-register` flag is set, we proceed
|
||||
;; to sign in the user directly, without email verification.
|
||||
(true? is-active)
|
||||
(-> (profile/strip-private-attrs profile)
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))
|
||||
(do
|
||||
(send-email-verification! cfg profile)
|
||||
(rph/with-meta {:email (:email profile)}
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "email-verification"}
|
||||
::audit/profile-id (:id profile)})))
|
||||
|
||||
;; In all other cases, send a verification email.
|
||||
:else
|
||||
(do
|
||||
(send-email-verification! conn (::setup/props cfg) profile)
|
||||
(rph/with-meta profile
|
||||
(let [elapsed? (elapsed-verify-threshold? profile)
|
||||
bounce? (eml/has-bounce-reports? conn (:email profile))
|
||||
action (if bounce?
|
||||
"ignore-because-bounce"
|
||||
(if elapsed?
|
||||
"resend-email-verification"
|
||||
"ignore"))]
|
||||
|
||||
(l/wrn :hint "repeated registry detected"
|
||||
:profile-id (str (:id profile))
|
||||
:profile-email (:email profile)
|
||||
:context-action action)
|
||||
|
||||
(when (= action "resend-email-verification")
|
||||
(db/update! conn :profile
|
||||
{:modified-at (dt/now)}
|
||||
{:id (:id profile)})
|
||||
(send-email-verification! cfg profile))
|
||||
|
||||
(rph/with-meta {:email (:email profile)}
|
||||
{::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})))))
|
||||
::audit/context {:action action}
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/name "register-profile-retry"})))))
|
||||
|
||||
(def schema:register-profile
|
||||
[:map {:title "register-profile"}
|
||||
@@ -427,7 +430,7 @@
|
||||
|
||||
;; ---- COMMAND: Request Profile Recovery
|
||||
|
||||
(defn request-profile-recovery
|
||||
(defn- request-profile-recovery
|
||||
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
|
||||
(letfn [(create-recovery-token [{:keys [id] :as profile}]
|
||||
(let [token (tokens/generate (::setup/props cfg)
|
||||
@@ -451,26 +454,38 @@
|
||||
nil))]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(when-let [profile (->> (profile/clean-email email)
|
||||
(profile/get-profile-by-email conn))]
|
||||
(when-not (eml/allow-send-emails? conn profile)
|
||||
(ex/raise :type :validation
|
||||
:code :profile-is-muted
|
||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
|
||||
(let [profile (->> (profile/clean-email email)
|
||||
(profile/get-profile-by-email conn))]
|
||||
|
||||
(when-not (:is-active profile)
|
||||
(ex/raise :type :validation
|
||||
:code :profile-not-verified
|
||||
:hint "the user need to validate profile before recover password"))
|
||||
(cond
|
||||
(not profile)
|
||||
(l/wrn :hint "attempt of profile recovery: no profile found"
|
||||
:profile-email email)
|
||||
|
||||
(when (eml/has-bounce-reports? conn (:email profile))
|
||||
(ex/raise :type :validation
|
||||
:code :email-has-permanent-bounces
|
||||
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
||||
(not (eml/allow-send-emails? conn profile))
|
||||
(l/wrn :hint "attempt of profile recovery: profile is muted"
|
||||
:profile-id (str (:id profile))
|
||||
:profile-email (:email profile))
|
||||
|
||||
(->> profile
|
||||
(create-recovery-token)
|
||||
(send-email-notification conn))))))
|
||||
(eml/has-bounce-reports? conn (:email profile))
|
||||
(l/wrn :hint "attempt of profile recovery: email has bounces"
|
||||
:profile-id (str (:id profile))
|
||||
:profile-email (:email profile))
|
||||
|
||||
(not (elapsed-verify-threshold? profile))
|
||||
(l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed"
|
||||
:profile-id (str (:id profile))
|
||||
:profile-email (:email profile))
|
||||
|
||||
|
||||
:else
|
||||
(do
|
||||
(db/update! conn :profile
|
||||
{:modified-at (dt/now)}
|
||||
{:id (:id profile)})
|
||||
(->> profile
|
||||
(create-recovery-token)
|
||||
(send-email-notification conn))))))))
|
||||
|
||||
|
||||
(def schema:request-profile-recovery
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.db.sql :as sql]
|
||||
[app.features.components-v2 :as feat.compv2]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.files-create :as files.create]
|
||||
@@ -24,7 +23,6 @@
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
@@ -102,9 +100,7 @@
|
||||
:revn revn
|
||||
:data nil
|
||||
:changes (blob/encode changes)})
|
||||
(rph/with-meta (rph/wrap nil)
|
||||
{::audit/replace-props {:file-id id
|
||||
:revn revn}}))))
|
||||
nil)))
|
||||
|
||||
;; --- MUTATION COMMAND: persist-temp-file
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.db :as db]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
@@ -73,7 +72,7 @@
|
||||
(rph/with-meta {::audit/props (:props profile)
|
||||
::audit/profile-id (:id profile)})))
|
||||
|
||||
(-> profile
|
||||
(-> (profile/strip-private-attrs profile)
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-meta {::audit/props (:props profile)
|
||||
::audit/profile-id (:id profile)}))))))
|
||||
|
||||
@@ -91,8 +91,8 @@
|
||||
|
||||
(defn get-profile
|
||||
"Get profile by id. Throws not-found exception if no profile found."
|
||||
[conn id & {:as attrs}]
|
||||
(-> (db/get-by-id conn :profile id attrs)
|
||||
[conn id & {:as opts}]
|
||||
(-> (db/get-by-id conn :profile id opts)
|
||||
(decode-row)))
|
||||
|
||||
;; --- MUTATION: Update Profile (own)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
@@ -82,8 +83,16 @@
|
||||
|
||||
(defmethod process-token :auth
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/get-profile conn profile-id)]
|
||||
(assoc claims :profile profile)))
|
||||
(let [profile (profile/get-profile conn profile-id {::sql/for-update true})
|
||||
props (merge (:props profile)
|
||||
(:props claims))]
|
||||
(when (not= props (:props profile))
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id}))
|
||||
|
||||
(let [profile (assoc profile :props props)]
|
||||
(assoc claims :profile profile))))
|
||||
|
||||
;; --- Team Invitation
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.auth :as cmd.auth]
|
||||
[app.rpc.commands.profile :as cmd.profile]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
@@ -38,13 +37,12 @@
|
||||
:or {is-active true}}]
|
||||
(when-let [system (get-current-system)]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [password (cmd.profile/derive-password system password)
|
||||
params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(let [params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(->> (cmd.auth/create-profile! conn params)
|
||||
(cmd.auth/create-profile-rels! conn))))))
|
||||
|
||||
|
||||
@@ -86,13 +86,11 @@
|
||||
|
||||
(defn resend-email-verification-email!
|
||||
[email]
|
||||
(let [sprops (:app.setup/props main/system)
|
||||
pool (:app.db/pool main/system)
|
||||
email (profile/clean-email email)
|
||||
profile (profile/get-profile-by-email pool email)]
|
||||
|
||||
(auth/send-email-verification! pool sprops profile)
|
||||
:email-sent))
|
||||
(db/tx-run! main/system
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [email (profile/clean-email email)
|
||||
profile (profile/get-profile-by-email conn email)]
|
||||
(#'auth/send-email-verification! cfg profile)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PROFILES MANAGEMENT
|
||||
|
||||
@@ -229,20 +229,51 @@
|
||||
(t/is (= "mtma" (:penpot/mtm-campaign props)))))))
|
||||
|
||||
(t/deftest prepare-register-and-register-profile-2
|
||||
(with-redefs [app.rpc.commands.auth/register-retry-threshold (dt/duration 500)]
|
||||
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||
(let [current-token (atom nil)]
|
||||
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||
(let [current-token (atom nil)]
|
||||
;; PREPARE REGISTER
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
(reset! current-token token))
|
||||
|
||||
;; PREPARE REGISTER
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (string? token))
|
||||
(reset! current-token token))
|
||||
;; DO REGISTRATION
|
||||
(let [data {::th/type :register-profile
|
||||
:token @current-token
|
||||
:fullname "foobar"
|
||||
:accept-terms-and-privacy true
|
||||
:accept-newsletter-subscription true}
|
||||
out (th/command! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 1 (:call-count @mock))))
|
||||
|
||||
;; DO REGISTRATION: try correct register attempt 1
|
||||
(th/reset-mock! mock)
|
||||
|
||||
;; PREPARE REGISTER: second attempt
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
(reset! current-token token))
|
||||
|
||||
;; DO REGISTRATION: second attempt
|
||||
(let [data {::th/type :register-profile
|
||||
:token @current-token
|
||||
:fullname "foobar"
|
||||
:accept-terms-and-privacy true
|
||||
:accept-newsletter-subscription true}
|
||||
out (th/command! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 0 (:call-count @mock))))
|
||||
|
||||
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
|
||||
:return true}]
|
||||
;; DO REGISTRATION: third attempt
|
||||
(let [data {::th/type :register-profile
|
||||
:token @current-token
|
||||
:fullname "foobar"
|
||||
@@ -250,44 +281,56 @@
|
||||
:accept-newsletter-subscription true}
|
||||
out (th/command! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 1 (:call-count @mock))))
|
||||
(t/is (= 1 (:call-count @mock))))))))
|
||||
|
||||
(th/reset-mock! mock)
|
||||
(t/deftest prepare-register-and-register-profile-3
|
||||
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||
(let [current-token (atom nil)]
|
||||
;; PREPARE REGISTER
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
(reset! current-token token))
|
||||
|
||||
;; PREPARE REGISTER without waiting for threshold
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :validation (-> out :error th/ex-type)))
|
||||
(t/is (= :email-already-exists (-> out :error th/ex-code))))
|
||||
;; DO REGISTRATION
|
||||
(let [data {::th/type :register-profile
|
||||
:token @current-token
|
||||
:fullname "foobar"
|
||||
:accept-terms-and-privacy true
|
||||
:accept-newsletter-subscription true}
|
||||
out (th/command! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 1 (:call-count @mock))))
|
||||
|
||||
(th/sleep {:millis 500})
|
||||
(th/reset-mock! mock)
|
||||
(th/reset-mock! mock)
|
||||
|
||||
;; PREPARE REGISTER waiting the threshold
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)]
|
||||
(th/db-update! :profile
|
||||
{:is-blocked true}
|
||||
{:email "hello@example.com"})
|
||||
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 0 (:call-count @mock)))
|
||||
;; PREPARE REGISTER: second attempt
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (th/success? out))
|
||||
(reset! current-token token))
|
||||
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result :token))
|
||||
(reset! current-token (:token result))))
|
||||
|
||||
;; DO REGISTRATION: try correct register attempt 1
|
||||
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
|
||||
:return true}]
|
||||
;; DO REGISTRATION: second attempt
|
||||
(let [data {::th/type :register-profile
|
||||
:token @current-token
|
||||
:fullname "foobar"
|
||||
:accept-terms-and-privacy true
|
||||
:accept-newsletter-subscription true}
|
||||
out (th/command! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count @mock))))))))
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 0 (:call-count @mock))))))))
|
||||
|
||||
|
||||
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-1
|
||||
@@ -359,13 +402,13 @@
|
||||
:email (:email profile)
|
||||
:password "foobar"}
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result :token)))))
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :email-already-exists (:code edata))))))
|
||||
(t/deftest prepare-register-profile-with-bounced-email
|
||||
|
||||
(t/deftest register-profile-with-bounced-email
|
||||
(let [pool (:app.db/pool th/*system*)
|
||||
data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
@@ -374,10 +417,9 @@
|
||||
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
||||
|
||||
(let [out (th/command! data)]
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :email-has-permanent-bounces (:code edata)))))))
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result :token))))))
|
||||
|
||||
(t/deftest register-profile-with-complained-email
|
||||
(let [pool (:app.db/pool th/*system*)
|
||||
@@ -455,7 +497,7 @@
|
||||
|
||||
(t/deftest request-profile-recovery
|
||||
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||
(let [profile1 (th/create-profile* 1)
|
||||
(let [profile1 (th/create-profile* 1 {:is-active false})
|
||||
profile2 (th/create-profile* 2 {:is-active true})
|
||||
pool (:app.db/pool th/*system*)
|
||||
data {::th/type :request-profile-recovery}]
|
||||
@@ -468,38 +510,47 @@
|
||||
|
||||
;; with valid email inactive user
|
||||
(let [data (assoc data :email (:email profile1))
|
||||
out (th/command! data)
|
||||
error (:error out)]
|
||||
out (th/command! data)]
|
||||
(t/is (= 0 (:call-count @mock)))
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :profile-not-verified)))
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (nil? (:error out))))
|
||||
|
||||
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
|
||||
:return true}]
|
||||
;; with valid email inactive user
|
||||
(let [data (assoc data :email (:email profile1))
|
||||
out (th/command! data)]
|
||||
(t/is (= 1 (:call-count @mock)))
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (nil? (:error out)))))
|
||||
|
||||
(th/reset-mock! mock)
|
||||
|
||||
;; with valid email and active user
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (= 1 (:call-count @mock))))
|
||||
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
|
||||
:return true}]
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (= 1 (:call-count @mock))))
|
||||
|
||||
;; with valid email and active user with global complaints
|
||||
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (= 2 (:call-count @mock))))
|
||||
;; with valid email and active user with global complaints
|
||||
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/command! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (= 2 (:call-count @mock))))
|
||||
|
||||
;; with valid email and active user with global bounce
|
||||
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/command! data)
|
||||
error (:error out)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (= 2 (:call-count @mock)))
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :email-has-permanent-bounces))))))
|
||||
;; with valid email and active user with global bounce
|
||||
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/command! data)]
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (nil? (:error out)))
|
||||
;; (th/print-result! out)
|
||||
(t/is (= 2 (:call-count @mock))))))))
|
||||
|
||||
|
||||
(t/deftest update-profile-password
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{:deps
|
||||
{org.clojure/clojure {:mvn/version "1.11.1"}
|
||||
{org.clojure/clojure {:mvn/version "1.11.2"}
|
||||
org.clojure/data.json {:mvn/version "2.5.0"}
|
||||
org.clojure/tools.cli {:mvn/version "1.0.219"}
|
||||
org.clojure/clojurescript {:mvn/version "1.11.132"}
|
||||
@@ -7,12 +7,12 @@
|
||||
org.clojure/data.fressian {:mvn/version "1.0.0"}
|
||||
|
||||
;; Logging
|
||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.22.1"}
|
||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.22.1"}
|
||||
org.apache.logging.log4j/log4j-web {:mvn/version "2.22.1"}
|
||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.22.1"}
|
||||
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.22.1"}
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.10"}
|
||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.23.1"}
|
||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.23.1"}
|
||||
org.apache.logging.log4j/log4j-web {:mvn/version "2.23.1"}
|
||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.23.1"}
|
||||
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.23.1"}
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.12"}
|
||||
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
|
||||
|
||||
selmer/selmer {:mvn/version "1.12.59"}
|
||||
@@ -28,7 +28,7 @@
|
||||
integrant/integrant {:mvn/version "0.8.1"}
|
||||
|
||||
org.apache.commons/commons-pool2 {:mvn/version "2.12.0"}
|
||||
org.graalvm.js/js {:mvn/version "23.0.2"}
|
||||
org.graalvm.js/js {:mvn/version "23.0.3"}
|
||||
|
||||
funcool/tubax {:mvn/version "2021.05.20-0"}
|
||||
funcool/cuerdas {:mvn/version "2023.11.09-407"}
|
||||
@@ -41,7 +41,7 @@
|
||||
:git/tag "3.0.0"
|
||||
:git/url "https://github.com/funcool/datoteka"}
|
||||
|
||||
lambdaisland/uri {:mvn/version "1.16.134"
|
||||
lambdaisland/uri {:mvn/version "1.19.155"
|
||||
:exclusions [org.clojure/data.json]}
|
||||
|
||||
frankiesardo/linked {:mvn/version "1.3.0"}
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}}
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.0" :git/sha "3a2c484"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
|
||||
@@ -274,13 +274,6 @@
|
||||
(catch #?(:clj Throwable :cljs :default) _cause
|
||||
[0 0 0])))
|
||||
|
||||
(defn hex->lum
|
||||
[color]
|
||||
(let [[r g b] (hex->rgb color)]
|
||||
(mth/sqrt (+ (* 0.241 r)
|
||||
(* 0.691 g)
|
||||
(* 0.068 b)))))
|
||||
|
||||
(defn- int->hex
|
||||
"Convert integer to hex string"
|
||||
[v]
|
||||
@@ -462,19 +455,3 @@
|
||||
|
||||
:else
|
||||
[r g (inc b)]))
|
||||
|
||||
(defn reduce-range
|
||||
[value range]
|
||||
(/ (mth/floor (* value range)) range))
|
||||
|
||||
(defn sort-colors
|
||||
[a b]
|
||||
(let [[ah _ av] (hex->hsv (:color a))
|
||||
[bh _ bv] (hex->hsv (:color b))
|
||||
ah (reduce-range (/ ah 60) 8)
|
||||
bh (reduce-range (/ bh 60) 8)
|
||||
av (/ av 255)
|
||||
bv (/ bv 255)
|
||||
a (+ (* ah 100) (* av 10))
|
||||
b (+ (* bh 100) (* bv 10))]
|
||||
(compare a b)))
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
|
||||
(ns app.common.files.defaults)
|
||||
|
||||
(def version 48)
|
||||
(def version 46)
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.svg :as csvg]
|
||||
[app.common.text :as txt]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape.shadow :as ctss]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -900,43 +898,6 @@
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
(defn migrate-up-47
|
||||
[data]
|
||||
(letfn [(fix-shape [page shape]
|
||||
(let [file {:id (:id data) :data data}
|
||||
component-file (:component-file shape)
|
||||
;; On cloning a file, the component-file of the shapes point to the old file id
|
||||
;; this is a workaround to be able to found the components in that case
|
||||
libraries {component-file {:id component-file :data data}}
|
||||
ref-shape (ctf/find-ref-shape file page libraries shape {:include-deleted? true :with-context? true})
|
||||
ref-parent (get (:objects (:container (meta ref-shape))) (:parent-id ref-shape))
|
||||
shape-swap-slot (ctk/get-swap-slot shape)
|
||||
ref-swap-slot (ctk/get-swap-slot ref-shape)]
|
||||
(if (and (some? shape-swap-slot)
|
||||
(= shape-swap-slot ref-swap-slot)
|
||||
(ctk/main-instance? ref-parent))
|
||||
(ctk/remove-swap-slot shape)
|
||||
shape)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals (partial fix-shape page)))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-page))))
|
||||
|
||||
(defn migrate-up-48
|
||||
[data]
|
||||
(letfn [(fix-shape [shape]
|
||||
(let [swap-slot (ctk/get-swap-slot shape)]
|
||||
(if (and (some? swap-slot)
|
||||
(not (ctk/subcopy-head? shape)))
|
||||
(ctk/remove-swap-slot shape)
|
||||
shape)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects update-vals fix-shape))]
|
||||
(-> data
|
||||
(update :pages-index update-vals update-page))))
|
||||
|
||||
(def migrations
|
||||
"A vector of all applicable migrations"
|
||||
[{:id 2 :migrate-up migrate-up-2}
|
||||
@@ -974,6 +935,4 @@
|
||||
{:id 43 :migrate-up migrate-up-43}
|
||||
{:id 44 :migrate-up migrate-up-44}
|
||||
{:id 45 :migrate-up migrate-up-45}
|
||||
{:id 46 :migrate-up migrate-up-46}
|
||||
{:id 47 :migrate-up migrate-up-47}
|
||||
{:id 48 :migrate-up migrate-up-48}])
|
||||
{:id 46 :migrate-up migrate-up-46}])
|
||||
|
||||
@@ -460,34 +460,6 @@
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:id shape) repair-component))))
|
||||
|
||||
(defmethod repair-error :misplaced-slot
|
||||
[_ {:keys [shape page-id] :as error} file-data _]
|
||||
(let [repair-shape
|
||||
(fn [shape]
|
||||
;; Remove the swap slot
|
||||
(log/debug :hint (str " -> remove swap-slot"))
|
||||
(ctk/remove-swap-slot shape))]
|
||||
|
||||
(log/dbg :hint "repairing shape :misplaced-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :missing-slot
|
||||
[_ {:keys [shape page-id args] :as error} file-data _]
|
||||
(let [repair-shape
|
||||
(fn [shape]
|
||||
;; Set the desired swap slot
|
||||
(let [slot (:swap-slot args)]
|
||||
(when (some? slot)
|
||||
(log/debug :hint (str " -> set swap-slot to " slot))
|
||||
(update shape :touched cfh/set-touched-group (ctk/build-swap-slot-group slot)))))]
|
||||
|
||||
(log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :default
|
||||
[_ error file _]
|
||||
(log/error :hint "Unknown error code, don't know how to repair" :code (:code error))
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.common.files.validate
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
@@ -51,9 +50,7 @@
|
||||
:not-head-copy-not-allowed
|
||||
:not-component-not-allowed
|
||||
:component-nil-objects-not-allowed
|
||||
:instance-head-not-frame
|
||||
:misplaced-slot
|
||||
:missing-slot})
|
||||
:instance-head-not-frame})
|
||||
|
||||
(def ^:private
|
||||
schema:error
|
||||
@@ -288,14 +285,6 @@
|
||||
"Shape inside main instance should not have shape-ref"
|
||||
shape file page)))
|
||||
|
||||
(defn- check-empty-swap-slot
|
||||
"Validate that this shape does not have any swap slot."
|
||||
[shape file page]
|
||||
(when (some? (ctk/get-swap-slot shape))
|
||||
(report-error :misplaced-slot
|
||||
"This shape should not have swap slot"
|
||||
shape file page)))
|
||||
|
||||
(defn- check-shape-main-root-top
|
||||
"Root shape of a top main instance:
|
||||
|
||||
@@ -307,7 +296,6 @@
|
||||
(check-component-main-head shape file page libraries)
|
||||
(check-component-root shape file page)
|
||||
(check-component-not-ref shape file page)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :main-top) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-main-root-nested
|
||||
@@ -319,7 +307,6 @@
|
||||
(check-component-main-head shape file page libraries)
|
||||
(check-component-not-root shape file page)
|
||||
(check-component-not-ref shape file page)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :main-nested) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-copy-root-top
|
||||
@@ -334,7 +321,6 @@
|
||||
(check-component-not-main-head shape file page libraries)
|
||||
(check-component-root shape file page)
|
||||
(check-component-ref shape file page libraries)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape))))
|
||||
|
||||
(defn- check-shape-copy-root-nested
|
||||
@@ -357,7 +343,6 @@
|
||||
(check-component-not-main-not-head shape file page)
|
||||
(check-component-not-root shape file page)
|
||||
(check-component-not-ref shape file page)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :main-any) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-copy-not-root
|
||||
@@ -366,7 +351,6 @@
|
||||
(check-component-not-main-not-head shape file page)
|
||||
(check-component-not-root shape file page)
|
||||
(check-component-ref shape file page libraries)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :copy-any) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-not-component
|
||||
@@ -376,7 +360,6 @@
|
||||
(check-component-not-main-not-head shape file page)
|
||||
(check-component-not-root shape file page)
|
||||
(check-component-not-ref shape file page)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(run! #(check-shape % file page libraries :context :not-component) (:shapes shape)))
|
||||
|
||||
(defn- check-shape
|
||||
@@ -471,8 +454,6 @@
|
||||
;; PUBLIC API: VALIDATION FUNCTIONS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare check-swap-slots)
|
||||
|
||||
(defn validate-file
|
||||
"Validate full referential integrity and semantic coherence on file data.
|
||||
|
||||
@@ -483,8 +464,6 @@
|
||||
|
||||
(doseq [page (filter :id (ctpl/pages-seq data))]
|
||||
(check-shape uuid/zero file page libraries)
|
||||
(when (str/includes? (:name file) "check-swap-slot")
|
||||
(check-swap-slots uuid/zero file page libraries))
|
||||
(->> (get-orphan-shapes page)
|
||||
(run! #(check-shape % file page libraries))))
|
||||
|
||||
@@ -538,41 +517,3 @@
|
||||
:hint "error on validating file referential integrity"
|
||||
:file-id (:id file)
|
||||
:details errors)))
|
||||
|
||||
|
||||
(declare compare-slots)
|
||||
|
||||
;; Optional check to look for missing swap slots.
|
||||
;; Search for copies that do not point the shape-ref to the near component but don't have swap slot
|
||||
;; (looking for position relative to the parent, in the copy and the main).
|
||||
;;
|
||||
;; This check cannot be generally enabled, because files that have been migrated from components v1
|
||||
;; may have copies with shapes that do not match by position, but have not been swapped. So we enable
|
||||
;; it for specific files only. To activate the check, you need to add the string "check-swap-slot" to
|
||||
;; the name of the file.
|
||||
(defn- check-swap-slots
|
||||
[shape-id file page libraries]
|
||||
(let [shape (ctst/get-shape page shape-id)]
|
||||
(if (and (ctk/instance-root? shape) (ctk/in-component-copy? shape))
|
||||
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true :with-context? true)
|
||||
container (:container (meta ref-shape))]
|
||||
(when (some? ref-shape)
|
||||
(compare-slots shape ref-shape file page container)))
|
||||
(doall (for [child-id (:shapes shape)]
|
||||
(check-swap-slots child-id file page libraries))))))
|
||||
|
||||
(defn- compare-slots
|
||||
[shape-copy shape-main file container-copy container-main]
|
||||
(if (and (not= (:shape-ref shape-copy) (:id shape-main))
|
||||
(nil? (ctk/get-swap-slot shape-copy)))
|
||||
(report-error :missing-slot
|
||||
"Shape has been swapped, should have swap slot"
|
||||
shape-copy file container-copy
|
||||
:swap-slot (or (ctk/get-swap-slot shape-main) (:id shape-main)))
|
||||
(when (nil? (ctk/get-swap-slot shape-copy))
|
||||
(let [children-id-pairs (d/zip-all (:shapes shape-copy) (:shapes shape-main))]
|
||||
(doall (for [[child-copy-id child-main-id] children-id-pairs]
|
||||
(let [child-copy (ctst/get-shape container-copy child-copy-id)
|
||||
child-main (ctst/get-shape container-main child-main-id)]
|
||||
(when (and (some? child-copy) (some? child-main))
|
||||
(compare-slots child-copy child-main file container-copy container-main)))))))))
|
||||
|
||||
@@ -130,15 +130,6 @@
|
||||
(and (some? (:component-id shape))
|
||||
(nil? (:component-root shape))))
|
||||
|
||||
(defn subcopy-head?
|
||||
"Check if this shape is the head of a subinstance that is a copy."
|
||||
[shape]
|
||||
;; This is redundant with the previous one, but may give more security
|
||||
;; in case of bugs.
|
||||
(and (some? (:component-id shape))
|
||||
(nil? (:component-root shape))
|
||||
(some? (:shape-ref shape))))
|
||||
|
||||
(defn instance-of?
|
||||
[shape file-id component-id]
|
||||
(and (some? (:component-id shape))
|
||||
@@ -236,6 +227,7 @@
|
||||
:shape-ref
|
||||
:touched))
|
||||
|
||||
|
||||
(defn- extract-ids [shape]
|
||||
(if (map? shape)
|
||||
(let [current-id (:id shape)
|
||||
|
||||
@@ -5,8 +5,8 @@ ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV NODE_VERSION=v20.11.1 \
|
||||
CLOJURE_VERSION=1.11.1.1435 \
|
||||
CLJKONDO_VERSION=2024.02.12 \
|
||||
BABASHKA_VERSION=1.3.188 \
|
||||
CLJKONDO_VERSION=2024.03.13 \
|
||||
BABASHKA_VERSION=1.3.189 \
|
||||
CLJFMT_VERSION=0.12.0 \
|
||||
LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
@@ -224,7 +224,7 @@ http {
|
||||
add_header Last-Modified $date_gmt;
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
if_modified_since off;
|
||||
try_files $uri /index.html$is_args$args =404;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,14 @@ cd ~;
|
||||
|
||||
source ~/.bashrc
|
||||
|
||||
set -e;
|
||||
|
||||
echo "[start-tmux.sh] Installing node dependencies"
|
||||
pushd ~/penpot/frontend/
|
||||
yarn install
|
||||
yarn run playwright install --with-deps chromium
|
||||
popd
|
||||
pushd ~/penpot/exporter/
|
||||
yarn install
|
||||
npx playwright install chromium
|
||||
yarn run playwright install --with-deps chromium
|
||||
popd
|
||||
|
||||
tmux -2 new-session -d -s penpot
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
version: "3.8"
|
||||
version: "3.5"
|
||||
|
||||
networks:
|
||||
penpot:
|
||||
|
||||
@@ -202,7 +202,7 @@ http {
|
||||
add_header Last-Modified $date_gmt;
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
if_modified_since off;
|
||||
try_files $uri /index.html$is_args$args =404;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
:dev
|
||||
{:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "2.27.4"}}}
|
||||
{thheller/shadow-cljs {:mvn/version "2.28.3"}}}
|
||||
|
||||
:shadow-cljs
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^6.0.1",
|
||||
"archiver": "^6.0.2",
|
||||
"cookies": "^0.9.1",
|
||||
"generic-pool": "^3.9.0",
|
||||
"inflation": "^2.1.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"luxon": "^3.4.4",
|
||||
"playwright": "^1.40.1",
|
||||
"playwright": "^1.43.0",
|
||||
"raw-body": "^2.5.2",
|
||||
"xml-js": "^1.6.11",
|
||||
"xregexp": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shadow-cljs": "2.27.4",
|
||||
"shadow-cljs": "2.28.3",
|
||||
"source-map-support": "^0.5.21"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -135,9 +135,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"archiver@npm:^6.0.1":
|
||||
version: 6.0.1
|
||||
resolution: "archiver@npm:6.0.1"
|
||||
"archiver@npm:^6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "archiver@npm:6.0.2"
|
||||
dependencies:
|
||||
archiver-utils: "npm:^4.0.1"
|
||||
async: "npm:^3.2.4"
|
||||
@@ -146,7 +146,7 @@ __metadata:
|
||||
readdir-glob: "npm:^1.1.2"
|
||||
tar-stream: "npm:^3.0.0"
|
||||
zip-stream: "npm:^5.0.1"
|
||||
checksum: 54c5a634b39691114e727d4b4f360439fa7cd40b414c9d909606fbfd7048037f7dccefa49337f9ed19b1f5c209e021ce5e1ff9c6b547907257bc71f1af6f8cf3
|
||||
checksum: 23a470d468c01cd40fc13b6bd3dbc6d04c4f7b770785dcc7e1e4af256c3d79c4ffd7f7e0e84ae320437e5b8d0a2117aecfca0586b8c0fbd6edc3e04977c438cc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -727,15 +727,15 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "exporter@workspace:."
|
||||
dependencies:
|
||||
archiver: "npm:^6.0.1"
|
||||
archiver: "npm:^6.0.2"
|
||||
cookies: "npm:^0.9.1"
|
||||
generic-pool: "npm:^3.9.0"
|
||||
inflation: "npm:^2.1.0"
|
||||
ioredis: "npm:^5.3.2"
|
||||
luxon: "npm:^3.4.4"
|
||||
playwright: "npm:^1.40.1"
|
||||
playwright: "npm:^1.43.0"
|
||||
raw-body: "npm:^2.5.2"
|
||||
shadow-cljs: "npm:2.27.4"
|
||||
shadow-cljs: "npm:2.28.3"
|
||||
source-map-support: "npm:^0.5.21"
|
||||
xml-js: "npm:^1.6.11"
|
||||
xregexp: "npm:^5.1.1"
|
||||
@@ -1546,27 +1546,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright-core@npm:1.40.1":
|
||||
version: 1.40.1
|
||||
resolution: "playwright-core@npm:1.40.1"
|
||||
"playwright-core@npm:1.43.0":
|
||||
version: 1.43.0
|
||||
resolution: "playwright-core@npm:1.43.0"
|
||||
bin:
|
||||
playwright-core: cli.js
|
||||
checksum: 56c283012974982313a6ae583b975ee4af76d52059fb9a25d9cc616a11224685ec64682b391910c795d2b12d2ab5c7eec31124722c75c0b1703a76ac9b6fd1c2
|
||||
checksum: 3afdcc79176e0c872b3bb66994ef6288b10819dfb800234b81134a35daffc8100596d65ccff10faa8941a450a259a78f446db570898e53031abba7f542602c6a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright@npm:^1.40.1":
|
||||
version: 1.40.1
|
||||
resolution: "playwright@npm:1.40.1"
|
||||
"playwright@npm:^1.43.0":
|
||||
version: 1.43.0
|
||||
resolution: "playwright@npm:1.43.0"
|
||||
dependencies:
|
||||
fsevents: "npm:2.3.2"
|
||||
playwright-core: "npm:1.40.1"
|
||||
playwright-core: "npm:1.43.0"
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
bin:
|
||||
playwright: cli.js
|
||||
checksum: 5dae164d1f69162da8d7eee52da651296fb885c76a8b36049f216975c751a0a826ff05795a1c0902dc0bd193fe606ae17d5def655f4cbcccb8d8b71afb74b950
|
||||
checksum: 82506d3937416439d42ed7940e03b62225651a049792b7c122cd1fb4318674e5789dbcd38e31c8e2f41d99e3b7dc9aa0898bfdd26dff5456eb3f89a166252bd8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1842,9 +1842,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"shadow-cljs@npm:2.27.4":
|
||||
version: 2.27.4
|
||||
resolution: "shadow-cljs@npm:2.27.4"
|
||||
"shadow-cljs@npm:2.28.3":
|
||||
version: 2.28.3
|
||||
resolution: "shadow-cljs@npm:2.28.3"
|
||||
dependencies:
|
||||
node-libs-browser: "npm:^2.2.1"
|
||||
readline-sync: "npm:^1.4.7"
|
||||
@@ -1854,7 +1854,7 @@ __metadata:
|
||||
ws: "npm:^7.4.6"
|
||||
bin:
|
||||
shadow-cljs: cli/runner.js
|
||||
checksum: bae23e71df9c2b2979259a0cde8747c923ee295f58ab4637c9d6b103d82542b40ef39172d4be2dbb94af2e6458a177d1ec96c1eb1e73b1d8f3a4ddb5eaaba7d4
|
||||
checksum: 623b536a0d95d7696dd465c09ab3cb5d921c867a577a33463ad58dbc40f51f5d0424ba2791a8803f33a94f5c877198de91c3c7f7616618a6b4ae90e80d5d213e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
5
frontend/.gitignore
vendored
5
frontend/.gitignore
vendored
@@ -5,3 +5,8 @@
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
@@ -30,11 +30,14 @@
|
||||
"translations:find-unused": "node ./scripts/find-unused-translations.js",
|
||||
"compile": "node ./scripts/compile.js",
|
||||
"watch": "node ./scripts/watch.js",
|
||||
"e2e:server": "NODE_NO_WARNINGS=1 http-server ./resources/public -p 3500 -a 0.0.0.0",
|
||||
"e2e:test": "playwright test",
|
||||
"storybook:compile": "gulp template:storybook && clojure -M:dev:shadow-cljs compile storybook",
|
||||
"storybook:watch": "npm run storybook:compile && concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006\"",
|
||||
"storybook:build": "npm run storybook:compile && storybook build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@storybook/addon-essentials": "^7.6.17",
|
||||
"@storybook/addon-interactions": "^7.6.17",
|
||||
"@storybook/addon-links": "^7.6.17",
|
||||
@@ -59,6 +62,7 @@
|
||||
"gulp-sass": "^5.1.0",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-svg-sprite": "^2.0.3",
|
||||
"http-server": "^14.1.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"map-stream": "0.0.7",
|
||||
"marked": "^12.0.0",
|
||||
|
||||
47
frontend/playwright.config.js
Normal file
47
frontend/playwright.config.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./playwright",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://0.0.0.0:3500",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: "yarn e2e:server",
|
||||
url: "http://0.0.0.0:3500",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
14
frontend/playwright/example.spec.js
Normal file
14
frontend/playwright/example.spec.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("has title", async ({ page }) => {
|
||||
await page.route("**/api/rpc/command/get-profile", (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/transit+json",
|
||||
path: "playwright/fixtures/get-profile-anonymous.json",
|
||||
});
|
||||
});
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page).toHaveTitle(/Penpot/);
|
||||
});
|
||||
4
frontend/playwright/fixtures/get-profile-anonymous.json
Normal file
4
frontend/playwright/fixtures/get-profile-anonymous.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:fullname": "Anonymous User"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -28,16 +28,9 @@ body {
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: thin;
|
||||
// transition: all .4s ease;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.global-zeroclipboard-container {
|
||||
transition: none;
|
||||
|
||||
|
||||
@@ -6,14 +6,6 @@
|
||||
|
||||
// SCROLLBAR
|
||||
.new-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(170, 181, 186, 0.3) transparent;
|
||||
&:hover {
|
||||
scrollbar-color: rgba(170, 181, 186, 0.7) transparent;
|
||||
}
|
||||
|
||||
// These rules do not apply in chrome - 121 or higher
|
||||
// We keep them to preserve backward compatibility.
|
||||
::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
@@ -593,9 +585,6 @@
|
||||
width: 100%;
|
||||
z-index: $z-index-modal;
|
||||
background-color: var(--overlay-color);
|
||||
&.onboarding-a-b-test {
|
||||
background-color: var(--overlay-color-onboarding-a-b-test);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-container-base {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
// Dark background
|
||||
--db-primary: #18181a;
|
||||
--db-primary-60: #{color.change(#18181a, $alpha: 0.6)};
|
||||
--db-primary-90: #{color.change(#18181a, $alpha: 0.9)};
|
||||
--db-secondary: #000000;
|
||||
--db-secondary-30: #{color.change(#000000, $alpha: 0.3)};
|
||||
--db-secondary-80: #{color.change(#000000, $alpha: 0.8)};
|
||||
@@ -36,7 +35,6 @@
|
||||
// Light background
|
||||
--lb-primary: #ffffff;
|
||||
--lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)};
|
||||
--lb-primary-90: #{color.change(#ffffff, $alpha: 0.9)};
|
||||
--lb-secondary: #e8eaee;
|
||||
--lb-secondary-30: #{color.change(#e8eaee, $alpha: 0.3)};
|
||||
--lb-secondary-80: #{color.change(#e8eaee, $alpha: 0.8)};
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
--color-info-foreground: var(--status-color-info-500);
|
||||
|
||||
--overlay-color: var(--db-primary-60);
|
||||
--overlay-color-onboarding-a-b-test: var(--db-primary-90);
|
||||
|
||||
--shadow-color: var(--db-secondary-30);
|
||||
--radio-button-box-shadow: 0 0 0 1px var(--db-secondary-30) inset;
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
--color-info-foreground: var(--status-color-info-500);
|
||||
|
||||
--overlay-color: var(--lb-primary-60);
|
||||
--overlay-color-onboarding-a-b-test: var(--lb-primary-90);
|
||||
|
||||
--shadow-color: var(--lf-secondary-40);
|
||||
--radio-button-box-shadow: 0 0 0 1px var(--lb-secondary) inset;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
|
||||
@@ -130,10 +130,6 @@
|
||||
(def worker-uri
|
||||
(obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||
|
||||
(defn external-feature-flag [flag value]
|
||||
(when-let [fn (obj/get global "externalFeatureFlag")]
|
||||
(fn flag value)))
|
||||
|
||||
;; --- Helper Functions
|
||||
|
||||
(defn ^boolean check-browser? [candidate]
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.libs.file-builder
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.builder :as fb]
|
||||
[app.common.media :as cm]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
@@ -74,7 +73,7 @@
|
||||
|
||||
manifest-stream
|
||||
(->> files-stream
|
||||
(rx/map #(e/create-manifest (uuid/next) (:id file) :all % cfeat/default-features))
|
||||
(rx/map #(e/create-manifest (uuid/next) (:id file) :all % false))
|
||||
(rx/map (fn [a]
|
||||
(vector "manifest.json" a))))
|
||||
|
||||
@@ -145,7 +144,7 @@
|
||||
(str (:current-page-id file)))
|
||||
|
||||
(addPage [_ name options]
|
||||
(set! file (fb/add-page file {:name name :options (parse-data options)}))
|
||||
(set! file (fb/add-page file {:name name :options options}))
|
||||
(str (:current-page-id file)))
|
||||
|
||||
(closePage [_]
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
[app.config :as cf]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.websocket :as ws]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[beicon.v2.core :as rx]
|
||||
@@ -231,8 +232,35 @@
|
||||
(rx/observe-on :async)))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(def ^:private schema:login-with-ldap
|
||||
(sm/define
|
||||
[:map
|
||||
[:email ::sm/email]
|
||||
[:password :string]]))
|
||||
|
||||
(defn login-with-ldap
|
||||
[params]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid params"
|
||||
(sm/check! schema:login-with-ldap params))
|
||||
|
||||
(ptk/reify ::login-with-ldap
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-error on-success]
|
||||
:or {on-error rx/throw
|
||||
on-success identity}} (meta params)]
|
||||
(->> (rp/cmd! :login-with-ldap params)
|
||||
(rx/tap on-success)
|
||||
(rx/map (fn [profile]
|
||||
(-> profile
|
||||
(with-meta {::ev/source "login-with-ldap"})
|
||||
(logged-in))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn login-from-token
|
||||
"Used mainly as flow continuation after token validation."
|
||||
[{:keys [profile] :as tdata}]
|
||||
(ptk/reify ::login-from-token
|
||||
ptk/WatchEvent
|
||||
@@ -656,3 +684,24 @@
|
||||
(->> (rp/cmd! :delete-access-token params)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn show-redirect-error
|
||||
"A helper event that interprets the OIDC redirect errors on the URI
|
||||
and shows an appropriate error message using the notification
|
||||
banners."
|
||||
[error]
|
||||
(ptk/reify ::show-redirect-error
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [hint (case error
|
||||
"registration-disabled"
|
||||
(tr "errors.registration-disabled")
|
||||
"profile-blocked"
|
||||
(tr "errors.profile-blocked")
|
||||
"auth-provider-not-allowed"
|
||||
(tr "errors.auth-provider-not-allowed")
|
||||
"email-domain-not-allowed"
|
||||
(tr "errors.email-domain-not-allowed")
|
||||
:else
|
||||
(tr "errors.generic"))]
|
||||
(rx/of (msg/warn hint))))))
|
||||
|
||||
@@ -143,12 +143,18 @@
|
||||
(map-indexed vector)
|
||||
(filter #(#{(:id group)} (second %)))
|
||||
(ffirst)
|
||||
inc)]
|
||||
inc)
|
||||
|
||||
;; Shapes that are in a component (including root) must be detached,
|
||||
;; because cannot be easyly synchronized back to the main component.
|
||||
shapes-to-detach (filter ctk/in-component-copy?
|
||||
(cfh/get-children-with-self objects (:id group)))]
|
||||
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/change-parent parent-id children index-in-parent)
|
||||
(pcb/remove-objects [(:id group)]))))
|
||||
(pcb/remove-objects [(:id group)])
|
||||
(pcb/update-shapes (map :id shapes-to-detach) ctk/detach-shape))))
|
||||
|
||||
(defn remove-frame-changes
|
||||
[it page-id frame objects]
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
[app.common.files.libraries-helpers :as cflh]
|
||||
[app.common.files.shapes-helpers :as cfsh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logging :as log]
|
||||
;; [app.common.logging :as log]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
@@ -51,26 +51,28 @@
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
|
||||
(log/set-level! :warn)
|
||||
;; ;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
|
||||
;; (log/set-level! :debug)
|
||||
|
||||
(defn- log-changes
|
||||
[changes file]
|
||||
(let [extract-change
|
||||
(fn [change]
|
||||
(let [shape (when (:id change)
|
||||
(cond
|
||||
(:page-id change)
|
||||
(get-in file [:pages-index
|
||||
(:page-id change)
|
||||
:objects
|
||||
(:id change)])
|
||||
(:component-id change)
|
||||
(get-in file [:components
|
||||
(:component-id change)
|
||||
:objects
|
||||
(:id change)])
|
||||
:else nil))
|
||||
(let [shape (if-let [obj (:obj change)]
|
||||
obj
|
||||
(when (:id change)
|
||||
(cond
|
||||
(:page-id change)
|
||||
(get-in file [:pages-index
|
||||
(:page-id change)
|
||||
:objects
|
||||
(:id change)])
|
||||
(:component-id change)
|
||||
(get-in file [:components
|
||||
(:component-id change)
|
||||
:objects
|
||||
(:id change)])
|
||||
:else nil)))
|
||||
|
||||
prefix (if (:component-id change) "[C] " "[P] ")
|
||||
|
||||
@@ -711,7 +713,7 @@
|
||||
(ptk/reify ::sync-head
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(log/info :msg "SYNC-head of shape" :id (str id))
|
||||
(dwlh/dbg-warn "SYNC-HEAD of shape" :id id)
|
||||
(let [file (wsh/get-local-file state)
|
||||
file-full (wsh/get-local-file-full state)
|
||||
libraries (wsh/get-libraries state)
|
||||
@@ -733,9 +735,7 @@
|
||||
(pcb/with-objects (:objects container))
|
||||
(dwlh/generate-sync-shape-direct file-full libraries container (:id head) false components-v2))]
|
||||
|
||||
(log/debug :msg "SYNC-head finished" :js/rchanges (log-changes
|
||||
(:redo-changes changes)
|
||||
file))
|
||||
(dwlh/dbg-info "SYNC-HEAD finished" :rchanges (dwlh/dbg-obj (log-changes (:redo-changes changes) file)))
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn reset-component
|
||||
@@ -747,7 +747,7 @@
|
||||
(ptk/reify ::reset-component
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(log/info :msg "RESET-COMPONENT of shape" :id (str id))
|
||||
(dwlh/dbg-warn "RESET-COMPONENT of shape" :id id)
|
||||
(let [file (wsh/get-local-file state)
|
||||
file-full (wsh/get-local-file-full state)
|
||||
libraries (wsh/get-libraries state)
|
||||
@@ -769,9 +769,7 @@
|
||||
(pcb/with-objects (:objects container))
|
||||
(dwlh/generate-sync-shape-direct file-full libraries container id true components-v2))]
|
||||
|
||||
(log/debug :msg "RESET-COMPONENT finished" :js/rchanges (log-changes
|
||||
(:redo-changes changes)
|
||||
file))
|
||||
(dwlh/dbg-info "RESET-COMPONENT finished" :rchanges (dwlh/dbg-obj (log-changes (:redo-changes changes) file)))
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
@@ -808,7 +806,7 @@
|
||||
(ptk/reify ::update-component
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(log/info :msg "UPDATE-COMPONENT of shape" :id (str id) :undo-group undo-group)
|
||||
(dwlh/dbg-warn "UPDATE-COMPONENT of shape" :id id :undo-group undo-group)
|
||||
(let [page-id (get state :current-page-id)
|
||||
local-file (wsh/get-local-file state)
|
||||
full-file (wsh/get-local-file-full state)
|
||||
@@ -844,13 +842,9 @@
|
||||
(update :redo-changes #(into [] xf-remove %))
|
||||
(update :undo-changes #(into [] xf-remove %)))]
|
||||
|
||||
(log/debug :msg "UPDATE-COMPONENT finished"
|
||||
:js/local-changes (log-changes
|
||||
(:redo-changes local-changes)
|
||||
file)
|
||||
:js/nonlocal-changes (log-changes
|
||||
(:redo-changes nonlocal-changes)
|
||||
file))
|
||||
(dwlh/dbg-info "UPDATE-COMPONENT finished"
|
||||
:local-changes (dwlh/dbg-obj (log-changes (:redo-changes local-changes) file))
|
||||
:nonlocal-changes (dwlh/dbg-obj (log-changes (:redo-changes nonlocal-changes) file)))
|
||||
|
||||
(rx/of
|
||||
(when (seq (:redo-changes local-changes))
|
||||
@@ -1038,10 +1032,10 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [undo-id (js/Symbol)]
|
||||
(log/info :msg "COMPONENT-SWAP"
|
||||
:file (dwlh/pretty-file file-id state)
|
||||
:id-new-component id-new-component
|
||||
:undo-id undo-id)
|
||||
(dwlh/dbg-warn "COMPONENT-SWAP"
|
||||
:file (dwlh/dbg-file file-id state)
|
||||
:id-new-component id-new-component
|
||||
:undo-id undo-id)
|
||||
(rx/concat
|
||||
(rx/of (dwu/start-undo-transaction undo-id))
|
||||
(rx/map #(component-swap % file-id id-new-component) (rx/from shapes))
|
||||
@@ -1091,12 +1085,12 @@
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(when (and (some? file-id) (some? library-id)) ; Prevent race conditions while navigating out of the file
|
||||
(log/info :msg "SYNC-FILE"
|
||||
:file (dwlh/pretty-file file-id state)
|
||||
:library (dwlh/pretty-file library-id state)
|
||||
:asset-type asset-type
|
||||
:asset-id asset-id
|
||||
:undo-group undo-group)
|
||||
(dwlh/dbg-warn "SYNC-FILE"
|
||||
:file (dwlh/dbg-file file-id state)
|
||||
:library (dwlh/dbg-file library-id state)
|
||||
:asset-type asset-type
|
||||
:asset-id asset-id
|
||||
:undo-group undo-group)
|
||||
(let [file (wsh/get-file state file-id)
|
||||
|
||||
sync-components? (or (nil? asset-type) (= asset-type :components))
|
||||
@@ -1136,9 +1130,8 @@
|
||||
(mapcat find-frames)
|
||||
distinct)]
|
||||
|
||||
(log/debug :msg "SYNC-FILE finished" :js/rchanges (log-changes
|
||||
(:redo-changes changes)
|
||||
file))
|
||||
(dwlh/dbg-info "SYNC-FILE finished"
|
||||
:rchanges (dwlh/dbg-obj (log-changes (:redo-changes changes) file)))
|
||||
(rx/concat
|
||||
(rx/of (set-updating-library false)
|
||||
(msg/hide-tag :sync-dialog))
|
||||
@@ -1306,9 +1299,9 @@
|
||||
|
||||
(if (d/not-empty? changed-components)
|
||||
(if save-undo?
|
||||
(do (log/info :msg "DETECTED COMPONENTS CHANGED"
|
||||
:ids (map str changed-components)
|
||||
:undo-group undo-group)
|
||||
(do (dwlh/dbg-warn "DETECTED COMPONENTS CHANGED"
|
||||
:ids (map dwlh/dbg-id changed-components)
|
||||
:undo-group undo-group)
|
||||
|
||||
(->> (rx/from changed-components)
|
||||
(rx/map #(component-changed % (:id old-data) undo-group))))
|
||||
@@ -1327,7 +1320,7 @@
|
||||
notifier-s
|
||||
(->> changes-s
|
||||
(rx/debounce 5000)
|
||||
(rx/tap #(log/trc :hint "buffer initialized")))]
|
||||
(rx/tap #(dwlh/dbg-log "buffer initialized")))]
|
||||
|
||||
(when components-v2?
|
||||
(->> (rx/merge
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.grid-layout :as gslg]
|
||||
[app.common.logging :as log]
|
||||
;; [app.common.logging :as log]
|
||||
[app.common.spec :as us]
|
||||
[app.common.text :as txt]
|
||||
[app.common.types.color :as ctc]
|
||||
@@ -27,10 +27,97 @@
|
||||
[app.common.types.typography :as cty]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.set :as set]))
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
|
||||
(log/set-level! :warn)
|
||||
;; ---- Logging utilities ----
|
||||
|
||||
(def dbg-level-log 1)
|
||||
(def dbg-level-info 2)
|
||||
(def dbg-level-warn 3)
|
||||
|
||||
;; Change this to one of the levels to display traces of this level or higher
|
||||
(def dbg-level dbg-level-info)
|
||||
|
||||
;; Add some uuids to this to filter log messages affecting only to those shapes
|
||||
(def dbg-filters #{})
|
||||
|
||||
(defn dbg-id
|
||||
[uuid]
|
||||
(let [uuid-str (str uuid)]
|
||||
(str "#" (subs uuid-str (- (count uuid-str) 6)))))
|
||||
|
||||
(defn dbg-file
|
||||
[file-id state]
|
||||
(if (= file-id (:current-file-id state))
|
||||
"<local>"
|
||||
(str "<" (get-in state [:workspace-libraries file-id :name]) "> " (dbg-id file-id))))
|
||||
|
||||
(defn dbg-page
|
||||
[page]
|
||||
(with-meta page {:dbg-type :page}))
|
||||
|
||||
(defn dbg-component
|
||||
[component]
|
||||
(with-meta component {:dbg-type :component}))
|
||||
|
||||
(defn dbg-container
|
||||
[container]
|
||||
(if (cfh/page? container)
|
||||
(with-meta container {:dbg-type :page})
|
||||
(with-meta container {:dbg-type :component})))
|
||||
|
||||
(defn dbg-shape
|
||||
[shape]
|
||||
(with-meta shape {:dbg-type :shape}))
|
||||
|
||||
(defn dbg-obj
|
||||
[obj]
|
||||
(clj->js obj))
|
||||
|
||||
(defn js-value?
|
||||
[[_key value]]
|
||||
;; (prn value)
|
||||
;; (prn (type value))
|
||||
(or (object? value) (array? value)))
|
||||
|
||||
(defn dbg-format-str
|
||||
[[key value]]
|
||||
(let [value-str (case (:dbg-type (meta value))
|
||||
:page (str "[P \"" (subs (:name value) 0 50) "\" " (dbg-id (:id value)) "]")
|
||||
:component (str "[C \"" (subs (:name value) 0 50) "\" " (dbg-id (:id value)) "]")
|
||||
:shape (str "\"" (subs (:name value) 0 50) "\" " (dbg-id (:id value)))
|
||||
(cond
|
||||
(uuid? value) (dbg-id value)
|
||||
(js-symbol? value) (.toString value)
|
||||
:else (d/nilv value "nil")))]
|
||||
(str (name key) "=" value-str)))
|
||||
|
||||
(defn dbg
|
||||
[level msg & args]
|
||||
(when (>= level dbg-level)
|
||||
(let [kvargs (apply sorted-map args)
|
||||
ids (into #{} (filter uuid? (vals kvargs)))]
|
||||
(when (or (empty? dbg-filters)
|
||||
(empty? ids)
|
||||
(seq (set/intersection dbg-filters ids)))
|
||||
(let [dbg-fn (condp = level
|
||||
dbg-level-log js/console.log
|
||||
dbg-level-info js/console.info
|
||||
dbg-level-warn js/console.warn)
|
||||
kv-objects (d/filterm js-value? kvargs)
|
||||
kv-rest (d/removem js-value? kvargs)]
|
||||
(dbg-fn (->> (cons msg (map dbg-format-str kv-rest))
|
||||
(str/join " ")))
|
||||
(when (seq kv-objects)
|
||||
(dorun (for [[key value] kv-objects]
|
||||
(dbg-fn (str key) value)))))))))
|
||||
|
||||
(defn dbg-log [& args] (apply dbg (cons dbg-level-log args)))
|
||||
(defn dbg-info [& args] (apply dbg (cons dbg-level-info args)))
|
||||
(defn dbg-warn [& args] (apply dbg (cons dbg-level-warn args)))
|
||||
|
||||
;; ---- Advanced declaration of helpers ----
|
||||
|
||||
(declare generate-sync-container)
|
||||
(declare generate-sync-shape)
|
||||
@@ -58,17 +145,6 @@
|
||||
(declare reposition-shape)
|
||||
(declare make-change)
|
||||
|
||||
(defn pretty-file
|
||||
[file-id state]
|
||||
(if (= file-id (:current-file-id state))
|
||||
"<local>"
|
||||
(str "<" (get-in state [:workspace-libraries file-id :name]) ">")))
|
||||
|
||||
(defn pretty-uuid
|
||||
[uuid]
|
||||
(let [uuid-str (str uuid)]
|
||||
(subs uuid-str (- (count uuid-str) 6))))
|
||||
|
||||
;; ---- Components and instances creation ----
|
||||
|
||||
(defn duplicate-component
|
||||
@@ -220,7 +296,7 @@
|
||||
with a component."
|
||||
[changes container libraries shape-id]
|
||||
(let [shape (ctn/get-shape container shape-id)]
|
||||
(log/debug :msg "Detach instance" :shape-id shape-id :container (:id container))
|
||||
(dbg-info "Detach instance" :shape-id shape-id :container (:id container))
|
||||
(generate-detach-recursive changes container libraries shape-id true (true? (:component-root shape)))))
|
||||
|
||||
(defn- generate-detach-recursive
|
||||
@@ -233,10 +309,6 @@
|
||||
; If the initial shape was component-root, first level subinstances are converted in top instances
|
||||
(pcb/update-shapes [shape-id] #(assoc % :component-root true))
|
||||
|
||||
:always
|
||||
; First level subinstances of a detached component can't have swap-slot
|
||||
(pcb/update-shapes [shape-id] ctk/remove-swap-slot)
|
||||
|
||||
:always
|
||||
; Near shape-refs need to be advanced one level
|
||||
(generate-advance-nesting-level nil container libraries (:id shape)))
|
||||
@@ -314,11 +386,11 @@
|
||||
(s/assert ::us/uuid file-id)
|
||||
(s/assert ::us/uuid library-id)
|
||||
|
||||
(log/info :msg "Sync file with library"
|
||||
(dbg-info "Sync file with library"
|
||||
:asset-type asset-type
|
||||
:asset-id asset-id
|
||||
:file (pretty-file file-id state)
|
||||
:library (pretty-file library-id state))
|
||||
:file (dbg-file file-id state)
|
||||
:library (dbg-file library-id state))
|
||||
|
||||
(let [file (wsh/get-file state file-id)
|
||||
components-v2 (get-in file [:options :components-v2])]
|
||||
@@ -351,11 +423,11 @@
|
||||
(s/assert ::us/uuid file-id)
|
||||
(s/assert ::us/uuid library-id)
|
||||
|
||||
(log/info :msg "Sync local components with library"
|
||||
(dbg-info "Sync local components with library"
|
||||
:asset-type asset-type
|
||||
:asset-id asset-id
|
||||
:file (pretty-file file-id state)
|
||||
:library (pretty-file library-id state))
|
||||
:file (dbg-file file-id state)
|
||||
:library (dbg-file library-id state))
|
||||
|
||||
(let [file (wsh/get-file state file-id)
|
||||
components-v2 (get-in file [:options :components-v2])]
|
||||
@@ -380,8 +452,8 @@
|
||||
[it asset-type asset-id library-id state container components-v2]
|
||||
|
||||
(if (cfh/page? container)
|
||||
(log/debug :msg "Sync page in local file" :page-id (:id container))
|
||||
(log/debug :msg "Sync component in local library" :component-id (:id container)))
|
||||
(dbg-info "Sync page in local file" :page (dbg-page container))
|
||||
(dbg-info "Sync component in local library" :component (dbg-component container)))
|
||||
|
||||
(let [linked-shapes (->> (vals (:objects container))
|
||||
(filter #(uses-assets? asset-type asset-id % library-id)))]
|
||||
@@ -436,7 +508,7 @@
|
||||
|
||||
(defmethod generate-sync-shape :colors
|
||||
[_ changes library-id state _ shape _]
|
||||
(log/debug :msg "Sync colors of shape" :shape (:name shape))
|
||||
(dbg-info "Sync colors of shape" :shape (dbg-shape shape))
|
||||
|
||||
;; Synchronize a shape that uses some colors of the library. The value of the
|
||||
;; color in the library is copied to the shape.
|
||||
@@ -447,7 +519,7 @@
|
||||
|
||||
(defmethod generate-sync-shape :typographies
|
||||
[_ changes library-id state container shape _]
|
||||
(log/debug :msg "Sync typographies of shape" :shape (:name shape))
|
||||
(dbg-info "Sync typographies of shape" :shape (dbg-shape shape))
|
||||
|
||||
;; Synchronize a shape that uses some typographies of the library. The attributes
|
||||
;; of the typography are copied to the shape."
|
||||
@@ -615,7 +687,7 @@
|
||||
"Generate changes to synchronize one shape that is the root of a component
|
||||
instance, and all its children, from the given component."
|
||||
[changes file libraries container shape-id reset? components-v2]
|
||||
(log/debug :msg "Sync shape direct" :shape-inst (str shape-id) :reset? reset?)
|
||||
(dbg-info "Sync shape direct" :shape-id shape-id :reset? reset?)
|
||||
(let [shape-inst (ctn/get-shape container shape-id)
|
||||
library (dm/get-in libraries [(:component-file shape-inst) :data])
|
||||
component (ctkl/get-component library (:component-id shape-inst) true)]
|
||||
@@ -652,6 +724,7 @@
|
||||
reset?
|
||||
initial-root?
|
||||
redirect-shaperef
|
||||
1
|
||||
components-v2)
|
||||
;; If the component is not found, because the master component has been
|
||||
;; deleted or the library unlinked, do nothing in v2 or detach in v1.
|
||||
@@ -678,10 +751,10 @@
|
||||
nil))))))
|
||||
|
||||
(defn- generate-sync-shape-direct-recursive
|
||||
[changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root? redirect-shaperef components-v2]
|
||||
(log/debug :msg "Sync shape direct recursive"
|
||||
:shape-inst (str (:name shape-inst) " " (pretty-uuid (:id shape-inst)))
|
||||
:component (:name component))
|
||||
[changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root? redirect-shaperef level components-v2]
|
||||
(dbg-info (str (str/repeat "-" level) "> Sync shape direct recursive")
|
||||
:shape-inst (dbg-shape shape-inst)
|
||||
:component (dbg-component component))
|
||||
|
||||
(if (nil? shape-main)
|
||||
;; This should not occur, but protect against it in any case
|
||||
@@ -737,8 +810,7 @@
|
||||
(map #(redirect-shaperef %) children-inst) children-inst)
|
||||
|
||||
only-inst (fn [changes child-inst]
|
||||
(log/trace :msg "Only inst"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))))
|
||||
(dbg-log " *Only inst:" :child-inst (dbg-shape child-inst))
|
||||
(if-not (and omit-touched?
|
||||
(contains? (:touched shape-inst)
|
||||
:shapes-group))
|
||||
@@ -749,8 +821,7 @@
|
||||
changes))
|
||||
|
||||
only-main (fn [changes child-main]
|
||||
(log/trace :msg "Only main"
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
(dbg-log " *Only main:" :child-main (dbg-shape child-main))
|
||||
(if-not (and omit-touched?
|
||||
(contains? (:touched shape-inst)
|
||||
:shapes-group))
|
||||
@@ -768,9 +839,9 @@
|
||||
changes))
|
||||
|
||||
both (fn [changes child-inst child-main]
|
||||
(log/trace :msg "Both"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
(dbg-log " *Both:"
|
||||
:child-inst (dbg-shape child-inst)
|
||||
:child-main (dbg-shape child-main))
|
||||
(generate-sync-shape-direct-recursive changes
|
||||
container
|
||||
child-inst
|
||||
@@ -784,19 +855,20 @@
|
||||
reset?
|
||||
initial-root?
|
||||
redirect-shaperef
|
||||
(inc level)
|
||||
components-v2))
|
||||
|
||||
swapped (fn [changes child-inst child-main]
|
||||
(log/trace :msg "Match slot"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
(dbg-log " *Match slot:"
|
||||
:child-inst (dbg-shape child-inst)
|
||||
:child-main (dbg-shape child-main))
|
||||
;; For now we don't make any sync here.
|
||||
changes)
|
||||
|
||||
moved (fn [changes child-inst child-main]
|
||||
(log/trace :msg "Move"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
(dbg-log " *Move:"
|
||||
:child-inst (dbg-shape child-inst)
|
||||
:child-main (dbg-shape child-main))
|
||||
(move-shape
|
||||
changes
|
||||
child-inst
|
||||
@@ -844,7 +916,7 @@
|
||||
"Generate changes to update the component a shape is linked to, from
|
||||
the values in the shape and all its children."
|
||||
[changes file libraries container shape-id components-v2]
|
||||
(log/debug :msg "Sync shape inverse" :shape (str shape-id))
|
||||
(dbg-info "Sync shape inverse" :shape-id shape-id)
|
||||
(let [redirect-shaperef (partial redirect-shaperef container libraries)
|
||||
shape-inst (ctn/get-shape container shape-id)
|
||||
library (dm/get-in libraries [(:component-file shape-inst) :data])
|
||||
@@ -881,14 +953,15 @@
|
||||
root-main
|
||||
initial-root?
|
||||
redirect-shaperef
|
||||
1
|
||||
components-v2)
|
||||
changes)))
|
||||
|
||||
(defn- generate-sync-shape-inverse-recursive
|
||||
[changes container shape-inst component library file libraries shape-main root-inst root-main initial-root? redirect-shaperef components-v2]
|
||||
(log/trace :msg "Sync shape inverse recursive"
|
||||
:shape (str (:name shape-inst))
|
||||
:component (:name component))
|
||||
[changes container shape-inst component library file libraries shape-main root-inst root-main initial-root? redirect-shaperef level components-v2]
|
||||
(dbg-log (str (str/repeat "-" level) "> Sync shape inverse recursive")
|
||||
:shape-inst (dbg-shape shape-inst)
|
||||
:component (dbg-component component))
|
||||
|
||||
(if (nil? shape-main)
|
||||
;; This should not occur, but protect against it in any case
|
||||
@@ -946,6 +1019,7 @@
|
||||
children-inst)
|
||||
|
||||
only-inst (fn [changes child-inst]
|
||||
(dbg-log " *Only inst:" :child-inst (dbg-shape child-inst))
|
||||
(add-shape-to-main changes
|
||||
child-inst
|
||||
(d/index-of children-inst
|
||||
@@ -958,12 +1032,16 @@
|
||||
components-v2))
|
||||
|
||||
only-main (fn [changes child-main]
|
||||
(dbg-log " *Only main:" :child-main (dbg-shape child-main))
|
||||
(remove-shape changes
|
||||
child-main
|
||||
component-container
|
||||
false))
|
||||
|
||||
both (fn [changes child-inst child-main]
|
||||
(dbg-log " *Both:"
|
||||
:child-inst (dbg-shape child-inst)
|
||||
:child-main (dbg-shape child-main))
|
||||
(generate-sync-shape-inverse-recursive changes
|
||||
container
|
||||
child-inst
|
||||
@@ -976,16 +1054,20 @@
|
||||
root-main
|
||||
initial-root?
|
||||
redirect-shaperef
|
||||
(inc level)
|
||||
components-v2))
|
||||
|
||||
swapped (fn [changes child-inst child-main]
|
||||
(log/trace :msg "Match slot"
|
||||
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
|
||||
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
|
||||
(dbg-log " *Match slot:"
|
||||
:child-inst (dbg-shape child-inst)
|
||||
:child-main (dbg-shape child-main))
|
||||
;; For now we don't make any sync here.
|
||||
changes)
|
||||
|
||||
moved (fn [changes child-inst child-main]
|
||||
(dbg-log " *Move:"
|
||||
:child-inst (dbg-shape child-inst)
|
||||
:child-main (dbg-shape child-main))
|
||||
(move-shape
|
||||
changes
|
||||
child-main
|
||||
@@ -1028,14 +1110,13 @@
|
||||
|
||||
(defn- compare-children
|
||||
[changes children-inst children-main container-inst container-main file libraries only-inst-cb only-main-cb both-cb swapped-cb moved-cb inverse? reset? components-v2]
|
||||
(log/trace :msg "Compare children")
|
||||
(dbg-log "Compare children")
|
||||
(loop [children-inst (seq (or children-inst []))
|
||||
children-main (seq (or children-main []))
|
||||
changes changes]
|
||||
(let [child-inst (first children-inst)
|
||||
child-main (first children-main)]
|
||||
(log/trace :main (str (:name child-main) " " (pretty-uuid (:id child-main)))
|
||||
:inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))))
|
||||
(dbg-log " " :main (dbg-shape child-main) :inst (dbg-shape child-inst))
|
||||
(cond
|
||||
(and (nil? child-inst) (nil? child-main))
|
||||
changes
|
||||
@@ -1097,10 +1178,7 @@
|
||||
|
||||
(defn- add-shape-to-instance
|
||||
[changes component-shape index component-page container root-instance root-main omit-touched? set-remote-synced? components-v2]
|
||||
(log/info :msg (str "ADD [P " (pretty-uuid (:id container)) "] "
|
||||
(:name component-shape)
|
||||
" "
|
||||
(pretty-uuid (:id component-shape))))
|
||||
(dbg-warn "ADD TO INST" :container (dbg-container container) :component-shape (dbg-shape component-shape))
|
||||
(let [component-parent-shape (ctn/get-shape component-page (:parent-id component-shape))
|
||||
parent-shape (d/seek #(ctk/is-main-of? component-parent-shape % components-v2)
|
||||
(cfh/get-children-with-self (:objects container)
|
||||
@@ -1129,7 +1207,7 @@
|
||||
update-original-shape (fn [original-shape _new-shape]
|
||||
original-shape)
|
||||
|
||||
[_ new-shapes _]
|
||||
[new-shape new-shapes _]
|
||||
(ctst/clone-shape component-shape
|
||||
(:id parent-shape)
|
||||
(get component-page :objects)
|
||||
@@ -1166,16 +1244,14 @@
|
||||
:shapes all-parents}))
|
||||
changes' (reduce del-obj-change changes' new-shapes)]
|
||||
|
||||
(dbg-log " " :parent-shape (dbg-shape parent-shape) :new-shape (dbg-shape new-shape))
|
||||
(if (and (cfh/touched-group? parent-shape :shapes-group) omit-touched?)
|
||||
changes
|
||||
changes')))
|
||||
|
||||
(defn- add-shape-to-main
|
||||
[changes shape index component component-container page root-instance root-main components-v2]
|
||||
(log/info :msg (str "ADD [C " (pretty-uuid (:id component-container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape))))
|
||||
(dbg-warn "ADD TO MAIN" :component-container (dbg-container component-container) :shape (dbg-shape shape))
|
||||
(let [parent-shape (ctn/get-shape page (:parent-id shape))
|
||||
component-parent-shape (d/seek #(ctk/is-main-of? % parent-shape components-v2)
|
||||
(cfh/get-children-with-self (:objects component-container)
|
||||
@@ -1193,7 +1269,7 @@
|
||||
(assoc original-shape
|
||||
:shape-ref (:id new-shape)))
|
||||
|
||||
[_new-shape new-shapes updated-shapes]
|
||||
[new-shape new-shapes updated-shapes]
|
||||
(ctst/clone-shape shape
|
||||
(:id component-parent-shape)
|
||||
(get page :objects)
|
||||
@@ -1271,16 +1347,12 @@
|
||||
changes' (reduce mod-obj-change changes' updated-shapes)
|
||||
changes' (reduce del-obj-change changes' new-shapes)]
|
||||
|
||||
(dbg-log " " :component-parent-shape (dbg-shape component-parent-shape) :new-shape (dbg-shape new-shape))
|
||||
changes'))
|
||||
|
||||
(defn- remove-shape
|
||||
[changes shape container omit-touched?]
|
||||
(log/info :msg (str "REMOVE-SHAPE "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape))))
|
||||
(dbg-warn "REMOVE SHAPE" :container (dbg-container container) :shape (dbg-shape shape))
|
||||
(let [objects (get container :objects)
|
||||
parents (cfh/get-parent-ids objects (:id shape))
|
||||
parent (first parents)
|
||||
@@ -1327,16 +1399,10 @@
|
||||
|
||||
(defn- move-shape
|
||||
[changes shape index-before index-after container omit-touched?]
|
||||
(log/info :msg (str "MOVE "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape))
|
||||
" "
|
||||
index-before
|
||||
" -> "
|
||||
index-after))
|
||||
(dbg-warn "MOVE"
|
||||
:container (dbg-container container)
|
||||
:shape (dbg-shape shape)
|
||||
:index (str index-before "->" index-after))
|
||||
(let [parent (ctn/get-shape container (:parent-id shape))
|
||||
|
||||
changes' (-> changes
|
||||
@@ -1367,13 +1433,11 @@
|
||||
(if (nil? (:shape-ref dest-shape))
|
||||
changes
|
||||
(do
|
||||
(log/info :msg (str "CHANGE-TOUCHED "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name dest-shape)
|
||||
" "
|
||||
(pretty-uuid (:id dest-shape)))
|
||||
:options options)
|
||||
(dbg-warn "CHANGE TOUCHED"
|
||||
:container (dbg-container container)
|
||||
:dest-shape (dbg-shape dest-shape)
|
||||
:reset-touched? reset-touched?
|
||||
:copy-touched? copy-touched?)
|
||||
(let [new-touched (cond
|
||||
reset-touched?
|
||||
nil
|
||||
@@ -1409,13 +1473,10 @@
|
||||
(if (nil? (:shape-ref shape))
|
||||
changes
|
||||
(do
|
||||
(log/info :msg (str "CHANGE-REMOTE-SYNCED? "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name shape)
|
||||
" "
|
||||
(pretty-uuid (:id shape)))
|
||||
:remote-synced remote-synced?)
|
||||
(dbg-warn "CHANGE REMOTE SYNCED"
|
||||
:container (dbg-container container)
|
||||
:shape (dbg-shape shape)
|
||||
:remote-synced? remote-synced?)
|
||||
(-> changes
|
||||
(update :redo-changes conj (make-change
|
||||
container
|
||||
@@ -1440,16 +1501,10 @@
|
||||
in the destination shape will not be copied."
|
||||
[changes dest-shape origin-shape dest-root origin-root container omit-touched?]
|
||||
|
||||
(log/info :msg (str "SYNC "
|
||||
(:name origin-shape)
|
||||
" "
|
||||
(pretty-uuid (:id origin-shape))
|
||||
" -> "
|
||||
(if (cfh/page? container) "[P " "[C ")
|
||||
(pretty-uuid (:id container)) "] "
|
||||
(:name dest-shape)
|
||||
" "
|
||||
(pretty-uuid (:id dest-shape))))
|
||||
(dbg-warn "SYNC"
|
||||
:origin-shape (dbg-shape origin-shape)
|
||||
:container (dbg-container container)
|
||||
:dest-shape (dbg-shape dest-shape))
|
||||
|
||||
(let [;; To synchronize geometry attributes we need to make a prior
|
||||
;; operation, because coordinates are absolute, but we need to
|
||||
|
||||
@@ -486,7 +486,6 @@
|
||||
duplicating-component? (or duplicating-component? (ctk/instance-head? obj))
|
||||
is-component-main? (ctk/main-instance? obj)
|
||||
subinstance-head? (ctk/subinstance-head? obj)
|
||||
instance-root? (ctk/instance-root? obj)
|
||||
|
||||
into-component? (and duplicating-component?
|
||||
(ctn/in-any-component? objects parent))
|
||||
@@ -509,9 +508,7 @@
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id)
|
||||
|
||||
(cond-> (and (not instance-root?)
|
||||
subinstance-head?
|
||||
remove-swap-slot?)
|
||||
(cond-> (and subinstance-head? remove-swap-slot?)
|
||||
(ctk/remove-swap-slot))
|
||||
|
||||
(dissoc :shapes
|
||||
@@ -584,9 +581,8 @@
|
||||
true
|
||||
(and remove-swap-slot?
|
||||
;; only remove swap slot of children when the current shape
|
||||
;; is not a subinstance head nor a instance root
|
||||
(not subinstance-head?)
|
||||
(not instance-root?))))
|
||||
;; is not a subinstance head
|
||||
(not subinstance-head?))))
|
||||
changes
|
||||
(map (d/getf objects) (:shapes obj)))))))
|
||||
|
||||
|
||||
@@ -133,10 +133,7 @@
|
||||
(defn- fetch-gfont-css
|
||||
[url]
|
||||
(->> (http/send! {:method :get :uri url :mode :cors :response-type :text})
|
||||
(rx/map :body)
|
||||
(rx/catch (fn [err]
|
||||
(.warn js/console "Cannot find the font" (obj/get err "message"))
|
||||
(rx/empty)))))
|
||||
(rx/map :body)))
|
||||
|
||||
(defmethod load-font :google
|
||||
[{:keys [id ::on-loaded] :as font}]
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
[:& (mf/provider ctx/current-route) {:value route}
|
||||
[:& (mf/provider ctx/current-profile) {:value profile}
|
||||
(if edata
|
||||
[:& static/exception-page {:data edata :route route}]
|
||||
[:& static/exception-page {:data edata}]
|
||||
[:*
|
||||
[:& msgs/notifications-hub]
|
||||
(when route
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth.login :refer [login-page]]
|
||||
[app.main.ui.auth.recovery :refer [recovery-page]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
|
||||
@@ -42,14 +44,18 @@
|
||||
{::mf/props :obj}
|
||||
[{:keys [route]}]
|
||||
(let [section (dm/get-in route [:data :name])
|
||||
params (:query-params route)]
|
||||
params (:query-params route)
|
||||
error (:error params)]
|
||||
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.default")))
|
||||
|
||||
(mf/with-effect [error]
|
||||
(when error
|
||||
(st/emit! (du/show-redirect-error error))))
|
||||
|
||||
[:main {:class (stl/css :auth-section)}
|
||||
[:h1 {:class (stl/css :logo-container)}
|
||||
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
|
||||
[:a {:href "#/" :class (stl/css :logo-btn)} i/logo]
|
||||
[:div {:class (stl/css :login-illustration)}
|
||||
i/login-illustration]
|
||||
|
||||
|
||||
@@ -24,16 +24,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
position: absolute;
|
||||
top: $s-20;
|
||||
left: $s-20;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: $s-120;
|
||||
margin-block-end: $s-52;
|
||||
}
|
||||
|
||||
.login-illustration {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -65,6 +55,14 @@
|
||||
}
|
||||
|
||||
.logo-btn {
|
||||
position: absolute;
|
||||
top: $s-20;
|
||||
left: $s-20;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
width: $s-120;
|
||||
margin-block-end: $s-52;
|
||||
|
||||
svg {
|
||||
width: $s-120;
|
||||
height: $s-40;
|
||||
|
||||
@@ -42,6 +42,10 @@
|
||||
{:type :warning
|
||||
:content (tr "auth.demo-warning")}])
|
||||
|
||||
(defn create-demo-profile
|
||||
[]
|
||||
(st/emit! (du/create-demo-profile)))
|
||||
|
||||
(defn- login-with-oidc
|
||||
[event provider params]
|
||||
(dom/prevent-default event)
|
||||
@@ -60,28 +64,6 @@
|
||||
:else
|
||||
(st/emit! (msg/error (tr "errors.generic"))))))))
|
||||
|
||||
(defn- login-with-ldap
|
||||
[event params]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(let [{:keys [on-error]} (meta params)]
|
||||
(->> (rp/cmd! :login-with-ldap params)
|
||||
(rx/subs! (fn [profile]
|
||||
(if-let [token (:invitation-token profile)]
|
||||
(st/emit! (rt/nav :auth-verify-token {} {:token token}))
|
||||
(st/emit! (du/login-from-token {:profile profile}))))
|
||||
(fn [{:keys [type code] :as error}]
|
||||
(cond
|
||||
(and (= type :restriction)
|
||||
(= code :ldap-not-initialized))
|
||||
(st/emit! (msg/error (tr "errors.ldap-disabled")))
|
||||
|
||||
(fn? on-error)
|
||||
(on-error error)
|
||||
|
||||
:else
|
||||
(st/emit! (msg/error (tr "errors.generic")))))))))
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
(s/def ::invitation-token ::us/not-empty-string)
|
||||
@@ -107,13 +89,18 @@
|
||||
:initial initial)
|
||||
|
||||
on-error
|
||||
(fn [err]
|
||||
(let [cause (ex-data err)]
|
||||
(fn [cause]
|
||||
(let [cause (ex-data cause)]
|
||||
(cond
|
||||
(and (= :restriction (:type cause))
|
||||
(= :profile-blocked (:code cause)))
|
||||
(reset! error (tr "errors.profile-blocked"))
|
||||
|
||||
(and (= :restriction (:type cause))
|
||||
(= :ldap-not-initialized (:code cause)))
|
||||
(st/emit! (msg/error (tr "errors.ldap-disabled")))
|
||||
|
||||
|
||||
(and (= :restriction (:type cause))
|
||||
(= :admin-only-profile (:code cause)))
|
||||
(reset! error (tr "errors.profile-blocked"))
|
||||
@@ -130,9 +117,10 @@
|
||||
(reset! error (tr "errors.generic")))))
|
||||
|
||||
on-success-default
|
||||
(fn [data]
|
||||
(when-let [token (:invitation-token data)]
|
||||
(st/emit! (rt/nav :auth-verify-token {} {:token token}))))
|
||||
(mf/use-fn
|
||||
(fn [data]
|
||||
(when-let [token (:invitation-token data)]
|
||||
(st/emit! (rt/nav :auth-verify-token {} {:token token})))))
|
||||
|
||||
on-success
|
||||
(fn [data]
|
||||
@@ -153,11 +141,15 @@
|
||||
(mf/use-callback
|
||||
(mf/deps form)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
|
||||
(reset! error nil)
|
||||
(let [params (:clean-data @form)]
|
||||
(login-with-ldap event (with-meta params
|
||||
{:on-error on-error
|
||||
:on-success on-success})))))
|
||||
(let [params (:clean-data @form)
|
||||
params (with-meta params
|
||||
{:on-error on-error
|
||||
:on-success on-success})]
|
||||
(st/emit! (du/login-with-ldap params)))))
|
||||
|
||||
on-recovery-request
|
||||
(mf/use-fn
|
||||
@@ -308,5 +300,11 @@
|
||||
[:& lk/link {:action go-register
|
||||
:class (stl/css :register-link)
|
||||
:data-test "register-submit"}
|
||||
(tr "auth.register-submit")]])]]))
|
||||
(tr "auth.register-submit")]])]
|
||||
|
||||
(when (contains? cf/flags :demo-users)
|
||||
[:div {:class (stl/css :link-entry :demo-account)}
|
||||
[:span (tr "auth.create-demo-profile") " "]
|
||||
[:& lk/link {:action create-demo-profile
|
||||
:data-test "demo-account-link"}
|
||||
(tr "auth.create-demo-account")]])]))
|
||||
|
||||
@@ -26,18 +26,20 @@
|
||||
|
||||
;; --- PAGE: Register
|
||||
|
||||
(defn- validate
|
||||
(defn- validate-password-length
|
||||
[errors data]
|
||||
(let [password (:password data)]
|
||||
(cond-> errors
|
||||
(> 8 (count password))
|
||||
(assoc :password {:message "errors.password-too-short"})
|
||||
:always
|
||||
(d/update-when :email
|
||||
(fn [{:keys [code] :as error}]
|
||||
(cond-> error
|
||||
(= code ::us/email)
|
||||
(assoc :message (tr "errors.email-invalid"))))))))
|
||||
(assoc :password {:message "errors.password-too-short"}))))
|
||||
|
||||
(defn- validate-email
|
||||
[errors _]
|
||||
(d/update-when errors :email
|
||||
(fn [{:keys [code] :as error}]
|
||||
(cond-> error
|
||||
(= code ::us/email)
|
||||
(assoc :message (tr "errors.email-invalid"))))))
|
||||
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
@@ -49,31 +51,20 @@
|
||||
(s/keys :req-un [::password ::email]
|
||||
:opt-un [::invitation-token]))
|
||||
|
||||
(defn- handle-prepare-register-error
|
||||
(defn- on-prepare-register-error
|
||||
[form cause]
|
||||
(let [{:keys [type code]} (ex-data cause)]
|
||||
(condp = [type code]
|
||||
[:restriction :registration-disabled]
|
||||
(st/emit! (msg/error (tr "errors.registration-disabled")))
|
||||
|
||||
[:restriction :profile-blocked]
|
||||
(st/emit! (msg/error (tr "errors.profile-blocked")))
|
||||
|
||||
[:validation :email-has-permanent-bounces]
|
||||
(let [email (get @form [:data :email])]
|
||||
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" email))))
|
||||
|
||||
[:validation :email-already-exists]
|
||||
(swap! form assoc-in [:errors :email]
|
||||
{:message "errors.email-already-exists"})
|
||||
|
||||
[:validation :email-as-password]
|
||||
(swap! form assoc-in [:errors :password]
|
||||
{:message "errors.email-as-password"})
|
||||
|
||||
(st/emit! (msg/error (tr "errors.generic"))))))
|
||||
|
||||
(defn- handle-prepare-register-success
|
||||
(defn- on-prepare-register-success
|
||||
[params]
|
||||
(st/emit! (rt/nav :auth-register-validate {} params)))
|
||||
|
||||
@@ -81,28 +72,30 @@
|
||||
[{:keys [params on-success-callback]}]
|
||||
(let [initial (mf/use-memo (mf/deps params) (constantly params))
|
||||
form (fm/use-form :spec ::register-form
|
||||
:validators [validate
|
||||
:validators [validate-password-length
|
||||
validate-email
|
||||
(fm/validate-not-empty :password (tr "auth.password-not-empty"))]
|
||||
:initial initial)
|
||||
submitted? (mf/use-state false)
|
||||
|
||||
on-success (fn [p]
|
||||
(if (nil? on-success-callback)
|
||||
(handle-prepare-register-success p)
|
||||
(on-success-callback p)))
|
||||
submitted? (mf/use-state false)
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps on-success-callback)
|
||||
(fn [form _event]
|
||||
(reset! submitted? true)
|
||||
(let [cdata (:clean-data @form)]
|
||||
(let [cdata (:clean-data @form)
|
||||
on-success (fn [data]
|
||||
(if (nil? on-success-callback)
|
||||
(on-prepare-register-success data)
|
||||
(on-success-callback data)))
|
||||
on-error (fn [data]
|
||||
(on-prepare-register-error form data))]
|
||||
|
||||
(->> (rp/cmd! :prepare-register-profile cdata)
|
||||
(rx/map #(merge % params))
|
||||
(rx/finalize #(reset! submitted? false))
|
||||
(rx/subs!
|
||||
on-success
|
||||
(partial handle-prepare-register-error form))))))]
|
||||
|
||||
(rx/subs! on-success on-error)))))]
|
||||
|
||||
[:& fm/form {:on-submit on-submit :form form}
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
@@ -126,7 +119,6 @@
|
||||
:data-test "register-form-submit"
|
||||
:class (stl/css :register-btn)}]]))
|
||||
|
||||
|
||||
(mf/defc register-methods
|
||||
{::mf/props :obj}
|
||||
[{:keys [params on-success-callback]}]
|
||||
@@ -162,22 +154,15 @@
|
||||
[:*
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :demo-account)}
|
||||
[:& lk/link {:action #(st/emit! (du/create-demo-profile))
|
||||
[:& lk/link {:action login/create-demo-profile
|
||||
:class (stl/css :demo-account-link)}
|
||||
(tr "auth.create-demo-account")]]])]])
|
||||
|
||||
;; --- PAGE: register validation
|
||||
|
||||
(defn- handle-register-error
|
||||
[form error]
|
||||
(case (:code error)
|
||||
:email-already-exists
|
||||
(swap! form assoc-in [:errors :email]
|
||||
{:message "errors.email-already-exists"})
|
||||
|
||||
(do
|
||||
(println (:explain error))
|
||||
(st/emit! (msg/error (tr "errors.generic"))))))
|
||||
[_form _data]
|
||||
(st/emit! (msg/error (tr "errors.generic"))))
|
||||
|
||||
(defn- handle-register-success
|
||||
[data]
|
||||
@@ -186,8 +171,6 @@
|
||||
(let [token (:invitation-token data)]
|
||||
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
|
||||
|
||||
;; The :is-active flag is true, when insecure-register is enabled
|
||||
;; or the user used external auth provider.
|
||||
(:is-active data)
|
||||
(st/emit! (du/login-from-register))
|
||||
|
||||
@@ -282,5 +265,3 @@
|
||||
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]
|
||||
[:div {:class (stl/css :notification-text-email)} (:email params "")]
|
||||
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]])
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
(def current-page-id (mf/create-context nil))
|
||||
(def current-file-id (mf/create-context nil))
|
||||
(def current-vbox (mf/create-context nil))
|
||||
(def current-svg-root-id (mf/create-context nil))
|
||||
|
||||
(def active-frames (mf/create-context nil))
|
||||
(def render-thumbnails (mf/create-context nil))
|
||||
|
||||
@@ -7,11 +7,15 @@
|
||||
(ns app.main.ui.dashboard.projects
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.dashboard.grid :refer [line-grid]]
|
||||
@@ -96,6 +100,80 @@
|
||||
(def builtin-templates
|
||||
(l/derived :builtin-templates st/state))
|
||||
|
||||
(mf/defc tutorial-project
|
||||
[{:keys [close-tutorial default-project-id] :as props}]
|
||||
(let [state (mf/use-state {:status :waiting
|
||||
:file nil})
|
||||
|
||||
templates (mf/deref builtin-templates)
|
||||
template (d/seek #(= (:id %) "tutorial-for-beginners") templates)
|
||||
|
||||
on-template-cloned-success
|
||||
(mf/use-fn
|
||||
(mf/deps default-project-id)
|
||||
(fn [response]
|
||||
(swap! state #(assoc % :status :success :file (:first response)))
|
||||
(st/emit! (dd/go-to-workspace {:id (first response) :project-id default-project-id :name "tutorial"})
|
||||
(du/update-profile-props {:viewed-tutorial? true}))))
|
||||
|
||||
on-template-cloned-error
|
||||
(mf/use-fn
|
||||
(fn [cause]
|
||||
(swap! state assoc :status :error)
|
||||
(errors/print-error! cause)
|
||||
(st/emit! (msg/error (tr "dashboard.libraries-and-templates.import-error")))))
|
||||
|
||||
download-tutorial
|
||||
(mf/use-fn
|
||||
(mf/deps template default-project-id)
|
||||
(fn []
|
||||
(let [mdata {:on-success on-template-cloned-success
|
||||
:on-error on-template-cloned-error}
|
||||
params {:project-id default-project-id
|
||||
:template-id (:id template)}]
|
||||
(swap! state #(assoc % :status :importing))
|
||||
(st/emit! (with-meta (dd/clone-template (with-meta params mdata))
|
||||
{::ev/origin "get-started-hero-block"})))))]
|
||||
[:article {:class (stl/css :tutorial)}
|
||||
[:div {:class (stl/css :thumbnail)}]
|
||||
[:div {:class (stl/css :text)}
|
||||
[:h2 {:class (stl/css :title)} (tr "dasboard.tutorial-hero.title")]
|
||||
[:p {:class (stl/css :info)} (tr "dasboard.tutorial-hero.info")]
|
||||
[:button {:class (stl/css :btn-primary :action)
|
||||
:on-click download-tutorial}
|
||||
(case (:status @state)
|
||||
:waiting (tr "dasboard.tutorial-hero.start")
|
||||
:importing [:span.loader i/loader-pencil]
|
||||
:success "")]]
|
||||
|
||||
[:button {:class (stl/css :close)
|
||||
:on-click close-tutorial
|
||||
:aria-label (tr "labels.close")}
|
||||
close-icon]]))
|
||||
|
||||
(mf/defc interface-walkthrough
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [close-walkthrough] :as props}]
|
||||
(let [handle-walkthrough-link
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "show-walkthrough"
|
||||
::ev/origin "get-started-hero-block"
|
||||
:section "dashboard"})))]
|
||||
[:article {:class (stl/css :walkthrough)}
|
||||
[:div {:class (stl/css :thumbnail)}]
|
||||
[:div {:class (stl/css :text)}
|
||||
[:h2 {:class (stl/css :title)} (tr "dasboard.walkthrough-hero.title")]
|
||||
[:p {:class (stl/css :info)} (tr "dasboard.walkthrough-hero.info")]
|
||||
[:a {:class (stl/css :btn-primary :action)
|
||||
:href " https://design.penpot.app/walkthrough"
|
||||
:target "_blank"
|
||||
:on-click handle-walkthrough-link}
|
||||
(tr "dasboard.walkthrough-hero.start")]]
|
||||
[:button {:class (stl/css :close)
|
||||
:on-click close-walkthrough
|
||||
:aria-label (tr "labels.close")}
|
||||
close-icon]]))
|
||||
|
||||
(mf/defc project-item
|
||||
[{:keys [project first? team files] :as props}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
@@ -287,7 +365,7 @@
|
||||
(l/derived :dashboard-recent-files st/state))
|
||||
|
||||
(mf/defc projects-section
|
||||
[{:keys [team projects profile] :as props}]
|
||||
[{:keys [team projects profile default-project-id] :as props}]
|
||||
(let [projects (->> (vals projects)
|
||||
(sort-by :modified-at)
|
||||
(reverse))
|
||||
@@ -300,6 +378,8 @@
|
||||
(:team-hero? props true)
|
||||
(not (:is-default team)))
|
||||
|
||||
tutorial-viewed? (:viewed-tutorial? props true)
|
||||
walkthrough-viewed? (:viewed-walkthrough? props true)
|
||||
is-my-penpot (= (:default-team-id profile) (:id team))
|
||||
|
||||
team-id (:id team)
|
||||
@@ -311,6 +391,28 @@
|
||||
(ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero"
|
||||
::ev/origin "dashboard"}))))
|
||||
|
||||
close-tutorial
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/update-profile-props {:viewed-tutorial? true})
|
||||
(ptk/data-event ::ev/event {::ev/name "dont-show-tutorial"
|
||||
::ev/origin "get-started-hero"
|
||||
:type "tutorial"
|
||||
:section "dashboard"}))))
|
||||
|
||||
close-walkthrough
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/update-profile-props {:viewed-walkthrough? true})
|
||||
(ptk/data-event ::ev/event {::ev/name "dont-show-walkthrough"
|
||||
::ev/origin "get-started-hero"
|
||||
:type "walkthrough"
|
||||
:section "dashboard"}))))
|
||||
|
||||
show-hero? (and is-my-penpot
|
||||
(or (not tutorial-viewed?)
|
||||
(not walkthrough-viewed?)))
|
||||
|
||||
show-team-hero? (and (not is-my-penpot) team-hero?)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
@@ -331,9 +433,22 @@
|
||||
(when team-hero?
|
||||
[:& team-hero {:team team :close-fn close-banner}])
|
||||
|
||||
(when (and (contains? cf/flags :dashboard-templates-section)
|
||||
show-hero?)
|
||||
[:div {:class (stl/css :hero-projects)}
|
||||
(when (and (not tutorial-viewed?) (:is-default team))
|
||||
[:& tutorial-project
|
||||
{:close-tutorial close-tutorial
|
||||
:default-project-id default-project-id}])
|
||||
|
||||
(when (and (not walkthrough-viewed?) (:is-default team))
|
||||
[:& interface-walkthrough
|
||||
{:close-walkthrough close-walkthrough}])])
|
||||
|
||||
[:div {:class (stl/css-case :dashboard-container true
|
||||
:no-bg true
|
||||
:dashboard-projects true
|
||||
:with-hero show-hero?
|
||||
:with-team-hero show-team-hero?)}
|
||||
(for [{:keys [id] :as project} projects]
|
||||
(let [files (when recent-map
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
height: calc(100vh - $s-64);
|
||||
}
|
||||
|
||||
.with-hero,
|
||||
.with-team-hero {
|
||||
height: calc(100vh - $s-280);
|
||||
}
|
||||
@@ -85,6 +86,8 @@
|
||||
color: var(--title-foreground-color-hover);
|
||||
cursor: pointer;
|
||||
height: $s-16;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-wrapper {
|
||||
@@ -239,3 +242,88 @@
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-projects {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: $s-32;
|
||||
margin: 0 $s-16 $s-16 $s-20;
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tutorial,
|
||||
.walkthrough {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
position: relative;
|
||||
border-radius: $br-8;
|
||||
min-height: $s-216;
|
||||
background-color: $db-tertiary;
|
||||
padding: $s-8;
|
||||
|
||||
.thumbnail {
|
||||
width: $s-200;
|
||||
height: $s-200;
|
||||
border-radius: $br-6;
|
||||
padding: $s-32;
|
||||
display: block;
|
||||
background-color: var(--color-canvas);
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: $br-4;
|
||||
margin-bottom: 0;
|
||||
width: $s-232;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: $s-32;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $df-primary;
|
||||
font-size: $fs-24;
|
||||
font-weight: $fw400;
|
||||
margin-bottom: $s-8;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
color: $df-secondary;
|
||||
margin-bottom: $s-20;
|
||||
font-size: $fs-16;
|
||||
}
|
||||
.invite {
|
||||
height: $s-32;
|
||||
}
|
||||
.action {
|
||||
width: $s-180;
|
||||
height: $s-40;
|
||||
}
|
||||
}
|
||||
.walkthrough {
|
||||
.thumbnail {
|
||||
background-image: url("/images/walkthrough-cover.png");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
.tutorial {
|
||||
.thumbnail {
|
||||
background-image: url("/images/hands-on-tutorial.png");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.loader {
|
||||
display: flex;
|
||||
svg#loader-pencil {
|
||||
width: $s-32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
[:div {:class (stl/css :img-container)}
|
||||
[:a {:id id
|
||||
:tab-index (if (or (not is-visible) collapsed) "-1" "0")
|
||||
:href "https://penpot.app/libraries-templates"
|
||||
:href "https://penpot.app/libraries-templates.html"
|
||||
:target "_blank"
|
||||
:on-click on-click
|
||||
:on-key-down on-key-down}
|
||||
|
||||
@@ -142,9 +142,7 @@
|
||||
(modal/show! {:type :onboarding-newsletter})
|
||||
|
||||
(contains? cf/flags :onboarding-team)
|
||||
(modal/show! {:type :onboarding-team}))))
|
||||
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
(modal/show! {:type :onboarding-team}))))]
|
||||
|
||||
(mf/with-effect [@slide]
|
||||
(when (not= :start @slide)
|
||||
@@ -153,8 +151,8 @@
|
||||
(fn []
|
||||
(reset! klass nil)
|
||||
(tm/dispose! sem))))
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class (dm/str @klass " " (stl/css :animated))}
|
||||
(case @slide
|
||||
:start [:& onboarding-welcome {:next #(navigate :opensource)}]
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.onboarding.newsletter
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
@@ -36,11 +35,9 @@
|
||||
(st/emit! (when (or @newsletter-updates @newsletter-news)
|
||||
(msg/success message))
|
||||
(modal/show {:type :onboarding-team})
|
||||
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))]
|
||||
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated.fadeInDown {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-left)}
|
||||
[:img {:src "images/deco-newsletter.png"
|
||||
|
||||
@@ -287,11 +287,9 @@
|
||||
(modal/show! {:type :onboarding-team})
|
||||
|
||||
:else
|
||||
(modal/hide!)))))
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
(modal/hide!)))))]
|
||||
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)
|
||||
:ref container}
|
||||
(case @step
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dmc]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as msg]
|
||||
@@ -85,16 +84,14 @@
|
||||
::ev/origin "onboarding"
|
||||
:step 1}))))
|
||||
|
||||
teams (mf/deref refs/teams)
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
teams (mf/deref refs/teams)]
|
||||
|
||||
(mf/with-effect [teams]
|
||||
(when (> (count teams) 1)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
(when (< (count teams) 2)
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
||||
[:& team-modal-left]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
@@ -215,11 +212,9 @@
|
||||
(if (> (count emails) 0)
|
||||
(on-invite-now form)
|
||||
(on-invite-later form))
|
||||
(modal/hide!))))
|
||||
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
(modal/hide!))))]
|
||||
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
||||
[:& team-modal-left]
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
[:h2 "Libraries & templates"]]
|
||||
[:div.modal-content
|
||||
[:p "We’ve created a new space on Penpot where you can share your libraries and templates and download the ones you like. Material Design, Cocomaterial or Penpot’s Design System are among them (and a lot more to come!)."]
|
||||
[:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates"} "Explore libraries & templates"]]]
|
||||
[:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates.html"} "Explore libraries & templates"]]]
|
||||
[:div.modal-navigation
|
||||
[:button.btn-secondary {:on-click finish} "Start!"]
|
||||
[:& c/navigation-bullets
|
||||
|
||||
@@ -8,203 +8,196 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; TODO: Review all copies and alt text
|
||||
(defmethod c/render-release-notes "2.0"
|
||||
[{:keys [slide klass next finish navigate version]}]
|
||||
(let [onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-intro-image.png"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "A graphic illustration with Penpot style"}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-intro-image.png"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "A graphic illustration with Penpot style"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Welcome to Penpot 2.0! "]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Welcome to Penpot 2.0! "]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"CSS Grid Layout: "]
|
||||
"Bring your designs to life, knowing that what you create is what developers code."]
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"CSS Grid Layout: "]
|
||||
"Bring your designs to life, knowing that what you create is what developers code."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"Sleeker UI: "]
|
||||
"We’ve polished Penpot to make your experience smoother and more enjoyable."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"Sleeker UI: "]
|
||||
"We’ve polished Penpot to make your experience smoother and more enjoyable."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"New Components System: "]
|
||||
"Managing and using your design components got a whole lot better."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
[:spam {:class (stl/css :feature-title)}
|
||||
"New Components System: "]
|
||||
"Managing and using your design components got a whole lot better."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"And that’s not all - we’ve fined tuned performance and "
|
||||
"accessibility to give you a better and more fluid design experience."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"And that’s not all - we’ve fined tuned performance and "
|
||||
"accessibility to give you a better and more fluid design experience."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
" Ready to dive in? Let 's get started!"]]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
" Ready to dive in? Let 's get started!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
|
||||
0
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-css-grid.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's CSS Grid Layout"}]
|
||||
0
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-css-grid.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's CSS Grid Layout"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"CSS Grid Layout - Design Meets Development"]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"CSS Grid Layout - Design Meets Development"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The much-awaited Grid Layout introduces 2-dimensional"
|
||||
" layout capabilities to Penpot, allowing for the creation"
|
||||
" of adaptive layouts by leveraging the power of CSS properties."]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The much-awaited Grid Layout introduces 2-dimensional"
|
||||
" layout capabilities to Penpot, allowing for the creation"
|
||||
" of adaptive layouts by leveraging the power of CSS properties."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"It’s a host of new features, including columns and"
|
||||
" rows management, flexible units such as FR (fractions),"
|
||||
" the ability to create and name areas, and tons of new "
|
||||
"and unique possibilities within a design tool."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"It’s a host of new features, including columns and"
|
||||
" rows management, flexible units such as FR (fractions),"
|
||||
" the ability to create and name areas, and tons of new "
|
||||
"and unique possibilities within a design tool."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Designers will learn CSS basics while working, "
|
||||
"and as always with Penpot, developers can pick"
|
||||
" up the design as code to take it from there."]]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Designers will learn CSS basics while working, "
|
||||
"and as always with Penpot, developers can pick"
|
||||
" up the design as code to take it from there."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
1
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-new-ui.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's UI Makeover"}]
|
||||
1
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-new-ui.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's UI Makeover"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"We've completely overhauled Penpot's user interface. "
|
||||
"The improvements in consistency, the introduction of "
|
||||
"new microinteractions, and attention to countless details"
|
||||
" will significantly enhance the productivity and enjoyment of using Penpot."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Furthermore, we’ve made several accessibility improvements, "
|
||||
"with better color contrast, keyboard navigation,"
|
||||
" and adherence to other best practices."]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"We've completely overhauled Penpot's user interface. "
|
||||
"The improvements in consistency, the introduction of "
|
||||
"new microinteractions, and attention to countless details"
|
||||
" will significantly enhance the productivity and enjoyment of using Penpot."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Furthermore, we’ve made several accessibility improvements, "
|
||||
"with better color contrast, keyboard navigation,"
|
||||
" and adherence to other best practices."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
2
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-components.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's new components system"}]
|
||||
2
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-components.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot's new components system"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"New Components System"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The new Penpot components system improves"
|
||||
" control over instances, including their "
|
||||
"inheritances and properties overrides. "
|
||||
"Main components are now accessible as design"
|
||||
" elements, allowing a better updating "
|
||||
"workflow through instant changes synchronization."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"And that’s not all, there are new capabilities "
|
||||
"such as component swapping and annotations "
|
||||
"that will help you to better manage your design systems."]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"New Components System"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The new Penpot components system improves"
|
||||
" control over instances, including their "
|
||||
"inheritances and properties overrides. "
|
||||
"Main components are now accessible as design"
|
||||
" elements, allowing a better updating "
|
||||
"workflow through instant changes synchronization."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"And that’s not all, there are new capabilities "
|
||||
"such as component swapping and annotations "
|
||||
"that will help you to better manage your design systems."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
3
|
||||
[:div {:class (stl/css-case :modal-overlay true
|
||||
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-html.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt " Penpot's HTML code generator"}]
|
||||
3
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.0-html.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt " Penpot's HTML code generator"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"And much more"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"In addition to all of this, we’ve included several other requested improvements:"]
|
||||
[:ul {:class (stl/css :feature-list)}
|
||||
[:li "Access HTML markup code directly in inspect mode"]
|
||||
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
|
||||
[:li "Enjoy new color themes with options for both dark and light modes"]
|
||||
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"And much more"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"In addition to all of this, we’ve included several other requested improvements:"]
|
||||
[:ul {:class (stl/css :feature-list)}
|
||||
[:li "Access HTML markup code directly in inspect mode"]
|
||||
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
|
||||
[:li "Enjoy new color themes with options for both dark and light modes"]
|
||||
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:div {:class (stl/css :navigation)}
|
||||
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 4}]
|
||||
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]]))))
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
||||
|
||||
|
||||
@@ -122,11 +122,7 @@
|
||||
(add! :stroke-cap-end)))
|
||||
|
||||
(cond-> text?
|
||||
(-> (add! :x)
|
||||
(add! :y)
|
||||
(add! :width)
|
||||
(add! :height)
|
||||
(add! :grow-type)
|
||||
(-> (add! :grow-type)
|
||||
(add! :content (comp json/encode uuid->string))
|
||||
(add! :position-data (comp json/encode uuid->string))))
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
(mf/fnc group-shape
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (unchecked-get props "childs")
|
||||
render-id (mf/use-ctx muc/render-id)
|
||||
@@ -37,31 +36,21 @@
|
||||
|
||||
mask-props (if ^boolean masked-group?
|
||||
#js {:mask (mask-url render-id mask)}
|
||||
#js {})
|
||||
|
||||
current-svg-root-id (mf/use-ctx muc/current-svg-root-id)
|
||||
|
||||
;; We need to create a "scope" for svg classes. The root of the imported SVG (first group) will
|
||||
;; be stored in the context. When rendering the styles we add its id as prefix.
|
||||
[svg-wrapper svg-wrapper-props]
|
||||
(if (and (contains? shape :svg-attrs) (not current-svg-root-id))
|
||||
[(mf/provider muc/current-svg-root-id) #js {:value (:id shape)}]
|
||||
[mf/Fragment #js {}])]
|
||||
#js {})]
|
||||
|
||||
;; We need to separate mask and clip into two because a bug in
|
||||
;; Firefox breaks when the group has clip+mask+foreignObject
|
||||
;; Clip and mask separated will work in every platform Firefox
|
||||
;; bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1734805
|
||||
[:> svg-wrapper svg-wrapper-props
|
||||
[:> wrapper clip-props
|
||||
[:> wrapper mask-props
|
||||
(when ^boolean masked-group?
|
||||
[:& render-mask {:mask mask}])
|
||||
[:> wrapper clip-props
|
||||
[:> wrapper mask-props
|
||||
(when ^boolean masked-group?
|
||||
[:& render-mask {:mask mask}])
|
||||
|
||||
(for [item childs]
|
||||
[:& shape-wrapper
|
||||
{:shape item
|
||||
:key (dm/str (dm/get-prop item :id))}])]]]))))
|
||||
(for [item childs]
|
||||
[:& shape-wrapper
|
||||
{:shape item
|
||||
:key (dm/str (dm/get-prop item :id))}])]]))))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -94,13 +94,6 @@
|
||||
(obj/unset! "disable-shadows?")
|
||||
(obj/set! "ref" ref)
|
||||
(obj/set! "id" (dm/fmt "shape-%" shape-id))
|
||||
|
||||
;; TODO: This is added for backward compatibility.
|
||||
(cond-> (and (cfh/text-shape? shape) (empty? (:position-data shape)))
|
||||
(-> (obj/set! "x" (:x shape))
|
||||
(obj/set! "y" (:y shape))
|
||||
(obj/set! "width" (:width shape))
|
||||
(obj/set! "height" (:height shape))))
|
||||
(obj/set! "style" styles))
|
||||
|
||||
wrapper-props
|
||||
|
||||
@@ -104,20 +104,9 @@
|
||||
svg-root? (and (map? content) (= tag :svg))
|
||||
svg-tag? (map? content)
|
||||
svg-leaf? (string? content)
|
||||
valid-tag? (contains? csvg/svg-tags tag)
|
||||
|
||||
current-svg-root-id (mf/use-ctx muc/current-svg-root-id)
|
||||
|
||||
;; We need to create a "scope" for svg classes. The root of the imported SVG (first group) will
|
||||
;; be stored in the context and with this we scoped the styles:
|
||||
style-content
|
||||
(when (= tag :style)
|
||||
(dm/str "#shape-" current-svg-root-id "{ " (->> shape :content :content (str/join "\n")) " }"))]
|
||||
valid-tag? (contains? csvg/svg-tags tag)]
|
||||
|
||||
(cond
|
||||
(= tag :style)
|
||||
[:style style-content]
|
||||
|
||||
^boolean svg-root?
|
||||
[:& svg-root {:shape shape}
|
||||
(for [item childs]
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.uri :as u]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
@@ -18,7 +16,6 @@
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.webapi :as wapi]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc error-container
|
||||
@@ -149,19 +146,15 @@
|
||||
|
||||
(mf/defc exception-page
|
||||
{::mf/props :obj}
|
||||
[{:keys [data route] :as props}]
|
||||
(let [type (:type data)
|
||||
path (:path route)
|
||||
query-params (u/map->query-string (:query-params route))]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params}))
|
||||
(case (:type data)
|
||||
:not-found
|
||||
[:& not-found]
|
||||
[{:keys [data] :as props}]
|
||||
(case (:type data)
|
||||
:not-found
|
||||
[:& not-found]
|
||||
|
||||
:bad-gateway
|
||||
[:& bad-gateway]
|
||||
:bad-gateway
|
||||
[:& bad-gateway]
|
||||
|
||||
:service-unavailable
|
||||
[:& service-unavailable]
|
||||
:service-unavailable
|
||||
[:& service-unavailable]
|
||||
|
||||
[:> internal-error props])))
|
||||
[:> internal-error props]))
|
||||
|
||||
@@ -332,7 +332,7 @@
|
||||
:title (tr "viewer.header.comments-section" (sc/get-tooltip :open-comments))}
|
||||
i/comments])
|
||||
|
||||
(when (or (:in-team permissions)
|
||||
(when (or (= (:type permissions) :membership)
|
||||
(and (= (:type permissions) :share-link)
|
||||
(= (:who-inspect permissions) "all")))
|
||||
[:button {:on-click go-to-inspect
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.workspace.colorpicker.libraries
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.colors :as c]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.events :as ev]
|
||||
@@ -25,7 +24,7 @@
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc libraries
|
||||
[{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity disable-image]}]
|
||||
[{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity disable-image]}]
|
||||
(let [selected (h/use-shared-state mdc/colorpicker-selected-broadcast-key :recent)
|
||||
current-colors (mf/use-state [])
|
||||
|
||||
@@ -44,17 +43,10 @@
|
||||
(parse-uuid event)))))
|
||||
|
||||
check-valid-color?
|
||||
(mf/use-fn
|
||||
(fn [color]
|
||||
(and (or (not disable-gradient) (not (:gradient color)))
|
||||
(or (not disable-opacity) (= 1 (:opacity color)))
|
||||
(or (not disable-image) (not (:image color))))))
|
||||
|
||||
;; Sort colors by hue and lightness
|
||||
get-sorted-colors
|
||||
(mf/use-fn
|
||||
(fn [colors]
|
||||
(sort c/sort-colors (into [] (filter check-valid-color?) colors))))
|
||||
(fn [color]
|
||||
(and (or (not disable-gradient) (not (:gradient color)))
|
||||
(or (not disable-opacity) (= 1 (:opacity color)))
|
||||
(or (not disable-image) (not (:image color)))))
|
||||
|
||||
toggle-palette
|
||||
(mf/use-fn
|
||||
@@ -97,15 +89,13 @@
|
||||
(sort-by :name)
|
||||
(map #(assoc % :file-id file-id)))))]
|
||||
|
||||
(if (not= @selected :recent)
|
||||
(reset! current-colors (get-sorted-colors colors))
|
||||
(reset! current-colors (into [] (filter check-valid-color? colors))))))
|
||||
(reset! current-colors (into [] (filter check-valid-color?) colors))))
|
||||
|
||||
;; If the file colors change and the file option is selected updates the state
|
||||
(mf/with-effect [file-colors]
|
||||
(when (= @selected :file)
|
||||
(let [colors (vals file-colors)]
|
||||
(reset! current-colors (get-sorted-colors colors)))))
|
||||
(reset! current-colors (into [] (filter check-valid-color?) colors)))))
|
||||
|
||||
[:div {:class (stl/css :libraries)}
|
||||
[:div {:class (stl/css :select-wrapper)}
|
||||
|
||||
@@ -272,21 +272,9 @@
|
||||
(def has-position? #{:frame :rect :image :text})
|
||||
|
||||
(defn parse-position
|
||||
[props node svg-data]
|
||||
(let [x (get-meta node :x d/parse-double)
|
||||
y (get-meta node :y d/parse-double)
|
||||
width (get-meta node :width d/parse-double)
|
||||
height (get-meta node :height d/parse-double)
|
||||
|
||||
values (->> (select-keys svg-data [:x :y :width :height])
|
||||
(d/mapm (fn [_ val] (d/parse-double val))))
|
||||
|
||||
values
|
||||
(cond-> values
|
||||
(some? x) (assoc :x x)
|
||||
(some? y) (assoc :y y)
|
||||
(some? width) (assoc :width width)
|
||||
(some? height) (assoc :height height))]
|
||||
[props svg-data]
|
||||
(let [values (->> (select-keys svg-data [:x :y :width :height])
|
||||
(d/mapm (fn [_ val] (d/parse-double val))))]
|
||||
(d/merge props values)))
|
||||
|
||||
(defn parse-circle
|
||||
@@ -404,7 +392,7 @@
|
||||
center (gpt/point center-x center-y)]
|
||||
(cond-> props
|
||||
(has-position? type)
|
||||
(parse-position node svg-data)
|
||||
(parse-position svg-data)
|
||||
|
||||
(= type :svg-raw)
|
||||
(add-svg-position node)
|
||||
|
||||
@@ -352,7 +352,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Lêers wat by biblioteke gevoeg is, sal hier verskyn. Probeer om jou lêers te "
|
||||
"deel of voeg by vanaf ons [Biblioteke en sjablone](https://penpot.app/"
|
||||
"libraries-templates)."
|
||||
"libraries-templates.html)."
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-github-submit"
|
||||
|
||||
@@ -306,7 +306,7 @@ msgstr "تكرير %s الملفات"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"أوه لا! ليس لديك ملفات بعد! إذا كنت تريد تجربة بعض القوالب ، فانتقل إلى "
|
||||
"[المكتبات والقوالب] (https://penpot.app/libraries-templates)"
|
||||
"[المكتبات والقوالب] (https://penpot.app/libraries-templates.html)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "تنزيل ملفات ٪s Penpot (.penpot)"
|
||||
|
||||
@@ -311,7 +311,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Encara no hi ha fitxers. Si voleu provar algunes plantilles, podeu anar a "
|
||||
"la secció [Biblioteques i "
|
||||
"plantilles](https://penpot.app/libraries-templates)"
|
||||
"plantilles](https://penpot.app/libraries-templates.html)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Baixa %s fitxers Penpot (.penpot)"
|
||||
|
||||
@@ -310,7 +310,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Zde se zobrazí soubory přidané do knihoven. Zkuste své soubory sdílet nebo "
|
||||
"je přidat z našich [Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates)."
|
||||
"templates](https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Stáhnout soubory %s Penpot (.penpot)"
|
||||
|
||||
@@ -406,7 +406,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Files added to Libraries will appear here. Try sharing your files or add "
|
||||
"from our [Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates)."
|
||||
"templates](https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Download %s Penpot files (.penpot)"
|
||||
@@ -968,6 +968,14 @@ msgstr "Your profile has emails muted (spam reports or high bounces)."
|
||||
msgid "errors.registration-disabled"
|
||||
msgstr "The registration is currently disabled."
|
||||
|
||||
#: src/app/main/data/users.cljs
|
||||
msgid "errors.auth-provider-not-allowed"
|
||||
msgstr "Auth provider not allowed for this profile"
|
||||
|
||||
#: src/app/main/data/users.cljs
|
||||
msgid "errors.email-domain-not-allowed"
|
||||
msgstr "Domain not allowed"
|
||||
|
||||
msgid "errors.team-leave.insufficient-members"
|
||||
msgstr "Insufficient members to leave team, you probably want to delete it."
|
||||
|
||||
|
||||
@@ -412,7 +412,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Los archivos agregados a las bibliotecas aparecerán aquí. Si quieres probar "
|
||||
"con alguna plantilla ve a [Bibliotecas y "
|
||||
"plantillas](https://penpot.app/libraries-templates)."
|
||||
"plantillas](https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Descargar %s archivos Penpot (.penpot)"
|
||||
@@ -991,6 +991,14 @@ msgstr ""
|
||||
msgid "errors.registration-disabled"
|
||||
msgstr "El registro está actualmente desactivado."
|
||||
|
||||
#: src/app/main/data/users.cljs
|
||||
msgid "errors.auth-provider-not-allowed"
|
||||
msgstr "El proveedor de autenticación no permitido para este perfil de usuario"
|
||||
|
||||
#: src/app/main/data/users.cljs
|
||||
msgid "errors.email-domain-not-allowed"
|
||||
msgstr "Dominio no permitido"
|
||||
|
||||
msgid "errors.team-leave.insufficient-members"
|
||||
msgstr ""
|
||||
"No hay miembros suficientes para salir del equipo, probablemente quieras "
|
||||
|
||||
@@ -258,7 +258,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Los archivos agregados a las Bibliotecas aparecerán aquí. Intente compartir "
|
||||
"sus archivos o agréguelos desde nuestras [Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates)."
|
||||
"templates](https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Descargar %s archivos Penpot (.penpot)"
|
||||
|
||||
@@ -309,7 +309,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Oh ez! Oraindik ez duzu fitxategirik! Txantiloi batekin proba egin nahi "
|
||||
"baduzu joan [Liburutegi eta "
|
||||
"txantiloiak](https://penpot.app/libraries-templates) atalera."
|
||||
"txantiloiak](https://penpot.app/libraries-templates.html) atalera."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Deskargatu %s Penpot fitxategi (.penpot)"
|
||||
|
||||
@@ -308,7 +308,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"وای نه! شما هنوز هیچ فایلی ندارید! اگر میخواهید چند الگو را امتحان کنید، "
|
||||
"به [کتابخانهها و الگوها] بروید "
|
||||
"(https://penpot.app/libraries-templates)"
|
||||
"(https://penpot.app/libraries-templates.html)"
|
||||
|
||||
#, fuzzy
|
||||
msgid "dashboard.export-binary-multi"
|
||||
|
||||
@@ -298,7 +298,7 @@ msgstr "Tvítak %s fílur"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Áh nei! Tú hevur ongar fílur enn! Um tú vilt royna við nøkrum skapilónum, "
|
||||
"vitja [Libraries & templates](https://penpot.app/libraries-templates)"
|
||||
"vitja [Libraries & templates](https://penpot.app/libraries-templates.html)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Heinta %s Penpot fílur (.penpot)"
|
||||
|
||||
@@ -397,7 +397,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Oh non ! Vous n'avez pas encore de fichiers ! Si vous voulez essayer avec "
|
||||
"des modèles, allez sur [Bibliothèques et modèles] "
|
||||
"(https://penpot.app/libraries-templates)."
|
||||
"(https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Télécharger %s fichiers Penpot (.penpot)"
|
||||
|
||||
@@ -306,7 +306,7 @@ msgstr "Duplicar % ficheiros"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Ai non! Ainda non tes ficheiros! Se queres facer a proba con algún modelo "
|
||||
"vai a [Bibliotecas e modelos] (https://penpot.app/libraries-templates)"
|
||||
"vai a [Bibliotecas e modelos] (https://penpot.app/libraries-templates.html)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Descargar %s ficheiros Penpot (.penpot)"
|
||||
|
||||
@@ -381,7 +381,7 @@ msgstr "שכפול %s קבצים"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"קבצים שנוספו לספריות יתווספו לכאן. כדאי לנסות לשתף את הקבצים שלך או להוסיף "
|
||||
"אותם מ[הספריות והתבניות](https://penpot.app/libraries-templates)."
|
||||
"אותם מ[הספריות והתבניות](https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "הורדת %s קובצי Penpot (.penpot)"
|
||||
|
||||
@@ -308,7 +308,7 @@ msgstr "Kopiraj %s datoteka"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"O ne! Još nemaš datoteka! Ako želiš isprobati neke predloške, idi na "
|
||||
"[Biblioteke i predlošci](https://penpot.app/libraries-templates)"
|
||||
"[Biblioteke i predlošci](https://penpot.app/libraries-templates.html)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Preuzmi %s Penpot datoteke (.penpot)"
|
||||
|
||||
@@ -401,7 +401,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Berkas yang ditambahkan ke Pustaka akan muncul di sini. Coba membagikan "
|
||||
"berkas Anda atau menambahkan dari [Pustaka & "
|
||||
"templat](https://penpot.app/libraries-templates) kami."
|
||||
"templat](https://penpot.app/libraries-templates.html) kami."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Unduh %s berkas Penpot (.penpot)"
|
||||
|
||||
@@ -304,7 +304,7 @@ msgstr "Duplicare %s file"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Oh no! Non hai ancora nessun file! Se desideri provare alcuni template vai "
|
||||
"su [Librerie e template](https://penpot.app/libraries-templates)"
|
||||
"su [Librerie e template](https://penpot.app/libraries-templates.html)"
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Scarica %s file Penpot (.penpot)"
|
||||
|
||||
@@ -267,7 +267,7 @@ msgstr "%s ファイルを複製"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"まだファイルがありません。もしいくつかのテンプレートを試してみたいなら、[Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates) をチェックしてみてください。"
|
||||
"templates](https://penpot.app/libraries-templates.html) をチェックしてみてください。"
|
||||
|
||||
msgid "dashboard.export-frames"
|
||||
msgstr "PDFでエクスポート"
|
||||
|
||||
@@ -297,7 +297,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Čia bus rodomi prie bibliotekų pridėti failai. Pabandykite bendrinti failus "
|
||||
"arba pridėti iš mūsų [Bibliotekos ir šablonai] "
|
||||
"(https://penpot.app/libraries-templates)"
|
||||
"(https://penpot.app/libraries-templates.html)"
|
||||
|
||||
msgid "dashboard.export-frames"
|
||||
msgstr "Eksportuokite darbalaukius į PDF"
|
||||
|
||||
@@ -404,7 +404,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Šeit tiks parādītas bibliotēkām pievienotās datnes. Mēģini koplietot datnes "
|
||||
"vai pievienot tās no mūsu [bibliotēkām un veidnēm](https://penpot.app/"
|
||||
"libraries-templates)."
|
||||
"libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Lejupielādēt %s Penpot datnes (.penpot)"
|
||||
|
||||
@@ -225,7 +225,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"ഇതുവരെയും ഇവിടെ ഫയലുകളില്ല. നിങ്ങൾക്ക് ചില ടെമ്പ്ലേറ്റുകൾ "
|
||||
"പരീക്ഷിക്കണമെന്നുണ്ടെങ്കിൽ [ലൈബ്രറികളുടെയും ടെമ്പ്ലേറ്റുകളുടെയും "
|
||||
"വിഭാഗത്തിലേക്ക്] (https://penpot.app/libraries-templates) പോകാവുന്നതാണ്"
|
||||
"വിഭാഗത്തിലേക്ക്] (https://penpot.app/libraries-templates.html) പോകാവുന്നതാണ്"
|
||||
|
||||
msgid "dashboard.export-frames"
|
||||
msgstr "ആർട്ട്ബോർഡുകൾ പിഡിഎഫായി എക്സ്പോർട്ട് ചെയ്യുക"
|
||||
|
||||
@@ -336,7 +336,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Fail yang ditambahkan pada Perpustakaan akan dipaparkan di sini. Cuba kongsi "
|
||||
"fail anda atau tambahkan daripada [Perpustakaan & templat](https://penpot."
|
||||
"app/libraries-templates) kami."
|
||||
"app/libraries-templates.html) kami."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Muat turun %s fail Penpot (.penpot)"
|
||||
|
||||
@@ -417,7 +417,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Bestanden die aan bibliotheken zijn toegevoegd, worden hier weergegeven. "
|
||||
"Probeer je bestanden te delen of toe te voegen vanuit onze [Bibliotheken & "
|
||||
"sjablonen] (https://penpot.app/libraries-templates)."
|
||||
"sjablonen] (https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "%s Penpot-bestanden downloaden (.penpot)"
|
||||
|
||||
@@ -309,7 +309,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Tutaj pojawią się pliki dodane do Bibliotek. Spróbuj udostępnić swoje pliki "
|
||||
"lub dodać z naszych [Bibliotek i "
|
||||
"szablonów](https://penpot.app/libraries-templates)."
|
||||
"szablonów](https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Pobierz %s plików Penpot (.penpot)"
|
||||
|
||||
@@ -307,7 +307,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Arquivos adicionados na biblioteca de ativos vão aparecer aqui. Tente "
|
||||
"compartilhar seus arquivos ou adicione das nossas [Bibliotecas & "
|
||||
"modelos](https://penpot.app/libraries-templates)."
|
||||
"modelos](https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Baixar %s arquivos Penpot (.penpot)"
|
||||
|
||||
@@ -400,7 +400,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Oh não! Ainda não tens ficheiros! Se quiseres experimentar podes começar "
|
||||
"com os nossos templates em [Libraries & "
|
||||
"templates](https://penpot.app/libraries-templates)."
|
||||
"templates](https://penpot.app/libraries-templates.html)."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Descarrega %s ficheiros Penpot (.penpot)"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user