Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
|
||||
44
CHANGES.md
@@ -1,6 +1,38 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.5.0 (Unreleased)
|
||||
## 2.5.2
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :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
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Improve Nginx entryponit to get the resolvers dinamically by default
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
## 2.5.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
@@ -50,8 +82,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 +97,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
|
||||
|
||||
@@ -384,8 +384,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 +541,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 +552,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]
|
||||
@@ -686,7 +688,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,36 @@
|
||||
|
||||
(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 [ids-map
|
||||
(into {}
|
||||
(comp
|
||||
(map #(get 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))
|
||||
|
||||
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]
|
||||
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 copy-cells main-cells omit-touched?)))))
|
||||
{: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"
|
||||
|
||||
@@ -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,16 @@
|
||||
"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
|
||||
|
||||
(if omit-touched?
|
||||
(letfn [(get-data [cells id]
|
||||
(dissoc (get cells id) :shapes :row :column :row-span :column-span))]
|
||||
(dissoc (get cells id) :row :column :row-span :column-span))
|
||||
|
||||
(merge-cells [source-cell target-cell]
|
||||
(-> source-cell
|
||||
(d/patch-object
|
||||
(dissoc target-cell :shapes :row :column :row-span :column-span))
|
||||
(cond-> (d/not-empty? (:shapes target-cell))
|
||||
(assoc :shapes (:shapes target-cell)))))]
|
||||
(let [deleted-cells
|
||||
(into #{}
|
||||
(filter #(not (contains? source-cells %)))
|
||||
@@ -1664,5 +1669,6 @@
|
||||
(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">
|
||||
|
||||
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
|
||||
|
||||
@@ -195,23 +195,23 @@ If you want to stop running Penpot, just type
|
||||
docker compose -p penpot -f docker-compose.yaml down
|
||||
```
|
||||
|
||||
|
||||
### Configure Penpot with Docker
|
||||
|
||||
The configuration is defined using environment variables in the <code class="language-bash">docker-compose.yaml</code>
|
||||
file. The default downloaded file already comes with the essential variables already set,
|
||||
The configuration is defined using flags and environment variables in the <code class="language-bash">docker-compose.yaml</code>
|
||||
file. The default downloaded file comes with the essential flags and variables already set,
|
||||
and other ones commented out with some explanations.
|
||||
|
||||
#### Create users using CLI
|
||||
You can find all configuration options in the [Configuration][1] section.
|
||||
|
||||
By default (or when <code class="language-bash">disable-email-verification</code> flag is used), the email verification process
|
||||
is completely disabled for new registrations but it is highly recommended enabling email
|
||||
verification or disabling registration if you are going to expose your penpot instance to
|
||||
the internet.
|
||||
### Using the CLI for administrative tasks
|
||||
|
||||
Penpot provides a script (`manage.py`) with some administrative tasks to perform in the server.
|
||||
|
||||
If you have registration disabled, you can create additional profiles using the
|
||||
command line interface:
|
||||
**NOTE**: this script will only work with the <code class="language-bash">enable-prepl-server</code>
|
||||
flag set in the docker-compose.yaml file. For older versions of docker-compose.yaml file,
|
||||
this flag is set in the backend service.
|
||||
|
||||
For instance, if the registration is disabled, the only way to create a new user is with this script:
|
||||
|
||||
```bash
|
||||
docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile
|
||||
@@ -221,12 +221,6 @@ docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile
|
||||
For example it could be <code class="language-bash">penpot-penpot-backend-1</code> or <code class="language-bash">penpot_penpot-backend-1</code>.
|
||||
You can check the correct name executing <code class="language-bash">docker ps</code>.
|
||||
|
||||
**NOTE:** This script only will works when you properly have the <code class="language-bash">enable-prepl-server</code>
|
||||
flag set on backend (is set by default on the latest docker-compose.yaml file)
|
||||
|
||||
You can find all configuration options in the [Configuration][1] section.
|
||||
|
||||
|
||||
### Update Penpot
|
||||
|
||||
To get the latest version of Penpot in your local installation, you just need to
|
||||
@@ -286,6 +280,126 @@ Postgres database and another one for the assets uploaded by your users (images
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
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.
|
||||
|
||||
If your Penpot installation is not working as intended, there are several places to look up searching for hints:
|
||||
|
||||
**Docker 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
|
||||
```
|
||||
|
||||
**Browser logs**
|
||||
|
||||
The browser provides as well useful information to corner the issue.
|
||||
|
||||
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.
|
||||
|
||||
<figure>
|
||||
<a href="/img/dev-tools-1.png" target="_blank">
|
||||
<img src="/img/dev-tools-1.png" alt="Devtools > Console" />
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
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.
|
||||
|
||||
<figure>
|
||||
<a href="/img/dev-tools-2.png" target="_blank">
|
||||
<img src="/img/dev-tools-2.png" alt="Devtools > Network" />
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
**Penpot Report**
|
||||
|
||||
When Penpot crashes, it provides a report with very useful information. Don't miss it!
|
||||
|
||||
<figure>
|
||||
<a href="/img/penpot-report.png" target="_blank">
|
||||
<img src="/img/penpot-report.png" alt="Penpot report" />
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
## Install with Kubernetes
|
||||
|
||||
@@ -297,7 +411,6 @@ 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
|
||||
|
||||
@@ -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>
|
||||
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);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@import "common/dependencies/fonts";
|
||||
@import "common/dependencies/animations";
|
||||
@import "common/dependencies/highlight.scss";
|
||||
@import "common/dependencies/storybook.scss";
|
||||
|
||||
@import "common/refactor/themes.scss";
|
||||
@import "common/refactor/design-tokens.scss";
|
||||
|
||||
@@ -63,20 +63,11 @@
|
||||
:browser
|
||||
:webworker))
|
||||
|
||||
(def default-flags
|
||||
[:enable-onboarding
|
||||
:enable-onboarding-team
|
||||
:enable-onboarding-questions
|
||||
:enable-onboarding-newsletter
|
||||
:enable-dashboard-templates-section
|
||||
:enable-google-fonts-provider
|
||||
:enable-component-thumbnails])
|
||||
|
||||
(defn- parse-flags
|
||||
[global]
|
||||
(let [flags (obj/get global "penpotFlags" "")
|
||||
flags (sequence (map keyword) (str/words flags))]
|
||||
(flags/parse flags/default default-flags flags)))
|
||||
(flags/parse flags/default flags)))
|
||||
|
||||
(defn- parse-version
|
||||
[global]
|
||||
@@ -105,8 +96,8 @@
|
||||
(def browser (parse-browser))
|
||||
(def platform (parse-platform))
|
||||
|
||||
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" "https://penpot.app/terms"))
|
||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" "https://penpot.app/privacy"))
|
||||
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI"))
|
||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI"))
|
||||
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
(def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot.app/penpothub/plugins"))
|
||||
|
||||
@@ -90,7 +90,6 @@
|
||||
(update :comments-local assoc :open id))
|
||||
(update :comments-local assoc :options nil)
|
||||
(update :comments-local dissoc :draft)
|
||||
(update :workspace-drawing dissoc :comment)
|
||||
(update-in [:comments id] assoc (:id comment) comment))))
|
||||
|
||||
ptk/WatchEvent
|
||||
@@ -146,7 +145,6 @@
|
||||
(update :comments-local assoc :open id)
|
||||
(update :comments-local assoc :options nil)
|
||||
(update :comments-local dissoc :draft)
|
||||
(update :workspace-drawing dissoc :comment)
|
||||
(update-in [:comments id] assoc (:id comment) comment))))
|
||||
|
||||
ptk/WatchEvent
|
||||
@@ -415,8 +413,8 @@
|
||||
(->> (rp/cmd! :get-comment-threads {:file-id file-id :share-id share-id})
|
||||
(rx/map comment-threads-fetched))
|
||||
|
||||
;; Refresh team members
|
||||
(rx/of (dtm/fetch-members)))))))
|
||||
(when (:workspace-local state)
|
||||
(rx/of (dtm/fetch-members))))))))
|
||||
|
||||
(defn retrieve-comments
|
||||
[thread-id]
|
||||
@@ -474,7 +472,7 @@
|
||||
(-> state
|
||||
(update :comments-local assoc :open id)
|
||||
(update :comments-local assoc :options nil)
|
||||
(update :workspace-drawing dissoc :comment)))))
|
||||
(update :comments-local dissoc :draft)))))
|
||||
|
||||
(defn close-thread
|
||||
[]
|
||||
@@ -482,8 +480,7 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :comments-local dissoc :open :draft :options)
|
||||
(update :workspace-drawing dissoc :comment)))))
|
||||
(update :comments-local dissoc :open :draft :options)))))
|
||||
|
||||
(defn update-filters
|
||||
[{:keys [mode show list] :as params}]
|
||||
@@ -524,7 +521,6 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-drawing assoc :comment params)
|
||||
(update :comments-local assoc :draft params)))))
|
||||
|
||||
(defn update-draft-thread
|
||||
@@ -533,7 +529,6 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(d/update-in-when [:workspace-drawing :comment] merge data)
|
||||
(d/update-in-when [:comments-local :draft] merge data)))))
|
||||
|
||||
(defn toggle-comment-options
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
:controls :inline-actions
|
||||
:type :inline
|
||||
:level level
|
||||
:accept {:label (tr "Refresh")
|
||||
:accept {:label (tr "labels.refresh")
|
||||
:callback force-reload!}
|
||||
:tag :notification))
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.websocket :as dws]
|
||||
[app.main.features :as features]
|
||||
@@ -186,8 +187,8 @@
|
||||
(ptk/reify ::show-file-menu-with-position
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :dashboard-local
|
||||
assoc :menu-open true
|
||||
(update state :dashboard-local assoc
|
||||
:menu-open true
|
||||
:menu-pos pos
|
||||
:file-id file-id))))
|
||||
|
||||
@@ -247,10 +248,10 @@
|
||||
(ptk/reify ::create-project
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [projects (get state :projects)
|
||||
(let [team-id (:current-team-id state)
|
||||
projects (dsh/lookup-team-projects state team-id)
|
||||
unames (cfh/get-used-names projects)
|
||||
name (cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))
|
||||
team-id (:current-team-id state)
|
||||
params {:name name
|
||||
:team-id team-id}
|
||||
{:keys [on-success on-error]
|
||||
@@ -459,10 +460,11 @@
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:files id] file)
|
||||
(assoc-in [:recent-files id] file)
|
||||
(update-in [:projects project-id :count] inc)))))
|
||||
(let [file (dissoc file :data)]
|
||||
(-> state
|
||||
(assoc-in [:files id] file)
|
||||
(assoc-in [:recent-files id] file)
|
||||
(update-in [:projects project-id :count] inc))))))
|
||||
|
||||
(defn create-file
|
||||
[{:keys [project-id name] :as params}]
|
||||
@@ -478,7 +480,7 @@
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
|
||||
files (get state :files)
|
||||
files (dsh/lookup-team-files state)
|
||||
unames (cfh/get-used-names files)
|
||||
name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
|
||||
features (-> (features/get-team-enabled-features state)
|
||||
@@ -587,10 +589,10 @@
|
||||
pparams (:path-params route)
|
||||
in-project? (contains? pparams :project-id)
|
||||
name (if in-project?
|
||||
(let [files (get state :files)
|
||||
(let [files (dsh/lookup-team-files state team-id)
|
||||
unames (cfh/get-used-names files)]
|
||||
(cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
|
||||
(let [projects (get state :projects)
|
||||
(let [projects (dsh/lookup-team-projects state team-id)
|
||||
unames (cfh/get-used-names projects)]
|
||||
(cfh/generate-unique-name unames (str (tr "dashboard.new-project-prefix") " 1"))))
|
||||
params (if in-project?
|
||||
|
||||
@@ -168,3 +168,21 @@
|
||||
[state]
|
||||
(when-let [{:keys [x y width height]} (get-in state [:workspace-local :vbox])]
|
||||
(gpt/point (+ x (/ width 2)) (+ y (/ height 2)))))
|
||||
|
||||
(defn lookup-team-files
|
||||
([state]
|
||||
(lookup-team-files state (:current-team-id state)))
|
||||
([state team-id]
|
||||
(->> state
|
||||
:files
|
||||
(filter #(= team-id (:team-id (val %))))
|
||||
(into {}))))
|
||||
|
||||
(defn lookup-team-projects
|
||||
([state]
|
||||
(lookup-team-projects (:current-team-id state)))
|
||||
([state team-id]
|
||||
(->> state
|
||||
:projects
|
||||
(filter #(= team-id (:team-id (val %))))
|
||||
(into {}))))
|
||||
|
||||
@@ -51,15 +51,12 @@
|
||||
[:label :string]
|
||||
[:callback ::sm/fn]]]]])
|
||||
|
||||
(def ^:private valid-notification?
|
||||
(sm/validator schema:notification))
|
||||
(def ^:private check-notification
|
||||
(sm/check-fn schema:notification))
|
||||
|
||||
(defn show
|
||||
[data]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid notification map"
|
||||
(valid-notification? data))
|
||||
(assert (check-notification data) "expected valid notification map")
|
||||
|
||||
(ptk/reify ::show
|
||||
ptk/UpdateEvent
|
||||
@@ -68,12 +65,16 @@
|
||||
(assoc state :notification notification)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(watch [_ state stream]
|
||||
(rx/merge
|
||||
(let [stopper (rx/filter (ptk/type? ::hide) stream)]
|
||||
(let [stopper (rx/filter (ptk/type? ::hide) stream)
|
||||
route-id (dm/get-in state [:route :data :name])]
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? :app.main.router/navigate))
|
||||
(rx/map (fn [_] (hide)))
|
||||
(rx/map deref)
|
||||
(rx/filter #(not= route-id (:id %)))
|
||||
(rx/map hide)
|
||||
(rx/take-until stopper)))
|
||||
(when (:timeout data)
|
||||
(let [stopper (rx/filter (ptk/type? ::show) stream)]
|
||||
|
||||
@@ -14,11 +14,12 @@
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.team :as-alias dtm]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.plugins.register :as plugins.register]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.storage :as storage]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
@@ -72,6 +73,7 @@
|
||||
(def profile-fetched?
|
||||
(ptk/type? ::profile-fetched))
|
||||
|
||||
;; FIXME: make it as general purpose handler, not only on profile
|
||||
(defn- on-fetch-profile-exception
|
||||
[cause]
|
||||
(let [data (ex-data cause)]
|
||||
@@ -238,25 +240,24 @@
|
||||
[:email-comments [::sm/one-of #{:all :partial :none}]]
|
||||
[:email-invites [::sm/one-of #{:all :none}]]])
|
||||
|
||||
(def ^:private check-update-notifications-params
|
||||
(sm/check-fn schema:update-notifications))
|
||||
|
||||
(defn update-notifications
|
||||
[data]
|
||||
(dm/assert!
|
||||
"expected valid parameters"
|
||||
(sm/check schema:update-notifications data))
|
||||
|
||||
(assert (check-update-notifications-params data))
|
||||
(ptk/reify ::update-notifications
|
||||
ev/Event
|
||||
(-data [_] {})
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:profile :props] assoc :notifications data))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-error on-success]
|
||||
:or {on-error identity
|
||||
on-success identity}} (meta data)]
|
||||
(->> (rp/cmd! :update-profile-notifications data)
|
||||
(rx/tap on-success)
|
||||
(rx/catch #(do (on-error %) (rx/empty)))
|
||||
(rx/ignore))))))
|
||||
(->> (rp/cmd! :update-profile-notifications data)
|
||||
(rx/map #(ntf/success (tr "dashboard.notifications.notifications-saved")))))))
|
||||
|
||||
(defn update-profile-props
|
||||
[props]
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
(dissoc state :current-project-id)
|
||||
state)))))
|
||||
|
||||
|
||||
(defn- files-fetched
|
||||
[project-id files]
|
||||
(ptk/reify ::files-fetched
|
||||
@@ -67,14 +66,14 @@
|
||||
(assoc project :count (count files))))))))
|
||||
|
||||
(defn fetch-files
|
||||
[project-id]
|
||||
(assert (uuid? project-id) "expected valid uuid for `project-id`")
|
||||
(ptk/reify ::fetch-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-project-files {:project-id project-id})
|
||||
(rx/map (partial files-fetched project-id))))))
|
||||
|
||||
([] (fetch-files nil))
|
||||
([project-id]
|
||||
(ptk/reify ::fetch-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [project-id (or project-id (:current-project-id state))]
|
||||
(->> (rp/cmd! :get-project-files {:project-id project-id})
|
||||
(rx/map (partial files-fetched project-id))))))))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as log]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.team :as ctt]
|
||||
@@ -118,8 +119,10 @@
|
||||
(let [team-id (:current-team-id state)
|
||||
teams (get state :teams)
|
||||
team (get teams team-id)]
|
||||
(rx/of (set-current-team team)
|
||||
(fetch-members))))))
|
||||
(if (not team)
|
||||
(rx/throw (ex/error :type :authentication))
|
||||
(rx/of (set-current-team team)
|
||||
(fetch-members)))))))
|
||||
|
||||
(defn initialize-team
|
||||
[team-id]
|
||||
@@ -224,26 +227,6 @@
|
||||
(->> (rp/cmd! :get-webhooks {:team-id team-id})
|
||||
(rx/map (partial webhooks-fetched team-id)))))))
|
||||
|
||||
(defn- shared-files-fetched
|
||||
[files]
|
||||
(ptk/reify ::shared-files-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [files (d/index-by :id files)]
|
||||
(assoc state :shared-files files)))))
|
||||
|
||||
(defn fetch-shared-files
|
||||
"Event mainly used for fetch a list of shared libraries for a team,
|
||||
this list does not includes the content of the library per se. It
|
||||
is used mainly for show available libraries and a summary of it."
|
||||
[]
|
||||
(ptk/reify ::fetch-shared-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
|
||||
(rx/map shared-files-fetched))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Modification
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -474,6 +457,13 @@
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn- team-deleted
|
||||
[id]
|
||||
(ptk/reify ::team-deleted
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :teams dissoc id))))
|
||||
|
||||
(defn delete-team
|
||||
[{:keys [id] :as params}]
|
||||
(ptk/reify ::delete-team
|
||||
@@ -485,7 +475,10 @@
|
||||
(meta params)]
|
||||
|
||||
(->> (rp/cmd! :delete-team {:id id})
|
||||
(rx/mapcat on-success)
|
||||
(rx/mapcat (fn [result]
|
||||
(rx/concat
|
||||
(rx/of (team-deleted id))
|
||||
(on-success result))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(defn delete-webhook
|
||||
@@ -554,6 +547,25 @@
|
||||
(rx/of (fetch-webhooks)))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
(defn- shared-files-fetched
|
||||
[files]
|
||||
(ptk/reify ::shared-files-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [files (d/index-by :id files)]
|
||||
(update state :shared-files merge files)))))
|
||||
|
||||
(defn fetch-shared-files
|
||||
"Event mainly used for fetch a list of shared libraries for a team,
|
||||
this list does not includes the content of the library per se. It
|
||||
is used mainly for show available libraries and a summary of it."
|
||||
([] (fetch-shared-files nil))
|
||||
([team-id]
|
||||
(ptk/reify ::fetch-shared-files
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [team-id (or team-id (:current-team-id state))]
|
||||
(->> (rp/cmd! :get-team-shared-files {:team-id team-id})
|
||||
(rx/map shared-files-fetched)))))))
|
||||
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
(declare go-to-frame-auto)
|
||||
|
||||
(defn bundle-fetched
|
||||
[{:keys [project file share-links libraries users permissions thumbnails] :as bundle}]
|
||||
[{:keys [project file team share-links libraries users permissions thumbnails] :as bundle}]
|
||||
(let [pages (->> (dm/get-in file [:data :pages])
|
||||
(map (fn [page-id]
|
||||
(let [data (get-in file [:data :pages-index page-id])]
|
||||
@@ -183,22 +183,26 @@
|
||||
(ptk/reify ::bundle-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :share-links share-links)
|
||||
(assoc :viewer {:libraries (d/index-by :id libraries)
|
||||
:users (d/index-by :id users)
|
||||
:permissions permissions
|
||||
:project project
|
||||
:pages pages
|
||||
:thumbnails thumbnails
|
||||
:file file})))
|
||||
(let [team-id (:id team)
|
||||
team {:members users}]
|
||||
(-> state
|
||||
(assoc :share-links share-links)
|
||||
(assoc :current-team-id team-id)
|
||||
(assoc :teams {team-id team})
|
||||
(assoc :viewer {:libraries (d/index-by :id libraries)
|
||||
:users (d/index-by :id users)
|
||||
:permissions permissions
|
||||
:project project
|
||||
:pages pages
|
||||
:thumbnails thumbnails
|
||||
:file file}))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
index (:index qparams)
|
||||
frame-id (:frame-id qparams)]
|
||||
index (some-> (:index qparams) parse-long)
|
||||
frame-id (some-> (:frame-id qparams) uuid/parse)]
|
||||
(rx/merge
|
||||
(rx/of (case (:zoom qparams)
|
||||
"fit" zoom-to-fit
|
||||
@@ -206,7 +210,7 @@
|
||||
nil))
|
||||
(rx/of
|
||||
(cond
|
||||
(some? frame-id) (go-to-frame (uuid frame-id))
|
||||
(some? frame-id) (go-to-frame frame-id)
|
||||
(some? index) (go-to-frame-by-index index)
|
||||
:else (go-to-frame-auto)))))))))
|
||||
|
||||
@@ -520,8 +524,8 @@
|
||||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
index (:index qparams)
|
||||
page-id (some-> (:page-id qparams) uuid/parse)
|
||||
index (some-> (:index qparams) parse-long)
|
||||
frames (get-in state [:viewer :pages page-id :frames])
|
||||
frame (get frames index)]
|
||||
(cond-> state
|
||||
@@ -538,7 +542,7 @@
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
page-id (some-> (:page-id qparams) uuid/parse)
|
||||
frames (get-in state [:viewer :pages page-id :frames])
|
||||
index (d/index-of-pred frames #(= (:id %) frame-id))]
|
||||
(rx/of (go-to-frame-by-index (or index 0))))))))
|
||||
@@ -550,7 +554,7 @@
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
page-id (some-> (:page-id qparams) uuid/parse)
|
||||
flows (get-in state [:viewer :pages page-id :options :flows])]
|
||||
(if (seq flows)
|
||||
(let [frame-id (:starting-frame (first flows))]
|
||||
@@ -622,7 +626,7 @@
|
||||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
page-id (some-> (:page-id qparams) uuid/parse)
|
||||
frames (dm/get-in state [:viewer :pages page-id :all-frames])
|
||||
frame (d/seek #(= (:id %) frame-id) frames)
|
||||
overlays (:viewer-overlays state)]
|
||||
@@ -654,7 +658,7 @@
|
||||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
page-id (some-> (:page-id qparams) uuid/parse)
|
||||
frames (get-in state [:viewer :pages page-id :all-frames])
|
||||
frame (d/seek #(= (:id %) frame-id) frames)
|
||||
overlays (:viewer-overlays state)]
|
||||
@@ -718,7 +722,7 @@
|
||||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
page-id (some-> (:page-id qparams) uuid/parse)
|
||||
objects (get-in state [:viewer :pages page-id :objects])
|
||||
selection (-> state
|
||||
(get-in [:viewer-local :selected] #{})
|
||||
@@ -734,8 +738,8 @@
|
||||
(update [_ state]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
page-id (:page-id qparams)
|
||||
index (:index qparams)
|
||||
page-id (some-> (:page-id qparams) uuid/parse)
|
||||
index (some-> (:index qparams) parse-long)
|
||||
objects (get-in state [:viewer :pages page-id :objects])
|
||||
frame-id (get-in state [:viewer :pages page-id :frames index :id])
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
@@ -50,6 +51,7 @@
|
||||
[app.main.data.workspace.collapse :as dwco]
|
||||
[app.main.data.workspace.colors :as dwcl]
|
||||
[app.main.data.workspace.comments :as dwcm]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.edition :as dwe]
|
||||
[app.main.data.workspace.fix-broken-shapes :as fbs]
|
||||
@@ -108,9 +110,6 @@
|
||||
(declare ^:private workspace-initialized)
|
||||
(declare ^:private fetch-libraries)
|
||||
(declare ^:private libraries-fetched)
|
||||
(declare ^:private preload-data-uris)
|
||||
|
||||
;; (declare go-to-layout)
|
||||
|
||||
;; --- Initialize Workspace
|
||||
|
||||
@@ -170,11 +169,13 @@
|
||||
(assoc file :data (d/removem (comp t/pointer? val) data))))))))))
|
||||
|
||||
(defn- libraries-fetched
|
||||
[libraries]
|
||||
[file-id libraries]
|
||||
(ptk/reify ::libraries-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [libraries (d/index-by :id libraries)]
|
||||
(let [libraries (->> libraries
|
||||
(map (fn [l] (assoc l :library-of file-id)))
|
||||
(d/index-by :id))]
|
||||
(update state :files merge libraries)))
|
||||
|
||||
ptk/WatchEvent
|
||||
@@ -190,8 +191,8 @@
|
||||
libraries)]
|
||||
|
||||
(when needs-check?
|
||||
(rx/concat (rx/timer 1000)
|
||||
(rx/of (dwl/notify-sync-file file-id))))))))
|
||||
(->> (rx/of (dwl/notify-sync-file file-id))
|
||||
(rx/delay 1000)))))))
|
||||
|
||||
(defn- fetch-libraries
|
||||
[file-id]
|
||||
@@ -210,7 +211,7 @@
|
||||
(rx/map #(assoc % :synced-at synced-at)))))
|
||||
(rx/merge-map resolve-file)
|
||||
(rx/reduce conj [])
|
||||
(rx/map libraries-fetched))
|
||||
(rx/map (partial libraries-fetched file-id)))
|
||||
(->> (rx/from libraries)
|
||||
(rx/map :id)
|
||||
(rx/mapcat (fn [file-id]
|
||||
@@ -273,6 +274,15 @@
|
||||
(rx/of (dws/select-shapes frames-id)
|
||||
dwz/zoom-to-selected-shape)))))
|
||||
|
||||
(defn- select-frame-tool
|
||||
[file-id page-id]
|
||||
(ptk/reify ::select-frame-tool
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page (dsh/lookup-page state file-id page-id)]
|
||||
(when (ctp/is-empty? page)
|
||||
(rx/of (dwd/select-for-drawing :frame)))))))
|
||||
|
||||
(defn- fetch-bundle
|
||||
"Multi-stage file bundle fetch coordinator"
|
||||
[file-id]
|
||||
@@ -314,13 +324,10 @@
|
||||
(defn initialize-workspace
|
||||
[file-id]
|
||||
(assert (uuid? file-id) "expected valud uuid for `file-id`")
|
||||
|
||||
(ptk/reify ::initialize-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(dissoc :files)
|
||||
(dissoc :workspace-ready)
|
||||
(assoc :recent-colors (:recent-colors storage/user))
|
||||
(assoc :recent-fonts (:recent-fonts storage/user))
|
||||
(assoc :current-file-id file-id)
|
||||
@@ -387,7 +394,7 @@
|
||||
|
||||
(defn finalize-workspace
|
||||
[file-id]
|
||||
(ptk/reify ::finalize-file
|
||||
(ptk/reify ::finalize-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
@@ -395,11 +402,9 @@
|
||||
(dissoc
|
||||
:current-file-id
|
||||
:workspace-editor-state
|
||||
:files
|
||||
:workspace-media-objects
|
||||
:workspace-persistence
|
||||
:workspace-presence
|
||||
:workspace-ready
|
||||
:workspace-undo)
|
||||
(update :workspace-global dissoc :read-only?)
|
||||
(assoc-in [:workspace-global :options-mode] :design)))
|
||||
@@ -412,6 +417,7 @@
|
||||
(dpj/finalize-project project-id)
|
||||
(dwsl/finalize-shape-layout)
|
||||
(dwcl/stop-picker)
|
||||
(dwc/set-workspace-visited)
|
||||
(modal/hide)
|
||||
(ntf/hide))))))
|
||||
|
||||
@@ -426,46 +432,68 @@
|
||||
;; Make this event callable through dynamic resolution
|
||||
(defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file))
|
||||
|
||||
(defn initialize-page
|
||||
[page-id]
|
||||
(assert (uuid? page-id) "expected valid uuid for `page-id`")
|
||||
|
||||
(ptk/reify ::initialize-page
|
||||
|
||||
(def ^:private xf:collect-file-media
|
||||
"Resolve and collect all file media on page objects"
|
||||
(comp (map second)
|
||||
(keep (fn [{:keys [metadata fill-image]}]
|
||||
(cond
|
||||
(some? metadata) (cf/resolve-file-media metadata)
|
||||
(some? fill-image) (cf/resolve-file-media fill-image))))))
|
||||
|
||||
|
||||
(defn- initialize-page*
|
||||
"Second phase of page initialization, once we know the page is
|
||||
available on the sate"
|
||||
[file-id page-id page]
|
||||
(ptk/reify ::initialize-page*
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(if-let [{:keys [id] :as page} (dsh/lookup-page state page-id)]
|
||||
;; we maintain a cache of page state for user convenience with the exception of the
|
||||
;; selection; when user abandon the current page, the selection is lost
|
||||
(let [local (dm/get-in state [:workspace-cache id] default-workspace-local)]
|
||||
(-> state
|
||||
(assoc :current-page-id id)
|
||||
(assoc :workspace-local (assoc local :selected (d/ordered-set)))
|
||||
(assoc :workspace-trimmed-page (dm/select-keys page [:id :name]))
|
||||
;; selection; when user abandon the current page, the selection is lost
|
||||
(let [local (dm/get-in state [:workspace-cache [file-id page-id]] default-workspace-local)]
|
||||
(-> state
|
||||
(assoc :current-page-id page-id)
|
||||
(assoc :workspace-local (assoc local :selected (d/ordered-set)))
|
||||
(assoc :workspace-trimmed-page (dm/select-keys page [:id :name]))
|
||||
|
||||
;; FIXME: this should be done on `initialize-layout` (?)
|
||||
(update :workspace-layout layout/load-layout-flags)
|
||||
(update :workspace-global layout/load-layout-state)))
|
||||
;; FIXME: this should be done on `initialize-layout` (?)
|
||||
(update :workspace-layout layout/load-layout-flags)
|
||||
(update :workspace-global layout/load-layout-state))))
|
||||
|
||||
state))
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(let [uris (into #{} xf:collect-file-media (:objects page))]
|
||||
(->> (rx/from uris)
|
||||
(rx/subs! #(http/fetch-data-uri % false)))))))
|
||||
|
||||
(defn initialize-page
|
||||
[file-id page-id]
|
||||
(assert (uuid? file-id) "expected valid uuid for `file-id`")
|
||||
|
||||
(ptk/reify ::initialize-page
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)]
|
||||
(rx/of (preload-data-uris page-id)
|
||||
(if-let [page (dsh/lookup-page state file-id page-id)]
|
||||
(rx/of (initialize-page* file-id page-id page)
|
||||
(dwth/watch-state-changes file-id page-id)
|
||||
(dwl/watch-component-changes))))))
|
||||
(dwl/watch-component-changes)
|
||||
(select-frame-tool file-id page-id))
|
||||
(rx/of (dcm/go-to-workspace :file-id file-id ::rt/replace true))))))
|
||||
|
||||
(defn finalize-page
|
||||
[page-id]
|
||||
[file-id page-id]
|
||||
(assert (uuid? file-id) "expected valid uuid for `file-id`")
|
||||
(assert (uuid? page-id) "expected valid uuid for `page-id`")
|
||||
|
||||
(ptk/reify ::finalize-page
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [local (-> (:workspace-local state)
|
||||
(dissoc :edition :edit-path :selected))
|
||||
exit? (not= :workspace (dm/get-in state [:route :data :name]))
|
||||
exit? (not= :workspace (rt/lookup-name state))
|
||||
state (-> state
|
||||
(update :workspace-cache assoc page-id local)
|
||||
(update :workspace-cache assoc [file-id page-id] local)
|
||||
(dissoc :current-page-id
|
||||
:workspace-local
|
||||
:workspace-trimmed-page
|
||||
@@ -474,22 +502,6 @@
|
||||
(cond-> state
|
||||
exit? (dissoc :workspace-drawing))))))
|
||||
|
||||
(defn- preload-data-uris
|
||||
"Preloads the image data so it's ready when necessary"
|
||||
[page-id]
|
||||
(ptk/reify ::preload-data-uris
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [xform (comp (map second)
|
||||
(keep (fn [{:keys [metadata fill-image]}]
|
||||
(cond
|
||||
(some? metadata) (cf/resolve-file-media metadata)
|
||||
(some? fill-image) (cf/resolve-file-media fill-image)))))
|
||||
uris (into #{} xform (dsh/lookup-page-objects state page-id))]
|
||||
|
||||
(->> (rx/from uris)
|
||||
(rx/subs! #(http/fetch-data-uri % false)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Workspace Page CRUD
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.comments :as dcmt]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.event :as ev]
|
||||
@@ -22,7 +20,6 @@
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.edition :as dwe]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.viewport :as dwv]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.streams :as ms]
|
||||
@@ -118,7 +115,7 @@
|
||||
:page-id (:page-id thread)))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
||||
(rx/filter (ptk/type? ::dcmt/comment-threads-fetched))
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rx/of (center-to-comment-thread thread)
|
||||
(dwd/select-for-drawing :comments)
|
||||
@@ -126,38 +123,32 @@
|
||||
{::ev/origin "workspace"}))))))))
|
||||
|
||||
(defn update-comment-thread-position
|
||||
([thread [new-x new-y]]
|
||||
(update-comment-thread-position thread [new-x new-y] nil))
|
||||
([thread [new-x new-y]]
|
||||
(update-comment-thread-position thread [new-x new-y] nil))
|
||||
|
||||
([thread [new-x new-y] frame-id]
|
||||
([thread [new-x new-y] frame-id]
|
||||
(dm/assert!
|
||||
"expected valid comment thread"
|
||||
(dcmt/check-comment-thread! thread))
|
||||
(ptk/reify ::update-comment-thread-position
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(watch [_ state _]
|
||||
(let [page (dsh/lookup-page state)
|
||||
page-id (:id page)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
frame-id (if (nil? frame-id)
|
||||
(ctst/get-frame-id-by-position objects (gpt/point new-x new-y))
|
||||
(:frame-id thread))
|
||||
|
||||
thread (-> thread
|
||||
(assoc :position (gpt/point new-x new-y))
|
||||
(assoc :frame-id frame-id))
|
||||
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-page page)
|
||||
(pcb/set-comment-thread-position thread))]
|
||||
thread-id (:id thread)]
|
||||
|
||||
(rx/concat
|
||||
(rx/merge
|
||||
(rx/of (dch/commit-changes changes))
|
||||
(->> (rp/cmd! :update-comment-thread-position thread)
|
||||
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
|
||||
(rx/ignore)))
|
||||
(rx/of (dcmt/refresh-comment-thread thread))))))))
|
||||
(rx/of #(update % :comment-threads assoc thread-id thread))
|
||||
(->> (rp/cmd! :update-comment-thread-position thread)
|
||||
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
|
||||
(rx/ignore))))))))
|
||||
|
||||
;; Move comment threads that are inside a frame when that frame is moved"
|
||||
(defmethod ptk/resolve ::move-frame-comment-threads
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
(ns app.main.data.workspace.common
|
||||
(:require
|
||||
[app.common.logging :as log]
|
||||
[app.config :as cf]
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.data.workspace.layout :as dwl]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
@@ -29,6 +31,15 @@
|
||||
[e]
|
||||
(= e :interrupt))
|
||||
|
||||
(defn set-workspace-visited
|
||||
[]
|
||||
(ptk/reify ::set-workspace-visited
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile (:profile state)
|
||||
props (get profile :props)]
|
||||
(when (and (cf/external-feature-flag "boards-03" "test") (not (:workspace-visited props)))
|
||||
(rx/of (du/update-profile-props {:workspace-visited true})))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; UNDO
|
||||
|
||||
@@ -713,8 +713,10 @@
|
||||
|
||||
(defn go-to-component-file
|
||||
[file-id component]
|
||||
(dm/assert! (uuid? file-id))
|
||||
(dm/assert! (some? component))
|
||||
|
||||
(assert (uuid? file-id) "expected an uuid for `file-id`")
|
||||
(assert (ctk/check-component component) "expected a valid component")
|
||||
|
||||
(ptk/reify ::nav-to-component-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -722,8 +724,7 @@
|
||||
(assoc :file-id file-id)
|
||||
(assoc :page-id (:main-instance-page component))
|
||||
(assoc :component-id (:id component)))]
|
||||
(rx/of (rt/nav :workspace params :new-window? true))))))
|
||||
|
||||
(rx/of (rt/nav :workspace params ::rt/new-window true))))))
|
||||
|
||||
(defn go-to-local-component
|
||||
[& {:keys [id] :as options}]
|
||||
@@ -741,12 +742,12 @@
|
||||
redirect-to-page
|
||||
(fn [page-id shape-id]
|
||||
(rx/merge
|
||||
(rx/of (dcm/go-to-workspace :page-id page-id))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::initialize-page))
|
||||
(rx/filter (ptk/type? ::dw/initialize-page))
|
||||
(rx/take 1)
|
||||
(rx/observe-on :async)
|
||||
(rx/mapcat (fn [_] (select-and-zoom shape-id))))))]
|
||||
(rx/mapcat (fn [_] (select-and-zoom shape-id))))
|
||||
(rx/of (dcm/go-to-workspace :page-id page-id))))]
|
||||
|
||||
(when-let [component (dm/get-in data [:components id])]
|
||||
(let [page-id (:main-instance-page component)
|
||||
@@ -1190,18 +1191,17 @@
|
||||
(ptk/reify ::notify-sync-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file (dm/get-in state [:files file-id])
|
||||
(let [file (dsh/lookup-file state file-id)
|
||||
|
||||
file-data (get file :data)
|
||||
ignore-until (get file :ignore-sync-until)
|
||||
|
||||
|
||||
;; FIXME: syntax of this can be improved
|
||||
libraries-need-sync
|
||||
(filter #(seq (assets-need-sync % file-data ignore-until))
|
||||
(vals (get state :files)))
|
||||
|
||||
do-more-info
|
||||
#(modal/show! :libraries-dialog {:starting-tab "updates"})
|
||||
#(modal/show! :libraries-dialog {:starting-tab "updates" :file-id file-id})
|
||||
|
||||
do-update
|
||||
#(do (apply st/emit! (map (fn [library]
|
||||
@@ -1211,8 +1211,7 @@
|
||||
(st/emit! (ntf/hide)))
|
||||
|
||||
do-dismiss
|
||||
#(do (st/emit! ignore-sync)
|
||||
(st/emit! (ntf/hide)))]
|
||||
#(st/emit! ignore-sync (ntf/hide))]
|
||||
|
||||
(when (seq libraries-need-sync)
|
||||
(rx/of (ntf/dialog
|
||||
@@ -1389,7 +1388,10 @@
|
||||
(let [libraries (:workspace-shared-files state)
|
||||
library (d/seek #(= (:id %) library-id) libraries)]
|
||||
(if library
|
||||
(update state :files assoc library-id (dissoc library :library-summary))
|
||||
(update state :files assoc library-id
|
||||
(-> library
|
||||
(dissoc :library-summary)
|
||||
(assoc :library-of file-id)))
|
||||
state)))
|
||||
|
||||
ptk/WatchEvent
|
||||
@@ -1402,6 +1404,8 @@
|
||||
(->> (rp/cmd! :get-file {:id library-id :features features})
|
||||
(rx/merge-map fpmap/resolve-file)
|
||||
;; FIXME: this should call the libraries-fetched event instead of ad-hoc assoc event
|
||||
(rx/map (fn [file]
|
||||
(assoc file :library-of file-id)))
|
||||
(rx/map (fn [file]
|
||||
(fn [state]
|
||||
(assoc-in state [:files library-id] file)))))
|
||||
|
||||
@@ -587,11 +587,6 @@
|
||||
:subsections [:shape]
|
||||
:fn #(emit-when-no-readonly (dw/create-bool :exclude))}
|
||||
|
||||
:fit-content-selected {:tooltip (ds/meta-shift (ds/alt "R"))
|
||||
:command (ds/c-mod "shift+alt+r")
|
||||
:subsections [:shape]
|
||||
:fn #(emit-when-no-readonly (dwt/selected-fit-content))}
|
||||
|
||||
;; THEME
|
||||
:toggle-theme {:tooltip (ds/alt "M")
|
||||
:command (ds/a-mod "m")
|
||||
|
||||