Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8affefbbab | ||
|
|
0225919a45 | ||
|
|
5155cf2b23 | ||
|
|
7403f60366 | ||
|
|
a8c34ccc1a | ||
|
|
8c501db2fa | ||
|
|
d2fbb9dfa7 | ||
|
|
05d6d2fcd4 | ||
|
|
61800d8945 | ||
|
|
f450c9dbe3 | ||
|
|
e3b3fa3342 | ||
|
|
fe04f3e45d | ||
|
|
363c1d5b56 | ||
|
|
3ee3df9b24 | ||
|
|
332657bd1b | ||
|
|
953f770fdd | ||
|
|
c83b9ea305 | ||
|
|
3007aa19a2 | ||
|
|
e20adda766 | ||
|
|
3d9fda7a21 | ||
|
|
7a5dea5cfe | ||
|
|
b47df2c230 | ||
|
|
b8b3cc641a | ||
|
|
09ff7372da | ||
|
|
f45fa95935 | ||
|
|
ce02cbc3f1 | ||
|
|
b386403fa8 | ||
|
|
0a6e884584 | ||
|
|
06f6a49bce | ||
|
|
afd309c62b | ||
|
|
214a89e20d | ||
|
|
e64cf9f283 | ||
|
|
3a34c51e43 | ||
|
|
0ff9c44246 | ||
|
|
5bfab454f5 | ||
|
|
5ebde405ea | ||
|
|
531b002a5c | ||
|
|
3eae3178a2 | ||
|
|
2cf3e37b7a | ||
|
|
e0b9751b16 | ||
|
|
ccea9b1564 | ||
|
|
5fcf889d3c | ||
|
|
7247db14b2 | ||
|
|
658e5dce22 | ||
|
|
f27cbfa0ec | ||
|
|
5754c393b9 | ||
|
|
c618efc29e | ||
|
|
3685f7b32b | ||
|
|
06b5304926 | ||
|
|
8f06fa1026 | ||
|
|
a549d783ba | ||
|
|
91efcd17a2 | ||
|
|
6c1e8c3fe8 | ||
|
|
7f9a9ad774 | ||
|
|
2219d91e4d | ||
|
|
fac2314d62 | ||
|
|
aac61ff229 | ||
|
|
15d09eb0d4 | ||
|
|
786383c25d | ||
|
|
662c3c64a9 | ||
|
|
9084c184e7 | ||
|
|
ae718c3328 | ||
|
|
702bd41047 | ||
|
|
9896275fa8 | ||
|
|
d2c800fc0f | ||
|
|
893f19fa5e | ||
|
|
624750ad16 | ||
|
|
24cb1728b0 | ||
|
|
dda9f62504 | ||
|
|
479f39338b | ||
|
|
befa5f4c7f | ||
|
|
6e92e3b765 | ||
|
|
0e73de17ec | ||
|
|
2dcf692853 | ||
|
|
66f2e0aa5e | ||
|
|
dd6ae81e83 | ||
|
|
cb8e31e7f8 | ||
|
|
ca9b5b1b8a | ||
|
|
a391d71b60 | ||
|
|
7d0c19fcc7 | ||
|
|
e4ee585704 | ||
|
|
5f61254a75 | ||
|
|
0784d6b62a | ||
|
|
7a7fa44f6b | ||
|
|
4b5d304a40 | ||
|
|
e7b9ae6415 | ||
|
|
4ac52c138c | ||
|
|
4744085426 | ||
|
|
19bae05f41 | ||
|
|
02f78d80d7 | ||
|
|
51202df105 | ||
|
|
cd1eefb214 | ||
|
|
869a412c74 | ||
|
|
d019afe667 | ||
|
|
c41aa56a60 | ||
|
|
7d840722c4 | ||
|
|
272bbdd54a | ||
|
|
fe3fec7a50 | ||
|
|
63524dce8d | ||
|
|
807b8d82e3 | ||
|
|
3f45863823 | ||
|
|
f9f5f0af7d | ||
|
|
f98dbef228 | ||
|
|
713d6a31df | ||
|
|
77f906ae37 | ||
|
|
6a5538bb15 | ||
|
|
0ce99968b3 | ||
|
|
0900b7a572 | ||
|
|
3412a0a18a | ||
|
|
5e3b47e455 | ||
|
|
83423a9509 | ||
|
|
ccabaf4552 | ||
|
|
ad15ac6c1e | ||
|
|
a9340709c8 | ||
|
|
faa3451da9 | ||
|
|
0aa95ea058 | ||
|
|
66182152cb | ||
|
|
b9629b7be6 | ||
|
|
6c9875e4f9 | ||
|
|
f90c63b5f0 | ||
|
|
680e611266 | ||
|
|
cad7d75590 | ||
|
|
8c81d48858 | ||
|
|
a7ed5228d3 | ||
|
|
6bb7fa26f4 | ||
|
|
8b6a9b373d | ||
|
|
8139ee3ef9 | ||
|
|
af93325fd9 | ||
|
|
d836cc66da | ||
|
|
5d56d28cb6 | ||
|
|
46d2359107 | ||
|
|
f8820695cc | ||
|
|
2d1d1fee1c | ||
|
|
4c6f086f82 | ||
|
|
688b9f2194 | ||
|
|
8992eb98ec | ||
|
|
638a8a8d3f | ||
|
|
fb6cd3d9d4 | ||
|
|
fb0e22c16b | ||
|
|
6b26adb187 | ||
|
|
8fe1271690 | ||
|
|
ceb90cd9e0 | ||
|
|
51f924a5e1 | ||
|
|
fb24a37e83 |
2
.github/workflows/commit-checker.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check Commit Type
|
||||
uses: gsactions/commit-message-checker@v2
|
||||
with:
|
||||
pattern: '^:(lipstick|globe_with_meridians|wrench|books|arrow_up|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):\s[A-Z].*[^.]$'
|
||||
pattern: '^:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):\s[A-Z].*[^.]$'
|
||||
flags: 'gm'
|
||||
error: 'Commit should match CONTRIBUTING.md guideline'
|
||||
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
|
||||
|
||||
55
CHANGES.md
@@ -1,8 +1,46 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.5.0 (Unreleased)
|
||||
## 2.5.4
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
### :sparkles: New features
|
||||
|
||||
- Add support for WEBP format on shape export [Github #6053](https://github.com/penpot/penpot/pull/6053) and [Github #6074](https://github.com/penpot/penpot/pull/6074)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix feature loading on workspace when opening a file in a background
|
||||
tab [Taiga #10377](https://tree.taiga.io/project/penpot/issue/10377)
|
||||
- Fix minor inconsistencies on RPC `get-file-libraries` and `get-file`
|
||||
methods (add missing team-id prop)
|
||||
- Fix problem with viewer role and inspect mode [Taiga #9751](https://tree.taiga.io/project/penpot/issue/9751)
|
||||
- Fix error when clicking on a comment at the viewer's sidebar [Taiga #10465](https://tree.taiga.io/project/penpot/issue/10465)
|
||||
|
||||
## 2.5.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Component sync issues with multiple tabs [Taiga #10471](https://tree.taiga.io/project/penpot/issue/10471)
|
||||
|
||||
## 2.5.2
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- When the workspace is empty, set default the board creation tool [Taiga #9425](https://tree.taiga.io/project/penpot/us/9425)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix scroll on storybook docs [taiga #9962](https://tree.taiga.io/project/penpot/issue/9962)
|
||||
- Navigate tracking event firing multiple times [Taiga #10415](https://tree.taiga.io/project/penpot/issue/10415)
|
||||
- Fix problem with selection colors [Taiga #10376](https://tree.taiga.io/project/penpot/issue/10376)
|
||||
- Fix scroll on storybook icons list [taiga #9962](https://tree.taiga.io/project/penpot/issue/9962)
|
||||
|
||||
## 2.5.1
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Improve Nginx entryponit to get the resolvers dinamically by default
|
||||
|
||||
## 2.5.0
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
@@ -32,9 +70,6 @@ If you have a big database and many cores available, you can reduce the time of
|
||||
all files by increasing paralelizacion changing the `max-jobs` value from 1 to N (where N
|
||||
is a number of cores)
|
||||
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- [GRADIENTS] New gradients UI with multi-stop support. [Taiga #3418](https://tree.taiga.io/project/penpot/epic/3418)
|
||||
@@ -50,8 +85,11 @@ is a number of cores)
|
||||
- [COMMENTS] Notifications in Backend, Profile Section, and Mention Email Notification [Taiga #9233](https://tree.taiga.io/project/penpot/us/9233)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix missing state refresh on notifications update [Taiga #10253](https://tree.taiga.io/project/penpot/issue/10253)
|
||||
- Fix icon visualization on select component [Taiga #8889](https://tree.taiga.io/project/penpot/issue/8889)
|
||||
- Fix typo on integration tests docs [Taiga #10112](https://tree.taiga.io/project/penpot/issue/10112)
|
||||
- Fix menu shadow color [Taiga #10102](https://tree.taiga.io/project/penpot/issue/10102)
|
||||
- Fix problem with alt key measures being stuck [Taiga #9348](https://tree.taiga.io/project/penpot/issue/9348)
|
||||
- Fix error when reseting stroke cap
|
||||
- Fix problem with strokes not refreshing in Safari [Taiga #9040](https://tree.taiga.io/project/penpot/issue/9040)
|
||||
@@ -62,6 +100,13 @@ is a number of cores)
|
||||
- Added upload svg with images method [#5489](https://github.com/penpot/penpot/issues/5489)
|
||||
- Fix problem with root frame parent reference [Taiga #9437](https://tree.taiga.io/project/penpot/issue/9437)
|
||||
- Fix change flex direction using plugins API [Taiga #9407](https://tree.taiga.io/project/penpot/issue/9407)
|
||||
- Fix problem opening url when page-id didn't exist [Taiga #10157](https://tree.taiga.io/project/penpot/issue/10157)
|
||||
- Fix problem with onboarding to a team [Taiga #10143](https://tree.taiga.io/project/penpot/issue/10143)
|
||||
- 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
|
||||
- Fix incorect handling of profile settings related to invitation notifications [Taiga #10252](https://tree.taiga.io/project/penpot/issue/10252)
|
||||
|
||||
## 2.4.3
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ Where type is:
|
||||
- :whale: `:whale:` a commit for docker related stuff
|
||||
- :paperclip: `:paperclip:` a commit with other not relevant changes
|
||||
- :arrow_up: `:arrow_up:` a commit with dependencies updates
|
||||
- :arrow_down: `:arrow_down:` a commit with dependencies downgrades
|
||||
- :fire: `:fire:` a commit that removes files or code
|
||||
|
||||
More info:
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
org.postgresql/postgresql {:mvn/version "42.7.5"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.48.0.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "6.0.0"}
|
||||
com.zaxxer/HikariCP {:mvn/version "6.2.1"}
|
||||
|
||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||
|
||||
@@ -59,8 +59,7 @@
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.30.7"}
|
||||
}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.28.26"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
|
||||
@@ -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}}§ion=interactions&index=0&share=true"
|
||||
<a href="{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}§ion=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>
|
||||
|
||||
@@ -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}}§ion=interactions&index=0&share=true
|
||||
{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}§ion=interactions&index=0&share=true
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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}}§ion=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}}§ion=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>
|
||||
|
||||
@@ -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}}§ion=interactions&index=0&share=true
|
||||
{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}§ion=interactions&index=0&share=true
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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}}§ion=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}}§ion=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>
|
||||
|
||||
@@ -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}}§ion=interactions&index=0&share=true
|
||||
|
||||
{{ public-uri }}/#/view?file-id={{file-id}}&page-id={{page-id}}§ion=interactions&index=0&share=true
|
||||
|
||||
|
||||
If you do not wish to grant access at this time, you can simply disregard this email.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
binfile format implementations and management rpc methods."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.helpers :as cfh]
|
||||
@@ -21,7 +20,6 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.features.components-v2 :as feat.compv2]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.features.file-migrations :as feat.fmigr]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
@@ -219,10 +217,8 @@
|
||||
"Given a set of file-id's, return all matching relations with the libraries"
|
||||
[cfg ids]
|
||||
|
||||
(dm/assert!
|
||||
"expected a set of uuids"
|
||||
(and (set? ids)
|
||||
(every? uuid? ids)))
|
||||
(assert (set? ids) "expected a set of uuids")
|
||||
(assert (every? uuid? ids) "expected a set of uuids")
|
||||
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids (db/create-array conn "uuid" ids)
|
||||
@@ -310,7 +306,7 @@
|
||||
|
||||
update-shapes
|
||||
(fn [data {:keys [page-id shape-id]}]
|
||||
(d/update-in-when data [:pages-index page-id :objects shape-id] cfh/relink-media-refs lookup-index))
|
||||
(d/update-in-when data [:pages-index page-id :objects shape-id] cfh/relink-refs lookup-index))
|
||||
|
||||
file
|
||||
(update file :data #(reduce update-shapes % media-refs))]
|
||||
@@ -378,7 +374,7 @@
|
||||
replace the old :component-file reference with the new
|
||||
ones, using the provided file-index."
|
||||
[data]
|
||||
(cfh/relink-media-refs data lookup-index))
|
||||
(cfh/relink-refs data lookup-index))
|
||||
|
||||
(defn- relink-media
|
||||
"A function responsible of process the :media attr of file data and
|
||||
@@ -503,9 +499,7 @@
|
||||
specific, should not be used outside of binfile domain"
|
||||
[{:keys [::timestamp] :as cfg} file & {:as opts}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid timestamp"
|
||||
(dt/instant? timestamp))
|
||||
(assert (dt/instant? timestamp) "expected valid timestamp")
|
||||
|
||||
(let [file (-> file
|
||||
(assoc :created-at timestamp)
|
||||
@@ -513,12 +507,11 @@
|
||||
(assoc :ignore-sync-until (dt/plus timestamp (dt/duration {:seconds 5})))
|
||||
(update :features
|
||||
(fn [features]
|
||||
(let [features (cfeat/check-supported-features! features)]
|
||||
(-> (::features cfg #{})
|
||||
(set/union features)
|
||||
;; We never want to store
|
||||
;; frontend-only features on file
|
||||
(set/difference cfeat/frontend-only-features))))))]
|
||||
(-> (::features cfg #{})
|
||||
(set/union features)
|
||||
;; We never want to store
|
||||
;; frontend-only features on file
|
||||
(set/difference cfeat/frontend-only-features)))))]
|
||||
|
||||
(when (contains? cf/flags :file-schema-validation)
|
||||
(fval/validate-file-schema! file))
|
||||
@@ -529,34 +522,3 @@
|
||||
(l/error :hint "file schema validation error" :cause result))))
|
||||
|
||||
(insert-file! cfg file opts)))
|
||||
|
||||
(defn register-pending-migrations!
|
||||
"All features that are enabled and requires explicit migration are
|
||||
added to the state for a posterior migration step."
|
||||
[cfg {:keys [id features] :as file}]
|
||||
(doseq [feature (-> (::features cfg)
|
||||
(set/difference cfeat/no-migration-features)
|
||||
(set/difference cfeat/backend-only-features)
|
||||
(set/difference features))]
|
||||
(vswap! *state* update :pending-to-migrate (fnil conj []) [feature id]))
|
||||
|
||||
file)
|
||||
|
||||
|
||||
(defn apply-pending-migrations!
|
||||
"Apply alredy registered pending migrations to files"
|
||||
[cfg]
|
||||
(doseq [[feature file-id] (-> *state* deref :pending-to-migrate)]
|
||||
(case feature
|
||||
"components/v2"
|
||||
(feat.compv2/migrate-file! cfg file-id
|
||||
:validate? (::validate cfg true)
|
||||
:skip-on-graphic-error? true)
|
||||
|
||||
"fdata/shape-data-type"
|
||||
nil
|
||||
|
||||
(ex/raise :type :internal
|
||||
:code :no-migration-defined
|
||||
:hint (str/ffmt "no migation for feature '%' on file importation" feature)
|
||||
:feature feature))))
|
||||
|
||||
45
backend/src/app/binfile/migrations.clj
Normal file
@@ -0,0 +1,45 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.binfile.migrations
|
||||
"A binfile related migrations handling"
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.features.components-v2 :as feat.compv2]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn register-pending-migrations!
|
||||
"All features that are enabled and requires explicit migration are
|
||||
added to the state for a posterior migration step."
|
||||
[cfg {:keys [id features] :as file}]
|
||||
(doseq [feature (-> (::features cfg)
|
||||
(set/difference cfeat/no-migration-features)
|
||||
(set/difference cfeat/backend-only-features)
|
||||
(set/difference features))]
|
||||
(vswap! bfc/*state* update :pending-to-migrate (fnil conj []) [feature id]))
|
||||
|
||||
file)
|
||||
|
||||
(defn apply-pending-migrations!
|
||||
"Apply alredy registered pending migrations to files"
|
||||
[cfg]
|
||||
(doseq [[feature file-id] (-> bfc/*state* deref :pending-to-migrate)]
|
||||
(case feature
|
||||
"components/v2"
|
||||
(feat.compv2/migrate-file! cfg file-id
|
||||
:validate? (::validate cfg true)
|
||||
:skip-on-graphic-error? true)
|
||||
|
||||
"fdata/shape-data-type"
|
||||
nil
|
||||
|
||||
(ex/raise :type :internal
|
||||
:code :no-migration-defined
|
||||
:hint (str/ffmt "no migation for feature '%' on file importation" feature)
|
||||
:feature feature))))
|
||||
@@ -9,6 +9,7 @@
|
||||
(:refer-clojure :exclude [assert])
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.binfile.migrations :as bfm]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
@@ -473,7 +474,7 @@
|
||||
(read-section options))))
|
||||
[:v1/metadata :v1/files :v1/rels :v1/sobjects])
|
||||
|
||||
(bfc/apply-pending-migrations! cfg)
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
|
||||
;; Knowing that the ids of the created files are in index,
|
||||
;; just lookup them and return it as a set
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:refer-clojure :exclude [read])
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.binfile.migrations :as bfm]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
@@ -735,7 +736,7 @@
|
||||
(bfc/process-file))]
|
||||
|
||||
|
||||
(bfc/register-pending-migrations! cfg file)
|
||||
(bfm/register-pending-migrations! cfg file)
|
||||
(bfc/save-file! cfg file ::db/return-keys false)
|
||||
|
||||
file-id')))
|
||||
@@ -875,14 +876,17 @@
|
||||
:manifest manifest))
|
||||
|
||||
;; Check if all files referenced on manifest are present
|
||||
(doseq [{file-id :id} (:files manifest)]
|
||||
(doseq [{file-id :id features :features} (:files manifest)]
|
||||
(let [path (str "files/" file-id ".json")]
|
||||
|
||||
(when-not (get-zip-entry input path)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-binfile-v3
|
||||
:hint "some files referenced on manifest not found"
|
||||
:path path
|
||||
:file-id file-id))))
|
||||
:file-id file-id))
|
||||
|
||||
(cfeat/check-supported-features! features)))
|
||||
|
||||
(events/tap :progress {:section :manifest})
|
||||
|
||||
@@ -912,7 +916,7 @@
|
||||
(import-file-media cfg)
|
||||
(import-file-thumbnails cfg)
|
||||
|
||||
(bfc/apply-pending-migrations! cfg)
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
|
||||
ids)))))))
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.flags :as flags]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uri :as u]
|
||||
[app.common.version :as v]
|
||||
[app.util.overrides]
|
||||
[app.util.time :as dt]
|
||||
@@ -228,19 +229,16 @@
|
||||
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]
|
||||
[:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
|
||||
|
||||
(def default-flags
|
||||
[:enable-backend-api-doc
|
||||
:enable-backend-openapi-doc
|
||||
:enable-backend-worker
|
||||
:enable-secure-session-cookies
|
||||
:enable-email-verification
|
||||
:enable-v2-migration])
|
||||
|
||||
(defn- parse-flags
|
||||
[config]
|
||||
(flags/parse flags/default
|
||||
default-flags
|
||||
(:flags config)))
|
||||
(let [public-uri (c/get config :public-uri)
|
||||
public-uri (some-> public-uri (u/uri))
|
||||
extra-flags (if (and public-uri
|
||||
(= (:scheme public-uri) "http")
|
||||
(not= (:host public-uri) "localhost"))
|
||||
#{:disable-secure-session-cookies}
|
||||
#{})]
|
||||
(flags/parse flags/default extra-flags (:flags config))))
|
||||
|
||||
(defn read-env
|
||||
[prefix]
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.binfile.v3 :as bf.v3]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -21,6 +22,7 @@
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.files-create :refer [create-file]]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.setup :as-alias setup]
|
||||
[app.srepl.helpers :as srepl]
|
||||
[app.storage :as-alias sto]
|
||||
@@ -317,7 +319,10 @@
|
||||
:hint "missing upload file"))
|
||||
|
||||
(let [profile (profile/get-profile pool profile-id)
|
||||
project-id (:default-project-id profile)]
|
||||
project-id (:default-project-id profile)
|
||||
team (teams/get-team pool
|
||||
:profile-id profile-id
|
||||
:project-id project-id)]
|
||||
|
||||
(when-not project-id
|
||||
(ex/raise :type :validation
|
||||
@@ -329,7 +334,8 @@
|
||||
cfg (assoc cfg
|
||||
::bfc/profile-id profile-id
|
||||
::bfc/project-id project-id
|
||||
::bfc/input path)]
|
||||
::bfc/input path
|
||||
::bfc/features (cfeat/get-team-enabled-features cf/flags team))]
|
||||
|
||||
(if (= format :binfile-v3)
|
||||
(bf.v3/import-files! cfg)
|
||||
|
||||
@@ -435,7 +435,10 @@
|
||||
:fn (mg/resource "app/migrations/sql/0137-add-file-migration-table.sql")}
|
||||
|
||||
{:name "0138-mod-file-data-fragment-table.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")}])
|
||||
:fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")}
|
||||
|
||||
{:name "0139-mod-file-change-table.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE file_change
|
||||
DROP CONSTRAINT file_change_file_id_fkey,
|
||||
DROP CONSTRAINT file_change_profile_id_fkey,
|
||||
ADD FOREIGN KEY (file_id) REFERENCES file(id) DEFERRABLE,
|
||||
ADD FOREIGN KEY (profile_id) REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE;
|
||||
@@ -10,8 +10,10 @@
|
||||
[app.binfile.common :as bfc]
|
||||
[app.binfile.v1 :as bf.v1]
|
||||
[app.binfile.v3 :as bf.v3]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.sse :as sse]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
@@ -20,6 +22,7 @@
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.tasks.file-gc]
|
||||
[app.util.services :as sv]
|
||||
@@ -91,41 +94,30 @@
|
||||
|
||||
;; --- Command: import-binfile
|
||||
|
||||
(defn- import-binfile-v1
|
||||
[{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}]
|
||||
(let [cfg (-> cfg
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/name name)
|
||||
(assoc ::bfc/input (:path file)))]
|
||||
|
||||
;; NOTE: the importation process performs some operations that are
|
||||
;; not very friendly with virtual threads, and for avoid
|
||||
;; unexpected blocking of other concurrent operations we dispatch
|
||||
;; that operation to a dedicated executor.
|
||||
(px/invoke! executor (partial bf.v1/import-files! cfg))))
|
||||
|
||||
(defn- import-binfile-v3
|
||||
[{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}]
|
||||
(let [cfg (-> cfg
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/name name)
|
||||
(assoc ::bfc/input (:path file)))]
|
||||
;; NOTE: the importation process performs some operations that are
|
||||
;; not very friendly with virtual threads, and for avoid
|
||||
;; unexpected blocking of other concurrent operations we dispatch
|
||||
;; that operation to a dedicated executor.
|
||||
(px/invoke! executor (partial bf.v3/import-files! cfg))))
|
||||
|
||||
(defn- import-binfile
|
||||
[{:keys [::db/pool] :as cfg} {:keys [project-id version] :as params}]
|
||||
(let [result (case (int version)
|
||||
1 (import-binfile-v1 cfg params)
|
||||
3 (import-binfile-v3 cfg params))]
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id project-id version name file]}]
|
||||
(let [team (teams/get-team pool
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
cfg (-> cfg
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/name name)
|
||||
(assoc ::bfc/input (:path file)))
|
||||
|
||||
;; NOTE: the importation process performs some operations that are
|
||||
;; not very friendly with virtual threads, and for avoid
|
||||
;; unexpected blocking of other concurrent operations we dispatch
|
||||
;; that operation to a dedicated executor.
|
||||
result (case (int version)
|
||||
1 (px/invoke! executor (partial bf.v1/import-files! cfg))
|
||||
3 (px/invoke! executor (partial bf.v3/import-files! cfg)))]
|
||||
|
||||
(db/update! pool :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id})
|
||||
|
||||
result))
|
||||
|
||||
(def ^:private schema:import-binfile
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
|
||||
file (-> (get-file cfg id :project-id project-id)
|
||||
(assoc :permissions perms)
|
||||
(assoc :team-id (:id team))
|
||||
(check-version!))]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
@@ -384,8 +385,10 @@
|
||||
f.revn,
|
||||
f.vern,
|
||||
f.is_shared,
|
||||
ft.media_id AS thumbnail_id
|
||||
ft.media_id AS thumbnail_id,
|
||||
p.team_id
|
||||
from file as f
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
left join file_thumbnail as ft on (ft.file_id = f.id
|
||||
and ft.revn = f.revn
|
||||
and ft.deleted_at is null)
|
||||
@@ -539,7 +542,8 @@
|
||||
f.modified_at,
|
||||
f.name,
|
||||
f.is_shared,
|
||||
ft.media_id
|
||||
ft.media_id,
|
||||
p.team_id
|
||||
from file as f
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn and ft.deleted_at is null)
|
||||
@@ -549,7 +553,6 @@
|
||||
and p.team_id = ?
|
||||
order by f.modified_at desc")
|
||||
|
||||
|
||||
(defn- get-library-summary
|
||||
[cfg {:keys [id data] :as file}]
|
||||
(letfn [(assets-sample [assets limit]
|
||||
@@ -611,6 +614,7 @@
|
||||
SELECT l.id,
|
||||
l.features,
|
||||
l.project_id,
|
||||
p.team_id,
|
||||
l.created_at,
|
||||
l.modified_at,
|
||||
l.deleted_at,
|
||||
@@ -620,6 +624,7 @@
|
||||
l.synced_at,
|
||||
l.is_shared
|
||||
FROM libs AS l
|
||||
INNER JOIN project AS p ON (p.id = l.project_id)
|
||||
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||
|
||||
(defn get-file-libraries
|
||||
@@ -686,7 +691,8 @@
|
||||
f.name,
|
||||
f.is_shared,
|
||||
ft.media_id AS thumbnail_id,
|
||||
row_number() over w as row_num
|
||||
row_number() over w as row_num,
|
||||
p.team_id
|
||||
from file as f
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
left join file_thumbnail as ft on (ft.file_id = f.id
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.rpc.commands.files-snapshot
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
@@ -22,7 +23,6 @@
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
@@ -58,26 +58,6 @@
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(get-file-snapshots conn file-id))))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.*,
|
||||
p.id AS project_id,
|
||||
p.team_id AS team_id
|
||||
FROM file AS f
|
||||
INNER JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?")
|
||||
|
||||
(defn- get-file
|
||||
[cfg file-id]
|
||||
(let [file (->> (db/exec-one! cfg [sql:get-file file-id])
|
||||
(feat.fdata/resolve-file-data cfg))]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(-> file
|
||||
(update :data blob/decode)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(update :data assoc ::id file-id)
|
||||
(update :data blob/encode)))))
|
||||
|
||||
(defn- generate-snapshot-label
|
||||
[]
|
||||
(let [ts (-> (dt/now)
|
||||
@@ -87,49 +67,53 @@
|
||||
(str "snapshot-" ts)))
|
||||
|
||||
(defn create-file-snapshot!
|
||||
[cfg profile-id file-id label]
|
||||
(let [file (get-file cfg file-id)
|
||||
[cfg file & {:keys [label created-by deleted-at profile-id]
|
||||
:or {deleted-at :default
|
||||
created-by :system}}]
|
||||
|
||||
(assert (#{:system :user :admin} created-by)
|
||||
"expected valid keyword for created-by")
|
||||
|
||||
(let [conn
|
||||
(db/get-connection cfg)
|
||||
|
||||
;; NOTE: final user never can provide label as `:system`
|
||||
;; keyword because the validator implies label always as
|
||||
;; string; keyword is used for signal a special case
|
||||
created-by
|
||||
(if (= label :system)
|
||||
"system"
|
||||
"user")
|
||||
(name created-by)
|
||||
|
||||
deleted-at
|
||||
(if (= label :system)
|
||||
(cond
|
||||
(= deleted-at :default)
|
||||
(dt/plus (dt/now) (cf/get-deletion-delay))
|
||||
|
||||
(dt/instant? deleted-at)
|
||||
deleted-at
|
||||
|
||||
:else
|
||||
nil)
|
||||
|
||||
label
|
||||
(if (= label :system)
|
||||
(str "internal/snapshot/" (:revn file))
|
||||
(or label (generate-snapshot-label)))
|
||||
(or label (generate-snapshot-label))
|
||||
|
||||
snapshot-id
|
||||
(uuid/next)]
|
||||
(uuid/next)
|
||||
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/project-id (:project-id file))
|
||||
(assoc ::quotes/team-id (:team-id file))
|
||||
(assoc ::quotes/file-id (:id file))
|
||||
(quotes/check! {::quotes/id ::quotes/snapshots-per-file}
|
||||
{::quotes/id ::quotes/snapshots-per-team}))
|
||||
data
|
||||
(blob/encode (:data file))
|
||||
|
||||
features
|
||||
(db/encode-pgarray (:features file) conn "text")]
|
||||
|
||||
(l/debug :hint "creating file snapshot"
|
||||
:file-id (str file-id)
|
||||
:file-id (str (:id file))
|
||||
:id (str snapshot-id)
|
||||
:label label)
|
||||
|
||||
(db/insert! cfg :file-change
|
||||
{:id snapshot-id
|
||||
:revn (:revn file)
|
||||
:data (:data file)
|
||||
:data data
|
||||
:version (:version file)
|
||||
:features (:features file)
|
||||
:features features
|
||||
:profile-id profile-id
|
||||
:file-id (:id file)
|
||||
:label label
|
||||
@@ -146,12 +130,25 @@
|
||||
|
||||
(sv/defmethod ::create-file-snapshot
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:create-file-snapshot}
|
||||
[cfg {:keys [::rpc/profile-id file-id label]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(create-file-snapshot! cfg profile-id file-id label))))
|
||||
::sm/params schema:create-file-snapshot
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id label]}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(let [file (bfc/get-file cfg file-id)
|
||||
project (db/get-by-id cfg :project (:project-id file))]
|
||||
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/project-id (:project-id file))
|
||||
(assoc ::quotes/team-id (:team-id project))
|
||||
(assoc ::quotes/file-id (:id file))
|
||||
(quotes/check! {::quotes/id ::quotes/snapshots-per-file}
|
||||
{::quotes/id ::quotes/snapshots-per-team}))
|
||||
|
||||
(create-file-snapshot! cfg file
|
||||
{:label label
|
||||
:profile-id profile-id
|
||||
:created-by :user})))
|
||||
|
||||
(defn restore-file-snapshot!
|
||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} file-id snapshot-id]
|
||||
@@ -237,8 +234,11 @@
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(create-file-snapshot! cfg profile-id file-id :system)
|
||||
(restore-file-snapshot! cfg file-id id))))
|
||||
(let [file (bfc/get-file cfg file-id)]
|
||||
(create-file-snapshot! cfg file
|
||||
{:profile-id profile-id
|
||||
:created-by :system})
|
||||
(restore-file-snapshot! cfg file-id id)))))
|
||||
|
||||
(def ^:private schema:update-file-snapshot
|
||||
[:map {:title "update-file-snapshot"}
|
||||
|
||||
@@ -406,12 +406,16 @@
|
||||
:prefix "penpot.template."
|
||||
:suffix ""
|
||||
:min-age "30m")
|
||||
format (bfc/parse-file-format template)
|
||||
|
||||
format (bfc/parse-file-format template)
|
||||
team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
cfg (-> cfg
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/input template))
|
||||
(assoc ::bfc/input template)
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
|
||||
|
||||
result (if (= format :binfile-v3)
|
||||
(px/invoke! executor (partial bf.v3/import-files! cfg))
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
[:welcome-file-id {:optional true} [:maybe ::sm/boolean]]
|
||||
[:release-notes-viewed {:optional true}
|
||||
[::sm/text {:max 100}]]
|
||||
[:notifications {:optional true} schema:props-notifications]])
|
||||
[:notifications {:optional true} schema:props-notifications]
|
||||
[:workspace-visited {:optional true} ::sm/boolean]])
|
||||
|
||||
(def schema:profile
|
||||
[:map {:title "Profile"}
|
||||
|
||||
@@ -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]
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
;; --- Mutation: Create Team Invitation
|
||||
|
||||
|
||||
(def sql:upsert-team-invitation
|
||||
"insert into team_invitation(id, team_id, email_to, created_by, role, valid_until)
|
||||
values (?, ?, ?, ?, ?, ?)
|
||||
@@ -79,27 +78,23 @@
|
||||
[:role ::types.team/role]
|
||||
[:email ::sm/email]])
|
||||
|
||||
(def ^:private check-create-invitation-params!
|
||||
(def ^:private check-create-invitation-params
|
||||
(sm/check-fn schema:create-invitation))
|
||||
|
||||
(defn- allow-invitation-emails?
|
||||
[member]
|
||||
(let [notifications (dm/get-in member [:props :notifications])]
|
||||
(not= :none (:email-invites notifications))))
|
||||
|
||||
(defn- create-invitation
|
||||
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid connection on cfg parameter"
|
||||
(db/connection? conn))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid params for `create-invitation` fn"
|
||||
(check-create-invitation-params! params))
|
||||
(assert (db/connection? conn) "expected valid connection on cfg parameter")
|
||||
(assert (check-create-invitation-params params))
|
||||
|
||||
(let [email (profile/clean-email email)
|
||||
member (profile/get-profile-by-email conn email)]
|
||||
|
||||
(teams/check-profile-muted conn member)
|
||||
(teams/check-email-bounce conn email true)
|
||||
(teams/check-email-spam conn email true)
|
||||
|
||||
;; When we have email verification disabled and invitation user is
|
||||
;; already present in the database, we proceed to add it to the
|
||||
;; team as-is, without email roundtrip.
|
||||
@@ -125,62 +120,65 @@
|
||||
|
||||
nil)
|
||||
|
||||
(let [id (uuid/next)
|
||||
expire (dt/in-future "168h") ;; 7 days
|
||||
invitation (db/exec-one! conn [sql:upsert-team-invitation id
|
||||
(:id team) (str/lower email)
|
||||
(:id profile)
|
||||
(name role) expire
|
||||
(name role) expire])
|
||||
updated? (not= id (:id invitation))
|
||||
profile-id (:id profile)
|
||||
tprops {:profile-id profile-id
|
||||
:invitation-id (:id invitation)
|
||||
:valid-until expire
|
||||
:team-id (:id team)
|
||||
:member-email (:email-to invitation)
|
||||
:member-id (:id member)
|
||||
:role role}
|
||||
itoken (create-invitation-token cfg tprops)
|
||||
ptoken (create-profile-identity-token cfg profile-id)]
|
||||
(do
|
||||
(some->> member (teams/check-profile-muted conn))
|
||||
(teams/check-email-bounce conn email true)
|
||||
(teams/check-email-spam conn email true)
|
||||
|
||||
(when (contains? cf/flags :log-invitation-tokens)
|
||||
(l/info :hint "invitation token" :token itoken))
|
||||
(let [id (uuid/next)
|
||||
expire (dt/in-future "168h") ;; 7 days
|
||||
invitation (db/exec-one! conn [sql:upsert-team-invitation id
|
||||
(:id team) (str/lower email)
|
||||
(:id profile)
|
||||
(name role) expire
|
||||
(name role) expire])
|
||||
updated? (not= id (:id invitation))
|
||||
profile-id (:id profile)
|
||||
tprops {:profile-id profile-id
|
||||
:invitation-id (:id invitation)
|
||||
:valid-until expire
|
||||
:team-id (:id team)
|
||||
:member-email (:email-to invitation)
|
||||
:member-id (:id member)
|
||||
:role role}
|
||||
itoken (create-invitation-token cfg tprops)
|
||||
ptoken (create-profile-identity-token cfg profile-id)]
|
||||
|
||||
(let [props (-> (dissoc tprops :profile-id)
|
||||
(audit/clean-props))
|
||||
evname (if updated?
|
||||
"update-team-invitation"
|
||||
"create-team-invitation")
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name evname)
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))
|
||||
(when (contains? cf/flags :log-invitation-tokens)
|
||||
(l/info :hint "invitation token" :token itoken))
|
||||
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/invite-to-team
|
||||
:public-uri (cf/get :public-uri)
|
||||
:to email
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:token itoken
|
||||
:extra-data ptoken})
|
||||
(let [props (-> (dissoc tprops :profile-id)
|
||||
(audit/clean-props))
|
||||
evname (if updated?
|
||||
"update-team-invitation"
|
||||
"create-team-invitation")
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name evname)
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))
|
||||
|
||||
itoken))))
|
||||
(when (allow-invitation-emails? member)
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/invite-to-team
|
||||
:public-uri (cf/get :public-uri)
|
||||
:to email
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:token itoken
|
||||
:extra-data ptoken}))
|
||||
|
||||
(defn- add-user-to-team
|
||||
[conn profile team role email]
|
||||
itoken)))))
|
||||
|
||||
(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 +203,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 +247,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 +576,3 @@
|
||||
|
||||
(with-meta {:request request}
|
||||
{::audit/props {:request 1}}))))
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.cond :as-alias cond]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]))
|
||||
[app.util.services :as sv]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- QUERY: View Only Bundle
|
||||
|
||||
@@ -26,6 +27,27 @@
|
||||
(update :pages (fn [pages] (filterv #(contains? allowed %) pages)))
|
||||
(update :pages-index select-keys allowed)))
|
||||
|
||||
(defn obfuscate-email
|
||||
[email]
|
||||
(let [[name domain]
|
||||
(str/split email "@" 2)
|
||||
|
||||
[_ rest]
|
||||
(str/split domain "." 2)
|
||||
|
||||
name
|
||||
(if (> (count name) 3)
|
||||
(str (subs name 0 1) (apply str (take (dec (count name)) (repeat "*"))))
|
||||
"****")]
|
||||
|
||||
(str name "@****." rest)))
|
||||
|
||||
(defn anonymize-member
|
||||
[member]
|
||||
(-> (select-keys member [:id :email :name :fullname :photo-id])
|
||||
(update :email obfuscate-email)
|
||||
(assoc :can-read true)))
|
||||
|
||||
(defn- get-view-only-bundle
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}]
|
||||
(let [file (files/get-file cfg file-id)
|
||||
@@ -37,7 +59,10 @@
|
||||
team (-> (db/get conn :team {:id (:team-id project)})
|
||||
(teams/decode-row))
|
||||
|
||||
members (teams/get-team-members conn (:team-id project))
|
||||
members (cond->> (teams/get-team-members conn (:team-id project))
|
||||
(= :share-link (:type perms))
|
||||
(mapv anonymize-member))
|
||||
|
||||
member-ids (into #{} (map :id) members)
|
||||
|
||||
perms (assoc perms :in-team (contains? member-ids profile-id))
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
[app.features.components-v2 :as feat.comp-v2]
|
||||
[app.main :as main]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.files-snapshot :as fsnap]))
|
||||
[app.rpc.commands.files-snapshot :as fsnap]
|
||||
[app.util.time :as dt]))
|
||||
|
||||
(def ^:dynamic *system* nil)
|
||||
|
||||
@@ -96,8 +97,11 @@
|
||||
(let [conn (db/get-connection system)]
|
||||
(->> (feat.comp-v2/get-and-lock-team-files conn team-id)
|
||||
(reduce (fn [result file-id]
|
||||
(fsnap/create-file-snapshot! system nil file-id label)
|
||||
(inc result))
|
||||
(let [file (fsnap/get-file-snapshots system file-id)]
|
||||
(fsnap/create-file-snapshot! system file
|
||||
{:label label
|
||||
:created-by :admin})
|
||||
(inc result)))
|
||||
0))))
|
||||
|
||||
(defn restore-team-snapshot!
|
||||
@@ -143,7 +147,10 @@
|
||||
(cfv/validate-file-schema! file'))
|
||||
|
||||
(when (string? label)
|
||||
(fsnap/create-file-snapshot! system nil file-id label))
|
||||
(fsnap/create-file-snapshot! system file
|
||||
{:label label
|
||||
:deleted-at (dt/in-future {:days 30})
|
||||
:created-by :admin}))
|
||||
|
||||
(let [file' (update file' :revn inc)]
|
||||
(bfc/update-file! system file')
|
||||
|
||||
@@ -431,25 +431,40 @@
|
||||
|
||||
process-file
|
||||
(fn [file-id idx tpoint]
|
||||
(try
|
||||
(l/trc :hint "process:file:start" :file-id (str file-id) :index idx)
|
||||
(let [system (assoc main/system ::db/rollback rollback?)]
|
||||
(db/tx-run! system (fn [system]
|
||||
(binding [h/*system* system]
|
||||
(h/process-file! system file-id update-fn opts)))))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/wrn :hint "unexpected error on processing file (skiping)"
|
||||
(let [thread-id (px/get-thread-id)]
|
||||
(try
|
||||
(l/trc :hint "process:file:start"
|
||||
:tid thread-id
|
||||
:file-id (str file-id)
|
||||
:index idx
|
||||
:cause cause))
|
||||
(finally
|
||||
(ps/release! sjobs)
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(l/trc :hint "process:file:end"
|
||||
:index idx)
|
||||
(let [system (assoc main/system ::db/rollback rollback?)]
|
||||
(db/tx-run! system (fn [system]
|
||||
(binding [h/*system* system]
|
||||
(h/process-file! system file-id update-fn opts)))))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/wrn :hint "unexpected error on processing file (skiping)"
|
||||
:tid thread-id
|
||||
:file-id (str file-id)
|
||||
:index idx
|
||||
:elapsed elapsed)))))
|
||||
:cause cause))
|
||||
(finally
|
||||
(when-let [pause (:pause opts)]
|
||||
(Thread/sleep (int pause)))
|
||||
|
||||
(ps/release! sjobs)
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(l/trc :hint "process:file:end"
|
||||
:tid thread-id
|
||||
:file-id (str file-id)
|
||||
:index idx
|
||||
:elapsed elapsed))))))
|
||||
|
||||
process-file*
|
||||
(fn [idx file-id]
|
||||
(ps/acquire! sjobs)
|
||||
(px/run! executor (partial process-file file-id idx (dt/tpoint)))
|
||||
(inc idx))
|
||||
|
||||
process-files
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
@@ -457,14 +472,12 @@
|
||||
(db/exec! conn ["SET idle_in_transaction_session_timeout = 0"])
|
||||
|
||||
(try
|
||||
(reduce (fn [idx file-id]
|
||||
(ps/acquire! sjobs)
|
||||
(px/run! executor (partial process-file file-id idx (dt/tpoint)))
|
||||
(inc idx))
|
||||
0
|
||||
(->> (db/cursor conn [query] {:chunk-size 1})
|
||||
(take max-items)
|
||||
(map :id)))
|
||||
(->> (db/plan conn [query])
|
||||
(transduce (comp
|
||||
(take max-items)
|
||||
(map :id))
|
||||
(completing process-file*)
|
||||
0))
|
||||
(finally
|
||||
;; Close and await tasks
|
||||
(pu/close! executor))))]
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
:file-id id
|
||||
:cause cause))))
|
||||
|
||||
;; Mark file change to be deleted
|
||||
(db/update! conn :file-change
|
||||
{:deleted-at deleted-at}
|
||||
{:file-id id})
|
||||
|
||||
;; Mark file media objects to be deleted
|
||||
(db/update! conn :file-media-object
|
||||
{:deleted-at deleted-at}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
(defn- delete-profiles!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-profiles min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-profiles min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [id photo-id]}]
|
||||
(l/trc :hint "permanently delete" :rel "profile" :id (str id))
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
(defn- delete-teams!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-teams min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-teams min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [id photo-id deleted-at]}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "team"
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
(defn- delete-fonts!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-fonts min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-fonts min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [id team-id deleted-at] :as font}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "team-font-variant"
|
||||
@@ -109,7 +109,7 @@
|
||||
|
||||
(defn- delete-projects!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-projects min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-projects min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [id team-id deleted-at]}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "project"
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
(defn- delete-files!
|
||||
[{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-files min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-files min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [id deleted-at project-id] :as file}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file"
|
||||
@@ -164,7 +164,7 @@
|
||||
|
||||
(defn delete-file-thumbnails!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-file-thumbnails min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-file-thumbnails min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [file-id revn media-id deleted-at]}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file-thumbnail"
|
||||
@@ -193,7 +193,7 @@
|
||||
|
||||
(defn delete-file-object-thumbnails!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-file-object-thumbnails min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-file-object-thumbnails min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [file-id object-id media-id deleted-at]}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file-tagged-object-thumbnail"
|
||||
@@ -222,7 +222,7 @@
|
||||
|
||||
(defn- delete-file-data-fragments!
|
||||
[{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-file-data-fragments min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-file-data-fragments min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [file-id id deleted-at data-ref-id]}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file-data-fragment"
|
||||
@@ -248,7 +248,7 @@
|
||||
|
||||
(defn- delete-file-media-objects!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-file-media-objects min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-file-media-objects min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [id file-id deleted-at] :as fmo}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file-media-object"
|
||||
@@ -275,9 +275,9 @@
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- delete-file-change!
|
||||
(defn- delete-file-changes!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-file-change min-age chunk-size] {:chunk-size 5})
|
||||
(->> (db/plan conn [sql:get-file-change min-age chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [id file-id deleted-at] :as xlog}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file-change"
|
||||
@@ -299,11 +299,11 @@
|
||||
#'delete-file-data-fragments!
|
||||
#'delete-file-object-thumbnails!
|
||||
#'delete-file-thumbnails!
|
||||
#'delete-file-changes!
|
||||
#'delete-files!
|
||||
#'delete-projects!
|
||||
#'delete-fonts!
|
||||
#'delete-teams!
|
||||
#'delete-file-change!])
|
||||
#'delete-teams!])
|
||||
|
||||
(defn- execute-proc!
|
||||
"A generic function that executes the specified proc iterativelly
|
||||
@@ -326,7 +326,7 @@
|
||||
[k v]
|
||||
{k (assoc v
|
||||
::min-age (cf/get-deletion-delay)
|
||||
::chunk-size 50)})
|
||||
::chunk-size 100)})
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
(th/db-query :file-change
|
||||
{:file-id (:id file)}
|
||||
{:order-by [:created-at]})]
|
||||
|
||||
(t/is (= 2 (count rows)))
|
||||
(t/is (= "user" (:created-by row1)))
|
||||
(t/is (= "system" (:created-by row2)))))
|
||||
|
||||
@@ -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))))))
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
(keep flag->feature))
|
||||
|
||||
(defn get-enabled-features
|
||||
"Get the globally enabled fratures set."
|
||||
"Get the globally enabled features set."
|
||||
[flags]
|
||||
(into default-features xf-flag-to-feature flags))
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
(let [data (::file-data (meta changes))]
|
||||
(dm/get-in data [:pages-index uuid/zero :objects])))
|
||||
|
||||
(defn- apply-changes-local
|
||||
(defn apply-changes-local
|
||||
[changes]
|
||||
(dm/assert!
|
||||
"expected valid changes"
|
||||
|
||||
@@ -570,10 +570,9 @@
|
||||
(into xform:collect-media-refs (vals (:components data)))
|
||||
(into (keys (:media data)))))
|
||||
|
||||
(defn relink-media-refs
|
||||
"A function responsible to analyze all file data and replace the
|
||||
old :component-file reference with the new ones, using the provided
|
||||
file-index."
|
||||
(defn relink-refs
|
||||
"A function responsible to analyze the file data or shape for references
|
||||
and apply lookup-index on it."
|
||||
[data lookup-index]
|
||||
(letfn [(process-map-form [form]
|
||||
(cond-> form
|
||||
@@ -724,7 +723,7 @@
|
||||
|
||||
(defn split-by-last-period
|
||||
"Splits a string into two parts:
|
||||
the text before and including the last period,
|
||||
the text before and including the last period,
|
||||
and the text after the last period."
|
||||
[s]
|
||||
(if-let [last-period (str/last-index-of s ".")]
|
||||
|
||||
@@ -7,13 +7,145 @@
|
||||
(ns app.common.flags
|
||||
"Flags parsing algorithm."
|
||||
(:require
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def login
|
||||
"Flags related to login features"
|
||||
#{;; Allows registration with login / password
|
||||
;; if disabled, it's still possible to register/login with providers
|
||||
:registration
|
||||
;; Redundant flag. TODO: remove it
|
||||
:login
|
||||
;; enables the section of Access Tokens on profile.
|
||||
:access-tokens
|
||||
;; Uses email and password as credentials.
|
||||
:login-with-password
|
||||
;; Uses Github authentication as credentials.
|
||||
:login-with-github
|
||||
;; Uses GitLab authentication as credentials.
|
||||
:login-with-gitlab
|
||||
;; Uses Google/Gmail authentication as credentials.
|
||||
:login-with-google
|
||||
;; Uses LDAP authentication as credentials.
|
||||
:login-with-ldap
|
||||
;; Uses any generic authentication provider that implements OIDC protocol as credentials.
|
||||
:login-with-oidc
|
||||
;; Allows registration with Open ID
|
||||
:oidc-registration
|
||||
;; This logs to console the invitation tokens. It's useful in case the SMTP is not configured.
|
||||
:log-invitation-tokens})
|
||||
|
||||
(def email
|
||||
"Flags related to email features"
|
||||
#{;; Uses the domains in whitelist as the only allowed domains to register in the application.
|
||||
;; Used with PENPOT_REGISTRATION_DOMAIN_WHITELIST
|
||||
:email-whitelist
|
||||
;; Prevents the domains in blacklist to register in the application.
|
||||
;; Used with PENPOT_REGISTRATION_DOMAIN_BLACKLIST
|
||||
:email-blacklist
|
||||
;; Skips the email verification process. Not recommended for production environments.
|
||||
:email-verification
|
||||
;; Only used if SMTP is disabled. Logs the emails into the console.
|
||||
:log-emails
|
||||
;; Enable it to configure email settings.
|
||||
:smtp
|
||||
;; Enables the debug mode of the SMTP library.
|
||||
:smtp-debug})
|
||||
|
||||
(def varia
|
||||
"Rest of the flags"
|
||||
#{:audit-log
|
||||
:audit-log-archive
|
||||
:audit-log-gc
|
||||
:auto-file-snapshot
|
||||
;; enables the `/api/doc` endpoint that lists all the rpc methods available.
|
||||
:backend-api-doc
|
||||
;; TODO: remove it and use only `backend-api-doc` flag
|
||||
:backend-openapi-doc
|
||||
;; Disable it to start the RPC without the worker.
|
||||
:backend-worker
|
||||
;; Only for development
|
||||
:component-thumbnails
|
||||
;; enables the default cors configuration that allows all domains (currently this configuration is only used for development).
|
||||
:cors
|
||||
;; Enables the templates dialog on Penpot dashboard.
|
||||
:dashboard-templates-section
|
||||
;; disabled by default. When enabled, Penpot create demo users with a 7 days expiration.
|
||||
:demo-users
|
||||
;; disabled by default. When enabled, it displays a warning that this is a test instance and data will be deleted periodically.
|
||||
:demo-warning
|
||||
;; Activates the schema validation during update file.
|
||||
:file-schema-validation
|
||||
;; Reports the schema validation errors internally.
|
||||
:soft-file-schema-validation
|
||||
;; Activates the referential integrity validation during update file; related to components-v2.
|
||||
:file-validation
|
||||
;; Reports the referential integrity validation errors internally.
|
||||
:soft-file-validation
|
||||
;; TODO: deprecate this flag and consolidate the code
|
||||
:frontend-svgo
|
||||
;; TODO: deprecate this flag and consolidate the code
|
||||
:exporter-svgo
|
||||
;; TODO: deprecate this flag and consolidate the code
|
||||
:backend-svgo
|
||||
;; If enabled, it makes the Google Fonts available.
|
||||
:google-fonts-provider
|
||||
;; Only for development.
|
||||
:nrepl-server
|
||||
;; Interactive repl. Only for development.
|
||||
:urepl-server
|
||||
;; Programatic access to the runtime, used in administrative tasks.
|
||||
;; It's mandatory to enable it to use the `manage.py` script.
|
||||
:prepl-server
|
||||
;; Shows the onboarding modals right after registration.
|
||||
:onboarding
|
||||
:quotes
|
||||
:soft-quotes
|
||||
;; Concurrency limit.
|
||||
:rpc-climit
|
||||
;; Rate limit.
|
||||
:rpc-rlimit
|
||||
;; Soft rate limit.
|
||||
:soft-rpc-rlimit
|
||||
;; Disable it if you want to serve Penpot under a different domain than `http://localhost` without HTTPS.
|
||||
:secure-session-cookies
|
||||
;; If `cors` enabled, this is ignored.
|
||||
:strict-session-cookies
|
||||
:telemetry
|
||||
:terms-and-privacy-checkbox
|
||||
;; Only for developtment.
|
||||
:tiered-file-data-storage
|
||||
:transit-readable-response
|
||||
:user-feedback
|
||||
;; TODO: remove this flag.
|
||||
:v2-migration
|
||||
:webhooks
|
||||
;; TODO: deprecate this flag and consolidate the code
|
||||
:export-file-v3
|
||||
:render-wasm-dpr
|
||||
:hide-release-modal})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
|
||||
(def default
|
||||
"A common flags that affects both: backend and frontend."
|
||||
"Flags with default configuration"
|
||||
[:enable-registration
|
||||
:enable-login-with-password
|
||||
:enable-export-file-v3
|
||||
:enable-login-with-password])
|
||||
:enable-frontend-svgo
|
||||
:enable-exporter-svgo
|
||||
:enable-backend-svgo
|
||||
:enable-backend-api-doc
|
||||
:enable-backend-openapi-doc
|
||||
:enable-backend-worker
|
||||
:enable-secure-session-cookies
|
||||
:enable-email-verification
|
||||
:enable-onboarding
|
||||
:enable-dashboard-templates-section
|
||||
:enable-google-fonts-provider
|
||||
:enable-component-thumbnails])
|
||||
|
||||
(defn parse
|
||||
[& flags]
|
||||
|
||||
@@ -212,8 +212,10 @@
|
||||
(if (= type :column)
|
||||
[:column :column-span]
|
||||
[:row :row-span])
|
||||
from-idx (dec (get cell prop))
|
||||
to-idx (+ (dec (get cell prop)) (get cell prop-span))
|
||||
from-idx (-> (dec (get cell prop))
|
||||
(mth/clamp 0 (dec (count track-list))))
|
||||
to-idx (-> (+ (dec (get cell prop)) (get cell prop-span))
|
||||
(mth/clamp 0 (dec (count track-list))))
|
||||
tracks (subvec track-list from-idx to-idx)]
|
||||
(some? (->> tracks (d/seek #(= :flex (:type %)))))))
|
||||
|
||||
@@ -291,8 +293,10 @@
|
||||
(fn [allocated cell]
|
||||
(let [shape-id (first (:shapes cell))
|
||||
|
||||
from-idx (dec (get cell prop))
|
||||
to-idx (+ (dec (get cell prop)) (get cell prop-span))
|
||||
from-idx (-> (dec (get cell prop))
|
||||
(mth/clamp 0 (dec (count track-list))))
|
||||
to-idx (-> (+ (dec (get cell prop)) (get cell prop-span))
|
||||
(mth/clamp 0 (dec (count track-list))))
|
||||
|
||||
indexed-tracks (subvec (d/enumerate track-list) from-idx to-idx)
|
||||
to-allocate (size-to-allocate type parent (get children-map shape-id) cell bounds objects)
|
||||
@@ -597,11 +601,10 @@
|
||||
row (nth row-tracks (dec (:row grid-cell)) nil)
|
||||
|
||||
column-start-p (:start-p column)
|
||||
row-start-p (:start-p row)
|
||||
|
||||
start-p (gpt/add origin
|
||||
(gpt/add
|
||||
(gpt/to-vec origin column-start-p)
|
||||
(gpt/to-vec origin row-start-p)))]
|
||||
|
||||
(assoc grid-cell :start-p start-p)))))
|
||||
row-start-p (:start-p row)]
|
||||
(when (and (some? column-start-p) (some? row-start-p))
|
||||
(let [start-p (gpt/add origin
|
||||
(gpt/add
|
||||
(gpt/to-vec origin column-start-p)
|
||||
(gpt/to-vec origin row-start-p)))]
|
||||
(assoc grid-cell :start-p start-p)))))))
|
||||
|
||||
@@ -114,61 +114,62 @@
|
||||
|
||||
(defn child-position-delta
|
||||
[parent child child-bounds child-width child-height layout-data cell-data]
|
||||
(let [cell-bounds (cell-bounds layout-data cell-data)
|
||||
child-origin (gpo/origin child-bounds)
|
||||
(if-let [cell-bounds (cell-bounds layout-data cell-data)]
|
||||
(let [child-origin (gpo/origin child-bounds)
|
||||
|
||||
align (:layout-align-items parent)
|
||||
justify (:layout-justify-items parent)
|
||||
align-self (:align-self cell-data)
|
||||
justify-self (:justify-self cell-data)
|
||||
align (:layout-align-items parent)
|
||||
justify (:layout-justify-items parent)
|
||||
align-self (:align-self cell-data)
|
||||
justify-self (:justify-self cell-data)
|
||||
|
||||
align-self (when (and align-self (not= align-self :auto)) align-self)
|
||||
justify-self (when (and justify-self (not= justify-self :auto)) justify-self)
|
||||
align-self (when (and align-self (not= align-self :auto)) align-self)
|
||||
justify-self (when (and justify-self (not= justify-self :auto)) justify-self)
|
||||
|
||||
align (or align-self align)
|
||||
justify (or justify-self justify)
|
||||
align (or align-self align)
|
||||
justify (or justify-self justify)
|
||||
|
||||
origin-h (gpo/project-point cell-bounds :h child-origin)
|
||||
origin-v (gpo/project-point cell-bounds :v child-origin)
|
||||
hv (partial gpo/start-hv cell-bounds)
|
||||
vv (partial gpo/start-vv cell-bounds)
|
||||
origin-h (gpo/project-point cell-bounds :h child-origin)
|
||||
origin-v (gpo/project-point cell-bounds :v child-origin)
|
||||
hv (partial gpo/start-hv cell-bounds)
|
||||
vv (partial gpo/start-vv cell-bounds)
|
||||
|
||||
[top-m right-m bottom-m left-m] (ctl/child-margins child)
|
||||
[top-m right-m bottom-m left-m] (ctl/child-margins child)
|
||||
|
||||
;; Adjust alignment/justify
|
||||
[from-h to-h]
|
||||
(case justify
|
||||
:end
|
||||
[(gpt/add origin-h (hv child-width))
|
||||
(gpt/subtract (nth cell-bounds 1) (hv right-m))]
|
||||
;; Adjust alignment/justify
|
||||
[from-h to-h]
|
||||
(case justify
|
||||
:end
|
||||
[(gpt/add origin-h (hv child-width))
|
||||
(gpt/subtract (nth cell-bounds 1) (hv right-m))]
|
||||
|
||||
:center
|
||||
[(gpt/add origin-h (hv (/ child-width 2)))
|
||||
(-> (gpo/project-point cell-bounds :h (gpo/center cell-bounds))
|
||||
(gpt/add (hv (/ left-m 2)))
|
||||
(gpt/subtract (hv (/ right-m 2))))]
|
||||
:center
|
||||
[(gpt/add origin-h (hv (/ child-width 2)))
|
||||
(-> (gpo/project-point cell-bounds :h (gpo/center cell-bounds))
|
||||
(gpt/add (hv (/ left-m 2)))
|
||||
(gpt/subtract (hv (/ right-m 2))))]
|
||||
|
||||
[origin-h
|
||||
(gpt/add (first cell-bounds) (hv left-m))])
|
||||
[origin-h
|
||||
(gpt/add (first cell-bounds) (hv left-m))])
|
||||
|
||||
[from-v to-v]
|
||||
(case align
|
||||
:end
|
||||
[(gpt/add origin-v (vv child-height))
|
||||
(gpt/subtract (nth cell-bounds 3) (vv bottom-m))]
|
||||
[from-v to-v]
|
||||
(case align
|
||||
:end
|
||||
[(gpt/add origin-v (vv child-height))
|
||||
(gpt/subtract (nth cell-bounds 3) (vv bottom-m))]
|
||||
|
||||
:center
|
||||
[(gpt/add origin-v (vv (/ child-height 2)))
|
||||
(-> (gpo/project-point cell-bounds :v (gpo/center cell-bounds))
|
||||
(gpt/add (vv top-m))
|
||||
(gpt/subtract (vv bottom-m)))]
|
||||
:center
|
||||
[(gpt/add origin-v (vv (/ child-height 2)))
|
||||
(-> (gpo/project-point cell-bounds :v (gpo/center cell-bounds))
|
||||
(gpt/add (vv top-m))
|
||||
(gpt/subtract (vv bottom-m)))]
|
||||
|
||||
[origin-v
|
||||
(gpt/add (first cell-bounds) (vv top-m))])]
|
||||
[origin-v
|
||||
(gpt/add (first cell-bounds) (vv top-m))])]
|
||||
|
||||
(-> (gpt/point)
|
||||
(gpt/add (gpt/to-vec from-h to-h))
|
||||
(gpt/add (gpt/to-vec from-v to-v)))))
|
||||
(-> (gpt/point)
|
||||
(gpt/add (gpt/to-vec from-h to-h))
|
||||
(gpt/add (gpt/to-vec from-v to-v))))
|
||||
(gpt/point 0 0)))
|
||||
|
||||
(defn child-modifiers
|
||||
[parent parent-bounds child child-bounds layout-data cell-data]
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.files.helpers :as cfh]
|
||||
[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.logic.shapes :as cls]
|
||||
[app.common.spec :as us]
|
||||
@@ -226,18 +225,19 @@
|
||||
changes
|
||||
(if (ctl/grid-layout? objects (:parent-id first-shape))
|
||||
(let [target-cell (-> position meta :cell)
|
||||
|
||||
[row column]
|
||||
(if (some? target-cell)
|
||||
[(:row target-cell) (:column target-cell)]
|
||||
(gslg/get-drop-cell (:parent-id first-shape) objects position))]
|
||||
(when (some? target-cell)
|
||||
[(:row target-cell) (:column target-cell)])]
|
||||
(-> changes
|
||||
(pcb/update-shapes
|
||||
[(:parent-id first-shape)]
|
||||
(fn [shape objects]
|
||||
(-> shape
|
||||
(ctl/assign-cells objects)
|
||||
(ctl/push-into-cell [(:id first-shape)] row column)
|
||||
(ctl/assign-cells objects)))
|
||||
(cond-> (and (some? row) (some? column))
|
||||
(-> (ctl/push-into-cell [(:id first-shape)] row column)
|
||||
(ctl/assign-cells objects)))))
|
||||
{:with-objects? true})
|
||||
(pcb/reorder-grid-children [(:parent-id first-shape)])))
|
||||
changes)
|
||||
@@ -747,42 +747,35 @@
|
||||
(let [omit-touched? (not reset?)
|
||||
clear-remote-synced? (and initial-root? reset?)
|
||||
set-remote-synced? (and (not initial-root?) reset?)
|
||||
changes (cond-> changes
|
||||
:always
|
||||
(update-attrs shape-inst
|
||||
shape-main
|
||||
root-inst
|
||||
root-main
|
||||
container
|
||||
omit-touched?)
|
||||
changes
|
||||
(cond-> changes
|
||||
:always
|
||||
(update-attrs shape-inst
|
||||
shape-main
|
||||
root-inst
|
||||
root-main
|
||||
container
|
||||
omit-touched?)
|
||||
|
||||
(ctl/flex-layout? shape-main)
|
||||
(update-flex-child-copy-attrs shape-main
|
||||
shape-inst
|
||||
library
|
||||
component
|
||||
container
|
||||
omit-touched?)
|
||||
(ctl/flex-layout? shape-main)
|
||||
(update-flex-child-copy-attrs shape-main
|
||||
shape-inst
|
||||
library
|
||||
component
|
||||
container
|
||||
omit-touched?)
|
||||
|
||||
(ctl/grid-layout? shape-main)
|
||||
(update-grid-copy-attrs shape-main
|
||||
shape-inst
|
||||
library
|
||||
component
|
||||
container
|
||||
omit-touched?)
|
||||
reset?
|
||||
(change-touched shape-inst
|
||||
shape-main
|
||||
container
|
||||
{:reset-touched? true})
|
||||
|
||||
reset?
|
||||
(change-touched shape-inst
|
||||
shape-main
|
||||
container
|
||||
{:reset-touched? true})
|
||||
clear-remote-synced?
|
||||
(change-remote-synced shape-inst container nil)
|
||||
|
||||
clear-remote-synced?
|
||||
(change-remote-synced shape-inst container nil)
|
||||
|
||||
set-remote-synced?
|
||||
(change-remote-synced shape-inst container true))
|
||||
set-remote-synced?
|
||||
(change-remote-synced shape-inst container true))
|
||||
|
||||
component-container (find-main-container container shape-inst shape-main library component)
|
||||
|
||||
@@ -859,23 +852,36 @@
|
||||
(d/index-of children-inst child-inst)
|
||||
(d/index-of children-main child-main)
|
||||
container
|
||||
omit-touched?))]
|
||||
omit-touched?))
|
||||
|
||||
(compare-children changes
|
||||
children-inst
|
||||
children-main
|
||||
container
|
||||
component-container
|
||||
file
|
||||
libraries
|
||||
only-inst
|
||||
only-main
|
||||
both
|
||||
swapped
|
||||
moved
|
||||
false
|
||||
reset?
|
||||
components-v2))))
|
||||
changes
|
||||
(compare-children changes
|
||||
children-inst
|
||||
children-main
|
||||
container
|
||||
component-container
|
||||
file
|
||||
libraries
|
||||
only-inst
|
||||
only-main
|
||||
both
|
||||
swapped
|
||||
moved
|
||||
false
|
||||
reset?
|
||||
components-v2)
|
||||
|
||||
changes
|
||||
(cond-> changes
|
||||
(ctl/grid-layout? shape-inst)
|
||||
(update-grid-copy-attrs
|
||||
(:id shape-inst)
|
||||
shape-main
|
||||
library
|
||||
component
|
||||
omit-touched?))]
|
||||
|
||||
changes)))
|
||||
|
||||
(defn generate-rename-component
|
||||
"Generate the changes for rename the component with the given id, in the current file library."
|
||||
@@ -1710,30 +1716,50 @@
|
||||
|
||||
(defn- update-grid-copy-attrs
|
||||
"Synchronizes the `layout-grid-cells` property from the main shape to the copies"
|
||||
[changes shape-main shape-copy main-container main-component copy-container omit-touched?]
|
||||
(let [ids-map
|
||||
(into {}
|
||||
(comp
|
||||
(map #(dm/get-in copy-container [:objects %]))
|
||||
(keep
|
||||
(fn [copy-shape]
|
||||
(let [main-shape (ctf/get-ref-shape main-container main-component copy-shape)]
|
||||
[(:id main-shape) (:id copy-shape)]))))
|
||||
(:shapes shape-copy))
|
||||
[changes shape-copy-id shape-main main-container main-component omit-touched?]
|
||||
(-> changes
|
||||
(pcb/apply-changes-local)
|
||||
(pcb/update-shapes
|
||||
[shape-copy-id]
|
||||
(fn [shape-copy objects]
|
||||
(let [component-page
|
||||
(ctf/get-component-page main-container main-component)
|
||||
|
||||
new-changes
|
||||
(-> (pcb/empty-changes)
|
||||
(pcb/with-container copy-container)
|
||||
(pcb/with-objects (:objects copy-container))
|
||||
(pcb/update-shapes
|
||||
[(:id shape-copy)]
|
||||
(fn [shape-copy]
|
||||
component-swap-children
|
||||
(->> shape-main
|
||||
:shapes
|
||||
(map #(get (:objects component-page) %))
|
||||
(filter #(some? (ctk/get-swap-slot %)))
|
||||
(group-by ctk/get-swap-slot))
|
||||
|
||||
ids-map
|
||||
(into {}
|
||||
(comp
|
||||
(map #(get objects %))
|
||||
(keep
|
||||
(fn [copy-shape]
|
||||
(let [main-shape
|
||||
(if (some? (ctk/get-swap-slot copy-shape))
|
||||
(first (get component-swap-children (ctk/get-swap-slot copy-shape)))
|
||||
(ctf/get-ref-shape main-container main-component copy-shape))]
|
||||
[(:id main-shape) (:id copy-shape)]))))
|
||||
(:shapes shape-copy))
|
||||
|
||||
remove-orphan-cells
|
||||
(fn [cells {:keys [shapes]}]
|
||||
(let [child? (set shapes)]
|
||||
(-> cells
|
||||
(update-vals
|
||||
(fn [cell]
|
||||
(update cell :shapes #(filterv child? %)))))))
|
||||
;; Take cells from main and remap the shapes to assign it to the copy
|
||||
(let [copy-cells (:layout-grid-cells shape-copy)
|
||||
main-cells (-> (ctl/remap-grid-cells shape-main ids-map) :layout-grid-cells)]
|
||||
(assoc shape-copy :layout-grid-cells (ctl/merge-cells copy-cells main-cells omit-touched?))))
|
||||
{:ignore-touched true}))]
|
||||
(pcb/concat-changes changes new-changes)))
|
||||
copy-cells (-> shape-copy :layout-grid-cells (remove-orphan-cells shape-copy))
|
||||
main-cells (-> shape-main (ctl/remap-grid-cells ids-map) :layout-grid-cells)]
|
||||
(-> shape-copy
|
||||
(assoc :layout-grid-cells
|
||||
(ctl/merge-cells main-cells copy-cells omit-touched?))
|
||||
(ctl/assign-cells objects))))
|
||||
{:ignore-touched true :with-objects? true})))
|
||||
|
||||
(defn- update-grid-main-attrs
|
||||
"Synchronizes the `layout-grid-cells` property from the copy to the main shape"
|
||||
|
||||
@@ -963,7 +963,6 @@
|
||||
{:title "string"
|
||||
:description "not whitespace string"
|
||||
:gen/gen (sg/word-string)
|
||||
:error/code "errors.invalid-text"
|
||||
:error/fn
|
||||
(fn [{:keys [value schema]}]
|
||||
(let [{:keys [max min] :as props} (properties schema)]
|
||||
@@ -971,16 +970,23 @@
|
||||
(and (string? value)
|
||||
(number? max)
|
||||
(> (count value) max))
|
||||
["errors.field-max-length" max]
|
||||
{:code ["errors.field-max-length" max]}
|
||||
|
||||
(and (string? value)
|
||||
(number? min)
|
||||
(< (count value) min))
|
||||
["errors.field-min-length" min]
|
||||
{:code ["errors.field-min-length" min]}
|
||||
|
||||
(and (string? value)
|
||||
(str/empty? value))
|
||||
{:code "errors.field-missing"}
|
||||
|
||||
(and (string? value)
|
||||
(str/blank? value))
|
||||
"errors.field-not-all-whitespace")))}})
|
||||
{:code "errors.field-not-all-whitespace"}
|
||||
|
||||
:else
|
||||
{:code "errors.invalid-text"})))}})
|
||||
|
||||
(register!
|
||||
{:type ::password
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
(sm/register! ::component schema:component)
|
||||
|
||||
(def check-component!
|
||||
(def check-component
|
||||
(sm/check-fn schema:component))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.types.page
|
||||
(:refer-clojure :exclude [empty?])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as-alias gpt]
|
||||
@@ -98,3 +99,8 @@
|
||||
(defn get-frame-flow
|
||||
[flows frame-id]
|
||||
(d/seek #(= (:starting-frame %) frame-id) (vals flows)))
|
||||
|
||||
(defn is-empty?
|
||||
"Check if page is empty or contains shapes"
|
||||
[page]
|
||||
(= 1 (count (:objects page))))
|
||||
|
||||
@@ -588,51 +588,51 @@
|
||||
;; - Blur
|
||||
;; - Border radius
|
||||
(def ^:private basic-extract-props
|
||||
[:fills
|
||||
:strokes
|
||||
:opacity
|
||||
#{:fills
|
||||
:strokes
|
||||
:opacity
|
||||
|
||||
;; Layout Item
|
||||
:layout-item-margin
|
||||
:layout-item-margin-type
|
||||
:layout-item-h-sizing
|
||||
:layout-item-v-sizing
|
||||
:layout-item-max-h
|
||||
:layout-item-min-h
|
||||
:layout-item-max-w
|
||||
:layout-item-min-w
|
||||
:layout-item-absolute
|
||||
:layout-item-z-index
|
||||
;; Layout Item
|
||||
:layout-item-margin
|
||||
:layout-item-margin-type
|
||||
:layout-item-h-sizing
|
||||
:layout-item-v-sizing
|
||||
:layout-item-max-h
|
||||
:layout-item-min-h
|
||||
:layout-item-max-w
|
||||
:layout-item-min-w
|
||||
:layout-item-absolute
|
||||
:layout-item-z-index
|
||||
|
||||
;; Constraints
|
||||
:constraints-h
|
||||
:constraints-v
|
||||
;; Constraints
|
||||
:constraints-h
|
||||
:constraints-v
|
||||
|
||||
:shadow
|
||||
:blur
|
||||
:shadow
|
||||
:blur
|
||||
|
||||
;; Radius
|
||||
:r1
|
||||
:r2
|
||||
:r3
|
||||
:r4])
|
||||
;; Radius
|
||||
:r1
|
||||
:r2
|
||||
:r3
|
||||
:r4})
|
||||
|
||||
(def ^:private layout-extract-props
|
||||
[:layout
|
||||
:layout-flex-dir
|
||||
:layout-gap-type
|
||||
:layout-gap
|
||||
:layout-wrap-type
|
||||
:layout-align-items
|
||||
:layout-align-content
|
||||
:layout-justify-items
|
||||
:layout-justify-content
|
||||
:layout-padding-type
|
||||
:layout-padding
|
||||
:layout-grid-dir
|
||||
:layout-grid-rows
|
||||
:layout-grid-columns
|
||||
:layout-grid-cells])
|
||||
#{:layout
|
||||
:layout-flex-dir
|
||||
:layout-gap-type
|
||||
:layout-gap
|
||||
:layout-wrap-type
|
||||
:layout-align-items
|
||||
:layout-align-content
|
||||
:layout-justify-items
|
||||
:layout-justify-content
|
||||
:layout-padding-type
|
||||
:layout-padding
|
||||
:layout-grid-dir
|
||||
:layout-grid-rows
|
||||
:layout-grid-columns
|
||||
:layout-grid-cells})
|
||||
|
||||
(defn extract-props
|
||||
"Retrieves an object with the 'pasteable' properties for a shape."
|
||||
@@ -668,10 +668,13 @@
|
||||
[props shape]
|
||||
(d/patch-object props (select-keys shape layout-extract-props)))]
|
||||
|
||||
(-> shape
|
||||
(select-keys basic-extract-props)
|
||||
(cond-> (cfh/text-shape? shape) (extract-text-props shape))
|
||||
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape)))))
|
||||
(let [;; For texts we don't extract the fill
|
||||
extract-props
|
||||
(cond-> basic-extract-props (cfh/text-shape? shape) (disj :fills))]
|
||||
(-> shape
|
||||
(select-keys extract-props)
|
||||
(cond-> (cfh/text-shape? shape) (extract-text-props shape))
|
||||
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape))))))
|
||||
|
||||
(defn patch-props
|
||||
"Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
(:require
|
||||
[app.common.schema :as sm]))
|
||||
|
||||
(def types #{:png :jpeg :svg :pdf})
|
||||
(def types #{:png :jpeg :webp :svg :pdf})
|
||||
|
||||
(def schema:export
|
||||
[:map {:title "ShapeExport"}
|
||||
|
||||
@@ -1307,9 +1307,9 @@
|
||||
"Push the shapes into the row/column cell and moves the rest"
|
||||
[parent shape-ids row column]
|
||||
|
||||
(let [cells (vec (get-cells parent {:sort? true}))
|
||||
(let [parent (-> parent (free-cell-shapes shape-ids))
|
||||
cells (vec (get-cells parent {:sort? true}))
|
||||
[start-index start-cell] (seek-indexed-cell cells row column)]
|
||||
|
||||
(if (some? start-cell)
|
||||
(let [;; start-index => to-index is the range where the shapes inserted will be added
|
||||
to-index (min (+ start-index (count shape-ids)) (dec (count cells)))]
|
||||
@@ -1642,11 +1642,11 @@
|
||||
"Given target cells update with source cells while trying to keep target as
|
||||
untouched as possible"
|
||||
[target-cells source-cells omit-touched?]
|
||||
(if (not omit-touched?)
|
||||
source-cells
|
||||
|
||||
(letfn [(get-data [cells id]
|
||||
(dissoc (get cells id) :shapes :row :column :row-span :column-span))]
|
||||
(if omit-touched?
|
||||
(letfn [(merge-cells [source-cell target-cell]
|
||||
(-> source-cell
|
||||
(d/patch-object
|
||||
(dissoc target-cell :row :column :row-span :column-span))))]
|
||||
(let [deleted-cells
|
||||
(into #{}
|
||||
(filter #(not (contains? source-cells %)))
|
||||
@@ -1654,15 +1654,13 @@
|
||||
|
||||
touched-cells
|
||||
(into #{}
|
||||
(filter #(and
|
||||
(not (contains? deleted-cells %))
|
||||
(not= (get-data source-cells %)
|
||||
(get-data target-cells %))))
|
||||
(filter #(not (contains? deleted-cells %)))
|
||||
(keys target-cells))]
|
||||
|
||||
(->> touched-cells
|
||||
(reduce
|
||||
(fn [cells id]
|
||||
(-> cells
|
||||
(d/update-when id d/patch-object (get-data target-cells id))))
|
||||
source-cells))))))
|
||||
(d/update-when id merge-cells (get target-cells id))))
|
||||
source-cells))))
|
||||
source-cells))
|
||||
|
||||
@@ -11,6 +11,7 @@ RUN set -ex; \
|
||||
ADD ./bundle-frontend/ /var/www/app/
|
||||
ADD ./files/config.js /var/www/app/js/config.js
|
||||
ADD ./files/nginx.conf /etc/nginx/nginx.conf.template
|
||||
ADD ./files/resolvers.conf /etc/nginx/overrides.d/resolvers.conf.template
|
||||
ADD ./files/nginx-mime.types /etc/nginx/mime.types
|
||||
ADD ./files/nginx-entrypoint.sh /entrypoint.sh
|
||||
|
||||
|
||||
@@ -21,10 +21,14 @@ update_flags /var/www/app/js/config.js
|
||||
|
||||
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060};
|
||||
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061};
|
||||
export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-127.0.0.11};
|
||||
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)";
|
||||
export PENPOT_INTERNAL_RESOLVER=${PENPOT_INTERNAL_RESOLVER:-$PENPOT_DEFAULT_INTERNAL_RESOLVER};
|
||||
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600}; # Default to 350MiB
|
||||
|
||||
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_INTERNAL_RESOLVER,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
|
||||
< /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
|
||||
< /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf;
|
||||
|
||||
envsubst "\$PENPOT_INTERNAL_RESOLVER" \
|
||||
< /etc/nginx/overrides.d/resolvers.conf.template > /etc/nginx/overrides.d/resolvers.conf;
|
||||
|
||||
exec "$@";
|
||||
|
||||
@@ -46,7 +46,6 @@ http {
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
|
||||
proxy_buffers 32 4k;
|
||||
resolver $PENPOT_INTERNAL_RESOLVER ipv6=off;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
|
||||
1
docker/images/files/resolvers.conf
Normal file
@@ -0,0 +1 @@
|
||||
resolver $PENPOT_INTERNAL_RESOLVER ipv6=off valid=10s;
|
||||
@@ -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>
|
||||
or ask a
|
||||
<a href="https://github.com/penpot/penpot/issues/new/choose">question</a>.
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="footer-inside">
|
||||
|
||||
@@ -12,7 +12,7 @@ templateClass: tmpl-contributing-guide
|
||||
{{ show_children(child) }}
|
||||
{%- endif -%}
|
||||
{%- if child.url == page.url -%}
|
||||
{{ content | toc(tags=['h2', 'h3']) | safe }}
|
||||
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- if loop.last -%}</ul>{%- endif -%}
|
||||
|
||||
@@ -12,7 +12,7 @@ templateClass: tmpl-user-guide
|
||||
{{ show_children(child) }}
|
||||
{%- endif -%}
|
||||
{%- if child.url == page.url -%}
|
||||
{{ content | toc(tags=['h2', 'h3']) | safe }}
|
||||
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- if loop.last -%}</ul>{%- endif -%}
|
||||
|
||||
BIN
docs/img/dev-tools-1.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/img/dev-tools-2.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/img/interface/youraccount-notifications.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/img/interface/youraccount-password.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/img/interface/youraccount-profile.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/img/interface/youraccount-settings.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/img/penpot-report.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/img/styling/color-picker-gradient.webp
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
@@ -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
|
||||
|
||||
@@ -1,435 +0,0 @@
|
||||
---
|
||||
title: 1. Self-hosting Guide
|
||||
---
|
||||
|
||||
# Self-hosting Guide
|
||||
|
||||
This guide explains how to get your own Penpot instance, running on a machine you control,
|
||||
to test it, use it by you or your team, or even customize and extend it any way you like.
|
||||
|
||||
If you need more context you can look at the <a
|
||||
href="https://community.penpot.app/t/self-hosting-penpot-i/2336" target="_blank">post
|
||||
about self-hosting</a> in Penpot community.
|
||||
|
||||
**There is absolutely no difference between <a
|
||||
href="https://design.penpot.app">our SaaS offer</a> for Penpot and your
|
||||
self-hosted Penpot platform!**
|
||||
|
||||
There are three main options for creating a Penpot instance:
|
||||
|
||||
1. Using the platform of our partner <a href="https://elest.io/open-source/penpot" target="_blank">Elestio</a>.
|
||||
2. Using <a href="https://docker.com" target="_blank">Docker</a> tool.
|
||||
3. Using <a href="https://kubernetes.io/" target="_blank">Kubernetes</a>.
|
||||
|
||||
<p class="advice">
|
||||
The recommended way is to use Elestio, since it's simpler, fully automatic and still greatly flexible.
|
||||
Use Docker if you already know the tool, if need full control of the process or have extra requirements
|
||||
and do not want to depend on any external provider, or need to do any special customization.
|
||||
</p>
|
||||
|
||||
Or you can try <a href="#unofficial-self-host-options">other options</a>,
|
||||
offered by Penpot community.
|
||||
|
||||
## Recommended settings
|
||||
To self-host Penpot, you’ll need a server with the following specifications:
|
||||
|
||||
* **CPU:** 1-2 CPUs
|
||||
* **RAM:** 4 GiB of RAM
|
||||
* **Disk Space:** Disk requirements depend on your usage. Disk usage primarily involves the database and any files uploaded by users.
|
||||
|
||||
This setup should be sufficient for a smooth experience with typical usage (your mileage may vary).
|
||||
|
||||
## Install with Elestio
|
||||
|
||||
This section explains how to get Penpot up and running using <a href="https://elest.io/open-source/penpot"
|
||||
target="_blank">Elestio</a>.
|
||||
|
||||
This platform 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. You’ll be relieved of the need to worry about DNS configuration, SMTP, backups,
|
||||
SSL certificates, OS & Penpot upgrades, and much more.
|
||||
|
||||
It uses the same Docker configuration as the other installation option, below, so all
|
||||
customization options are the same.
|
||||
|
||||
### Get an Elestio account
|
||||
|
||||
<p class="advice">
|
||||
Skip this section if you already have an Elestio account.
|
||||
</p>
|
||||
|
||||
To create your Elestio account <a href="https://dash.elest.io/deploy?soft=Penpot&id=121"
|
||||
target="_blank">click here</a>. You can choose to deploy on any one of five leading cloud
|
||||
providers or on-premise.
|
||||
|
||||
### Deploy Penpot using Elestio
|
||||
|
||||
Now you can Create your service in “Services”:
|
||||
1. Look for Penpot.
|
||||
2. Select a Service Cloud Provider.
|
||||
3. Select Service Cloud Region.
|
||||
4. Select Service Plan (for a team of 20 you should be fine with 2GB RAM).
|
||||
5. Select Elestio Service Support.
|
||||
6. Provide Service Name (this will show in the URL of your instance) & Admin email (used
|
||||
to create the admin account).
|
||||
7. Select Advanced Configuration options (you can also do this later).
|
||||
8. Hit “Create Service” on the bottom right.
|
||||
|
||||
It will take a couple of minutes to get the instance launched. When the status turns to
|
||||
“Service is running” you are ready to get started.
|
||||
|
||||
By clicking on the Service you go to all the details and configuration options.
|
||||
|
||||
In Network/CNAME you can find the URL of your instance. Copy and paste this into a browser
|
||||
and start using Penpot.
|
||||
|
||||
### Configure Penpot with Elestio
|
||||
|
||||
If you want to make changes to your Penpot setup click on the “Update config” button in
|
||||
Software. Here you can see the “Docker compose” used to create the instance. In “ENV” top
|
||||
middle left you can make configuration changes that will be reflected in the Docker
|
||||
compose.
|
||||
|
||||
In this file, a “#” at the start of the line means it is text and not considered part of
|
||||
the configuration. This means you will need to delete it to get some of the configuration
|
||||
options to work. Once you made all your changes hit “Update & restart”. After a couple of
|
||||
minutes, your changes will be active.
|
||||
|
||||
You can find all configuration options in the [Configuration][1] section.
|
||||
|
||||
Get in contact with us through <a href="mailto:support@penpot.app">support@penpot.app</a>
|
||||
if you have any questions or need help.
|
||||
|
||||
|
||||
### Update Penpot
|
||||
|
||||
Elestio will update your instance automatically to the latest release unless you don't
|
||||
want this. In that case you need to “Disable auto updates” in Software auto updates.
|
||||
|
||||
|
||||
## Install with Docker
|
||||
|
||||
This section details everything you need to know to get Penpot up and running in
|
||||
production environments using Docker. For this, we provide a series of *Dockerfiles* and a
|
||||
*docker-compose* file that orchestrate all.
|
||||
|
||||
### Install Docker
|
||||
|
||||
<p class="advice">
|
||||
Skip this section if you already have docker installed, up and running.
|
||||
</p>
|
||||
|
||||
Currently, Docker comes into two different flavours:
|
||||
|
||||
#### Docker Desktop
|
||||
|
||||
This is the only option to have Docker in a Windows or MacOS. Recently it's also available
|
||||
for Linux, in the most popular distributions (Debian, Ubuntu and Fedora).
|
||||
|
||||
You can install it following the <a href="https://docs.docker.com/desktop/"
|
||||
target="_blank">official guide</a>.
|
||||
|
||||
Docker Desktop has a graphical control panel (GUI) to manage the service and view the
|
||||
containers, images and volumes. But need the command line (Terminal in Linux and Mac, or
|
||||
PowerShell in Windows) to build and run the containers, and execute other operations.
|
||||
|
||||
It already includes **docker compose** utility, needed by Penpot.
|
||||
|
||||
#### Docker Engine
|
||||
|
||||
This is the classic and default Docker setup for Linux machines, and the only option for a
|
||||
Linux VPS without graphical interface.
|
||||
|
||||
You can install it following the <a href="https://docs.docker.com/engine/"
|
||||
target="_blank">official guide</a>.
|
||||
|
||||
And you also need the [docker
|
||||
compose](https://docs.docker.com/compose/cli-command/#installing-compose-v2) (V2)
|
||||
plugin. You can use the old **docker-compose** tool, but all the documentation supposes
|
||||
you are using the V2.
|
||||
|
||||
You can easily check which version of **docker compose** you have. If you can execute
|
||||
<code class="language-bash">docker compose</code> command, then you have V2. If you need to write <code class="language-bash">docker-compose</code> (with a
|
||||
<code class="language-bash">-</code>) for it to work, you have the old version.
|
||||
|
||||
### Start Penpot
|
||||
|
||||
As first step you will need to obtain the <code class="language-bash">docker-compose.yaml</code> file. You can download it
|
||||
<a
|
||||
href="https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml"
|
||||
target="_blank">from Penpot repository</a>.
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
|
||||
```
|
||||
or
|
||||
```bash
|
||||
curl -o docker-compose.yaml https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
|
||||
```
|
||||
|
||||
Then simply launch composer:
|
||||
|
||||
```bash
|
||||
docker compose -p penpot -f docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
At the end it will start listening on http://localhost:9001
|
||||
|
||||
<p class="advice">
|
||||
If you don't change anything, by default this will use the latest image published in dockerhub.
|
||||
</p>
|
||||
|
||||
If you want to have more control over the version (which is recommended), you can use the PENPOT_VERSION envvar in the common ways:
|
||||
- setting the value in the .env file
|
||||
- or passing the envvar in the command line
|
||||
|
||||
```bash
|
||||
PENPOT_VERSION=2.4.3 docker compose -p penpot -f docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
### Stop Penpot
|
||||
|
||||
If you want to stop running Penpot, just type
|
||||
|
||||
```bash
|
||||
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,
|
||||
and other ones commented out with some explanations.
|
||||
|
||||
#### Create users using CLI
|
||||
|
||||
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.
|
||||
|
||||
|
||||
If you have registration disabled, you can create additional profiles using the
|
||||
command line interface:
|
||||
|
||||
```bash
|
||||
docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile
|
||||
```
|
||||
|
||||
**NOTE:** the exact container name depends on your docker version and platform.
|
||||
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
|
||||
execute:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yaml pull
|
||||
```
|
||||
|
||||
This will fetch the latest images. When you do <code class="language-bash">docker compose up</code> again, the containers will be recreated with the latest version.
|
||||
|
||||
<p class="advice">
|
||||
It is strongly recommended to update the Penpot version in small increments, rather than updating between two distant versions.
|
||||
</p>
|
||||
|
||||
**Important: Upgrade from version 1.x to 2.0**
|
||||
|
||||
The migration to version 2.0, due to the incorporation of the new v2 components, includes
|
||||
an additional process that runs automatically as soon as the application starts. If your
|
||||
on-premises Penpot instance contains a significant amount of data (such as hundreds of
|
||||
penpot files, especially those utilizing SVG components and assets extensively), this
|
||||
process may take a few minutes.
|
||||
|
||||
In some cases, such as when the script encounters an error, it may be convenient to run
|
||||
the process manually. To do this, you can disable the automatic migration process using
|
||||
the <code class="language-bash">disable-v2-migration</code> flag in <code
|
||||
class="language-bash">PENPOT_FLAGS</code> environment variable. You can then execute the
|
||||
migration process manually with the following command:
|
||||
|
||||
```bash
|
||||
docker exec -ti <container-name-or-id> ./run.sh app.migrations.v2
|
||||
```
|
||||
|
||||
**IMPORTANT:** this script should be executed on passing from 1.19.x to 2.0.x. Executing
|
||||
it on versions greater or equal to 2.1 of penpot will not work correctly. It is known that
|
||||
this script is removed since 2.4.3
|
||||
|
||||
|
||||
### Backup Penpot
|
||||
|
||||
Penpot uses <a href="https://docs.docker.com/storage/volumes" target="_blank">Docker
|
||||
volumes</a> to store all persistent data. This allows you to delete and recreate
|
||||
containers whenever you want without losing information.
|
||||
|
||||
This also means you need to do regular backups of the contents of the volumes. You cannot
|
||||
directly copy the contents of the volume data folder. Docker provides you a <a
|
||||
href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"
|
||||
target="_blank">volume backup procedure</a>, that uses a temporary container to mount one
|
||||
or more volumes, and copy their data to an archive file stored outside of the container.
|
||||
|
||||
If you use Docker Desktop, <a
|
||||
href="https://www.docker.com/blog/back-up-and-share-docker-volumes-with-this-extension/"
|
||||
target="_blank">there is an extension</a> that may ease the backup process.
|
||||
|
||||
If you use the default **docker compose** file, there are two volumes used: one for the
|
||||
Postgres database and another one for the assets uploaded by your users (images and svg
|
||||
clips). There may be more volumes if you enable other features, as explained in the file
|
||||
itself.
|
||||
|
||||
|
||||
## Install with Kubernetes
|
||||
|
||||
This section details everything you need to know to get Penpot up and running in
|
||||
production environments using a Kubernetes cluster of your choice. To do this, we have
|
||||
created a <a href="https://helm.sh/" target="_blank">Helm</a> repository with everything
|
||||
you need.
|
||||
|
||||
Therefore, your prerequisite will be to have a Kubernetes cluster on which we can install
|
||||
Helm.
|
||||
|
||||
|
||||
### What is Helm
|
||||
|
||||
*Helm* is the package manager for Kubernetes. A *Chart* is a Helm package. It contains
|
||||
all of the resource definitions necessary to run an application, tool, or service inside
|
||||
of a Kubernetes cluster. Think of it like the Kubernetes equivalent of a Homebrew
|
||||
formula, an Apt dpkg, or a Yum RPM file.
|
||||
|
||||
A Repository is the place where charts can be collected and shared. It's like Perl's CPAN
|
||||
archive or the Fedora Package Database, but for Kubernetes packages.
|
||||
|
||||
A Release is an instance of a chart running in a Kubernetes cluster. One chart can often
|
||||
be installed many times into the same cluster. And each time it is installed, a new
|
||||
release is created. Consider a MySQL chart. If you want two databases running in your
|
||||
cluster, you can install that chart twice. Each one will have its own release, which will
|
||||
in turn have its own release name.
|
||||
|
||||
With these concepts in mind, we can now explain Helm like this:
|
||||
|
||||
> Helm installs charts into Kubernetes clusters, creating a new release for each
|
||||
> installation. To find new charts, you can search Helm chart repositories.
|
||||
|
||||
|
||||
### Install Helm
|
||||
|
||||
<p class="advice">
|
||||
Skip this section if you already have Helm installed in your system.
|
||||
</p>
|
||||
|
||||
You can install Helm by following the <a href="https://helm.sh/docs/intro/install/" target="_blank">official guide</a>.
|
||||
There are different ways to install Helm, depending on your infrastructure and operating
|
||||
system.
|
||||
|
||||
|
||||
### Add Penpot repository
|
||||
|
||||
To add the Penpot Helm repository, run the following command:
|
||||
|
||||
```bash
|
||||
helm repo add penpot http://helm.penpot.app
|
||||
```
|
||||
|
||||
This will add the Penpot repository to your Helm configuration, so you can install all
|
||||
the Penpot charts stored there.
|
||||
|
||||
|
||||
### Install Penpot Chart
|
||||
|
||||
To install the chart with the release name `my-release`:
|
||||
|
||||
```bash
|
||||
helm install my-release penpot/penpot
|
||||
```
|
||||
|
||||
You can customize the installation specify each parameter using the `--set key=value[,key=value]`
|
||||
argument to helm install. For example,
|
||||
|
||||
```bash
|
||||
helm install my-release \
|
||||
--set global.postgresqlEnabled=true \
|
||||
--set global.redisEnabled=true \
|
||||
--set persistence.assets.enabled=true \
|
||||
penpot/penpot
|
||||
```
|
||||
|
||||
Alternatively, a YAML file that specifies the values for the above parameters can be
|
||||
provided while installing the chart. For example,
|
||||
|
||||
```bash
|
||||
helm install my-release -f values.yaml penpot/penpot
|
||||
```
|
||||
|
||||
|
||||
### Configure Penpot with Helm Chart
|
||||
|
||||
In the previous section we have shown how to configure penpot during installation by
|
||||
using parameters or by using a yaml file.
|
||||
|
||||
The default values are defined in the
|
||||
<a href="https://github.com/penpot/penpot-helm/blob/main/charts/penpot/values.yaml" target="_blank">`values.yml`</a>
|
||||
file itself, which you can use as a basis for creating your own settings.
|
||||
|
||||
You can also consult the list of parameters on the
|
||||
<a href="https://artifacthub.io/packages/helm/penpot/penpot#parameters" target="_blank">ArtifactHub page of the project</a>.
|
||||
|
||||
|
||||
### Upgrade Penpot
|
||||
|
||||
When a new version of Penpot's chart is released, or when you want to change the
|
||||
configuration of your release, you can use the helm upgrade command.
|
||||
|
||||
```bash
|
||||
helm upgrade my-release -f values.yaml penpot/penpot
|
||||
```
|
||||
|
||||
An upgrade takes an existing release and upgrades it according to the information you
|
||||
provide. Because Kubernetes charts can be large and complex, Helm tries to perform the
|
||||
least invasive upgrade. It will only update things that have changed since the last
|
||||
release.
|
||||
|
||||
After each upgrade, a new *revision* will be generated. You can check the revision
|
||||
history of a release with `helm history my-release` and go back to the previous revision
|
||||
if something went wrong with `helm rollback my-release 1` (`1` is the revision number of
|
||||
the previous release revision).
|
||||
|
||||
|
||||
### Backup Penpot
|
||||
|
||||
The Penpot's Helm Chart uses different Persistent Volumes to store all persistent data.
|
||||
This allows you to delete and recreate the instance whenever you want without losing
|
||||
information.
|
||||
|
||||
You back up data from a Persistent Volume via snapshots, so you will want to ensure that
|
||||
your container storage interface (CSI) supports volume snapshots. There are a couple of
|
||||
different options for the CSI driver that you choose. All of the major cloud providers
|
||||
have their respective CSI drivers.
|
||||
|
||||
At last, there are two Persistent Volumes used: one for the Postgres database and another
|
||||
one for the assets uploaded by your users (images and svg clips). There may be more
|
||||
volumes if you enable other features, as explained in the file itself.
|
||||
|
||||
You have to back up your custom settings too (the yaml file or the list of parameters you
|
||||
are using during you setup).
|
||||
|
||||
|
||||
## Unofficial self-host options
|
||||
|
||||
There are some other options, **NOT SUPPORTED BY PENPOT**:
|
||||
|
||||
* Install with <a href="https://community.penpot.app/t/how-to-develop-penpot-with-podman-penpotman/2113" target="_blank">Podman</a> instead of Docker.
|
||||
* Try the under development <a href="https://github.com/author-more/penpot-desktop/releases/latest" target="_blank">Penpot Desktop app</a>.
|
||||
* Try a simple Kubernetes Deployment option <a href="https://github.com/degola/penpot-kubernetes" target="_blank">penpot-kubernetes</a>.
|
||||
* Or try a fully manual installation if you have a really specific use case.. For help, you can look at the [Architecture][2] section and the <a href="https://github.com/penpot/penpot/tree/develop/docker/images" target="_blank">Docker configuration files</a>.
|
||||
|
||||
[1]: /technical-guide/configuration/
|
||||
[2]: /technical-guide/developer/architecture
|
||||
239
docs/technical-guide/getting-started/docker.md
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: 1.3 Install with Docker
|
||||
---
|
||||
|
||||
# Install with Docker
|
||||
|
||||
This section details everything you need to know to get Penpot up and running in
|
||||
production environments using Docker. For this, we provide a series of *Dockerfiles* and a
|
||||
*docker-compose* file that orchestrate all.
|
||||
|
||||
## Install Docker
|
||||
|
||||
<p class="advice">
|
||||
Skip this section if you already have docker installed, up and running.
|
||||
</p>
|
||||
|
||||
Currently, Docker comes into two different flavours:
|
||||
|
||||
### Docker Desktop
|
||||
|
||||
This is the only option to have Docker in a Windows or MacOS. Recently it's also available
|
||||
for Linux, in the most popular distributions (Debian, Ubuntu and Fedora).
|
||||
|
||||
You can install it following the <a href="https://docs.docker.com/desktop/"
|
||||
target="_blank">official guide</a>.
|
||||
|
||||
Docker Desktop has a graphical control panel (GUI) to manage the service and view the
|
||||
containers, images and volumes. But you need the command line (Terminal in Linux and Mac, or
|
||||
PowerShell in Windows) to build and run the containers, and execute other operations.
|
||||
|
||||
It already includes **docker compose** utility, needed by Penpot.
|
||||
|
||||
### Docker Engine
|
||||
|
||||
This is the classic and default Docker setup for Linux machines, and the only option for a
|
||||
Linux VPS without graphical interface.
|
||||
|
||||
You can install it following the <a href="https://docs.docker.com/engine/"
|
||||
target="_blank">official guide</a>.
|
||||
|
||||
And you also need the [docker
|
||||
compose](https://docs.docker.com/compose/cli-command/#installing-compose-v2) (V2)
|
||||
plugin. You can use the old **docker-compose** tool, but all the documentation supposes
|
||||
you are using the V2.
|
||||
|
||||
You can easily check which version of **docker compose** you have. If you can execute
|
||||
<code class="language-bash">docker compose</code> command, then you have V2. If you need to write <code class="language-bash">docker-compose</code> (with a
|
||||
<code class="language-bash">-</code>) for it to work, you have the old version.
|
||||
|
||||
## Start Penpot
|
||||
|
||||
As a first step you will need to obtain the <code class="language-bash">docker-compose.yaml</code> file. You can download it
|
||||
<a
|
||||
href="https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml"
|
||||
target="_blank">from the Penpot repository</a>.
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
|
||||
```
|
||||
or
|
||||
```bash
|
||||
curl -o docker-compose.yaml https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
|
||||
```
|
||||
|
||||
Then simply launch composer:
|
||||
|
||||
```bash
|
||||
docker compose -p penpot -f docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
At the end it will start listening on http://localhost:9001
|
||||
|
||||
<p class="advice">
|
||||
If you don't change anything, by default this will use the latest image published in dockerhub.
|
||||
</p>
|
||||
|
||||
If you want to have more control over the version (which is recommended), you can use the PENPOT_VERSION envvar in the common ways:
|
||||
- setting the value in the .env file
|
||||
- or passing the envvar in the command line
|
||||
|
||||
```bash
|
||||
PENPOT_VERSION=2.4.3 docker compose -p penpot -f docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
## Stop Penpot
|
||||
|
||||
If you want to stop running Penpot, just type
|
||||
|
||||
```bash
|
||||
docker compose -p penpot -f docker-compose.yaml down
|
||||
```
|
||||
|
||||
## Configure Penpot with Docker
|
||||
|
||||
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.
|
||||
|
||||
You can find all configuration options in the [Configuration][1] section.
|
||||
|
||||
## Using the CLI for administrative tasks
|
||||
|
||||
Penpot provides a script (`manage.py`) with some administrative tasks to perform in the server.
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
**NOTE:** the exact container name depends on your docker version and platform.
|
||||
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>.
|
||||
|
||||
## Update Penpot
|
||||
|
||||
To get the latest version of Penpot in your local installation, you just need to
|
||||
execute:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yaml pull
|
||||
```
|
||||
|
||||
This will fetch the latest images. When you do <code class="language-bash">docker compose up</code> again, the containers will be recreated with the latest version.
|
||||
|
||||
<p class="advice">
|
||||
It is strongly recommended to update the Penpot version in small increments, rather than updating between two distant versions.
|
||||
</p>
|
||||
|
||||
**Important: Upgrade from version 1.x to 2.0**
|
||||
|
||||
The migration to version 2.0, due to the incorporation of the new v2 components, includes
|
||||
an additional process that runs automatically as soon as the application starts. If your
|
||||
on-premises Penpot instance contains a significant amount of data (such as hundreds of
|
||||
penpot files, especially those utilizing SVG components and assets extensively), this
|
||||
process may take a few minutes.
|
||||
|
||||
In some cases, such as when the script encounters an error, it may be convenient to run
|
||||
the process manually. To do this, you can disable the automatic migration process using
|
||||
the <code class="language-bash">disable-v2-migration</code> flag in <code
|
||||
class="language-bash">PENPOT_FLAGS</code> environment variable. You can then execute the
|
||||
migration process manually with the following command:
|
||||
|
||||
```bash
|
||||
docker exec -ti <container-name-or-id> ./run.sh app.migrations.v2
|
||||
```
|
||||
|
||||
**IMPORTANT:** this script should be executed on passing from 1.19.x to 2.0.x. Executing
|
||||
it on versions greater or equal to 2.1 of penpot will not work correctly. It is known that
|
||||
this script is removed since 2.4.3
|
||||
|
||||
|
||||
## Backup Penpot
|
||||
|
||||
Penpot uses <a href="https://docs.docker.com/storage/volumes" target="_blank">Docker
|
||||
volumes</a> to store all persistent data. This allows you to delete and recreate
|
||||
containers whenever you want without losing information.
|
||||
|
||||
This also means you need to do regular backups of the contents of the volumes. You cannot
|
||||
directly copy the contents of the volume data folder. Docker provides you a <a
|
||||
href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"
|
||||
target="_blank">volume backup procedure</a>, that uses a temporary container to mount one
|
||||
or more volumes, and copy their data to an archive file stored outside of the container.
|
||||
|
||||
If you use Docker Desktop, <a
|
||||
href="https://www.docker.com/blog/back-up-and-share-docker-volumes-with-this-extension/"
|
||||
target="_blank">there is an extension</a> that may ease the backup process.
|
||||
|
||||
If you use the default **docker compose** file, there are two volumes used: one for the
|
||||
Postgres database and another one for the assets uploaded by your users (images and svg
|
||||
clips). There may be more volumes if you enable other features, as explained in the file
|
||||
itself.
|
||||
|
||||
## Configure the proxy
|
||||
|
||||
Your host configuration needs to make a proxy to http://localhost:9001.
|
||||
|
||||
### Example with NGINX
|
||||
|
||||
```bash
|
||||
server {
|
||||
listen 80;
|
||||
server_name penpot.mycompany.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name penpot.mycompany.com;
|
||||
|
||||
# This value should be in sync with the corresponding in the docker-compose.yml
|
||||
# PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
|
||||
client_max_body_size 31457280;
|
||||
|
||||
# Logs: Configure your logs following the best practices inside your company
|
||||
access_log /path/to/penpot.access.log;
|
||||
error_log /path/to/penpot.error.log;
|
||||
|
||||
# TLS: Configure your TLS following the best practices inside your company
|
||||
ssl_certificate /path/to/fullchain;
|
||||
ssl_certificate_key /path/to/privkey;
|
||||
|
||||
# Websockets
|
||||
location /ws/notifications {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_pass http://localhost:9001/ws/notifications;
|
||||
}
|
||||
|
||||
# Proxy pass
|
||||
location / {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_redirect off;
|
||||
proxy_pass http://localhost:9001/;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example with CADDY SERVER
|
||||
|
||||
```bash
|
||||
penpot.mycompany.com {
|
||||
reverse_proxy :9001
|
||||
tls /path/to/fullchain.pem /path/to/privkey.pem
|
||||
log {
|
||||
output file /path/to/penpot.log
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[1]: /technical-guide/configuration/
|
||||
68
docs/technical-guide/getting-started/elestio.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: 1.2 Install with Elestio
|
||||
---
|
||||
|
||||
# Install with Elestio
|
||||
|
||||
This section explains how to get Penpot up and running using <a href="https://elest.io/open-source/penpot"
|
||||
target="_blank">Elestio</a>.
|
||||
|
||||
This platform 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. You’ll be relieved of the need to worry about DNS configuration, SMTP, backups,
|
||||
SSL certificates, OS & Penpot upgrades, and much more.
|
||||
|
||||
## Get an Elestio account
|
||||
|
||||
<p class="advice">
|
||||
Skip this section if you already have an Elestio account.
|
||||
</p>
|
||||
|
||||
To create your Elestio account <a href="https://dash.elest.io/deploy?soft=Penpot&id=121"
|
||||
target="_blank">click here</a>. You can choose to deploy on any one of five leading cloud
|
||||
providers or on-premise.
|
||||
|
||||
## Deploy Penpot using Elestio
|
||||
|
||||
Now you can Create your service in “Services”:
|
||||
1. Look for Penpot.
|
||||
2. Select a Service Cloud Provider.
|
||||
3. Select Service Cloud Region.
|
||||
4. Select Service Plan (for a team of 20 you should be fine with 2GB RAM).
|
||||
5. Select Elestio Service Support.
|
||||
6. Provide Service Name (this will show in the URL of your instance) & Admin email (used
|
||||
to create the admin account).
|
||||
7. Select Advanced Configuration options (you can also do this later).
|
||||
8. Hit “Create Service” on the bottom right.
|
||||
|
||||
It will take a couple of minutes to get the instance launched. When the status turns to
|
||||
“Service is running” you are ready to get started.
|
||||
|
||||
By clicking on the Service you go to all the details and configuration options.
|
||||
|
||||
In Network/CNAME you can find the URL of your instance. Copy and paste this into a browser
|
||||
and start using Penpot.
|
||||
|
||||
## Configure Penpot with Elestio
|
||||
|
||||
If you want to make changes to your Penpot setup click on the “Update config” button in
|
||||
Software. Here you can see the “Docker compose” used to create the instance. In “ENV” top
|
||||
middle left you can make configuration changes that will be reflected in the Docker
|
||||
compose.
|
||||
|
||||
In this file, a “#” at the start of the line means it is text and not considered part of
|
||||
the configuration. This means you will need to delete it to get some of the configuration
|
||||
options to work. Once you made all your changes hit “Update & restart”. After a couple of
|
||||
minutes, your changes will be active.
|
||||
|
||||
You can find all configuration options in the [Configuration][1] section.
|
||||
|
||||
Get in contact with us through <a href="mailto:support@penpot.app">support@penpot.app</a>
|
||||
if you have any questions or need help.
|
||||
|
||||
## Update Penpot
|
||||
|
||||
Elestio will update your instance automatically to the latest release unless you don't
|
||||
want this. In that case you need to “Disable auto updates” in Software auto updates.
|
||||
|
||||
[1]: /technical-guide/configuration/
|
||||
31
docs/technical-guide/getting-started/index.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: 1. Self-hosting Guide
|
||||
---
|
||||
|
||||
# Self-hosting Guide
|
||||
|
||||
This guide explains how to get your own Penpot instance, running on a machine you control,
|
||||
to test it, use it by you or your team, or even customize and extend it any way you like.
|
||||
|
||||
If you need more context you can look at the <a
|
||||
href="https://community.penpot.app/t/self-hosting-penpot-i/2336" target="_blank">post
|
||||
about self-hosting</a> in Penpot community.
|
||||
|
||||
**There is absolutely no difference between <a
|
||||
href="https://design.penpot.app">our SaaS offer</a> for Penpot and your
|
||||
self-hosted Penpot platform!**
|
||||
|
||||
There are three main options for creating a Penpot instance:
|
||||
|
||||
1. Using the platform of our partner <a href="https://elest.io/open-source/penpot" target="_blank">Elestio</a>.
|
||||
2. Using <a href="https://docker.com" target="_blank">Docker</a> tool.
|
||||
3. Using <a href="https://kubernetes.io/" target="_blank">Kubernetes</a>.
|
||||
|
||||
<p class="advice">
|
||||
The recommended way is to use Elestio, since it's simpler, fully automatic and still greatly flexible.
|
||||
Use Docker if you already know the tool, if need full control of the process or have extra requirements
|
||||
and do not want to depend on any external provider, or need to do any special customization.
|
||||
</p>
|
||||
|
||||
Or you can try <a href="#unofficial-self-host-options">other options</a>,
|
||||
offered by Penpot community.
|
||||
136
docs/technical-guide/getting-started/kubernetes.md
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: 1.4 Install with Kubernetes
|
||||
---
|
||||
|
||||
# Install with Kubernetes
|
||||
|
||||
This section details everything you need to know to get Penpot up and running in
|
||||
production environments using a Kubernetes cluster of your choice. To do this, we have
|
||||
created a <a href="https://helm.sh/" target="_blank">Helm</a> repository with everything
|
||||
you need.
|
||||
|
||||
Therefore, your prerequisite will be to have a Kubernetes cluster on which we can install
|
||||
Helm.
|
||||
|
||||
## What is Helm
|
||||
|
||||
*Helm* is the package manager for Kubernetes. A *Chart* is a Helm package. It contains
|
||||
all of the resource definitions necessary to run an application, tool, or service inside
|
||||
of a Kubernetes cluster. Think of it like the Kubernetes equivalent of a Homebrew
|
||||
formula, an Apt dpkg, or a Yum RPM file.
|
||||
|
||||
A Repository is the place where charts can be collected and shared. It's like Perl's CPAN
|
||||
archive or the Fedora Package Database, but for Kubernetes packages.
|
||||
|
||||
A Release is an instance of a chart running in a Kubernetes cluster. One chart can often
|
||||
be installed many times into the same cluster. And each time it is installed, a new
|
||||
release is created. Consider a MySQL chart. If you want two databases running in your
|
||||
cluster, you can install that chart twice. Each one will have its own release, which will
|
||||
in turn have its own release name.
|
||||
|
||||
With these concepts in mind, we can now explain Helm like this:
|
||||
|
||||
> Helm installs charts into Kubernetes clusters, creating a new release for each
|
||||
> installation. To find new charts, you can search Helm chart repositories.
|
||||
|
||||
|
||||
## Install Helm
|
||||
|
||||
<p class="advice">
|
||||
Skip this section if you already have Helm installed in your system.
|
||||
</p>
|
||||
|
||||
You can install Helm by following the <a href="https://helm.sh/docs/intro/install/" target="_blank">official guide</a>.
|
||||
There are different ways to install Helm, depending on your infrastructure and operating
|
||||
system.
|
||||
|
||||
|
||||
## Add Penpot repository
|
||||
|
||||
To add the Penpot Helm repository, run the following command:
|
||||
|
||||
```bash
|
||||
helm repo add penpot http://helm.penpot.app
|
||||
```
|
||||
|
||||
This will add the Penpot repository to your Helm configuration, so you can install all
|
||||
the Penpot charts stored there.
|
||||
|
||||
|
||||
## Install Penpot Chart
|
||||
|
||||
To install the chart with the release name `my-release`:
|
||||
|
||||
```bash
|
||||
helm install my-release penpot/penpot
|
||||
```
|
||||
|
||||
You can customize the installation by specifying each parameter using the `--set key=value[,key=value]`
|
||||
argument to helm install. For example,
|
||||
|
||||
```bash
|
||||
helm install my-release \
|
||||
--set global.postgresqlEnabled=true \
|
||||
--set global.redisEnabled=true \
|
||||
--set persistence.assets.enabled=true \
|
||||
penpot/penpot
|
||||
```
|
||||
|
||||
Alternatively, a YAML file that specifies the values for the above parameters can be
|
||||
provided while installing the chart. For example,
|
||||
|
||||
```bash
|
||||
helm install my-release -f values.yaml penpot/penpot
|
||||
```
|
||||
|
||||
|
||||
## Configure Penpot with Helm Chart
|
||||
|
||||
In the previous section we have shown how to configure penpot during installation by
|
||||
using parameters or by using a yaml file.
|
||||
|
||||
The default values are defined in the
|
||||
<a href="https://github.com/penpot/penpot-helm/blob/main/charts/penpot/values.yaml" target="_blank">`values.yml`</a>
|
||||
file itself, which you can use as a basis for creating your own settings.
|
||||
|
||||
You can also consult the list of parameters on the
|
||||
<a href="https://artifacthub.io/packages/helm/penpot/penpot#parameters" target="_blank">ArtifactHub page of the project</a>.
|
||||
|
||||
|
||||
## Upgrade Penpot
|
||||
|
||||
When a new version of Penpot's chart is released, or when you want to change the
|
||||
configuration of your release, you can use the helm upgrade command.
|
||||
|
||||
```bash
|
||||
helm upgrade my-release -f values.yaml penpot/penpot
|
||||
```
|
||||
|
||||
An upgrade takes an existing release and upgrades it according to the information you
|
||||
provide. Because Kubernetes charts can be large and complex, Helm tries to perform the
|
||||
least invasive upgrade. It will only update things that have changed since the last
|
||||
release.
|
||||
|
||||
After each upgrade, a new *revision* will be generated. You can check the revision
|
||||
history of a release with `helm history my-release` and go back to the previous revision
|
||||
if something went wrong with `helm rollback my-release 1` (`1` is the revision number of
|
||||
the previous release revision).
|
||||
|
||||
|
||||
## Backup Penpot
|
||||
|
||||
The Penpot's Helm Chart uses different Persistent Volumes to store all persistent data.
|
||||
This allows you to delete and recreate the instance whenever you want without losing
|
||||
information.
|
||||
|
||||
You back up data from a Persistent Volume via snapshots, so you will want to ensure that
|
||||
your container storage interface (CSI) supports volume snapshots. There are a couple of
|
||||
different options for the CSI driver that you choose. All of the major cloud providers
|
||||
have their respective CSI drivers.
|
||||
|
||||
At last, there are two Persistent Volumes used: one for the Postgres database and another
|
||||
one for the assets uploaded by your users (images and svg clips). There may be more
|
||||
volumes if you enable other features, as explained in the file itself.
|
||||
|
||||
You have to back up your custom settings too (the yaml file or the list of parameters you
|
||||
are using during you setup).
|
||||
13
docs/technical-guide/getting-started/recommended-settings.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: 1.1 Recommended Settings
|
||||
---
|
||||
|
||||
# Recommended settings
|
||||
|
||||
To self-host Penpot, you’ll need a server with the following specifications:
|
||||
|
||||
* **CPU:** 1-2 CPUs
|
||||
* **RAM:** 4 GiB of RAM
|
||||
* **Disk Space:** Disk requirements depend on your usage. Disk usage primarily involves the database and any files uploaded by users.
|
||||
|
||||
This setup should be sufficient for a smooth experience with typical usage (your mileage may vary).
|
||||
14
docs/technical-guide/getting-started/unofficial-options.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: 1.5 Unofficial self-host options
|
||||
---
|
||||
|
||||
# Unofficial self-host options
|
||||
|
||||
There are some other options, **NOT SUPPORTED BY PENPOT**:
|
||||
|
||||
* Install with <a href="https://community.penpot.app/t/how-to-develop-penpot-with-podman-penpotman/2113" target="_blank">Podman</a> instead of Docker.
|
||||
* Try the under development <a href="https://github.com/author-more/penpot-desktop/releases/latest" target="_blank">Penpot Desktop app</a>.
|
||||
* Try a simple Kubernetes Deployment option <a href="https://github.com/degola/penpot-kubernetes" target="_blank">penpot-kubernetes</a>.
|
||||
* Or try a fully manual installation if you have a really specific use case.. For help, you can look at the [Architecture][1] section and the <a href="https://github.com/penpot/penpot/tree/develop/docker/images" target="_blank">Docker configuration files</a>.
|
||||
|
||||
[1]: /technical-guide/developer/architecture
|
||||
@@ -37,6 +37,11 @@ Also, if you are a developer, you can get into the code, to explore it, learn ho
|
||||
or extend it and contribute with new functionality. For this, we have a different Docker installation.
|
||||
In the [Developer Guide][6] you can find how to setup a development environment and many other dev-oriented documentation.
|
||||
|
||||
## Troubleshooting Penpot
|
||||
|
||||
The [Troubleshooting][8] section guides you through the different logs in Penpot so you can easily identify
|
||||
any issue that may arise as well as report it comprehensively.
|
||||
|
||||
[1]: /technical-guide/getting-started/#install-with-elestio
|
||||
[2]: /technical-guide/getting-started/#install-with-docker
|
||||
[3]: /technical-guide/configuration/
|
||||
@@ -44,3 +49,4 @@ In the [Developer Guide][6] you can find how to setup a development environment
|
||||
[5]: /technical-guide/integration/
|
||||
[6]: /technical-guide/developer/
|
||||
[7]: /technical-guide/getting-started/#install-with-kubernetes
|
||||
[8]: /technical-guide/troubleshooting/
|
||||
|
||||
62
docs/technical-guide/troubleshooting.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: 5. Troubleshooting Penpot
|
||||
---
|
||||
|
||||
# Troubleshooting Penpot
|
||||
|
||||
Knowing how to do Penpot troubleshooting can be very useful; on the one hand, it helps to create issues easier to resolve,
|
||||
since they include relevant information from the beginning which also makes them get solved faster;
|
||||
on the other hand, many times troubleshooting gives the necessary information to resolve a problem autonomously,
|
||||
without even creating an issue.
|
||||
|
||||
Troubleshooting requires patience and practice; you have to read the stacktrace carefully, even if it looks like a mess at first.
|
||||
It takes some practice to learn how to read the traces properly and extract important information.
|
||||
|
||||
So, if your Penpot installation is not working as intended, there are several places to look up searching for hints.
|
||||
|
||||
## Browser logs
|
||||
|
||||
Regardless of the type of installation you have performed, you can find useful information about Penpot in your browser.
|
||||
|
||||
First, use the devtools to ensure which version and flags you're using. Go to your Penpot instance in the browser and press F12;
|
||||
you'll see the devtools. In the <code class="language-bash">Console</code>, you can see the exact version that's being used.
|
||||
|
||||

|
||||
|
||||
Other interesting tab in the devtools is the <code class="language-bash">Network</code> tab, to check if there is a request that throws errors.
|
||||
|
||||

|
||||
|
||||
## Penpot report
|
||||
|
||||
When Penpot crashes, it provides a report with very useful information. Don't miss it!
|
||||
|
||||

|
||||
|
||||
## Docker logs
|
||||
|
||||
If you are using the Docker installation, this is an easy way to take a look at the logs.
|
||||
|
||||
Check if all containers are up and running:
|
||||
|
||||
```bash
|
||||
docker compose -p penpot -f docker-compose.yaml ps
|
||||
```
|
||||
|
||||
Check logs of all Penpot:
|
||||
|
||||
```bash
|
||||
docker compose -p penpot -f docker-compose.yaml logs -f
|
||||
```
|
||||
|
||||
If there is too much information and you'd like to check just one service at a time:
|
||||
|
||||
```bash
|
||||
docker compose -p penpot -f docker-compose.yaml logs penpot-frontend -f
|
||||
```
|
||||
|
||||
You can always check the logs form a specific container:
|
||||
|
||||
```bash
|
||||
docker logs -f penpot-penpot-postgres-1
|
||||
```
|
||||
@@ -27,7 +27,7 @@ title: 07· Exporting objects
|
||||
<ul>
|
||||
<li><strong>Size</strong> - Options for the most common sizing scales.</li>
|
||||
<li><strong>Suffix</strong> - Especially useful if you are exporting at different scales.</li>
|
||||
<li><strong>File format</strong> - PNG, SVG, JPEG, PDF.</li>
|
||||
<li><strong>File format</strong> - PNG, JPEG, WEBP, SVG, PDF.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="export-multiple-elements">Exporting multiple elements</h2>
|
||||
|
||||
@@ -166,7 +166,7 @@ a design.</p>
|
||||
|
||||
<h2 id="curves">Curves (freehand)</h2>
|
||||
<p>The curve tool allows a path to be created directly in a freehand mode.
|
||||
Select the curve tool by clicking on the icon at the toolbar or pressing <kbd>Ctrl/⌘</kbd> + <kbd>c</kbd>.
|
||||
Select the curve tool by clicking on the icon at the toolbar or pressing <kbd>Shift/⇧</kbd> + <kbd>c</kbd>.
|
||||
<p>The path created will contain a lot of points, but it is edited the same way as any other curve.</p>
|
||||
|
||||
<h2 id="paths">Paths (bezier)</h2>
|
||||
@@ -206,7 +206,7 @@ You can choose to edit individual nodes or create new ones. Press <kbd>Esc</kbd>
|
||||
<h3>Insert images</h3>
|
||||
<p>There are several options for inserting an image into a Penpot file:</p>
|
||||
<ul>
|
||||
<li>Use the <strong>image tool</strong> at the toolbar or press <kbd>K</kbd> to inspect images in your file system.</li>
|
||||
<li>Use the <strong>image tool</strong> at the toolbar or press <kbd>K</kbd> to insert images in your file system.</li>
|
||||
<li><strong>Drag</strong> an image from your computer to the viewport.</li>
|
||||
<li>Copy an image & paste it or drag it right from a <strong>browser</strong>.</li>
|
||||
<li>Drag an image from a Penpot <strong>library</strong>.</li>
|
||||
|
||||
@@ -51,13 +51,24 @@ title: 06· Styling
|
||||
<ol>
|
||||
<li><strong>Eyedropper</strong> - Allows you to pick any color of the objects at the viewport.</li>
|
||||
<li><strong>Color profiles</strong> - Select between RGB, the Harmony Wheel or HSV.</li>
|
||||
<li><strong>Color type</strong> - Solid, linear gradient, radial gradient or image.</li>
|
||||
<li><strong>Color type</strong> - Solid, gradient, or image.</li>
|
||||
<li><strong>Sliders</strong> - Easily manage settings like brightness, saturation or opacity.</li>
|
||||
<li><strong>Values</strong> - Set precise color values of red(R), green(G), blue(B) and transparency(A).</li>
|
||||
<li><strong>Libraries</strong> - Switch between recent colors and libraries.</li>
|
||||
<li><strong>Color palette</strong> - A quick launcher of the palette with the selected library.</li>
|
||||
</ol>
|
||||
|
||||
<h3 id="color-picker-gradients">Gradients</h3>
|
||||
<p>You can apply gradient fills to layers. To do that select the Gradient type at the color picker.</p>
|
||||
<figure>
|
||||
<img alt="Gradient" src="/img/styling/color-picker-gradient.webp"/>
|
||||
</figure>
|
||||
<p>You can choose between two types of gradients:</p>
|
||||
<ul>
|
||||
<li><strong>Linear Gradient:</strong> A smooth transition between two or more colors along a straight line, with the option to adjust the angle.</li>
|
||||
<li><strong>Radial Gradient:</strong> A circular color transition that starts with one color at the center and gradually shifts to another at the edges, which could be a different color or a fade to transparency.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="color-palette">Color palette</h2>
|
||||
<p>The color palette allows you to have a selected color library in plain sight.</p>
|
||||
<figure>
|
||||
|
||||
@@ -5,52 +5,6 @@ title: 02· The interface
|
||||
<h1 id="the-interface">The interface</h1>
|
||||
<p class="main-paragraph">The Penpot interface has three main areas: Dashboard, Workspace and View mode. Lets take a look at their composition and main features.</p>
|
||||
|
||||
|
||||
<h2 id="interface-dashboard">Dashboard</h2>
|
||||
<p>The Dashboard is the place where you will be able to organize your files, libraries, projects and teams.</p>
|
||||
<figure>
|
||||
<a href="/img/interface/dashboard-dark.webp" target="_blank">
|
||||
<img src="/img/interface/dashboard-dark.webp" alt="Penpot's dashboard" />
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
<p class="hint">
|
||||
<strong>1)</strong> Teams
|
||||
<strong>2)</strong> Search files
|
||||
<strong>3)</strong> Projects
|
||||
<strong>4)</strong> Drafts
|
||||
<strong>5)</strong> Shared Libraries
|
||||
<strong>6)</strong> Custom fonts
|
||||
<strong>7)</strong> Pinned projects
|
||||
<strong>8)</strong> User area
|
||||
<strong>9)</strong> Comment notifications
|
||||
<strong>10)</strong> Create project
|
||||
<strong>11)</strong> File card
|
||||
<strong>12)</strong> Libraries & Templates module
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Teams:</strong> A team allows you to collaborate with other Penpot users. Team members are allowed to work with any project or file within the team depending on their permissions. Members with admin permissions can also invite other members. Create or join as many teams as you need with different groups of people.</li>
|
||||
<li><strong>Search:</strong> If you are looking for a specific file just type its title at the search box.</li>
|
||||
<li><strong>Projects:</strong> A project allows you to group design files. It works pretty much like a folder in a file system. You can create as many projects as you need. If you are going to work with more people in a project, you should create it inside a team.</li>
|
||||
<li><strong>Drafts:</strong> The drafts section is where you can find the design files that are not inside any project.</li>
|
||||
<li><strong>Shared Libraries:</strong> In this section you will find all the design files that have been added as shared libraries. That way you will be able to better control the files that are sharing their assets. </li>
|
||||
<li><strong>Custom fonts:</strong> If you have purchased or own personal fonts that are not included in the catalog provided by Penpot, you can upload them from your computer and use them across the files of a team.</li>
|
||||
<li><strong>Pinned projects:</strong> If you want to keep some projects handy (for instance because you’re currently working on them) you can pin them to make them quickly available at the sidebar.</li>
|
||||
<li><strong>User area:</strong> This must be you! Access your profile settings, Penpot tutorials, the Penpot Community and more. You can also find here a way to leave us feedback. We’d love to read your thoughts :)</li>
|
||||
<li><strong>Comments notifications:</strong> Here you will be able to see if you have unread comments inside the files of the team.</li>
|
||||
<li><strong>Create project:</strong> Create as many projects as you need to organize your designs.</li>
|
||||
<li><strong>File card:</strong> Basic information about a file at plain sight. A preview, update info or if it’s added as a Shared Library. From there you can perform several actions over the file (rename, duplicate, move, download, delete).</li>
|
||||
<li><strong>Libraries & Templates module:</strong> A curated selection of Libraries & Templates files ready to import.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
<h3 id="your-account">Your account</h3>
|
||||
<p>Your account settings can be changed at the user area, in <b>Your account</b>. Here you can make changes to your profile, password or account language, as well as generate personal access tokens and access release notes.</p>
|
||||
<p>If you want to change the email address associated to your account or remove your account entirely, this can be done in the <b>Profile</b> section.</p>
|
||||
|
||||
|
||||
|
||||
<h2 id="interface-workspace">Workspace</h2>
|
||||
<p>The Workspace is where you actually create your designs. You have an infinite canvas where you can work directly but you also have the ability to create and work inside boards that will help you to create pages and exportation units.</p>
|
||||
|
||||
@@ -102,8 +56,6 @@ title: 02· The interface
|
||||
<li><strong>Assets panel:</strong> Each file has a default library (File Library) where you can store elements and styles that are likely to be reused within a project. That includes components, colors and typographies. To add an asset to a library just click the “+” button at the header of each asset group.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
|
||||
<h2 id="interface-viewmode">View mode</h2>
|
||||
<p>Launch the view mode to present and share your designs, comment on them and play with the interactions set at the workspace. You also have an Inspect mode where you can get properties specifications and code snippets. <a href="/user-guide/view-mode/">More about the View mode.</a></p>
|
||||
|
||||
@@ -145,6 +97,83 @@ title: 02· The interface
|
||||
<li><strong>Navigation buttons:</strong> Forward and backwards buttons.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="interface-dashboard">Dashboard</h2>
|
||||
<p>The Dashboard is the place where you will be able to organize your files, libraries, projects and teams.</p>
|
||||
<figure>
|
||||
<a href="/img/interface/dashboard-dark.webp" target="_blank">
|
||||
<img src="/img/interface/dashboard-dark.webp" alt="Penpot's dashboard" />
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
<p class="hint">
|
||||
<strong>1)</strong> Teams
|
||||
<strong>2)</strong> Search files
|
||||
<strong>3)</strong> Projects
|
||||
<strong>4)</strong> Drafts
|
||||
<strong>5)</strong> Shared Libraries
|
||||
<strong>6)</strong> Custom fonts
|
||||
<strong>7)</strong> Pinned projects
|
||||
<strong>8)</strong> User area
|
||||
<strong>9)</strong> Comment notifications
|
||||
<strong>10)</strong> Create project
|
||||
<strong>11)</strong> File card
|
||||
<strong>12)</strong> Libraries & Templates module
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Teams:</strong> A team allows you to collaborate with other Penpot users. Team members are allowed to work with any project or file within the team depending on their permissions. Members with admin permissions can also invite other members. Create or join as many teams as you need with different groups of people.</li>
|
||||
<li><strong>Search:</strong> If you are looking for a specific file just type its title at the search box.</li>
|
||||
<li><strong>Projects:</strong> A project allows you to group design files. It works pretty much like a folder in a file system. You can create as many projects as you need. If you are going to work with more people in a project, you should create it inside a team.</li>
|
||||
<li><strong>Drafts:</strong> The drafts section is where you can find the design files that are not inside any project.</li>
|
||||
<li><strong>Shared Libraries:</strong> In this section you will find all the design files that have been added as shared libraries. That way you will be able to better control the files that are sharing their assets. </li>
|
||||
<li><strong>Custom fonts:</strong> If you have purchased or own personal fonts that are not included in the catalog provided by Penpot, you can upload them from your computer and use them across the files of a team.</li>
|
||||
<li><strong>Pinned projects:</strong> If you want to keep some projects handy (for instance because you’re currently working on them) you can pin them to make them quickly available at the sidebar.</li>
|
||||
<li><strong>User area:</strong> This must be you! Access your profile settings, Penpot tutorials, the Penpot Community and more. You can also find here a way to leave us feedback. We’d love to read your thoughts :)</li>
|
||||
<li><strong>Comments notifications:</strong> Here you will be able to see if you have unread comments inside the files of the team.</li>
|
||||
<li><strong>Create project:</strong> Create as many projects as you need to organize your designs.</li>
|
||||
<li><strong>File card:</strong> Basic information about a file at plain sight. A preview, update info or if it’s added as a Shared Library. From there you can perform several actions over the file (rename, duplicate, move, download, delete).</li>
|
||||
<li><strong>Libraries & Templates module:</strong> A curated selection of Libraries & Templates files ready to import.</li>
|
||||
</ol>
|
||||
|
||||
<h3 id="your-account">Your account</h3>
|
||||
<p>Your account settings can be changed at the user area, in <b>Your account</b>. Here you can make changes to your profile, password or account language, as well as generate personal access tokens and access release notes.</p>
|
||||
|
||||
<h4 id="your-account-profile">Profile
|
||||
<a class="direct-link" href="#your-account-profile">#</a>
|
||||
</h3>
|
||||
<p>If you want to change the email address associated to your account or remove your account entirely, this can be done in the <b>Profile</b> section.</p>
|
||||
<figure>
|
||||
<img src="/img/interface/youraccount-profile.webp" alt="Penpot's profile" />
|
||||
</figure>
|
||||
|
||||
<h4 id="your-account-password">Password
|
||||
<a class="direct-link" href="#your-account-password">#</a>
|
||||
</h3>
|
||||
<p>If you want to change your password to a new one, this can be done in the <b>Password</b> section.</p>
|
||||
<figure>
|
||||
<img src="/img/interface/youraccount-password.webp" alt="Penpot's password" />
|
||||
</figure>
|
||||
|
||||
<h4 id="your-account-notifications">Notifications
|
||||
<a class="direct-link" href="#your-account-notifications">#</a>
|
||||
</h3>
|
||||
<p>At the <strong>Notifications</strong> section you can configure the email and dashboard notifications.</p>
|
||||
<figure>
|
||||
<img src="/img/interface/youraccount-notifications.webp" alt="Penpot's notifications" />
|
||||
</figure>
|
||||
|
||||
<h4 id="your-account-settings">Settings
|
||||
<a class="direct-link" href="#your-account-settings">#</a>
|
||||
</h3>
|
||||
<p>At the <strong>Settings</strong> section you can change the language and the UI color theme.</p>
|
||||
<figure>
|
||||
<img src="/img/interface/youraccount-settings.webp" alt="Penpot's settings" />
|
||||
</figure>
|
||||
|
||||
<h4 id="your-account-accesstokens">Access tokens
|
||||
<a class="direct-link" href="#your-account-accesstokens">#</a>
|
||||
</h3>
|
||||
<p>At the <strong>Asset tokens</strong> section you can manage your access tokens. <a href="https://help.penpot.app/technical-guide/integration/#access-tokens" target="_blank">Read more about access tokens here</a>.</p>
|
||||
|
||||
<h2 id="interface-ui-theme">UI Theme</h2>
|
||||
<p>Penpot's default interface is dark but you can switch anytime to a light option. You have 2 ways to change the theme:</p>
|
||||
@@ -170,4 +199,4 @@ title: 02· The interface
|
||||
<img src="/img/interface/viewmode-light.webp" alt="Penpot's view mode" />
|
||||
</a>
|
||||
<figcaption>Penpot's view mode in light mode</figcaption>
|
||||
</figure>
|
||||
</figure>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
(s/def ::type #{:jpeg :png :pdf :svg})
|
||||
(s/def ::type #{:png :jpeg :webp :pdf :svg})
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::share-id ::us/uuid)
|
||||
@@ -40,6 +40,7 @@
|
||||
(case type
|
||||
:png (rb/render params on-object)
|
||||
:jpeg (rb/render params on-object)
|
||||
:webp (rb/render params on-object)
|
||||
:pdf (rp/render params on-object)
|
||||
:svg (rs/render params on-object)))
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@
|
||||
(bw/wait-for node)
|
||||
(case type
|
||||
:png (bw/screenshot node {:omit-background? true :type type :path path})
|
||||
:jpeg (bw/screenshot node {:omit-background? false :type type :path path}))
|
||||
:jpeg (bw/screenshot node {:omit-background? false :type type :path path})
|
||||
:webp (p/let [png-path (sh/tempfile :prefix "penpot.tmp.render.bitmap." :suffix ".png")]
|
||||
;; playwright only supports jpg and png, we need to convert it afterwards
|
||||
(bw/screenshot node {:omit-background? true :type :png :path png-path})
|
||||
(sh/run-cmd! (str "convert " png-path " -quality 100 WEBP:" path))))
|
||||
(on-object (assoc object :path path))))
|
||||
|
||||
(render [uri page]
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
(case type
|
||||
:png ".png"
|
||||
:jpeg ".jpg"
|
||||
:webp ".webp"
|
||||
:svg ".svg"
|
||||
:pdf ".pdf"
|
||||
:zip ".zip"))
|
||||
@@ -26,6 +27,7 @@
|
||||
:pdf "application/pdf"
|
||||
:svg "image/svg+xml"
|
||||
:jpeg "image/jpeg"
|
||||
:png "image/png"))
|
||||
:png "image/png"
|
||||
:webp "image/webp"))
|
||||
|
||||
|
||||
|
||||
1
frontend/playwright/data/dashboard/delete-team.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"~#set": [
|
||||
{
|
||||
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
|
||||
"~:name": "Lorem Ipsum",
|
||||
"~:revn": 2,
|
||||
"~:modified-at": "~m1739356261950",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u69b52fcf-7de0-81cd-8005-b9b180a0bfb5",
|
||||
"~:thumbnail-id": "~u55bb9e08-6eed-4a64-a94d-2bcce7006e79",
|
||||
"~:is-shared": true,
|
||||
"~:project-id": "~u1ad2931c-eb80-8098-8005-b86c1d9d26c2",
|
||||
"~:created-at": "~m1739356217030",
|
||||
"~:library-summary": {
|
||||
"~:components": {
|
||||
"~:count": 0,
|
||||
"~:sample": []
|
||||
},
|
||||
"~:media": {
|
||||
"~:count": 0,
|
||||
"~:sample": []
|
||||
},
|
||||
"~:colors": {
|
||||
"~:count": 1,
|
||||
"~:sample": [
|
||||
{
|
||||
"~:path": "",
|
||||
"~:color": "#0087ff",
|
||||
"~:name": "#0087ff",
|
||||
"~:modified-at": "~m1739356244863",
|
||||
"~:opacity": 1,
|
||||
"~:id": "~u0449ccff-62fe-805c-8005-b9b194b094dd"
|
||||
}
|
||||
]
|
||||
},
|
||||
"~:typographies": {
|
||||
"~:count": 0,
|
||||
"~:sample": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
[
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true
|
||||
},
|
||||
"~:name": "Default",
|
||||
"~:modified-at": "~m1713533116375",
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
|
||||
"~:created-at": "~m1713533116375",
|
||||
"~:is-default": true
|
||||
},
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true
|
||||
},
|
||||
"~:name": "Second team",
|
||||
"~:modified-at": "~m1701164272671",
|
||||
"~:id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
|
||||
"~:created-at": "~m1701164272671",
|
||||
"~:is-default": false
|
||||
}
|
||||
]
|
||||
115
frontend/playwright/data/workspace/get-file-10113.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "10113 - Emtpy lib",
|
||||
"~:revn": 1,
|
||||
"~:modified-at": "~m1739365936352",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
"legacy-2",
|
||||
"legacy-3",
|
||||
"legacy-5",
|
||||
"legacy-6",
|
||||
"legacy-7",
|
||||
"legacy-8",
|
||||
"legacy-9",
|
||||
"legacy-10",
|
||||
"legacy-11",
|
||||
"legacy-12",
|
||||
"legacy-13",
|
||||
"legacy-14",
|
||||
"legacy-16",
|
||||
"legacy-17",
|
||||
"legacy-18",
|
||||
"legacy-19",
|
||||
"legacy-25",
|
||||
"legacy-26",
|
||||
"legacy-27",
|
||||
"legacy-28",
|
||||
"legacy-29",
|
||||
"legacy-31",
|
||||
"legacy-32",
|
||||
"legacy-33",
|
||||
"legacy-34",
|
||||
"legacy-36",
|
||||
"legacy-37",
|
||||
"legacy-38",
|
||||
"legacy-39",
|
||||
"legacy-40",
|
||||
"legacy-41",
|
||||
"legacy-42",
|
||||
"legacy-43",
|
||||
"legacy-44",
|
||||
"legacy-45",
|
||||
"legacy-46",
|
||||
"legacy-47",
|
||||
"legacy-48",
|
||||
"legacy-49",
|
||||
"legacy-50",
|
||||
"legacy-51",
|
||||
"legacy-52",
|
||||
"legacy-53",
|
||||
"legacy-54",
|
||||
"legacy-55",
|
||||
"legacy-56",
|
||||
"legacy-57",
|
||||
"legacy-59",
|
||||
"legacy-62",
|
||||
"legacy-65",
|
||||
"legacy-66",
|
||||
"legacy-67"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~u1ad2931c-eb80-8098-8005-b86c1d9d26c2",
|
||||
"~:created-at": "~m1739365911709",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u5b7ebd2b-2907-80db-8005-b9d67c20cf2f"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u5b7ebd2b-2907-80db-8005-b9d67c20cf2f": {
|
||||
"~#penpot/pointer": [
|
||||
"~u5b7ebd2b-2907-80db-8005-b9d67c21cbd3",
|
||||
{
|
||||
"~:created-at": "~m1739365911687"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
|
||||
"~:options": {
|
||||
"~:components-v2": true
|
||||
},
|
||||
"~:colors": {
|
||||
"~u84a1567d-3f0f-804e-8005-b9d6907e3c8a": {
|
||||
"~:path": "",
|
||||
"~:color": "#0087ff",
|
||||
"~:name": "#0087ff",
|
||||
"~:modified-at": "~m1739365936355",
|
||||
"~:opacity": 1,
|
||||
"~:id": "~u84a1567d-3f0f-804e-8005-b9d6907e3c8a"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
frontend/playwright/data/workspace/get-file-fragment-10113.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c21cbd3",
|
||||
"~:file-id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
|
||||
"~:created-at": "~m1739365911680",
|
||||
"~:data": {
|
||||
"~:options": {},
|
||||
"~:objects": {
|
||||
"~u00000000-0000-0000-0000-000000000000": {
|
||||
"~#shape": {
|
||||
"~:y": 0,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:name": "Root Frame",
|
||||
"~:width": 0.01,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.01
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 0,
|
||||
"~:proportion": 1.0,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 0,
|
||||
"~:y": 0,
|
||||
"~:width": 0.01,
|
||||
"~:height": 0.01,
|
||||
"~:x1": 0,
|
||||
"~:y1": 0,
|
||||
"~:x2": 0.01,
|
||||
"~:y2": 0.01
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 0.01,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2f",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"~:id": "~u5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
|
||||
"~:name": "10113 - Emtpy lib",
|
||||
"~:is-shared": true
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export class DashboardPage extends BaseWebSocketPage {
|
||||
|
||||
this.draftsLink = this.sidebar.getByText("Drafts");
|
||||
this.fontsLink = this.sidebar.getByText("Fonts");
|
||||
this.libsLink = this.sidebar.getByText("Libraries");
|
||||
this.librariesLink = this.sidebar.getByText("Libraries");
|
||||
|
||||
this.searchButton = page.getByRole("button", { name: "dashboard-search" });
|
||||
this.searchInput = page.getByPlaceholder("Search…");
|
||||
@@ -155,6 +155,9 @@ export class DashboardPage extends BaseWebSocketPage {
|
||||
await this.mockRPC("search-files", "dashboard/search-files.json", {
|
||||
method: "POST",
|
||||
});
|
||||
await this.mockRPC("delete-team", "dashboard/delete-team.json", {
|
||||
method: "POST",
|
||||
});
|
||||
await this.mockRPC("search-files", "dashboard/search-files.json");
|
||||
await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json");
|
||||
}
|
||||
@@ -278,6 +281,13 @@ export class DashboardPage extends BaseWebSocketPage {
|
||||
|
||||
await this.userProfileOption.click();
|
||||
}
|
||||
|
||||
async goToLibraries() {
|
||||
await this.page.goto(
|
||||
`#/dashboard/libraries?team-id=${DashboardPage.anyTeamId}`,
|
||||
);
|
||||
await expect(this.mainHeading).toHaveText("Libraries");
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
|
||||
@@ -70,6 +70,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
);
|
||||
this.toolbarOptions = page.getByTestId("toolbar-options");
|
||||
this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
|
||||
this.moveButton = page.getByRole("button", { name: "Move (V)" });
|
||||
this.boardButton = page.getByRole("button", { name: "Board (B)" });
|
||||
this.toggleToolbarButton = page.getByRole("button", {
|
||||
name: "Toggle toolbar",
|
||||
@@ -221,7 +222,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async openLibrariesModal(clickOptions = {}) {
|
||||
await this.sidebar.getByText("Libraries").click(clickOptions);
|
||||
await this.sidebar.getByTestId("libraries").click(clickOptions);
|
||||
await expect(this.librariesModal).toBeVisible();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ test("Bug 7549 - User clicks on color swatch to display the color picker next to
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
|
||||
await workspacePage.goToWorkspace();
|
||||
await workspacePage.moveButton.click();
|
||||
const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" });
|
||||
const swatchBox = await swatch.boundingBox();
|
||||
await swatch.click();
|
||||
@@ -171,6 +172,7 @@ test("Bug 9900 - Color picker has no inputs for HSV values", async ({
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
|
||||
await workspacePage.goToWorkspace();
|
||||
await workspacePage.moveButton.click();
|
||||
const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" });
|
||||
await swatch.click();
|
||||
|
||||
|
||||
33
frontend/playwright/ui/specs/dashboard-libraries.spec.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import DashboardPage from "../pages/DashboardPage";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await DashboardPage.init(page);
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in-no-onboarding.json",
|
||||
);
|
||||
});
|
||||
|
||||
test("BUG 10421 - Fix libraries context menu", async ({ page }) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.mockRPC(
|
||||
"get-team-shared-files?team-id=*",
|
||||
"dashboard/get-team-shared-files-10142.json",
|
||||
);
|
||||
|
||||
await dashboardPage.mockRPC(
|
||||
"get-all-projects",
|
||||
"dashboard/get-all-projects.json",
|
||||
);
|
||||
|
||||
await dashboardPage.goToLibraries();
|
||||
|
||||
const libraryItem = page.getByTitle(/Lorem Ipsum/);
|
||||
|
||||
await expect(libraryItem).toBeVisible();
|
||||
await libraryItem.getByRole("button", { name: "Options" }).click();
|
||||
|
||||
await expect(page.getByText("Rename")).toBeVisible();
|
||||
});
|
||||
@@ -84,6 +84,33 @@ test("User has context menu options for edit file", async ({ page }) => {
|
||||
await expect(dashboardPage.page.getByText("delete")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Multiple elements in context", async ({ page }) => {
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-all-projects",
|
||||
"dashboard/get-all-projects.json",
|
||||
);
|
||||
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDrafts();
|
||||
await dashboardPage.goToDrafts();
|
||||
|
||||
const button = dashboardPage.page.getByRole("button", { name: /New File 1/ });
|
||||
await button.click();
|
||||
|
||||
const button2 = dashboardPage.page.getByRole("button", {
|
||||
name: /New File 2/,
|
||||
});
|
||||
await button2.click({ modifiers: ["Shift"] });
|
||||
|
||||
await button.click({ button: "right" });
|
||||
|
||||
await expect(page.getByTestId("duplicate-multi")).toBeVisible();
|
||||
await expect(page.getByTestId("file-move-multi")).toBeVisible();
|
||||
await expect(page.getByTestId("file-binary-export-multi")).toBeVisible();
|
||||
await expect(page.getByTestId("file-delete-multi")).toBeVisible();
|
||||
});
|
||||
|
||||
test("User has create file button", async ({ page }) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDrafts();
|
||||
@@ -131,3 +158,31 @@ test("Bug 9927, Don't show the banner to invite team members if the user has dis
|
||||
await expect(page.getByText("Second team")).toBeVisible();
|
||||
await expect(page.getByText("Team Up")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Bug 10141, The team does not disappear from the team list after deletion", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"logged-in-user/get-teams-complete-owner.json",
|
||||
);
|
||||
await dashboardPage.goToDashboard();
|
||||
await dashboardPage.teamDropdown.click();
|
||||
await expect(page.getByText("Second Team")).toBeVisible();
|
||||
await page.getByText("Second Team").click();
|
||||
await page.getByRole("button", { name: "team-management" }).click();
|
||||
await page.getByTestId("delete-team").click();
|
||||
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"logged-in-user/get-teams-default.json",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Delete team" }).click();
|
||||
await dashboardPage.teamDropdown.click();
|
||||
await expect(page.getByText("Second Team")).not.toBeVisible();
|
||||
});
|
||||
|
||||
@@ -72,3 +72,41 @@ test("Bug 9056 - 'More info' doesn't open the update tab", async ({ page }) => {
|
||||
/library updates/i,
|
||||
);
|
||||
});
|
||||
|
||||
test("Bug 10113 - Empty library modal for non-empty library", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page);
|
||||
|
||||
await workspace.setupEmptyFile(page);
|
||||
await workspace.mockRPC(/get\-file\?/, "workspace/get-file-10113.json");
|
||||
await workspace.mockRPC(
|
||||
"get-file-fragment?file-id=*&fragment-id=*",
|
||||
"workspace/get-file-fragment-10113.json",
|
||||
);
|
||||
await workspace.mockRPC(/get\-file\?/, "workspace/get-file-10113.json");
|
||||
await workspace.mockRPC(
|
||||
"get-team-shared-files?team-id=*",
|
||||
"workspace/get-team-shared-files-empty.json",
|
||||
);
|
||||
await workspace.mockRPC(
|
||||
"set-file-shared",
|
||||
"workspace/set-file-shared-10113.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
fileId: "5b7ebd2b-2907-80db-8005-b9d67c20cf2e",
|
||||
pageId: "5b7ebd2b-2907-80db-8005-b9d67c20cf2f",
|
||||
});
|
||||
|
||||
await workspace.clickAssets();
|
||||
await workspace.openLibrariesModal();
|
||||
|
||||
await workspace.librariesModal
|
||||
.getByRole("button", { name: "Publish" })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
workspace.page.getByText("Publish empty library"),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
@@ -15,6 +15,17 @@ test("User loads worskpace with empty file", async ({ page }) => {
|
||||
await expect(workspacePage.pageName).toHaveText("Page 1");
|
||||
});
|
||||
|
||||
test("User opens a file with a bad page id", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
pageId: "badpage",
|
||||
});
|
||||
|
||||
await expect(workspacePage.pageName).toHaveText("Page 1");
|
||||
});
|
||||
|
||||
test("User receives presence notifications updates in the workspace", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -161,6 +172,49 @@ test("User adds a library and its automatically selected in the color palette",
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.goToWorkspace();
|
||||
await workspacePage.moveButton.click();
|
||||
|
||||
await workspacePage.page.keyboard.press("Alt+p");
|
||||
|
||||
await expect(
|
||||
workspacePage.palette.getByText(
|
||||
"There are no color styles in your library yet",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "#E8E9EA" }).click();
|
||||
await expect(page.getByTestId("colorpicker")).toBeVisible();
|
||||
const handler = await page.getByTestId("ramp-handler");
|
||||
await expect(handler).toBeVisible();
|
||||
const saturation_selection = await page.getByTestId(
|
||||
"value-saturation-selector",
|
||||
);
|
||||
await expect(saturation_selection).toBeVisible();
|
||||
const saturation_box = await saturation_selection.boundingBox();
|
||||
const handler_box = await handler.boundingBox();
|
||||
await page.mouse.move(
|
||||
handler_box.x + handler_box.width,
|
||||
handler_box.y + handler_box.height / 2,
|
||||
);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(
|
||||
saturation_box.x + saturation_box.width / 2,
|
||||
saturation_box.y + saturation_box.height / 2,
|
||||
);
|
||||
await page.mouse.up();
|
||||
await expect(
|
||||
workspacePage.palette.getByText(
|
||||
"There are no color styles in your library yet",
|
||||
),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-shortcut", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -47,7 +47,7 @@ test("User goes to an empty libraries page", async ({ page }) => {
|
||||
await dashboardPage.setupLibrariesEmpty();
|
||||
|
||||
await dashboardPage.goToDashboard();
|
||||
await dashboardPage.libsLink.click();
|
||||
await dashboardPage.librariesLink.click();
|
||||
|
||||
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
||||
await expect(dashboardPage.page).toHaveScreenshot();
|
||||
@@ -100,7 +100,7 @@ test("User goes to a full library page", async ({ page }) => {
|
||||
await dashboardPage.setupDashboardFull();
|
||||
|
||||
await dashboardPage.goToDashboard();
|
||||
await dashboardPage.libsLink.click();
|
||||
await dashboardPage.librariesLink.click();
|
||||
|
||||
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
||||
await expect(dashboardPage.page).toHaveScreenshot();
|
||||
|
||||
BIN
frontend/resources/images/features/2.5-copy.gif
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
frontend/resources/images/features/2.5-gradients.gif
Normal file
|
After Width: | Height: | Size: 697 KiB |
BIN
frontend/resources/images/features/2.5-link.gif
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
frontend/resources/images/features/2.5-mention.gif
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
frontend/resources/images/features/2.5-slide-0.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
10
frontend/resources/styles/common/dependencies/storybook.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
.sb-show-main.sb-main-fullscreen,
|
||||
.sb-show-main.sb-main-padded {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -215,7 +215,7 @@
|
||||
--menu-shortcut-foreground-color: var(--color-foreground-secondary);
|
||||
--menu-shortcut-foreground-color-selected: var(--color-foreground-primary);
|
||||
--menu-shortcut-foreground-color-hover: var(--color-foreground-primary);
|
||||
--menu-shadow-color: var(--color-shadow);
|
||||
--menu-shadow-color: var(--color-shadow-dark);
|
||||
--menu-background-color-disabled: var(--color-background-primary);
|
||||
--menu-foreground-color-disabled: var(--color-foreground-secondary);
|
||||
--menu-border-color-disabled: var(--color-background-quaternary);
|
||||
|
||||