0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-21 06:16:28 -05:00

Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2025-02-19 17:17:54 +01:00
commit d79fac7729
29 changed files with 555 additions and 461 deletions

View file

@ -84,6 +84,7 @@ is a number of cores)
- Fix problem with grid layout crashing [Taiga #10127](https://tree.taiga.io/project/penpot/issue/10127)
- Fix rename locked boards [Taiga #10174](https://tree.taiga.io/project/penpot/issue/10174)
- Fix update-libraries dialog disappear when clicking outside [Taiga #10238](https://tree.taiga.io/project/penpot/issue/10238)
- Fix incorrect handling of team access requests with deleted/recreated users
## 2.4.3

View file

@ -207,7 +207,7 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true"
<a href="{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
</td>
@ -251,4 +251,4 @@
</div>
</body>
</html>
</html>

View file

@ -6,7 +6,7 @@ Since this file is in your Penpot team, you can provide access by sending a view
To proceed, please click the link below to generate and send the view-only link:
{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true
{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true

View file

@ -230,9 +230,9 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
<a href="{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
</td>
</tr>
</table>
@ -274,4 +274,4 @@
</div>
</body>
</html>
</html>

View file

@ -19,7 +19,7 @@ Alternatively, you can create and share a view-only link to the file. This will
Click the link below to generate and send the link:
{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true
{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true

View file

@ -214,7 +214,7 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape }}"
<a href="{{ public-uri }}/#/dashboard/members?team-id={{team-id}}&invite-email={{requested-by-email|urlescape }}"
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
target="_blank"> GIVE ACCESS TO “{{team-name|abbreviate:25}}” TEAM </a>
</td>
@ -247,9 +247,9 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
<a href="{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true"
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
target="_blank"> SEND A VIEW-ONLY LINK </a>
</td>
</tr>
</table>
@ -292,4 +292,4 @@
</div>
</body>
</html>
</html>

View file

@ -13,7 +13,7 @@ This will automatically include {{requested-by|abbreviate:25}} in the team, so t
Click the link below to provide team access:
{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}
{{ public-uri }}/#/dashboard/members?team-id{{team-id}}&invite-email={{requested-by-email|urlescape}}
@ -23,8 +23,7 @@ Alternatively, you can create and share a view-only link to the file. This will
Click the link below to generate and send the link:
{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}&section=interactions&index=0&share=true
{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}&section=interactions&index=0&share=true
If you do not wish to grant access at this time, you can simply disregard this email.

View file

@ -205,7 +205,7 @@
<td align="center" bgcolor="#6911d4" role="presentation"
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
valign="middle">
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}"
<a href="{{ public-uri }}/#/dashboard/members?team-id={{team-id}}&invite-email={{requested-by-email|urlescape}}"
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
target="_blank"> GIVE ACCESS TO “{{team-name|abbreviate:25}}” TEAM </a>
</td>
@ -249,4 +249,4 @@
</div>
</body>
</html>
</html>

View file

@ -4,7 +4,7 @@ Hello!
To provide access, please click the link below:
{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}
{{ public-uri }}/#/dashboard/members?team-id={{team-id}}&invite-email={{requested-by-email|urlescape}}
If you do not wish to grant access at this time, you can simply disregard this email.

View file

@ -6,6 +6,7 @@
(ns app.rpc.commands.teams-invitations
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
@ -15,7 +16,6 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.email :as eml]
[app.loggers.audit :as audit]
[app.main :as-alias main]
@ -168,19 +168,16 @@
itoken))))
(defn- add-user-to-team
[conn profile team role email]
(defn- add-member-to-team
[conn profile team role member]
(let [team-id (:id team)
member (db/get* conn :profile
{:email (str/lower email)}
{::sql/columns [:id :email]})
params (merge
{:team-id team-id
:profile-id (:id member)}
(get types.team/permissions-for-role role))]
;; Do not allow blocked users to join teams.
;; Do not allow blocked users to join teams.
(when (:is-blocked member)
(ex/raise :type :restriction
:code :profile-blocked))
@ -205,29 +202,33 @@
(eml/send! {::eml/conn conn
::eml/factory eml/join-team
:public-uri (cf/get :public-uri)
:to email
:to (:email member)
:invited-by (:fullname profile)
:team (:name team)
:team-id (:id team)})))
(def sql:valid-requests-email
"SELECT p.email
(def ^:private sql:valid-access-request-profiles
"SELECT p.id, p.email, p.is_blocked
FROM team_access_request AS tr
JOIN profile AS p ON (tr.requester_id = p.id)
WHERE tr.team_id = ?
AND tr.auto_join_until > now()")
AND tr.auto_join_until > now()
AND (p.deleted_at IS NULL OR
p.deleted_at > now())")
(defn- get-valid-requests-email
(defn- get-valid-access-request-profiles
[conn team-id]
(db/exec! conn [sql:valid-requests-email team-id]))
(db/exec! conn [sql:valid-access-request-profiles team-id]))
(def ^:private xf:map-email
(map :email))
(def ^:private xf:map-email (map :email))
(defn- create-team-invitations
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
(let [join-requests (into #{} xf:map-email
(get-valid-requests-email conn (:id team)))
(let [emails (set emails)
join-requests (->> (get-valid-access-request-profiles conn (:id team))
(d/index-by :email))
team-members (into #{} xf:map-email
(teams/get-team-members conn (:id team)))
@ -245,8 +246,10 @@
;; For requested invitations, do not send invitation emails, add
;; the user directly to the team
(->> (filter join-requests emails)
(run! (partial add-user-to-team conn profile team role)))
(->> join-requests
(filter #(contains? emails (key %)))
(map val)
(run! (partial add-member-to-team conn profile team role)))
invitations))
@ -572,5 +575,3 @@
(with-meta {:request request}
{::audit/props {:request 1}}))))

View file

@ -37,18 +37,17 @@
:role :editor}]
;; invite external user without complaints
(let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)
(let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)
;; retrieve the value from the database and check its content
invitation (db/exec-one!
th/*pool*
["select count(*) as num from team_invitation where team_id = ? and email_to = ?"
(:team-id data) "foo@bar.com"])]
invitations (th/db-query :team-invitation
{:team-id (:team-id data)
:email-to "foo@bar.com"})]
;; (th/print-result! out)
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock))))
(t/is (= 1 (:num invitation))))
(t/is (= 1 (count invitations))))
;; invite internal user without complaints
(th/reset-mock! mock)
@ -102,6 +101,105 @@
(t/is (= :validation (:type edata)))
(t/is (= :member-is-muted (:code edata))))))))
(t/deftest create-team-invitations-with-request-access
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})
requester (th/create-profile* 2 {:is-active true :email "requester@example.com"})
team (th/create-team* 1 {:profile-id (:id profile1)})
proj (th/create-project* 1 {:profile-id (:id profile1)
:team-id (:id team)})
file (th/create-file* 1 {:profile-id (:id profile1)
:project-id (:id proj)})]
(let [data {::th/type :create-team-access-request
::rpc/profile-id (:id requester)
:file-id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))
(th/reset-mock! mock)
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:team-id (:id team)
:role :editor
:emails ["requester@example.com"]}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
;; Check that request is properly removed
(let [requests (th/db-query :team-access-request
{:requester-id (:id requester)})]
(t/is (= 0 (count requests))))
(let [rows (th/db-query :team-profile-rel {:team-id (:id team)})]
(t/is (= 2 (count rows))))))))
(t/deftest create-team-invitations-with-request-access-2
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})
requester (th/create-profile* 2 {:is-active true
:email "requester@example.com"})
team (th/create-team* 1 {:profile-id (:id profile1)})
proj (th/create-project* 1 {:profile-id (:id profile1)
:team-id (:id team)})
file (th/create-file* 1 {:profile-id (:id profile1)
:project-id (:id proj)})]
;; Create the first access request
(let [data {::th/type :create-team-access-request
::rpc/profile-id (:id requester)
:file-id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))
(th/reset-mock! mock)
;; Proceed to delete the requester user
(th/db-update! :profile
{:deleted-at (dt/in-past "1h")}
{:id (:id requester)})
;; Create a new profile with the same email
(let [requester' (th/create-profile* 3 {:is-active true :email "requester@example.com"})]
;; Create a request access with new requester
(let [data {::th/type :create-team-access-request
::rpc/profile-id (:id requester')
:file-id (:id file)}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))
(th/reset-mock! mock)
;; Create an invitation for the requester email
(let [data {::th/type :create-team-invitations
::rpc/profile-id (:id profile1)
:team-id (:id team)
:role :editor
:emails ["requester@example.com"]}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))
;; Check that request is properly removed
(let [requests (th/db-query :team-access-request
{:requester-id (:id requester')})]
(t/is (= 0 (count requests))))
(let [[r1 r2 :as rows] (th/db-query :team-profile-rel
{:team-id (:id team)}
{:order-by [:created-at]})]
(t/is (= 2 (count rows)))
(t/is (= (:profile-id r1) (:id profile1)))
(t/is (= (:profile-id r2) (:id requester'))))))))
(t/deftest invitation-tokens
(with-mocks [mock {:target 'app.email/send! :return nil}]
@ -486,14 +584,12 @@
;; request success
(let [out (th/command! data)
;; retrieve the value from the database and check its content
request (db/exec-one!
th/*pool*
["select count(*) as num from team_access_request where team_id = ? and requester_id = ?"
(:id team) (:id requester)])]
requests (th/db-query :team-access-request
{:team-id (:id team)
:requester-id (:id requester)})]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock)))
(t/is (= 1 (:num request))))
(t/is (= 1 (count requests))))
;; request again fails
(th/reset-mock! mock)
@ -509,10 +605,10 @@
;; request again when is expired success
(th/reset-mock! mock)
(db/exec-one!
th/*pool*
["update team_access_request set valid_until = ? where team_id = ? and requester_id = ?"
(dt/in-past "1h") (:id team) (:id requester)])
(th/db-update! :team-access-request
{:valid-until (dt/in-past "1h")}
{:team-id (:id team)
:requester-id (:id requester)})
(t/is (th/success? (th/command! data)))
(t/is (= 1 (:call-count @mock))))))

View file

@ -71,7 +71,9 @@
</main>
<div class="pre-footer">
<a href="https://github.com/penpot/penpot/blob/main/docs/{{ page.inputPath }}">Edit this page on GitHub</a>
<a href="https://github.com/penpot/penpot/blob/main/docs/{{ page.inputPath }}">Edit this page on GitHub</a>
&nbsp;or ask a&nbsp;
<a href="https://github.com/penpot/penpot/issues/new/choose">question</a>.
</div>
<footer class="footer">
<div class="footer-inside">

View file

@ -2,73 +2,107 @@
title: 2. Penpot Configuration
---
# Penpot Configuration #
# Penpot Configuration
This section intends to explain all available configuration options, when you
are self-hosting Penpot or also if you are using the Penpot developer setup.
This section explains the configuration options, both for self-hosting and developer setup.
Penpot is configured using environment variables. All variables start with <code class="language-bash">PENPOT_</code>
prefix.
<p class="advice">
Penpot is configured using environment variables and flags.
</p>
Variables are initialized in the <code class="language-bash">docker-compose.yaml</code> file, as explained in the
Self-hosting guide with [Elestio][1] or [Docker][2].
## How the configuration works
Additionally, if you are using the developer environment, you may override their values in
the startup scripts, as explained in the [Developer Guide][3].
Penpot is configured using environment variables and flags. **Environment variables** start
with <code class="language-bash">PENPOT_</code>. **Flags** use the format
<code class="language-bash"><enable|disable>-<flag-name></code>.
**NOTE**: All the examples that have values represent the **default** values, and the
examples that do not have values are optional, and inactive by default.
## Common ##
This section will list all common configuration between backend and frontend.
There are two types of configuration: options (properties that require some value) and
flags (that just enables or disables something). All flags are set in a single
<code class="language-bash">PENPOT_FLAGS</code> environment variable. The envvar is a list of strings using this
format: <code class="language-bash"><enable|disable>-\<flag-name></code>. For example:
Flags are used to enable/disable a feature or behaviour (registration, feedback),
while environment variables are used to configure the settings (auth, smtp, etc).
Flags and evironment variables are also used together; for example:
```bash
PENPOT_FLAGS: enable-smtp disable-registration disable-email-verification
# This flag enables the use of SMTP email
PENPOT_FLAGS: enable-smtp
# These environment variables configure the specific SMPT service
# Backend
PENPOT_SMTP_HOST: <host>
PENPOT_SMTP_PORT: 587
```
### Registration ###
**Flags** are configured in a single list, no matter they affect the backend, the frontend,
the exporter, or all of them; on the other hand, **environment variables** are configured for
each specific service. For example:
Penpot comes with an option to completely disable the registration process;
for this, use the following variable:
```bash
PENPOT_FLAGS: enable-login-with-google
# Backend
PENPOT_GOOGLE_CLIENT_ID: <client-id>
PENPOT_GOOGLE_CLIENT_SECRET: <client-secret>
```
Check the configuration guide for [Elestio][1] or [Docker][2]. Additionally, if you are using
the developer environment, you may override its values in the startup scripts,
as explained in the [Developer Guide][3].
**NOTE**: All the examples that have value represent the **default** value, and the
examples that do not have value are optional, and inactive or disabled by default.
## Telemetries
Penpot uses anonymous telemetries from the self-hosted instances to improve the platform experience.
Consider sharing these anonymous telemetries enabling the corresponding flag:
```bash
PENPOT_FLAGS: enable-telemetries
```
## Registration and authentication
There are different ways of registration and authentication in Penpot:
- email/password
- Authentication providers like Google, Github or GitLab
- LDAP
You can choose one of them or combine several methods, depending on your needs.
By default, the email/password registration is enabled and the rest are disabled.
### Penpot
This method of registration and authentication is enabled by default. For a production environment,
it should be configured next to the SMTP settings, so there is a proper registration and verification
process.
You may want to restrict the registrations to a closed list of domains,
or exclude a specific list of domains:
```bash
# Backend
# comma separated list of domains
PENPOT_REGISTRATION_DOMAIN_WHITELIST:
# Backend
# or a file with a domain per line
PENPOT_EMAIL_DOMAIN_WHITELIST: path/to/whitelist.txt
PENPOT_EMAIL_DOMAIN_BLACKLIST: path/to/blacklist.txt
```
__Since version 2.1__
Email whitelisting should be explicitly
enabled with <code class="language-bash">enable-email-whitelist</code> flag. For backward compatibility, we
autoenable it when <code class="language-bash">PENPOT_REGISTRATION_DOMAIN_WHITELIST</code> is set with
not-empty content.
Penpot also comes with an option to completely disable the registration process;
for this, use the following flag:
```bash
PENPOT_FLAGS: [...] disable-registration
```
You may also want to restrict the registrations to a closed list of domains:
```bash
# comma separated list of domains (backend only)
PENPOT_REGISTRATION_DOMAIN_WHITELIST:
# OR (backend only)
PENPOT_EMAIL_DOMAIN_WHITELIST: path/to/whitelist.txt
```
**NOTE**: Since version 2.1, email whitelisting should be explicitly
enabled with <code class="language-bash">enable-email-whitelist</code> flag. For backward compatibility, we
autoenable it when <code class="language-bash">PENPOT_REGISTRATION_DOMAIN_WHITELIST</code> is set with
not-empty content.
### Demo users ###
Penpot comes with facilities for fast creation of demo users without the need of a
registration process. The demo users by default have an expiration time of 7 days, and
once expired they are completely deleted with all the generated content. Very useful for
testing or demonstration purposes.
You can enable demo users using the following variable:
```bash
PENPOT_FLAGS: [...] enable-demo-users
```
This option is only recommended for demo instances, not for production environments.
### Authentication Providers
@ -82,7 +116,6 @@ The callback has the following format:
https://<your_domain>/api/auth/oauth/<oauth_provider>/callback
```
You will need to change <your_domain> and <oauth_provider> according to your setup.
This is how it looks with Gitlab provider:
@ -90,22 +123,6 @@ This is how it looks with Gitlab provider:
https://<your_domain>/api/auth/oauth/gitlab/callback
```
#### Penpot
Consists on registration and authentication via email / password. It is enabled by default,
but login can be disabled with the following flags:
```bash
PENPOT_FLAGS: [...] disable-login-with-password
```
And the registration also can be disabled with:
```bash
PENPOT_FLAGS: [...] disable-registration
```
#### Google
Allows integrating with Google as OAuth provider:
@ -145,7 +162,7 @@ PENPOT_GITHUB_CLIENT_SECRET: <client-secret>
#### OpenID Connect
**NOTE:** Since version 1.5.0
__Since version 1.5.0__
Allows integrating with a generic authentication provider that implements the OIDC
protocol (usually used for SSO).
@ -155,7 +172,7 @@ All the other options are backend only:
```bash
PENPOT_FLAGS: [...] enable-login-with-oidc
## Backend only
# Backend
PENPOT_OIDC_CLIENT_ID: <client-id>
# Mainly used for auto discovery the openid endpoints
@ -231,7 +248,6 @@ register with another method.
PENPOT_FLAGS: [...] enable-oidc-registration
```
#### Azure Active Directory using OpenID Connect
Allows integrating with Azure Active Directory as authentication provider:
@ -240,12 +256,12 @@ Allows integrating with Azure Active Directory as authentication provider:
# Backend & Frontend
PENPOT_OIDC_CLIENT_ID: <client-id>
## Backend only
# Backend
PENPOT_OIDC_BASE_URI: https://login.microsoftonline.com/<tenant-id>/v2.0/
PENPOT_OIDC_CLIENT_SECRET: <client-secret>
```
### LDAP ###
### LDAP
Penpot comes with support for *Lightweight Directory Access Protocol* (LDAP). This is the
example configuration we use internally for testing this authentication backend.
@ -253,7 +269,7 @@ example configuration we use internally for testing this authentication backend.
```bash
PENPOT_FLAGS: [...] enable-login-with-ldap
## Backend only
# Backend
PENPOT_LDAP_HOST: ldap
PENPOT_LDAP_PORT: 10389
PENPOT_LDAP_SSL: false
@ -268,39 +284,34 @@ PENPOT_LDAP_ATTRS_FULLNAME: cn
PENPOT_LDAP_ATTRS_PHOTO: jpegPhoto
```
If you miss something, please open an issue and we discuss it.
## Penpot URI
## Backend ##
This section enumerates the backend only configuration variables.
### Database
We only support PostgreSQL and we highly recommend >=13 version. If you are using official
docker images this is already solved for you.
Essential database configuration:
You will need to set the <code class="language-bash">PENPOT_PUBLIC_URI</code> environment variable in case you go to serve Penpot to the users;
it should point to public URI where users will access the application:
```bash
# Backend
PENPOT_DATABASE_USERNAME: penpot
PENPOT_DATABASE_PASSWORD: penpot
PENPOT_DATABASE_URI: postgresql://127.0.0.1/penpot
PENPOT_PUBLIC_URI: https://penpot.mycompany.com
# Frontend
PENPOT_PUBLIC_URI: https://penpot.mycompany.com
# Exporter
PENPOT_PUBLIC_URI: https://penpot.mycompany.com
```
The username and password are optional. These settings should be compatible with the ones
in the postgres configuration:
If you're using the official <code class="language-bash">docker-compose.yml</code> you only need to configure the
<code class="language-bash">PENPOT_PUBLIC_URI</code> envvar in the top of the file.
```bash
# Postgres
POSTGRES_DATABASE: penpot
POSTGRES_USER: penpot
POSTGRES_PASSWORD: penpot
```
<p class="advice">
If you plan to serve Penpot under different domain than `localhost` without HTTPS,
you need to disable the `secure` flag on cookies, with the `disable-secure-session-cookies` flag.
This is a configuration NOT recommended for production environments; as some browser APIs do
not work properly under non-https environments, this unsecure configuration
may limit the usage of Penpot; as an example, the clipboard does not work with HTTP.
</p>
### Email (SMTP)
## Email configuration
By default, <code class="language-bash">smpt</code> flag is disabled, the email will be
printed to the console, which means that the emails will be shown in the stdout.
@ -326,6 +337,7 @@ Enable SMTP:
```bash
PENPOT_FLAGS: [...] enable-smtp
# Backend
PENPOT_SMTP_HOST: <host>
PENPOT_SMTP_PORT: 587
@ -334,14 +346,108 @@ PENPOT_SMTP_PASSWORD: <password>
PENPOT_SMTP_TLS: true
```
If you are not using SMTP configuration and want to log the emails in the console, you should use the following flag:
```bash
PENPOT_FLAGS: [...] enable-log-emails
```
## Redis
The Redis configuration is very simple, just provide a valid redis URI. Redis is used
mainly for websocket notifications coordination.
```bash
# Backend
PENPOT_REDIS_URI: redis://localhost/0
# Exporter
PENPOT_REDIS_URI: redis://localhost/0
```
If you are using the official docker compose file, this is already configurRed.
## Demo environment
Penpot comes with facilities to create a demo environment so you can test the system quickly.
This is an example of a demo configuration:
```bash
PENPOT_FLAGS: disable-registration enable-demo-users enable-demo-warning
```
**disable-registration** prevents any user from registering in the platform.
**enable-demo-users** creates users with a default expiration time of 7 days, and
once expired they are completely deleted with all the generated content.
From the registration page, there is a link with a `Create demo account` which creates one of these
users and logs in automatically.
**enable-demo-warning** is a modal in the registration and login page saying that the
environment is a testing one and the data may be wiped without notice.
Another way to work in a demo environment is allowing users to register but removing the
verification process:
```bash
PENPOT_FLAGS: disable-email-verification enable-demo-warning
```
## Backend
This section enumerates the backend only configuration variables.
### Secret key
The <code class="language-bash">PENPOT_SECRET_KEY</code> envvar serves a master key from which other keys
for subsystems (eg http sessions, or invitations) are derived.
If you don't use it, all created sessions and invitations will become invalid on container restart
or service restart.
To use it, we recommend using a truly randomly generated 512 bits base64 encoded string here.
You can generate one with:
```bash
python3 -c "import secrets; print(secrets.token_urlsafe(64))"
```
And configure it:
```bash
# Backend
PENPOT_SECRET_KEY: my-super-secure-key
```
### Database
Penpot only supports PostgreSQL and we highly recommend >=13 version. If you are using official
docker images this is already solved for you.
Essential database configuration:
```bash
# Backend
PENPOT_DATABASE_USERNAME: penpot
PENPOT_DATABASE_PASSWORD: penpot
PENPOT_DATABASE_URI: postgresql://127.0.0.1/penpot
```
The username and password are optional. These settings should be compatible with the ones
in the postgres configuration:
```bash
# Postgres
POSTGRES_DATABASE: penpot
POSTGRES_USER: penpot
POSTGRES_PASSWORD: penpot
```
### Storage
Storage refers to storage used for store the user uploaded assets.
Storage refers to storing the user uploaded assets.
Assets storage is implemented using "plugable" backends. Currently there are three
Assets storage is implemented using "plugable" backends. Currently there are two
backends available: <code class="language-bash">fs</code> and <code class="language-bash">s3</code> (for AWS S3).
#### FS Backend (default) ####
#### FS Backend (default)
This is the default backend when you use the official docker images and the default
configuration looks like this:
@ -360,8 +466,7 @@ configure the nginx yourself.
In case you want understand how it internally works, you can take a look on the [nginx
configuration file][4] used in the docker images.
#### AWS S3 Backend ####
#### AWS S3 Backend
This backend uses AWS S3 bucket for store the user uploaded assets. For use it you should
have an appropriate account on AWS cloud and have the credentials, region and the bucket.
@ -369,11 +474,9 @@ have an appropriate account on AWS cloud and have the credentials, region and th
This is how configuration looks for S3 backend:
```bash
# AWS Credentials
# Backend
AWS_ACCESS_KEY_ID: <you-access-key-id-here>
AWS_SECRET_ACCESS_KEY: <your-secret-access-key-here>
# Backend configuration
PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
PENPOT_STORAGE_ASSETS_S3_REGION: <aws-region>
PENPOT_STORAGE_ASSETS_S3_BUCKET: <bucket-name>
@ -382,38 +485,11 @@ PENPOT_STORAGE_ASSETS_S3_BUCKET: <bucket-name>
PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <endpoint-uri>
```
### Redis
The redis configuration is very simple, just provide with a valid redis URI. Redis is used
mainly for websocket notifications coordination.
```bash
# Backend
PENPOT_REDIS_URI: redis://localhost/0
```
If you are using the official docker compose file, this is already configured.
### HTTP
You will need to set the <code class="language-bash">PENPOT_PUBLIC_URI</code> environment
variable in case you go to serve Penpot to the users; it should point to public URI
where users will access the application:
```bash
PENPOT_PUBLIC_URI: http://localhost:9001
```
<p class="advice">
If you plan to serve Penpot under different domain than `localhost` without HTTPS,
you need to disable the `secure` flag on cookies, with the `disable-secure-session-cookies` flag.
This is a configuration NOT recommended for production environments.
These settings are equally useful if you have a Minio storage system.
</p>
Check all the [flags](#other-flags) to fully customize your instance.
## Frontend ##
## Frontend
In comparison with backend, frontend only has a small number of runtime configuration
options, and they are located in the <code class="language-bash">\<dist>/js/config.js</code> file.
@ -422,10 +498,7 @@ If you are using the official docker images, the best approach to set any config
using environment variables, and the image automatically generates the <code class="language-bash">config.js</code> from
them.
**NOTE**: many frontend related configuration variables are explained in the
[Common](#common) section, this section explains **frontend only** options.
But in case you have a custom setup you probably need setup the following environment
In case you have a custom setup, you probably need to configure the following environment
variables on the frontend container:
To connect the frontend to the exporter and backend, you need to fill out these environment variables.
@ -438,54 +511,36 @@ PENPOT_EXPORTER_URI: http://your-penpot-exporter:6061
These variables are used for generate correct nginx.conf file on container startup.
### Demo warning ###
If you want to show a warning in the register and login page saying that this is a
demonstration purpose instance (no backups, periodical data wipe, ...), set the following
variable:
```bash
PENPOT_FLAGS: [...] enable-demo-warning
```
## Other flags
There are other flags that are useful for a more customized Penpot experience. This section has the list of the flags meant
for the user:
- <code class="language-bash">enable-cors</code>: Enables the default cors cofiguration that allows all domains
(this configuration is designed only for dev purposes right now)
- <code class="language-bash">enable-backend-api-doc</code>: Enables the <code class="language-bash">/api/doc</code>
endpoint that lists all rpc methods available on backend
- <code class="language-bash">disable-email-verification</code>: Deactivates the email verification process
(only recommended for local or internal setups)
- <code class="language-bash">disable-secure-session-cookies</code>: By default, Penpot uses the
<code class="language-bash">secure</code> flag on cookies, this flag disables it;
it is useful if you plan to serve Penpot under different
domain than <code class="language-bash">localhost</code> without HTTPS
- <code class="language-bash">disable-login-with-password</code>: allows disable password based login form
- <code class="language-bash">disable-registration</code>: disables registration (still enabled for invitations only).
- <code class="language-bash">enable-prepl-server</code>: enables PREPL server, used by manage.py and other additional
tools for communicate internally with Penpot backend
tools to communicate internally with Penpot backend. Check the [CLI section][5] to get more detail.
__Since version 1.13.0__
- <code class="language-bash">enable-log-invitation-tokens</code>: for cases where you don't have email configured, this
will log to console the invitation tokens
- <code class="language-bash">enable-log-emails</code>: if you want to log in console send emails. This only works if smtp
is not configured
will log to console the invitation tokens.
__Since version 2.0.0__
- <code class="language-bash">disable-onboarding-team</code>: for disable onboarding team creation modal
- <code class="language-bash">disable-onboarding-newsletter</code>: for disable onboarding newsletter modal
- <code class="language-bash">disable-onboarding-questions</code>: for disable onboarding survey
- <code class="language-bash">disable-onboarding</code>: for disable onboarding modal
- <code class="language-bash">disable-dashboard-templates-section</code>: for hide the templates section from dashboard
- <code class="language-bash">enable-webhooks</code>: for enable webhooks
- <code class="language-bash">enable-access-tokens</code>: for enable access tokens
- <code class="language-bash">disable-google-fonts-provider</code>: disables the google fonts provider (frontend)
- <code class="language-bash">disable-onboarding</code>: disables the onboarding modals.
- <code class="language-bash">disable-dashboard-templates-section</code>: hides the templates section from dashboard.
- <code class="language-bash">enable-webhooks</code>: enables webhooks. More detail about this configuration in [webhooks section][6].
- <code class="language-bash">enable-access-tokens</code>: enables access tokens. More detail about this configuration in [access tokens section][7].
- <code class="language-bash">disable-google-fonts-provider</code>: disables the google fonts provider.
[1]: /technical-guide/getting-started#configure-penpot-with-elestio
[2]: /technical-guide/getting-started#configure-penpot-with-docker
[3]: /technical-guide/developer/common#dev-environment
[4]: https://github.com/penpot/penpot/blob/main/docker/images/files/nginx.conf
[5]: /technical-guide/getting-started/#using-the-cli-for-administrative-tasks
[6]: /technical-guide/integration/#webhooks
[7]: /technical-guide/integration/#access-tokens

View file

@ -195,23 +195,23 @@ If you want to stop running Penpot, just type
docker compose -p penpot -f docker-compose.yaml down
```
### Configure Penpot with Docker
The configuration is defined using environment variables in the <code class="language-bash">docker-compose.yaml</code>
file. The default downloaded file already comes with the essential variables already set,
The configuration is defined using flags and environment variables in the <code class="language-bash">docker-compose.yaml</code>
file. The default downloaded file comes with the essential flags and variables already set,
and other ones commented out with some explanations.
#### Create users using CLI
You can find all configuration options in the [Configuration][1] section.
By default (or when <code class="language-bash">disable-email-verification</code> flag is used), the email verification process
is completely disabled for new registrations but it is highly recommended enabling email
verification or disabling registration if you are going to expose your penpot instance to
the internet.
### Using the CLI for administrative tasks
Penpot provides a script (`manage.py`) with some administrative tasks to perform in the server.
If you have registration disabled, you can create additional profiles using the
command line interface:
**NOTE**: this script will only work with the <code class="language-bash">enable-prepl-server</code>
flag set in the docker-compose.yaml file. For older versions of docker-compose.yaml file,
this flag is set in the backend service.
For instance, if the registration is disabled, the only way to create a new user is with this script:
```bash
docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile
@ -221,12 +221,6 @@ docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile
For example it could be <code class="language-bash">penpot-penpot-backend-1</code> or <code class="language-bash">penpot_penpot-backend-1</code>.
You can check the correct name executing <code class="language-bash">docker ps</code>.
**NOTE:** This script only will works when you properly have the <code class="language-bash">enable-prepl-server</code>
flag set on backend (is set by default on the latest docker-compose.yaml file)
You can find all configuration options in the [Configuration][1] section.
### Update Penpot
To get the latest version of Penpot in your local installation, you just need to

View file

@ -72,6 +72,7 @@
(def profile-fetched?
(ptk/type? ::profile-fetched))
;; FIXME: make it as general purpose handler, not only on profile
(defn- on-fetch-profile-exception
[cause]
(let [data (ex-data cause)]

View file

@ -206,7 +206,7 @@
nil))
(rx/of
(cond
(some? frame-id) (go-to-frame (uuid frame-id))
(some? frame-id) (go-to-frame frame-id)
(some? index) (go-to-frame-by-index index)
:else (go-to-frame-auto)))))))))

View file

@ -8,12 +8,10 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.schema :as sm]
[app.common.types.shape-tree :as ctst]
[app.main.data.changes :as dch]
[app.main.data.comments :as dcmt]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
@ -125,38 +123,32 @@
{::ev/origin "workspace"}))))))))
(defn update-comment-thread-position
([thread [new-x new-y]]
(update-comment-thread-position thread [new-x new-y] nil))
([thread [new-x new-y]]
(update-comment-thread-position thread [new-x new-y] nil))
([thread [new-x new-y] frame-id]
([thread [new-x new-y] frame-id]
(dm/assert!
"expected valid comment thread"
(dcmt/check-comment-thread! thread))
(ptk/reify ::update-comment-thread-position
ptk/WatchEvent
(watch [it state _]
(watch [_ state _]
(let [page (dsh/lookup-page state)
page-id (:id page)
objects (dsh/lookup-page-objects state page-id)
frame-id (if (nil? frame-id)
(ctst/get-frame-id-by-position objects (gpt/point new-x new-y))
(:frame-id thread))
thread (-> thread
(assoc :position (gpt/point new-x new-y))
(assoc :frame-id frame-id))
changes (-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/set-comment-thread-position thread))]
thread-id (:id thread)]
(rx/concat
(rx/merge
(rx/of (dch/commit-changes changes))
(->> (rp/cmd! :update-comment-thread-position thread)
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
(rx/ignore)))
(rx/of (dcmt/refresh-comment-thread thread))))))))
(rx/of #(update % :comment-threads assoc thread-id thread))
(->> (rp/cmd! :update-comment-thread-position thread)
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
(rx/ignore))))))))
;; Move comment threads that are inside a frame when that frame is moved"
(defmethod ptk/resolve ::move-frame-comment-threads

View file

@ -587,11 +587,6 @@
:subsections [:shape]
:fn #(emit-when-no-readonly (dw/create-bool :exclude))}
:fit-content-selected {:tooltip (ds/meta-shift (ds/alt "R"))
:command (ds/c-mod "shift+alt+r")
:subsections [:shape]
:fn #(emit-when-no-readonly (dwt/selected-fit-content))}
;; THEME
:toggle-theme {:tooltip (ds/alt "M")
:command (ds/a-mod "m")

View file

@ -90,22 +90,29 @@
(dom/set-data! "fullname" fullname)
(obj/set! "textContent" fullname)))
(defn- current-text-node*
"Retrieves the text node and the offset that the cursor is positioned on"
[node anchor-node]
(when (.contains node anchor-node)
(let [span-node (if (instance? js/Text anchor-node)
(dom/get-parent anchor-node)
anchor-node)
container (dom/get-parent span-node)]
(when (= node container)
span-node))))
(defn- current-text-node
"Retrieves the text node and the offset that the cursor is positioned on"
[node]
(assert (some? node) "expected valid node")
(let [selection (wapi/get-selection)
range (wapi/get-range selection 0)
anchor-node (wapi/range-start-container range)
anchor-offset (wapi/range-start-offset range)]
(when (and node (.contains node anchor-node))
(let [span-node
(if (instance? js/Text anchor-node)
(dom/get-parent anchor-node)
anchor-node)
container (dom/get-parent span-node)]
(when (= node container)
[span-node anchor-offset])))))
(when-let [selection (wapi/get-selection)]
(let [range (wapi/get-range selection 0)
anchor-node (wapi/range-start-container range)
offset (wapi/range-start-offset range)
span-node (current-text-node* node anchor-node)]
(when span-node
[span-node offset]))))
(defn- absolute-offset
[node child offset]
@ -156,7 +163,8 @@
mentions-s (mf/use-ctx mentions-context)
cur-mention (mf/use-var nil)
prev-selection (mf/use-var nil)
prev-selection-ref
(mf/use-ref)
init-input
(mf/use-fn
@ -203,58 +211,59 @@
handle-select
(mf/use-fn
(fn []
(let [node (mf/ref-val local-ref)
selection (wapi/get-selection)
range (wapi/get-range selection 0)
anchor-node (wapi/range-start-container range)]
(when (and (= node anchor-node) (.-collapsed range))
(wapi/set-cursor-after! anchor-node)))
(when-let [node (mf/ref-val local-ref)]
(when-let [selection (wapi/get-selection)]
(let [range (wapi/get-range selection 0)
anchor-node (wapi/range-start-container range)
offset (wapi/range-start-offset range)]
(let [node (mf/ref-val local-ref)
[span-node offset] (current-text-node node)
[prev-span prev-offset] @prev-selection]
(when (and (= node anchor-node) (.-collapsed ^js range))
(wapi/set-cursor-after! anchor-node))
(reset! prev-selection #js [span-node offset])
(when-let [span-node (current-text-node* node anchor-node)]
(let [[prev-span prev-offset]
(mf/ref-val prev-selection-ref)
(when (= (dom/get-data span-node "type") "mention")
(let [from-offset (absolute-offset node prev-span prev-offset)
to-offset (absolute-offset node span-node offset)
node-text
(subs (dom/get-text span-node) 0 offset)
[_ prev next]
(->> node
(dom/seq-nodes)
(d/with-prev-next)
(filter (fn [[elem _ _]] (= elem span-node)))
(first))]
current-at-symbol
(str/last-index-of (subs node-text 0 offset) "@")
(if (> from-offset to-offset)
(wapi/set-cursor-after! prev)
(wapi/set-cursor-before! next))))
mention-text
(subs node-text current-at-symbol)
(when span-node
(let [node-text (subs (dom/get-text span-node) 0 offset)
at-symbol-inside-word?
(and (> current-at-symbol 0)
(str/word? (str/slice node-text (- current-at-symbol 1) current-at-symbol)))]
current-at-symbol
(str/last-index-of (subs node-text 0 offset) "@")
(mf/set-ref-val! prev-selection-ref #js [span-node offset])
mention-text
(subs node-text current-at-symbol)
(when (= (dom/get-data span-node "type") "mention")
(let [from-offset (absolute-offset node prev-span prev-offset)
to-offset (absolute-offset node span-node offset)
at-symbol-inside-word?
(and (> current-at-symbol 0)
(str/word? (str/slice node-text (- current-at-symbol 1) current-at-symbol)))]
[_ prev next]
(->> node
(dom/seq-nodes)
(d/with-prev-next)
(filter (fn [[elem _ _]] (= elem span-node)))
(first))]
(if (> from-offset to-offset)
(wapi/set-cursor-after! prev)
(wapi/set-cursor-before! next))))
(if (and (not at-symbol-inside-word?)
(re-matches #"@\w*" mention-text))
(do
(reset! cur-mention mention-text)
(rx/push! mentions-s {:type :display-mentions})
(let [mention (subs mention-text 1)]
(when (d/not-empty? mention)
(rx/push! mentions-s {:type :filter-mentions :data mention}))))
(do
(reset! cur-mention nil)
(rx/push! mentions-s {:type :hide-mentions}))))))))
(if (and (not at-symbol-inside-word?)
(re-matches #"@\w*" mention-text))
(do
(reset! cur-mention mention-text)
(rx/push! mentions-s {:type :display-mentions})
(let [mention (subs mention-text 1)]
(when (d/not-empty? mention)
(rx/push! mentions-s {:type :filter-mentions :data mention}))))
(do
(reset! cur-mention nil)
(rx/push! mentions-s {:type :hide-mentions}))))))))))
handle-focus
(mf/use-fn
@ -279,9 +288,8 @@
(mf/use-fn
(mf/deps on-change)
(fn [data]
(let [node (mf/ref-val local-ref)
[span-node offset] (current-text-node node)]
(when span-node
(when-let [node (mf/ref-val local-ref)]
(when-let [[span-node offset] (current-text-node node)]
(let [node-text
(dom/get-text span-node)
@ -314,8 +322,8 @@
handle-insert-at-symbol
(mf/use-fn
(fn []
(let [node (mf/ref-val local-ref) [span-node] (current-text-node node)]
(when span-node
(when-let [node (mf/ref-val local-ref)]
(when-let [[span-node] (current-text-node node)]
(let [node-text (dom/get-text span-node)
at-symbol (if (blank-content? node-text) "@" " @")]
@ -327,66 +335,62 @@
(mf/deps on-esc on-ctrl-enter handle-select handle-input)
(fn [event]
(handle-select event)
(when-let [node (mf/ref-val local-ref)]
(when-let [[span-node offset] (current-text-node node)]
(cond
(and @cur-mention (kbd/enter? event))
(do (dom/prevent-default event)
(dom/stop-propagation event)
(rx/push! mentions-s {:type :insert-selected-mention}))
(let [node (mf/ref-val local-ref)
[span-node offset] (current-text-node node)]
(and @cur-mention (kbd/down-arrow? event))
(do (dom/prevent-default event)
(dom/stop-propagation event)
(rx/push! mentions-s {:type :insert-next-mention}))
(cond
(and @cur-mention (kbd/enter? event))
(do (dom/prevent-default event)
(dom/stop-propagation event)
(rx/push! mentions-s {:type :insert-selected-mention}))
(and @cur-mention (kbd/up-arrow? event))
(do (dom/prevent-default event)
(dom/stop-propagation event)
(rx/push! mentions-s {:type :insert-prev-mention}))
(and @cur-mention (kbd/down-arrow? event))
(do (dom/prevent-default event)
(dom/stop-propagation event)
(rx/push! mentions-s {:type :insert-next-mention}))
(and @cur-mention (kbd/esc? event))
(do (dom/prevent-default event)
(dom/stop-propagation event)
(rx/push! mentions-s {:type :hide-mentions}))
(and @cur-mention (kbd/up-arrow? event))
(do (dom/prevent-default event)
(dom/stop-propagation event)
(rx/push! mentions-s {:type :insert-prev-mention}))
(and (kbd/esc? event) (fn? on-esc))
(on-esc event)
(and @cur-mention (kbd/esc? event))
(do (dom/prevent-default event)
(dom/stop-propagation event)
(rx/push! mentions-s {:type :hide-mentions}))
(and (kbd/mod? event) (kbd/enter? event) (fn? on-ctrl-enter))
(on-ctrl-enter event)
(and (kbd/esc? event) (fn? on-esc))
(on-esc event)
(and (kbd/mod? event) (kbd/enter? event) (fn? on-ctrl-enter))
(on-ctrl-enter event)
(kbd/enter? event)
(let [sel (wapi/get-selection)
range (.getRangeAt sel 0)]
(dom/prevent-default event)
(dom/stop-propagation event)
(let [[span-node offset] (current-text-node node)]
(.deleteContents range)
(handle-input)
(when span-node
(let [txt (.-textContent span-node)]
(dom/set-html! span-node (dm/str (subs txt 0 offset) "\n" zero-width-space (subs txt offset)))
(wapi/set-cursor! span-node (inc offset))
(handle-input)))))
(kbd/backspace? event)
(let [prev-node (get-prev-node node span-node)]
(when (and (some? prev-node)
(= "mention" (dom/get-data prev-node "type"))
(= offset 1))
(kbd/enter? event)
(let [sel (wapi/get-selection)
range (.getRangeAt sel 0)]
(dom/prevent-default event)
(dom/stop-propagation event)
(.remove prev-node)))))))]
(let [[span-node offset] (current-text-node node)]
(.deleteContents range)
(handle-input)
(mf/use-layout-effect
(mf/deps autofocus)
(fn []
(when autofocus
(dom/focus! (mf/ref-val local-ref)))))
(when span-node
(let [txt (.-textContent span-node)]
(dom/set-html! span-node (dm/str (subs txt 0 offset) "\n" zero-width-space (subs txt offset)))
(wapi/set-cursor! span-node (inc offset))
(handle-input)))))
(kbd/backspace? event)
(let [prev-node (get-prev-node node span-node)]
(when (and (some? prev-node)
(= "mention" (dom/get-data prev-node "type"))
(= offset 1))
(dom/prevent-default event)
(dom/stop-propagation event)
(.remove prev-node))))))))]
(mf/with-layout-effect [autofocus]
(when ^boolean autofocus
(dom/focus! (mf/ref-val local-ref))))
;; Creates the handlers for selection
(mf/with-effect [handle-select]
@ -410,12 +414,12 @@
;; Auto resize input to display the comment
(mf/with-layout-effect nil
(let [^js node (mf/ref-val local-ref)]
(when-let [^js node (mf/ref-val local-ref)]
(set! (.-height (.-style node)) "0")
(set! (.-height (.-style node)) (str (+ 2 (.-scrollHeight node)) "px"))))
(mf/with-effect [value prev-value]
(let [node (mf/ref-val local-ref)]
(when-let [node (mf/ref-val local-ref)]
(cond
(and (d/not-empty? prev-value) (empty? value))
(do (dom/set-html! node "")

View file

@ -109,7 +109,11 @@
;; avoids some race conditions that causes unexpected redirects
;; on invitations workflows (and probably other cases).
(->> (rp/cmd! :get-profile)
(rx/subs! (fn [{:keys [id] :as profile}]
(rx/mapcat (fn [profile]
(->> (rp/cmd! :get-teams {})
(rx/map (fn [teams]
(assoc profile ::teams (into #{} (map :id) teams)))))))
(rx/subs! (fn [{:keys [id ::teams] :as profile}]
(cond
(= id uuid/zero)
(do
@ -117,10 +121,12 @@
(st/emit! (rt/nav :auth-login)))
empty-path?
(let [team-id (or (dtm/get-last-team-id)
(:default-team-id profile))]
(st/emit! (rt/nav :dashboard-recent
(assoc query-params :team-id team-id))))
(let [team-id (dtm/get-last-team-id)]
(if (contains? teams team-id)
(st/emit! (rt/nav :dashboard-recent
(assoc query-params :team-id team-id)))
(st/emit! (rt/nav :dashboard-recent
(assoc query-params :team-id (:default-team-id profile))))))
:else
(st/emit! (rt/assign-exception {:type :not-found})))))))))

View file

@ -116,7 +116,7 @@
assets-tab
(mf/html [:& assets-toolbox {:size (- size 58)}])
(mf/html [:& assets-toolbox {:size (- size 58) :file-id file}])
tokens-tab
(when design-tokens?

View file

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.types.components-list :as ctkl]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.data.workspace.assets :as dwa]
@ -73,7 +74,7 @@
(mf/defc assets-toolbox
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [size]}]
[{:keys [size file-id]}]
(let [components-v2 (mf/use-ctx ctx/components-v2)
read-only? (mf/use-ctx ctx/workspace-read-only?)
filters* (mf/use-state
@ -89,7 +90,10 @@
section (:section filters)
ordering (:ordering filters)
reverse-sort? (= :desc ordering)
num-libs (count (mf/deref refs/libraries))
libs (mf/deref refs/libraries)
num-libs (count libs)
file (get libs (:id file-id))
components (mf/with-memo [file] (ctkl/components (:data file)))
toggle-ordering
(mf/use-fn
@ -159,7 +163,7 @@
[:article {:class (stl/css :assets-bar)}
[:div {:class (stl/css :assets-header)}
(when-not ^boolean read-only?
(if (= num-libs 1)
(if (and (= num-libs 1) (empty? components))
[:button {:class (stl/css :add-library-button)
:on-click show-libraries-dialog
:data-testid "libraries"}
@ -168,9 +172,7 @@
[:button {:class (stl/css :libraries-button)
:on-click show-libraries-dialog
:data-testid "libraries"}
[:span {:class (stl/css :libraries-icon)}
i/library]
(tr "workspace.assets.libraries")]))
(tr "workspace.assets.manage-library")]))
[:div {:class (stl/css :search-wrapper)}

View file

@ -26,42 +26,22 @@
margin-bottom: $s-4;
border-radius: $s-8;
.libraries-icon {
@include flexCenter;
width: $s-24;
height: 100%;
svg {
@include flexCenter;
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
&:hover {
background-color: var(--button-secondary-background-color-hover);
color: var(--button-secondary-foreground-color-hover);
border: $s-1 solid var(--button-secondary-border-color-hover);
svg {
stroke: var(--button-secondary-foreground-color-hover);
}
}
&:focus {
background-color: var(--button-secondary-background-color-focus);
color: var(--button-secondary-foreground-color-focus);
border: $s-1 solid var(--button-secondary-border-color-focus);
svg {
stroke: var(--button-secondary-foreground-color-focus);
}
}
}
.add-library-button {
@extend .button-primary;
text-transform: uppercase;
@include uppercaseTitleTipography;
gap: $s-2;
height: $s-32;
width: 100%;

View file

@ -301,7 +301,6 @@
(when show-comments?
[:> comments/comments-layer* {:vbox vbox
:page-id page-id
:file-id file-id
:vport vport
:zoom zoom

View file

@ -9,30 +9,14 @@
(:require
[app.common.data.macros :as dm]
[app.main.data.comments :as dcm]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.comments :as dwcm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.comments :as cmt]
[okulary.core :as l]
[rumext.v2 :as mf]))
(defn- update-position
[positions {:keys [id] :as thread}]
(if (contains? positions id)
(-> thread
(assoc :position (dm/get-in positions [id :position]))
(assoc :frame-id (dm/get-in positions [id :frame-id])))
thread))
(def ^:private ref:thread-positions
(l/derived (fn [state]
(-> (dsh/lookup-page state)
(get :comment-thread-positions)))
st/state))
(mf/defc comments-layer*
[{:keys [vbox vport zoom drawing file-id page-id]}]
[{:keys [vbox vport zoom drawing file-id]}]
(let [vbox-x (dm/get-prop vbox :x)
vbox-y (dm/get-prop vbox :y)
vport-w (dm/get-prop vport :width)
@ -44,16 +28,7 @@
profile (mf/deref refs/profile)
local (mf/deref refs/comments-local)
positions (mf/deref ref:thread-positions)
threads-map (mf/deref refs/threads)
threads-map (mf/with-memo [threads-map page-id positions]
(reduce-kv (fn [threads id thread]
(if (= (:page-id thread) page-id)
(assoc threads id (update-position positions thread))
threads))
{}
threads-map))
threads
(mf/with-memo [threads-map local profile]
@ -93,7 +68,7 @@
(when-let [thread (get threads-map id)]
(when (seq (dcm/apply-filters local profile [thread]))
[:> cmt/comment-floating-thread*
{:thread (update-position positions thread)
{:thread thread
:viewport viewport
:zoom zoom}])))

View file

@ -343,7 +343,6 @@
(when show-comments?
[:> comments/comments-layer* {:vbox vbox
:page-id page-id
:vport vport
:zoom zoom
:drawing drawing}])

View file

@ -282,9 +282,12 @@
(.selectAllChildren selection node))
(defn get-selection
"Only returns valid selection"
[]
(when-let [document globals/document]
(.getSelection document)))
(let [selection (.getSelection document)]
(when (not= (.-type selection) "None")
selection))))
(defn get-anchor-node
[^js selection]

View file

@ -3618,10 +3618,6 @@ msgstr "Export shapes"
msgid "shortcuts.fit-all"
msgstr "Zoom to fit all"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:590
msgid "shortcuts.fit-content-selected"
msgstr "Resize board to fit content"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:113
msgid "shortcuts.flip-horizontal"
msgstr "Flip horizontally"
@ -4226,6 +4222,9 @@ msgstr "Align top (%s)"
msgid "workspace.assets.add-library"
msgstr "Add library"
msgid "workspace.assets.manage-library"
msgstr "Manage library"
#: src/app/main/ui/workspace/sidebar/assets.cljs
#, unused
msgid "workspace.assets.assets"
@ -4295,10 +4294,6 @@ msgstr "Group"
msgid "workspace.assets.group-name"
msgstr "Group name"
#: src/app/main/ui/workspace/sidebar/assets.cljs:186
msgid "workspace.assets.libraries"
msgstr "Libraries"
#: src/app/main/ui/workspace/sidebar/assets/components.cljs:501
msgid "workspace.assets.list-view"
msgstr "List view"

View file

@ -3514,10 +3514,6 @@ msgstr "Diferencia"
msgid "shortcuts.bool-exclude"
msgstr "Exclusión"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:590
msgid "shortcuts.fit-content-selected"
msgstr "Redimensionar para ajustar al contenido"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:86
msgid "shortcuts.bool-intersection"
msgstr "Interescción"
@ -4234,6 +4230,9 @@ msgstr "Alinear arriba (%s)"
msgid "workspace.assets.add-library"
msgstr "Añadir biblioteca"
msgid "workspace.assets.manage-library"
msgstr "Gestionar biblioteca"
#: src/app/main/ui/workspace/sidebar/assets.cljs
#, unused
msgid "workspace.assets.assets"
@ -4305,10 +4304,6 @@ msgstr "Agrupar"
msgid "workspace.assets.group-name"
msgstr "Nombre del grupo"
#: src/app/main/ui/workspace/sidebar/assets.cljs:186
msgid "workspace.assets.libraries"
msgstr "Bibliotecas"
#: src/app/main/ui/workspace/sidebar/assets/components.cljs:501
msgid "workspace.assets.list-view"
msgstr "Ver como lista"