Compare commits

...

122 Commits

Author SHA1 Message Date
Andrey Antukh
ee6350189f Merge remote-tracking branch 'origin/staging' into main 2021-09-13 12:54:44 +02:00
Andrés Moya
46189c0ff1 🐛 Fix wrong timeout in download artboards message 2021-09-13 12:52:14 +02:00
alonso.torres
45d55e87eb 🐛 Fix problem while moving imported SVG's 2021-09-13 11:56:01 +02:00
alonso.torres
8a158146cd 🐛 Fix problem with import broken images 2021-09-13 11:56:01 +02:00
Andrés Moya
fe6623b342 🐛 Fix interactions in viewer 2021-09-10 12:55:21 +02:00
Andrey Antukh
de8220245c Merge branch 'release-1.8-onboarding' into staging 2021-09-10 11:50:16 +02:00
elhombretecla
562f0d9872 🎉 Update 1.8 release onboarding info 2021-09-10 11:50:01 +02:00
elhombretecla
ed89f858e1 🎉 add new onboarding images 2021-09-10 11:50:01 +02:00
Andrey Antukh
5da2e5e7b7 🎉 Add Catalan language to the supporter languages list. 2021-09-10 10:56:23 +02:00
alonso.torres
22b45266bf 🐛 Fix problem with path not closing on escape 2021-09-09 15:08:47 +02:00
Andrey Antukh
b280b5a517 Merge pull request #1194 from penpot/fix-pdf-pages
Fix pdf pages
2021-09-09 14:27:24 +02:00
Andrés Moya
60cb358cce 🐛 Fix extra blank pages when exporting to PDF 2021-09-09 14:11:50 +02:00
Andrey Antukh
f03a74abc7 🐛 Fix next frame shortcut on viewer. 2021-09-09 12:05:15 +02:00
Andrey Antukh
34885b64bd 🐛 Fix style on viewer header. 2021-09-09 11:41:18 +02:00
elhombretecla
f3bfa4e587 Update CHANGES.md 2021-09-09 11:09:21 +02:00
Andrey Antukh
3136ce7dc2 Add missing frame index on viewer. 2021-09-09 11:07:47 +02:00
Andrey Antukh
85a1c61880 Improve 404 and add broken link static page on viewer. 2021-09-08 13:52:11 +02:00
Andrey Antukh
15991d0226 Merge pull request #1189 from penpot/sequential-export
 Change frame exports to be sequential
2021-09-08 13:48:12 +02:00
Andrés Moya
413bc41695 Change frame exports to be sequential 2021-09-08 13:11:32 +02:00
Andrey Antukh
36137808f0 📎 Sort translation strings. 2021-09-08 12:59:24 +02:00
Andrey Antukh
12c1852297 Merge remote-tracking branch 'weblate/develop' into translations 2021-09-08 12:54:20 +02:00
Andrey Antukh
95e3c3eafc 📎 Enable by default demo users. 2021-09-08 11:14:19 +02:00
Andrey Antukh
c458fa6441 📎 Update changelog. 2021-09-08 11:14:19 +02:00
Andrey Antukh
66c1e386ce 🐛 Fix style issues on share link dialog. 2021-09-08 11:14:19 +02:00
Andrey Antukh
59e203fd52 🐛 Fix messages z-index issue.
Happens when modals and messages are visible
in the same time.
2021-09-08 11:14:19 +02:00
Andrey Antukh
7e0c097f23 🎉 Add linter for check duplicte potok types. 2021-09-07 11:48:14 +02:00
Andrey Antukh
926fa483b9 Improve event registry. 2021-09-07 11:48:14 +02:00
Rubén
2ebc92a167 🌐 Add translations for: Spanish.
Currently translated at 98.8% (679 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2021-09-04 17:33:02 +02:00
Rubén
eb511757db 🌐 Add translations for: Catalan.
Currently translated at 99.7% (685 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2021-09-04 17:33:02 +02:00
alonso.torres
b5b97f7626 🐛 Fix problem with multiple selection conflicting with hover 2021-09-03 15:31:45 +02:00
Andrés Moya
ba0f7416bb Add some user feedback while exporting artboards 2021-09-03 15:13:32 +02:00
Andrey Antukh
f6e18de6af Add more metrics to update-file. 2021-09-03 10:36:34 +02:00
Andrey Antukh
320a4552bc Merge pull request #1172 from penpot/export-artboards
🎉 Export to PDF all artboards of one page
2021-09-02 16:33:47 +02:00
Andrés Moya
203473c965 🎉 Export to PDF all artboards of one page 2021-09-02 15:21:33 +02:00
Andrés Moya
255177d12b 🐛 Fix linter error 2021-09-02 15:21:01 +02:00
Andrey Antukh
290bf00b2d Use compact transit encoding instead of verbose. 2021-09-02 14:29:53 +02:00
Andrey Antukh
8464e6a822 Disable response streaming.
Because it is really slow.
2021-09-02 14:17:12 +02:00
Andrey Antukh
8af46ac7fc Minor improvements on section initialization. 2021-09-02 09:45:43 +02:00
Andrey Antukh
daeaf14032 Merge pull request #1169 from penpot/bugfixing
Bugfixing
2021-08-31 17:09:15 +02:00
alonso.torres
bd52a7c926 🐛 Fix minor visual issue 2021-08-31 16:10:42 +02:00
alonso.torres
c8c43de510 🐛 Fix group renaming problem 2021-08-31 15:52:39 +02:00
alonso.torres
bb49071088 🐛 Fix SVG components preview 2021-08-31 15:22:38 +02:00
alonso.torres
7a523a9d89 🐛 Fix problems with order in groups 2021-08-31 14:52:59 +02:00
alonso.torres
885d7de11b 🐛 Fix rename typography on text options 2021-08-31 14:52:06 +02:00
alonso.torres
f44675a1e4 🐛 Fix repeated fetch on file selection 2021-08-31 14:52:06 +02:00
alonso.torres
ce912c7430 🐛 Fix problems with export components 2021-08-31 14:12:12 +02:00
Andrey Antukh
e9fdd74a99 🐛 Fix unexpected text wrapping on exporting. 2021-08-31 12:17:52 +02:00
Andrés Moya
df8269bc7f 🐛 Fix color of texts in pdf exported files 2021-08-31 12:05:43 +02:00
alonso.torres
23e4fa82c8 Add translations to onboarding 2021-08-31 12:01:05 +02:00
alonso.torres
9bea604a46 🐛 Fix thumbnail cropping issue 2021-08-31 11:46:43 +02:00
alonso.torres
119fbd114d 🐛 Fix typos in mailing list 2021-08-31 11:46:43 +02:00
alonso.torres
1b6e6ec2e4 🐛 Fix problem with borders on shape export 2021-08-31 11:46:43 +02:00
alonso.torres
2dfa4f9ec9 Add export/import to custom caps 2021-08-30 12:56:22 +02:00
alonso.torres
3cd3e89679 🐛 Fix problem with caps in thumbnails 2021-08-30 12:56:22 +02:00
alonso.torres
c3be1c870d 🐛 Fix problem with zoom and selection 2021-08-30 12:54:07 +02:00
Andrey Antukh
6b571fd2bb 🐛 Fix wrong pages filtering on view-only-bundle rpc output. 2021-08-30 12:39:06 +02:00
alonso.torres
92df7abcf0 🐛 Fix lint error 2021-08-30 12:22:53 +02:00
Andrey Antukh
498d1570ce 📎 Fix linter issues. 2021-08-27 13:37:55 +02:00
Andrey Antukh
e587179359 ♻️ Refactor flags handling on frontend. 2021-08-27 13:19:36 +02:00
Andrey Antukh
c9985121c4 📎 Allow overwrite archive task props. 2021-08-27 09:42:58 +02:00
Andrey Antukh
e768600df3 ♻️ Enable receiving frontend audit log on backend. 2021-08-25 14:01:43 +02:00
Andrés Moya
3dffb9c8a0 Enable line caps in component sync and svg upload 2021-08-24 16:27:58 +02:00
Andrés Moya
eb40297a35 🎉 Enhance line caps selectors 2021-08-24 16:27:58 +02:00
elhombretecla
837985ccc5 💄 Fix ui constraints color 2021-08-20 10:48:02 +02:00
Andrés Moya
1def4b0f0c Merge pull request #1151 from penpot/niwinz-exporter-and-docker
Exporter resource management improvements.
2021-08-19 15:10:15 +02:00
Andrey Antukh
4c430cedf5 ♻️ Refactor exporter browser management.
Replace the cluster dependency with generic-pool.
2021-08-19 14:17:51 +02:00
Andrey Antukh
18d9212253 Enable aarch64 build for exporter docker image. 2021-08-19 14:16:53 +02:00
Andrey Antukh
36314691f1 ⬆️ Update devenv dockerfile. 2021-08-19 14:16:34 +02:00
Andrey Antukh
24da25f0f7 📎 Update changelog and increase version (minor). 2021-08-19 11:15:30 +02:00
Andrey Antukh
84ba8e6dde Add better error reporting when ldap is not configured correctly. 2021-08-19 11:04:08 +02:00
Andrey Antukh
c6fe035939 🐛 Fix demo user login issue. 2021-08-19 11:04:08 +02:00
Andrés Moya
be9073f0b7 🎉 Add stroke caps to path ends 2021-08-19 09:13:22 +02:00
Andrey Antukh
ac6c07b771 🐛 Fix demo user login issue. 2021-08-18 16:54:56 +02:00
Andrey Antukh
c8102f4bff 🎉 Share link & pages on viewer. 2021-08-18 16:54:56 +02:00
Andrey Antukh
df1fcd5e22 📎 Update changelog. 2021-08-18 15:08:25 +02:00
Andrey Antukh
de87da9c91 🐛 Fix font uploading issue on windows. 2021-08-18 15:06:19 +02:00
Andrey Antukh
3532263af4 🐛 Fix font uploading issue on windows. 2021-08-18 13:14:02 +02:00
Andrés Moya
a9cf4dad82 🎉 Allow increment font size by 0.1 with alt 2021-08-18 10:56:33 +02:00
Andrés Moya
1de1eb6b9b 🐛 Fix initial shape names 2021-08-13 13:42:33 +02:00
Andrés Moya
f6742d1bbf 📚 Update changes. 2021-08-13 10:05:13 +02:00
Andrés Moya
a377c602cc 🐛 Fix naming of duplicated objects in copy&paste and others 2021-08-13 09:49:42 +02:00
Andrey Antukh
58f0ad999c Merge pull request #1144 from penpot/colorpicker-tooltips
🎉 Add tooltips to color picker tabs
2021-08-12 11:23:34 +02:00
Andrés Moya
f612d35daf ♻️ Remove locale translation 2021-08-12 11:17:31 +02:00
Andrés Moya
7d202cb492 🎉 Add tooltips to color picker tabs 2021-08-12 10:56:08 +02:00
Andrés Moya
39bb7f209d Use penpot metadata only for whole file export 2021-08-11 12:26:50 +02:00
Mahmoud A. Rabo
bbd38a7e47 🌐 Add translations for: Arabic.
Currently translated at 75.8% (521 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2021-08-11 01:33:17 +02:00
Voxybuns
d8b2cc7e1b 🌐 Add translations for: French.
Currently translated at 100.0% (687 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2021-08-11 01:33:15 +02:00
Andrés Moya
09b328167c Truncate 2 decimals in font size 2021-08-10 11:24:54 +02:00
Andrés Moya
4439ef07b6 🎉 Allow orthogonal movement 2021-08-10 11:20:43 +02:00
Andrés Moya
f8491e9631 🎉 Increment font size by 10 with shift+arrows 2021-08-10 08:51:23 +02:00
Andrés Moya
63259b3f92 🎉 Add shortut Ctrl+Shift+K to detach instances 2021-08-09 12:02:20 +02:00
Andrés Moya
10db35eab4 Hide options for drafts project in dashboard 2021-08-09 09:37:48 +02:00
Eranot
0fa79c7a46 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 72.9% (501 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2021-08-08 03:35:05 +02:00
Andrés Moya
e20f557bd6 Enhance resize from center, work when locked proportions 2021-08-06 13:42:50 +02:00
Andrés Moya
25d8d76524 🐛 Fix "Allow resizing from center"
This reverts commit cc0f99333f.
2021-08-06 09:20:32 +02:00
Andrés Moya
cc0f99333f Revert "🎉 Allow resizing from center"
This reverts commit 2a70964dce.
2021-08-05 15:39:09 +02:00
Andrés Moya
982aa874f2 🐛 Disable path conversion for raw-svg, to avoid errors 2021-08-05 14:54:54 +02:00
Andrés Moya
2a70964dce 🎉 Allow resizing from center 2021-08-05 14:54:54 +02:00
Mahmoud A. Rabo
3051a185e5 🌐 Add translations for: Arabic.
Currently translated at 47.5% (327 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2021-08-04 12:32:22 +02:00
Andrés Moya
5e788fff99 Merge remote-tracking branch 'origin/main' into develop 2021-08-04 12:00:13 +02:00
Andrey Antukh
326c52604b 🐛 Don't dissoc :current-team-id on finalizing workspace. 2021-08-04 11:54:54 +02:00
Andrey Antukh
e7d1647769 🐛 Don't allow remove default teams. 2021-08-04 10:54:31 +02:00
Andrey Antukh
1e35116d8f 🐛 Don't allow remove default projects. 2021-08-04 10:50:21 +02:00
Andrey Antukh
35ca3ec895 🐛 Fix loggin issue when user uses the same email as previously deleted profile. 2021-08-04 10:42:22 +02:00
Andrés Moya
7c30cccc97 📚 Add contribution 2021-08-03 09:50:09 +02:00
Andrés Moya
4194abe4f2 🧹 Remove unneeded function 2021-08-03 09:50:09 +02:00
Eduard Aymerich
0b698576da fix: remove top right button in settings. #1123 2021-08-03 09:50:09 +02:00
Andrés Moya
3fbd73129e Set email fields to email type to help editing 2021-08-03 09:50:09 +02:00
Andrés Moya
bbd6d171be 🎉 Allow to navigate undo history 2021-08-03 09:50:09 +02:00
Andrés Moya
f7929bbf93 📚 Some cleanup in CHANGES.md 2021-08-03 09:50:09 +02:00
Andrés Moya
29cd8530a3 🎉 Remember displacements when duplicating several shapes in a row 2021-08-03 09:50:09 +02:00
Andrés Moya
574387acac Move artboards when duplicating 2021-08-03 09:50:09 +02:00
Andrés Moya
6a1ab4d73c 🎉 Allow to zoom with ctrl + middle button 2021-08-03 09:50:09 +02:00
Andrés Moya
29e0c32679 Start panning with space+click instread of just space 2021-08-03 09:50:09 +02:00
Andrey Antukh
db7fe023c6 📎 Set next version to 1.8.0-alpha. 2021-08-03 09:50:09 +02:00
Maemolee
ccf3d7a285 🌐 Add translations for: Chinese (Simplified).
Currently translated at 97.3% (669 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2021-07-30 18:34:06 +02:00
Wang Jiaxiang
fb59d5d268 🌐 Add translations for: Chinese (Simplified).
Currently translated at 82.9% (570 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2021-07-16 17:35:11 +02:00
Guilherme Dimas
05cf14846c 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 64.3% (442 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2021-07-15 16:33:38 +02:00
Çağlar Yeşilyurt
1d6a421388 🌐 Add translations for: Turkish.
Currently translated at 93.4% (642 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2021-07-05 19:02:21 +02:00
Eranot
f73880e565 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 61.4% (422 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2021-06-29 13:34:09 +02:00
nautilusx
ce13902680 🌐 Add translations for: German.
Currently translated at 91.8% (631 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2021-06-27 10:33:22 +02:00
Eranot
fdbf94f415 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 54.4% (374 of 687 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2021-06-21 05:32:46 +02:00
204 changed files with 12785 additions and 5338 deletions

View File

@@ -23,6 +23,9 @@
{:unsorted-required-namespaces
{:level :warning}
:potok/reify-type
{:level :error}
:unresolved-namespace
{:level :warning
:exclude [data_readers]}

View File

@@ -10,15 +10,34 @@
sname])]
{:node result}))
(def registry (atom {}))
(defn potok-reify
[{:keys [:node]}]
[{:keys [:node :filename] :as params}]
(let [[rnode rtype & other] (:children node)
result (api/list-node
(into [(api/token-node (symbol "deftype"))
(api/token-node (gensym (name (:k rtype))))
(api/vector-node [])]
other))]
{:node result}))
rsym (symbol (str "event-type-" (name (:k rtype))))
reg (get @registry filename #{})]
(when-not (:namespaced? rtype)
(let [{:keys [:row :col]} (meta rtype)]
(api/reg-finding! {:message "ptk/reify type should be namespaced"
:type :potok/reify-type
:row row
:col col})))
(if (contains? reg rsym)
(let [{:keys [:row :col]} (meta rtype)]
(api/reg-finding! {:message (str "duplicate type: " (name (:k rtype)))
:type :potok/reify-type
:row row
:col col}))
(swap! registry update filename (fnil conj #{}) rsym))
(let [result (api/list-node
(into [(api/token-node (symbol "deftype"))
(api/token-node rsym)
(api/vector-node [])]
other))]
{:node result})))
(defn clojure-specify
[{:keys [:node]}]

View File

@@ -1,18 +1,77 @@
# CHANGELOG #
# CHANGELOG
## :rocket: Next
### :boom: Breaking changes
### :sparkles: New features
### :bug: Bugs fixed
### :arrow_up: Deps updates
### :boom: Breaking changes
### :heart: Community contributions by (Thank you!)
## 1.8.0-alpha
### :boom: Breaking changes
- This release includes a new approach for handling share links, and
this feature is incompatible with the previous one. This means that
all the public share links generated previously will stop working.
### :sparkles: New features
- Add tooltips to color picker tabs [Taiga #1814](https://tree.taiga.io/project/penpot/us/1814).
- Add styling to the end point of any open paths [Taiga #1107](https://tree.taiga.io/project/penpot/us/1107).
- Allow to zoom with ctrl + middle button [Taiga #1428](https://tree.taiga.io/project/penpot/us/1428).
- Auto placement of duplicated objects [Taiga #1386](https://tree.taiga.io/project/penpot/us/1386).
- Enable penpot SVG metadata only when exporting complete files [Taiga #1914](https://tree.taiga.io/project/penpot/us/1914?milestone=295883).
- Export to PDF all artboards of one page [Taiga #1895](https://tree.taiga.io/project/penpot/us/1895).
- Go to a undo step clicking on a history element of the list [Taiga #1374](https://tree.taiga.io/project/penpot/us/1374).
- Increment font size by 10 with shift+arrows [1047](https://github.com/penpot/penpot/issues/1047).
- New shortcut to detach components Ctrl+Shift+K [Taiga #1799](https://tree.taiga.io/project/penpot/us/1799).
- Set email inputs to type "email", to aid keyboard entry [Taiga #1921](https://tree.taiga.io/project/penpot/issue/1921).
- Use shift+move to move element orthogonally [#823](https://github.com/penpot/penpot/issues/823).
- Use space + mouse drag to pan, instead of only space [Taiga #1800](https://tree.taiga.io/project/penpot/us/1800).
- Allow navigate through pages on the viewer [Taiga #1550](https://tree.taiga.io/project/penpot/us/1550).
- Allow create share links with specific pages [Taiga #1844](https://tree.taiga.io/project/penpot/us/1844).
### :bug: Bugs fixed
- Prevent adding numeric suffix to layer names when not needed [Taiga #1929](https://tree.taiga.io/project/penpot/us/1929).
- Prevent deleting or moving the drafts project [Taiga #1935](https://tree.taiga.io/project/penpot/issue/1935).
- Fix problem with zoom and selection [Taiga #1919](https://tree.taiga.io/project/penpot/issue/1919)
- Fix problem with borders on shape export [#1092](https://github.com/penpot/penpot/issues/1092)
- Fix thumbnail cropping issue [Taiga #1964](https://tree.taiga.io/project/penpot/issue/1964)
- Fix repeated fetch on file selection [Taiga #1933](https://tree.taiga.io/project/penpot/issue/1933)
- Fix rename typography on text options [Taiga #1963](https://tree.taiga.io/project/penpot/issue/1963)
- Fix problems with order in groups [Taiga #1960](https://tree.taiga.io/project/penpot/issue/1960)
- Fix SVG components preview [#1134](https://github.com/penpot/penpot/issues/1134)
- Fix group renaming problem [Taiga #1969](https://tree.taiga.io/project/penpot/issue/1969)
- Fix problem with import broken images links [#1197](https://github.com/penpot/penpot/issues/1197)
- Fix problem while moving imported SVG's [#1199](https://github.com/penpot/penpot/issues/1199)
### :arrow_up: Deps updates
### :boom: Breaking changes
### :heart: Community contributions by (Thank you!)
- eduayme [#1129](https://github.com/penpot/penpot/pull/1129).
## 1.7.4-alpha
### :bug: Bugs fixed
- Fix demo user creation (self-hosted only)
- Add better ldap response validation and reporting (self-hosted only)
## 1.7.3-alpha
### :bug: Bugs fixed
- Fix font uploading issue on Windows.
## 1.7.2-alpha
### :sparkles: New features
@@ -32,7 +91,6 @@
- Update frontend build tooling.
### :boom: Breaking changes
### :heart: Community contributions by (Thank you!)
- soultipsy [#1100](https://github.com/penpot/penpot/pull/1100)
@@ -83,10 +141,6 @@
- Fix dynamic alignment enabled with hidden objects [#1063](https://github.com/penpot/penpot/issues/1063)
### :arrow_up: Deps updates
### :boom: Breaking changes
### :heart: Community contributions by (Thank you!)
## 1.6.5-alpha
### :bug: Bugs fixed

View File

@@ -22,7 +22,7 @@
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
<mj-text>
Thanks for signing up for your Penpot account! Please verify your
email using the link below adn get started building mockups and
email using the link below and get started building mockups and
prototypes today!
</mj-text>
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">

View File

@@ -173,7 +173,7 @@
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below adn get started building mockups and prototypes today!</div>
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below and get started building mockups and prototypes today!</div>
</td>
</tr>
<tr>
@@ -465,4 +465,4 @@
</div>
</body>
</html>
</html>

View File

@@ -1,7 +1,7 @@
Hello {{name}}!
Thanks for signing up for your Penpot account! Please verify your email using the
link below adn get started building mockups and prototypes today!
link below and get started building mockups and prototypes today!
{{ public-uri }}/#/auth/verify-token?token={{token}}

View File

@@ -231,9 +231,9 @@
(defn get-by-params
([ds table params]
(get-by-params ds table params nil))
([ds table params {:keys [uncheked] :or {uncheked false} :as opts}]
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
(let [res (exec-one! ds (sql/select table params opts))]
(when (and (not uncheked) (or (not res) (is-deleted? res)))
(when (and check-not-found (or (not res) (is-deleted? res)))
(ex/raise :type :not-found
:table table
:hint "database object not found"))
@@ -267,13 +267,28 @@
(instance? PGpoint v))
(defn pgarray?
[v]
(instance? PgArray v))
([v] (instance? PgArray v))
([v type]
(and (instance? PgArray v)
(= type (.getBaseTypeName ^PgArray v)))))
(defn pgarray-of-uuid?
[v]
(and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v))))
(defn decode-pgarray
([v] (into [] (.getArray ^PgArray v)))
([v in] (into in (.getArray ^PgArray v)))
([v in xf] (into in xf (.getArray ^PgArray v))))
(defn pgarray->set
[v]
(set (.getArray ^PgArray v)))
(defn pgarray->vector
[v]
(vec (.getArray ^PgArray v)))
(defn pgpoint
[p]
(PGpoint. (:x p) (:y p)))
@@ -285,7 +300,6 @@
(.createArrayOf conn ^String type (into-array Object objects))
(.createArrayOf conn ^String type objects))))
(defn decode-pgpoint
[^PGpoint v]
(gpt/point (.-x v) (.-y v)))
@@ -369,15 +383,6 @@
(.setType "jsonb")
(.setValue (json/encode-str data))))
(defn pgarray->set
[v]
(set (.getArray ^PgArray v)))
(defn pgarray->vector
[v]
(vec (.getArray ^PgArray v)))
;; --- Locks
(defn- xact-check-param

View File

@@ -114,9 +114,14 @@
(s/def ::storage map?)
(s/def ::assets map?)
(s/def ::feedback fn?)
(s/def ::error-report-handler fn?)
(s/def ::audit-http-handler fn?)
(defmethod ig/pre-init-spec ::router [_]
(s/keys :req-un [::rpc ::session ::mtx/metrics ::oauth ::storage ::assets ::feedback]))
(s/keys :req-un [::rpc ::session ::mtx/metrics
::oauth ::storage ::assets ::feedback
::error-report-handler
::audit-http-handler]))
(defmethod ig/init-key ::router
[_ {:keys [session rpc oauth metrics assets feedback] :as cfg}]
@@ -136,9 +141,7 @@
["/webhooks"
["/sns" {:post (:sns-webhook cfg)}]]
["/api" {:middleware [
;; Temporary disabled
#_[middleware/etag]
["/api" {:middleware [[middleware/etag]
[middleware/format-response-body]
[middleware/params]
[middleware/multipart-params]
@@ -149,10 +152,12 @@
["/feedback" {:middleware [(:middleware session)]
:post feedback}]
["/auth/oauth/:provider" {:post (:handler oauth)}]
["/auth/oauth/:provider/callback" {:get (:callback-handler oauth)}]
["/audit/events" {:middleware [(:middleware session)]
:post (:audit-http-handler cfg)}]
["/rpc" {:middleware [(:middleware session)]}
["/query/:type" {:get (:query-handler rpc)
:post (:query-handler rpc)}]

View File

@@ -13,7 +13,6 @@
[buddy.core.codecs :as bc]
[buddy.core.hash :as bh]
[clojure.java.io :as io]
[ring.core.protocols :as rp]
[ring.middleware.cookies :refer [wrap-cookies]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
@@ -74,33 +73,15 @@
{:name ::parse-request-body
:compile (constantly wrap-parse-request-body)})
(defn- transit-streamable-body
[data opts]
(reify rp/StreamableResponseBody
(write-body-to-stream [_ response output-stream]
(try
(let [tw (t/writer output-stream opts)]
(t/write! tw data))
(finally
(.close ^java.io.OutputStream output-stream))))))
(defn- impl-format-response-body
[response _request]
(let [body (:body response)
opts {:type :json-verbose}]
opts {:type :json}]
(cond
(coll? body)
(-> response
(update :headers assoc "content-type" "application/transit+json")
(assoc :body (transit-streamable-body body opts)))
;; ;; Temporary disabled
;; (-> response
;; (update :headers assoc "content-type" "application/transit+json")
;; (assoc :body
;; (if (= :post (:request-method request))
;; (transit-streamable-body body opts)
;; (t/encode body opts))))
(assoc :body (t/encode body opts)))
(nil? body)
(assoc response :status 204 :body "")

View File

@@ -23,7 +23,8 @@
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[lambdaisland.uri :as u]))
[lambdaisland.uri :as u]
[promesa.exec :as px]))
(defn parse-client-ip
[{:keys [headers] :as request}]
@@ -67,6 +68,65 @@
(update event :props #(-> % clean-common clean-profile-id clean-complex-data))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP Handler
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare persist-http-events)
(s/def ::profile-id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::type ::us/string)
(s/def ::props (s/map-of ::us/keyword any?))
(s/def ::timestamp dt/instant?)
(s/def ::context (s/map-of ::us/keyword any?))
(s/def ::event
(s/keys :req-un [::type ::name ::props ::timestamp ::profile-id]
:opt-un [::context]))
(s/def ::events (s/every ::event))
(defmethod ig/init-key ::http-handler
[_ {:keys [executor enabled] :as cfg}]
(fn [{:keys [params _headers _cookies profile-id] :as request}]
(when enabled
(let [events (->> (:events params)
(remove #(not= profile-id (:profile-id %)))
(us/conform ::events))
ip-addr (parse-client-ip request)
cfg (-> cfg
(assoc :source "frontend")
(assoc :events events)
(assoc :ip-addr ip-addr))]
(px/run! executor #(persist-http-events cfg))))
{:status 204 :body ""}))
(defn- persist-http-events
[{:keys [pool events ip-addr source] :as cfg}]
(try
(let [columns [:id :name :source :type :tracked-at :profile-id :ip-addr :props :context]
prepare-xf (map (fn [event]
[(uuid/next)
(:name event)
source
(:type event)
(:timestamp event)
(:profile-id event)
(db/inet ip-addr)
(db/tjson (:props event))
(db/tjson (d/without-nils (:context event)))]))
events (us/conform ::events events)
rows (into [] prepare-xf events)]
(db/insert-multi! pool :audit-log columns rows))
(catch Throwable e
(let [xdata (ex-data e)]
(if (= :spec-validation (:code xdata))
(l/error ::l/raw (str "spec validation on persist-events:\n"
(:explain xdata)))
(l/error :hint "error on persist-events"
:cause e))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Collector
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -103,7 +163,9 @@
(recur)))
(fn [& {:keys [cmd] :as params}]
(let [params (dissoc params :cmd)]
(let [params (-> params
(dissoc :cmd)
(assoc :tracked-at (dt/now)))]
(case cmd
:stop (a/close! input)
:submit (when-not (a/offer! input params)
@@ -117,13 +179,14 @@
(:name event)
(:type event)
(:profile-id event)
(:tracked-at event)
(some-> (:ip-addr event) db/inet)
(db/tjson (:props event))])]
(db/tjson (:props event))
"backend"])]
(aa/with-thread executor
(db/with-atomic [conn pool]
(db/insert-multi! conn :audit-log
[:id :name :type :profile-id :ip-addr :props]
[:id :name :type :profile-id :tracked-at :ip-addr :props :source]
(sequence (map event->row) events))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -144,16 +207,22 @@
(defmethod ig/init-key ::archive-task
[_ {:keys [uri enabled] :as cfg}]
(fn [_]
(when (and enabled (not uri))
(ex/raise :type :internal
:code :task-not-configured
:hint "archive task not configured, missing uri"))
(loop []
(let [res (archive-events cfg)]
(when (= res :continue)
(aa/thread-sleep 200)
(recur))))))
(fn [props]
;; NOTE: this let allows overwrite default configured values from
;; the repl, when manually invoking the task.
(let [enabled (or enabled (:enabled props false))
uri (or uri (:uri props))
cfg (assoc cfg :uri uri)]
(when (and enabled (not uri))
(ex/raise :type :internal
:code :task-not-configured
:hint "archive task not configured, missing uri"))
(when enabled
(loop []
(let [res (archive-events cfg)]
(when (= res :continue)
(aa/thread-sleep 200)
(recur))))))))
(def sql:retrieve-batch-of-audit-log
"select * from audit_log
@@ -164,22 +233,27 @@
(defn archive-events
[{:keys [pool uri tokens] :as cfg}]
(letfn [(decode-row [{:keys [props ip-addr] :as row}]
(letfn [(decode-row [{:keys [props ip-addr context] :as row}]
(cond-> row
(db/pgobject? props)
(assoc :props (db/decode-transit-pgobject props))
(db/pgobject? context)
(assoc :context (db/decode-transit-pgobject context))
(db/pgobject? ip-addr "inet")
(assoc :ip-addr (db/decode-inet ip-addr))))
(row->event [{:keys [name type created-at profile-id props ip-addr]}]
(cond-> {:type type
:name name
:timestamp created-at
:profile-id profile-id
:props props}
(some? ip-addr)
(update :context assoc :source-ip ip-addr)))
(row->event [row]
(select-keys row [:type
:name
:source
:created-at
:tracked-at
:profile-id
:ip-addr
:props
:context]))
(send [events]
(let [token (tokens :generate {:iss "authentication"

View File

@@ -28,11 +28,24 @@
{:name "actions_profile_register_count"
:help "A global counter of user registrations."
:type :counter}
:profile-activation
{:name "actions_profile_activation_count"
:help "A global counter of profile activations"
:type :counter}
:update-file-changes
{:name "rpc_update_file_changes_total"
:help "A total number of changes submitted to update-file."
:type :counter}
:update-file-bytes-processed
{:name "rpc_update_file_bytes_processed_total"
:help "A total number of bytes processed by update-file."
:type :counter}}}
:app.migrations/all
{:main (ig/ref :app.migrations/migrations)}
@@ -95,6 +108,7 @@
:storage (ig/ref :app.storage/storage)
:sns-webhook (ig/ref :app.http.awsns/handler)
:feedback (ig/ref :app.http.feedback/handler)
:audit-http-handler (ig/ref :app.loggers.audit/http-handler)
:error-report-handler (ig/ref :app.loggers.mattermost/handler)}
:app.http.assets/handlers
@@ -289,6 +303,11 @@
:app.loggers.zmq/receiver
{:endpoint (cf/get :loggers-zmq-uri)}
:app.loggers.audit/http-handler
{:enabled (cf/get :audit-enabled false)
:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}
:app.loggers.audit/collector
{:enabled (cf/get :audit-enabled false)
:pool (ig/ref :app.db/pool)

View File

@@ -92,18 +92,14 @@
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
(reify
clojure.lang.IDeref
(deref [_] instance)
clojure.lang.IFn
(invoke [_ cmd]
(.inc ^Counter instance))
(invoke [_ cmd labels]
(.. ^Counter instance
(labels (into-array String labels))
(inc))))))
{::instance instance
::fn (fn [{:keys [by labels] :or {by 1}}]
(if labels
(.. ^Counter instance
(labels (into-array String labels))
(inc by))
(.inc ^Counter instance by)))}))
(defn make-gauge
[{:keys [name help registry reg labels] :as props}]
@@ -115,21 +111,16 @@
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
(reify
clojure.lang.IDeref
(deref [_] instance)
clojure.lang.IFn
(invoke [_ cmd]
(case cmd
:inc (.inc ^Gauge instance)
:dec (.dec ^Gauge instance)))
(invoke [_ cmd labels]
(let [labels (into-array String [labels])]
(case cmd
:inc (.. ^Gauge instance (labels labels) (inc))
:dec (.. ^Gauge instance (labels labels) (dec))))))))
{::instance instance
::fn (fn [{:keys [cmd by labels] :or {by 1}}]
(if labels
(let [labels (into-array String [labels])]
(case cmd
:inc (.. ^Gauge instance (labels labels) (inc by))
:dec (.. ^Gauge instance (labels labels) (dec by))))
(case cmd
:inc (.inc ^Gauge instance by)
:dec (.dec ^Gauge instance by))))}))
(def default-quantiles
[[0.75 0.02]
@@ -150,18 +141,14 @@
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
(reify
clojure.lang.IDeref
(deref [_] instance)
clojure.lang.IFn
(invoke [_ cmd val]
(.observe ^Summary instance val))
(invoke [_ cmd val labels]
(.. ^Summary instance
(labels (into-array String labels))
(observe val))))))
{::instance instance
::fn (fn [{:keys [val labels]}]
(if labels
(.. ^Summary instance
(labels (into-array String labels))
(observe val))
(.observe ^Summary instance val)))}))
(def default-histogram-buckets
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
@@ -177,18 +164,14 @@
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
(reify
clojure.lang.IDeref
(deref [_] instance)
clojure.lang.IFn
(invoke [_ cmd val]
(.observe ^Histogram instance val))
(invoke [_ cmd val labels]
(.. ^Histogram instance
(labels (into-array String labels))
(observe val))))))
{::instance instance
::fn (fn [{:keys [val labels]}]
(if labels
(.. ^Histogram instance
(labels (into-array String labels))
(observe val))
(.observe ^Histogram instance val)))}))
(defn create
[{:keys [type] :as props}]
@@ -205,19 +188,19 @@
(with-meta
(fn
([a]
(mobj :inc)
((::fn mobj) nil)
(origf a))
([a b]
(mobj :inc)
((::fn mobj) nil)
(origf a b))
([a b c]
(mobj :inc)
((::fn mobj) nil)
(origf a b c))
([a b c d]
(mobj :inc)
((::fn mobj) nil)
(origf a b c d))
([a b c d & more]
(mobj :inc)
((::fn mobj) nil)
(apply origf a b c d more)))
(assoc mdata ::original origf))))
([rootf mobj labels]
@@ -226,13 +209,13 @@
(with-meta
(fn
([a]
(mobj :inc labels)
((::fn mobj) {:labels labels})
(origf a))
([a b]
(mobj :inc labels)
((::fn mobj) {:labels labels})
(origf a b))
([a b & more]
(mobj :inc labels)
((::fn mobj) {:labels labels})
(apply origf a b more)))
(assoc mdata ::original origf)))))
@@ -245,15 +228,15 @@
([a]
(with-measure
:expr (origf a)
:cb #(mobj :observe %)))
:cb #((::fn mobj) {:val %})))
([a b]
(with-measure
:expr (origf a b)
:cb #(mobj :observe %)))
:cb #((::fn mobj) {:val %})))
([a b & more]
(with-measure
:expr (apply origf a b more)
:cb #(mobj :observe %))))
:cb #((::fn mobj) {:val %}))))
(assoc mdata ::original origf))))
([rootf mobj labels]
@@ -264,26 +247,26 @@
([a]
(with-measure
:expr (origf a)
:cb #(mobj :observe % labels)))
:cb #((::fn mobj) {:val % :labels labels})))
([a b]
(with-measure
:expr (origf a b)
:cb #(mobj :observe % labels)))
:cb #((::fn mobj) {:val % :labels labels})))
([a b & more]
(with-measure
:expr (apply origf a b more)
:cb #(mobj :observe % labels))))
:cb #((::fn mobj) {:val % :labels labels}))))
(assoc mdata ::original origf)))))
(defn instrument-vars!
[vars {:keys [wrap] :as props}]
(let [obj (create props)]
(cond
(instance? Counter @obj)
(instance? Counter (::instance obj))
(doseq [var vars]
(alter-var-root var (or wrap wrap-counter) obj))
(instance? Summary @obj)
(instance? Summary (::instance obj))
(doseq [var vars]
(alter-var-root var (or wrap wrap-summary) obj))
@@ -294,13 +277,13 @@
[f {:keys [wrap] :as props}]
(let [obj (create props)]
(cond
(instance? Counter @obj)
(instance? Counter (::instance obj))
((or wrap wrap-counter) f obj)
(instance? Summary @obj)
(instance? Summary (::instance obj))
((or wrap wrap-summary) f obj)
(instance? Histogram @obj)
(instance? Histogram (::instance obj))
((or wrap wrap-summary) f obj)
:else

View File

@@ -193,6 +193,15 @@
{:name "0061-mod-file-table"
:fn (mg/resource "app/migrations/sql/0061-mod-file-table.sql")}
{:name "0062-fix-metadata-media"
:fn (mg/resource "app/migrations/sql/0062-fix-metadata-media.sql")}
{:name "0063-add-share-link-table"
:fn (mg/resource "app/migrations/sql/0063-add-share-link-table.sql")}
{:name "0064-mod-audit-log-table"
:fn (mg/resource "app/migrations/sql/0064-mod-audit-log-table.sql")}
])

View File

@@ -0,0 +1,12 @@
CREATE TABLE share_link (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE DEFERRABLE,
owner_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
pages uuid[],
flags text[]
);
CREATE INDEX share_link_file_id_idx ON share_link(file_id);
CREATE INDEX share_link_owner_id_idx ON share_link(owner_id);

View File

@@ -0,0 +1,13 @@
ALTER TABLE audit_log
ADD COLUMN tracked_at timestamptz NULL DEFAULT clock_timestamp(),
ADD COLUMN source text NULL,
ADD COLUMN context jsonb NULL;
ALTER TABLE audit_log
ALTER COLUMN source SET STORAGE external,
ALTER COLUMN context SET STORAGE external;
UPDATE audit_log SET source = 'backend', tracked_at=created_at;
-- ALTER TABLE audit_log ALTER COLUMN source SET NOT NULL;
-- ALTER TABLE audit_log ALTER COLUMN tracked_at SET NOT NULL;

View File

@@ -117,14 +117,14 @@
profile-id (or (:profile-id params')
(:profile-id result)
(::audit/profile-id resultm))
props (d/merge params (::audit/props resultm))]
props (d/merge params' (::audit/props resultm))]
(audit :cmd :submit
:type (::type cfg)
:name (or (::audit/name resultm)
(::sv/name mdata))
:profile-id profile-id
:ip-addr (audit/parse-client-ip request)
:props (audit/profile->props props))))
:props props)))
result))))
@@ -175,6 +175,7 @@
'app.rpc.mutations.management
'app.rpc.mutations.ldap
'app.rpc.mutations.fonts
'app.rpc.mutations.share-link
'app.rpc.mutations.verify-token)
(map (partial process-method cfg))
(into {}))))

View File

@@ -12,6 +12,7 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.metrics :as mtx]
[app.rpc.permissions :as perms]
[app.rpc.queries.files :as files]
[app.rpc.queries.projects :as proj]
@@ -291,7 +292,7 @@
(simpl/del-object backend file)))
(defn- update-file
[{:keys [conn] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}]
[{:keys [conn metrics] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}]
(when (> (:revn params)
(:revn file))
@@ -301,14 +302,22 @@
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
(let [changes (if changes-with-metadata
(let [mtx1 (get-in metrics [:definitions :update-file-changes])
mtx2 (get-in metrics [:definitions :update-file-bytes-processed])
changes (if changes-with-metadata
(mapcat :changes changes-with-metadata)
changes)
;; Trace the number of changes processed
_ ((::mtx/fn mtx1) {:by (count changes)})
ts (dt/now)
file (-> (files/retrieve-data cfg file)
(update :revn inc)
(update :data (fn [data]
;; Trace the length of bytes of processed data
((::mtx/fn mtx2) {:by (alength data)})
(-> data
(blob/decode)
(assoc :id (:id file))

View File

@@ -6,6 +6,7 @@
(ns app.rpc.mutations.fonts
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -49,6 +50,7 @@
(let [data (media/run cfg {:cmd :generate-fonts :input data :rlimit :font})
storage (media/configure-assets-storage storage conn)
otf (when-let [fdata (get data "font/otf")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/otf"}))
@@ -65,6 +67,13 @@
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/woff2"}))]
(when (and (nil? otf)
(nil? ttf)
(nil? woff1)
(nil? woff2))
(ex/raise :type :validation
:code :invalid-font-upload))
(db/insert! conn :team-font-variant
{:id (uuid/next)
:team-id (:team-id params)

View File

@@ -13,11 +13,20 @@
[app.loggers.audit :as audit]
[app.rpc.mutations.profile :as profile-m]
[app.rpc.queries.profile :as profile-q]
[app.util.logging :as l]
[app.util.services :as sv]
[clj-ldap.client :as ldap]
[clojure.spec.alpha :as s]
[clojure.string]))
(s/def ::fullname ::us/not-empty-string)
(s/def ::email ::us/email)
(s/def ::backend ::us/not-empty-string)
(s/def ::info-data
(s/keys :req-un [::fullname ::email ::backend]))
(defn ^java.lang.AutoCloseable connect
[]
(let [params {:ssl? (cfg/get :ldap-ssl)
@@ -57,6 +66,13 @@
(ex/raise :type :validation
:code :wrong-credentials))
(when-not (s/valid? ::info-data info)
(let [explain (s/explain-str ::info-data info)]
(l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain))
(ex/raise :type :restriction
:code :wrong-ldap-response
:reason explain)))
(let [profile (login-or-register cfg {:email (:email info)
:backend (:backend info)
:fullname (:fullname info)})]
@@ -94,7 +110,9 @@
(cfg/get :ldap-attrs-fullname)]
base-dn (cfg/get :ldap-base-dn)
params {:filter query :sizelimit 1 :attributes attrs}]
params {:filter query
:sizelimit 1
:attributes attrs}]
(first (ldap/search cpool base-dn params))))
(defn- authenticate

View File

@@ -15,6 +15,7 @@
[app.http.oauth :refer [extract-props]]
[app.loggers.audit :as audit]
[app.media :as media]
[app.metrics :as mtx]
[app.rpc.mutations.projects :as projects]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
@@ -150,7 +151,8 @@
transaction is completed."
[metrics]
(fn []
((get-in metrics [:definitions :profile-register]) :inc)))
(let [mobj (get-in metrics [:definitions :profile-register])]
((::mtx/fn mobj) {:by 1}))))
(defn register-profile
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]

View File

@@ -117,11 +117,15 @@
(s/def ::delete-project
(s/keys :req-un [::id ::profile-id]))
;; TODO: right now, we just don't allow delete default projects, in a
;; future we need to ensure raise a correct exception signaling that
;; this is not allowed.
(sv/defmethod ::delete-project
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(proj/check-edition-permissions! conn profile-id id)
(db/update! conn :project
{:deleted-at (dt/now)}
{:id id})
{:id id :is-default false})
nil))

View File

@@ -0,0 +1,67 @@
;; 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) UXBOX Labs SL
(ns app.rpc.mutations.share-link
"Share link related rpc mutation methods."
(:require
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc.queries.files :as files]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::flags (s/every ::us/string :kind set?))
(s/def ::pages (s/every ::us/uuid :kind set?))
;; --- Mutation: Create Share Link
(declare create-share-link)
(s/def ::create-share-link
(s/keys :req-un [::profile-id ::file-id ::flags]
:opt-un [::pages]))
(sv/defmethod ::create-share-link
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(create-share-link conn params)))
(defn create-share-link
[conn {:keys [profile-id file-id pages flags]}]
(let [pages (db/create-array conn "uuid" pages)
flags (->> (map name flags)
(db/create-array conn "text"))
slink (db/insert! conn :share-link
{:id (uuid/next)
:file-id file-id
:flags flags
:pages pages
:owner-id profile-id})]
(-> slink
(update :pages db/decode-pgarray #{})
(update :flags db/decode-pgarray #{}))))
;; --- Mutation: Delete Share Link
(declare delete-share-link)
(s/def ::delete-share-link
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-share-link
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [slink (db/get-by-id conn :share-link id)]
(files/check-edition-permissions! conn profile-id (:file-id slink))
(db/delete! conn :share-link {:id id})
nil)))

View File

@@ -125,6 +125,10 @@
(s/def ::delete-team
(s/keys :req-un [::profile-id ::id]))
;; TODO: right now just don't allow delete default team, in future it
;; should raise a speific exception for signal that this acction is
;; not allowed.
(sv/defmethod ::delete-team
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
@@ -135,7 +139,7 @@
(db/update! conn :team
{:deleted-at (dt/now)}
{:id id})
{:id id :is-default false})
nil)))

View File

@@ -9,6 +9,7 @@
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.metrics :as mtx]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
@@ -42,7 +43,8 @@
transaction is completed."
[metrics]
(fn []
((get-in metrics [:definitions :profile-activation]) :inc)))
(let [mobj (get-in metrics [:definitions :profile-activation])]
((::mtx/fn mobj) {:by 1}))))
(defmethod process-token :verify-email
[{:keys [conn session metrics] :as cfg} _ {:keys [profile-id] :as claims}]

View File

@@ -37,6 +37,41 @@
:is-admin false
:can-edit false)))
(defn make-edition-predicate-fn
"A simple factory for edition permission predicate functions."
[qfn]
(us/assert fn? qfn)
(fn [& args]
(let [rows (apply qfn args)]
(when-not (or (empty? rows)
(not (or (some :can-edit rows)
(some :is-admin rows)
(some :is-owner rows))))
rows))))
(defn make-read-predicate-fn
"A simple factory for read permission predicate functions."
[qfn]
(us/assert fn? qfn)
(fn [& args]
(let [rows (apply qfn args)]
(when (seq rows)
rows))))
(defn make-check-fn
"Helper that converts a predicate permission function to a check
function (function that raises an exception)."
[pred]
(fn [& args]
(when-not (seq (apply pred args))
(ex/raise :type :not-found
:code :object-not-found
:hint "not found"))))
;; TODO: the following functions are deprecated and replaced with the
;; new ones. Should not be used.
(defn make-edition-check-fn
"A simple factory for edition permission check functions."
[qfn]

View File

@@ -61,16 +61,23 @@
(defn- retrieve-file-permissions
[conn profile-id file-id]
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id]))
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(def has-edit-permissions?
(perms/make-edition-predicate-fn retrieve-file-permissions))
(def has-read-permissions?
(perms/make-read-predicate-fn retrieve-file-permissions))
(def check-edition-permissions!
(perms/make-edition-check-fn retrieve-file-permissions))
(perms/make-check-fn has-edit-permissions?))
(def check-read-permissions!
(perms/make-read-check-fn retrieve-file-permissions))
(perms/make-check-fn has-read-permissions?))
;; --- Query: Files search

View File

@@ -92,11 +92,16 @@
profile))
(def ^:private sql:profile-by-email
"select p.* from profile as p
where p.email = ?
and (p.deleted_at is null or
p.deleted_at > now())")
(defn retrieve-profile-data-by-email
[conn email]
(try
(db/get-by-params conn :profile {:email (str/lower email)})
(catch Exception _e)))
(ex/ignoring
(db/exec-one! conn [sql:profile-by-email (str/lower email)])))
;; --- Attrs Helpers

View File

@@ -14,24 +14,98 @@
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Query: View Only Bundle
(defn- decode-share-link-row
[row]
(-> row
(update :flags db/decode-pgarray #{})
(update :pages db/decode-pgarray #{})))
(defn- retrieve-project
[conn id]
(db/get-by-id conn :project id {:columns [:id :name :team-id]}))
(defn- retrieve-share-link
[{:keys [conn]} file-id id]
(some-> (db/get-by-params conn :share-link
{:id id :file-id file-id}
{:check-not-found false})
(decode-share-link-row)))
(defn- retrieve-bundle
[{:keys [conn] :as cfg} file-id]
(let [file (files/retrieve-file cfg file-id)
project (retrieve-project conn (:project-id file))
libs (files/retrieve-file-libraries cfg false file-id)
users (teams/retrieve-users conn (:team-id project))
links (->> (db/query conn :share-link {:file-id file-id})
(mapv decode-share-link-row))
fonts (db/query conn :team-font-variant
{:team-id (:team-id project)
:deleted-at nil})]
{:file file
:users users
:fonts fonts
:project project
:share-links links
:libraries libs}))
(s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::share-id ::us/uuid)
(s/def ::view-only-bundle
(s/keys :req-un [::file-id] :opt-un [::profile-id ::share-id]))
(sv/defmethod ::view-only-bundle {:auth false}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)
bundle (retrieve-bundle cfg file-id)
slink (retrieve-share-link cfg file-id share-id)]
;; When we have neither profile nor share, we just return a not
;; found response to the user.
(when (and (not profile-id)
(not slink))
(ex/raise :type :not-found
:code :object-not-found))
;; When we have only profile, we need to check read permissiones
;; on file.
(when (and profile-id (not slink))
(files/check-read-permissions! conn profile-id file-id))
(cond-> bundle
;; If we have current profile, put
(some? profile-id)
(as-> $ (let [edit? (boolean (files/has-edit-permissions? conn profile-id file-id))
read? (boolean (files/has-read-permissions? conn profile-id file-id))]
(-> (assoc $ :permissions {:read read? :edit edit?})
(cond-> (not edit?) (dissoc :share-links)))))
(some? slink)
(assoc :share slink)
(and (some? slink)
(not (contains? (:flags slink) "view-all-pages")))
(update-in [:file :data] (fn [data]
(let [allowed-pages (:pages slink)]
(-> data
(update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages)))
(update :pages-index (fn [index] (select-keys index allowed-pages)))))))))))
;; --- Query: Viewer Bundle (by Page ID)
;; DEPRECATED: should be removed in 1.9.x
(declare check-shared-token!)
(declare retrieve-shared-token)
(def ^:private
sql:project
"select p.id, p.name, p.team_id
from project as p
where p.id = ?
and p.deleted_at is null")
(defn- retrieve-project
[conn id]
(db/exec-one! conn [sql:project id]))
(s/def ::id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::page-id ::us/uuid)
(s/def ::token ::us/string)
@@ -81,6 +155,3 @@
[conn file-id page-id]
(let [sql "select * from file_share_token where file_id=? and page_id=?"]
(db/exec-one! conn [sql file-id page-id])))

View File

@@ -60,7 +60,7 @@
(defmethod handle-deletion :team-font-variant
[{:keys [conn storage]} {:keys [id] :as props}]
(let [font (db/get-by-id conn :team-font-variant id {:uncheked true})
(let [font (db/get-by-id conn :team-font-variant id {:check-not-found false})
storage (assoc storage :conn conn)]
(when (:deleted-at font)
(db/delete! conn :team-font-variant {:id id})

View File

@@ -16,18 +16,18 @@
(t/use-fixtures :each th/database-reset)
(t/deftest retrieve-bundle
(let [prof (th/create-profile* 1 {:is-active true})
prof2 (th/create-profile* 2 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
(let [prof (th/create-profile* 1 {:is-active true})
prof2 (th/create-profile* 2 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
file (th/create-file* 1 {:profile-id (:id prof)
:project-id proj-id
:is-shared false})
token (atom nil)]
file (th/create-file* 1 {:profile-id (:id prof)
:project-id proj-id
:is-shared false})
share-id (atom nil)]
(t/testing "authenticated with page-id"
(let [data {::th/type :viewer-bundle
(let [data {::th/type :view-only-bundle
:profile-id (:id prof)
:file-id (:id file)
:page-id (get-in file [:data :pages 0])}
@@ -38,64 +38,67 @@
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (contains? result :token))
(t/is (contains? result :page))
(t/is (contains? result :share-links))
(t/is (contains? result :permissions))
(t/is (contains? result :libraries))
(t/is (contains? result :file))
(t/is (contains? result :project)))))
(t/testing "generate share token"
(let [data {::th/type :create-file-share-token
(let [data {::th/type :create-share-link
:profile-id (:id prof)
:file-id (:id file)
:page-id (get-in file [:data :pages 0])}
:pages #{(get-in file [:data :pages 0])}
:flags #{}}
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (string? (:token result)))
(reset! token (:token result)))))
(t/is (uuid? (:id result)))
(reset! share-id (:id result)))))
(t/testing "not authenticated with page-id"
(let [data {::th/type :viewer-bundle
(let [data {::th/type :view-only-bundle
:profile-id (:id prof2)
:file-id (:id file)
:page-id (get-in file [:data :pages 0])}
out (th/query! data)]
;; (th/print-result! out)
(let [error (:error out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))
(t/is (= (:code error-data) :object-not-found)))))
;; (t/testing "authenticated with token & profile"
;; (let [data {::sq/type :viewer-bundle
;; :profile-id (:id prof2)
;; :token @token
;; :file-id (:id file)
;; :page-id (get-in file [:data :pages 0])}
;; out (th/try-on! (sq/handle data))]
(t/testing "authenticated with token & profile"
(let [data {::th/type :view-only-bundle
:profile-id (:id prof2)
:share-id @share-id
:file-id (:id file)
:page-id (get-in file [:data :pages 0])}
out (th/query! data)]
;; ;; (th/print-result! out)
;; (th/print-result! out)
(t/is (nil? (:error out)))
;; (let [result (:result out)]
;; (t/is (contains? result :page))
;; (t/is (contains? result :file))
;; (t/is (contains? result :project)))))
(let [result (:result out)]
(t/is (contains? result :share))
(t/is (contains? result :file))
(t/is (contains? result :project)))))
;; (t/testing "authenticated with token"
;; (let [data {::sq/type :viewer-bundle
;; :token @token
;; :file-id (:id file)
;; :page-id (get-in file [:data :pages 0])}
;; out (th/try-on! (sq/handle data))]
(t/testing "authenticated with token"
(let [data {::th/type :view-only-bundle
:share-id @share-id
:file-id (:id file)
:page-id (get-in file [:data :pages 0])}
out (th/query! data)]
;; ;; (th/print-result! out)
;; (th/print-result! out)
(let [result (:result out)]
(t/is (contains? result :file))
(t/is (contains? result :share))
(t/is (contains? result :project)))))
;; (let [result (:result out)]
;; (t/is (contains? result :page))
;; (t/is (contains? result :file))
;; (t/is (contains? result :project)))))
))

View File

@@ -228,9 +228,12 @@
([params] (update-file* *pool* params))
([conn {:keys [file-id changes session-id profile-id revn]
:or {session-id (uuid/next) revn 0}}]
(let [file (db/get-by-id conn :file file-id)
msgbus (:app.msgbus/msgbus *system*)]
(#'files/update-file {:conn conn :msgbus msgbus}
(let [file (db/get-by-id conn :file file-id)
msgbus (:app.msgbus/msgbus *system*)
metrics (:app.metrics/metrics *system*)]
(#'files/update-file {:conn conn
:msgbus msgbus
:metrics metrics}
{:file file
:revn revn
:changes changes

View File

@@ -0,0 +1,32 @@
;; 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) UXBOX Labs SL
(ns app.common.flags
"Flags parsing algorithm."
(:require
[cuerdas.core :as str]))
(defn parse
[default flags]
(loop [flags (seq flags)
result default]
(let [item (first flags)]
(if (nil? item)
result
(let [sname (name item)]
(cond
(str/starts-with? sname "enable-")
(recur (rest flags)
(conj result (keyword (subs sname 7))))
(str/starts-with? sname "disable-")
(recur (rest flags)
(disj result (keyword (subs sname 8))))
:else
(recur (rest flags) result)))))))

View File

@@ -487,6 +487,7 @@
(d/parse-double)
(* (get-in modifiers [:resize-vector :x] 1))
(* (get-in modifiers [:resize-vector-2 :x] 1))
(mth/precision 2)
(str))]
(attrs/merge attrs {:font-size font-size})))]
(update shape :content #(txt/transform-nodes

View File

@@ -37,6 +37,8 @@
:stroke-style :stroke-group
:stroke-width :stroke-group
:stroke-alignment :stroke-group
:stroke-cap-start :stroke-group
:stroke-cap-end :stroke-group
:rx :radius-group
:ry :radius-group
:r1 :radius-group

View File

@@ -15,7 +15,7 @@
(def empty-page-data
{:options {}
:name "Page"
:name "Page-1"
:objects
{root
{:id root
@@ -38,7 +38,7 @@
(def ^:private minimal-shapes
[{:type :rect
:name "Rect"
:name "Rect-1"
:fill-color default-color
:fill-opacity 1
:stroke-style :none
@@ -52,7 +52,7 @@
{:type :image}
{:type :circle
:name "Circle"
:name "Circle-1"
:fill-color default-color
:fill-opacity 1
:stroke-style :none
@@ -62,7 +62,7 @@
:stroke-opacity 0}
{:type :path
:name "Path"
:name "Path-1"
:stroke-style :solid
:stroke-alignment :center
:stroke-width 2
@@ -70,7 +70,7 @@
:stroke-opacity 1}
{:type :frame
:name "Artboard"
:name "Artboard-1"
:fill-color "#ffffff"
:fill-opacity 1
:stroke-style :none
@@ -80,7 +80,7 @@
:stroke-opacity 0}
{:type :text
:name "Text"
:name "Text-1"
:content nil}
{:type :svg-raw}])

View File

@@ -10,6 +10,7 @@
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[clojure.set :as set]
[clojure.spec.alpha :as s]))
;; --- Specs
@@ -254,6 +255,17 @@
(s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?))
(s/def :internal.shape/stroke-opacity ::safe-number)
(s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none :svg})
(def stroke-caps-line #{:round :square})
(def stroke-caps-marker #{:line-arrow :triangle-arrow :square-marker :circle-marker :diamond-marker})
(def stroke-caps (set/union stroke-caps-line stroke-caps-marker))
(s/def :internal.shape/stroke-cap-start stroke-caps)
(s/def :internal.shape/stroke-cap-end stroke-caps)
(defn has-caps?
[shape]
(= (:type shape) :path))
(s/def :internal.shape/stroke-width ::safe-number)
(s/def :internal.shape/stroke-alignment #{:center :inner :outer})
(s/def :internal.shape/text-align #{"left" "right" "center" "justify"})
@@ -342,6 +354,8 @@
:internal.shape/stroke-style
:internal.shape/stroke-width
:internal.shape/stroke-alignment
:internal.shape/stroke-cap-start
:internal.shape/stroke-cap-end
:internal.shape/text-align
:internal.shape/transform
:internal.shape/transform-inverse

View File

@@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=v14.17.3 \
CLOJURE_VERSION=1.10.3.929 \
CLJKONDO_VERSION=2021.06.18 \
BABASHKA_VERSION=0.5.0 \
ENV NODE_VERSION=v14.17.5 \
CLOJURE_VERSION=1.10.3.933 \
CLJKONDO_VERSION=2021.07.28 \
BABASHKA_VERSION=0.5.1 \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8
@@ -44,6 +44,7 @@ RUN set -ex; \
python \
build-essential \
imagemagick \
ghostscript \
netpbm \
potrace \
webp \
@@ -97,6 +98,15 @@ RUN set -ex; \
; \
rm -rf /var/lib/apt/lists/*;
RUN set -x; \
apt-get -qq update; \
curl -LfsSo /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb; \
dpkg -i /tmp/chrome.deb; \
apt-get -fy install; \
rm -rf /var/lib/apt/lists/*; \
rm -rf /tmp/chrome.deb;
RUN set -ex; \
curl -LfsSo /tmp/openjdk.tar.gz https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz; \
mkdir -p /usr/lib/jvm/openjdk16; \

View File

@@ -92,6 +92,7 @@ services:
ports:
- "1080:1080"
# https://github.com/rroemhild/docker-test-openldap
ldap:
image: rroemhild/test-openldap:2.1
expose:

View File

@@ -9,6 +9,7 @@ FROM gitpod/workspace-postgres
RUN set -ex; \
brew install redis; \
brew install imagemagick; \
brew install ghostscript; \
brew install mailhog; \
brew install openldap; \
sudo mkdir -p /var/log/nginx; \

View File

@@ -1,11 +1,11 @@
FROM ubuntu:20.04
FROM debian:bullseye
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive
ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
NODE_VERSION=v14.16.0
NODE_VERSION=v14.17.5
RUN set -ex; \
mkdir -p /etc/resolvconf/resolv.conf.d; \
@@ -20,6 +20,7 @@ RUN set -ex; \
apt-get -qq update; \
apt-get -qqy install \
imagemagick \
ghostscript \
netpbm \
potrace \
gconf-service \
@@ -55,9 +56,9 @@ RUN set -ex; \
libxss1 \
libxtst6 \
fonts-liberation \
libappindicator1 \
libnss3 \
libgbm1 \
chromium \
; \
rm -rf /var/lib/apt/lists/*;

View File

@@ -4,7 +4,7 @@
binaryage/devtools {:mvn/version "RELEASE"}
metosin/reitit-core {:mvn/version "0.5.13"}
lambdaisland/glogi {:mvn/version "1.0.106"}
funcool/beicon {:mvn/version "2021.04.29-0"}
funcool/beicon {:mvn/version "2021.07.05-1"}
}
:aliases
{:outdated
@@ -14,7 +14,7 @@
:dev
{:extra-deps
{thheller/shadow-cljs {:mvn/version "2.14.1"}}}
{thheller/shadow-cljs {:mvn/version "2.15.2"}}}
:shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}

View File

@@ -9,18 +9,18 @@
"author": "UXBOX LABS SL",
"license": "SEE LICENSE IN <LICENSE>",
"dependencies": {
"generic-pool": "^3.8.2",
"inflation": "^2.0.0",
"jszip": "^3.6.0",
"jszip": "^3.7.0",
"koa": "^2.13.0",
"luxon": "^1.27.0",
"puppeteer": "^10.0.0",
"puppeteer-cluster": "^0.22.0",
"luxon": "^2.0.1",
"puppeteer-core": "^10.1.0",
"raw-body": "^2.4.1",
"xml-js": "^1.6.11",
"xregexp": "^5.0.2"
},
"devDependencies": {
"shadow-cljs": "^2.14.2",
"shadow-cljs": "^2.15.2",
"source-map-support": "^0.5.19"
}
}

View File

@@ -6,8 +6,10 @@
(ns app.browser
(:require
["puppeteer-cluster" :as ppc]
["puppeteer-core" :as pp]
["generic-pool" :as gp]
[app.common.data :as d]
[app.common.uuid :as uuid]
[app.config :as cf]
[lambdaisland.glogi :as log]
[promesa.core :as p]))
@@ -20,12 +22,6 @@
(str "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"))
(defn exec!
[browser f]
(.execute ^js browser (fn [props]
(let [page (unchecked-get props "page")]
(f page)))))
(defn set-cookie!
[page {:keys [key value domain]}]
(.setCookie ^js page #js {:name key
@@ -73,12 +69,14 @@
(defn pdf
([page] (pdf page nil))
([page {:keys [viewport omit-background? prefer-css-page-size?]
([page {:keys [viewport omit-background? prefer-css-page-size? save-path]
:or {viewport {}
omit-background? true
prefer-css-page-size? true}}]
prefer-css-page-size? true
save-path nil}}]
(let [viewport (d/merge default-viewport viewport)]
(.pdf ^js page #js {:width (:width viewport)
(.pdf ^js page #js {:path save-path
:width (:width viewport)
:height (:height viewport)
:scale (:scale viewport)
:omitBackground omit-background?
@@ -100,36 +98,76 @@
;; --- BROWSER STATE
(def instance (atom nil))
(defonce pool (atom nil))
(defonce pool-browser-id (atom 1))
(defn- create-browser
[concurrency strategy]
(let [strategy (case strategy
:browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster)
:incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster)
:page (.-CONCURRENCY_PAGE ^js ppc/Cluster))
opts #js {:concurrency strategy
:maxConcurrency concurrency
:puppeteerOptions #js {:args #js ["--no-sandbox"]}}]
(.launch ^js ppc/Cluster opts)))
(def browser-pool-factory
(letfn [(create []
(let [path (cf/get :browser-executable-path "/usr/bin/google-chrome")]
(-> (pp/launch #js {:executablePath path :args #js ["--no-sandbox"]})
(p/then (fn [browser]
(let [id (deref pool-browser-id)]
(log/info :origin "factory" :action "create" :browser-id id)
(unchecked-set browser "__num_use" 0)
(unchecked-set browser "__id" id)
(swap! pool-browser-id inc)
browser))))))
(destroy [obj]
(let [id (unchecked-get obj "__id")]
(log/info :origin "factory" :action "destroy" :browser-id id)
(.close ^js obj)))
(validate [obj]
(let [max-use (cf/get :browser-max-usage 10)
num-use (unchecked-get obj "__num_use")
id (unchecked-get obj "__id")]
(log/info :origin "factory" :action "validate" :browser-id id :max-use max-use :num-use num-use :obj obj)
(if (> num-use max-use)
(p/resolved false)
(do
(unchecked-set obj "__num_use" (inc num-use))
(p/resolved (.isConnected ^js obj))))))]
#js {:create create
:destroy destroy
:validate validate}))
(defn init
[]
(let [concurrency (cf/get :browser-concurrency)
strategy (cf/get :browser-strategy)]
(-> (create-browser concurrency strategy)
(p/then #(reset! instance %))
(p/catch (fn [error]
(log/error :msg "failed to initialize browser")
(js/console.error error))))))
(log/info :msg "initializing browser pool")
(let [opts #js {:max (cf/get :browser-pool-max 3)
:min (cf/get :browser-pool-min 0)
:testOnBorrow true
:evictionRunIntervalMillis 30000
:numTestsPerEvictionRun 5
:acquireTimeoutMillis 120000 ; 2min
:idleTimeoutMillis 30000}]
(reset! pool (gp/createPool browser-pool-factory opts))
(p/resolved nil)))
(defn stop
[]
(if-let [instance @instance]
(p/do!
(.idle ^js instance)
(.close ^js instance)
(log/info :msg "shutdown headless browser"))
(p/resolved nil)))
(when-let [pool (deref pool)]
(log/info :msg "finalizing browser pool")
(-> (.drain ^js pool)
(p/then (fn [] (.clear ^js pool))))))
(defn exec!
[f]
(letfn [(on-acquire [pool browser]
(p/let [ctx (.createIncognitoBrowserContext ^js browser)
page (.newPage ^js ctx)]
(-> (p/do! (f page))
(p/handle
(fn [result error]
(-> (p/do! (.close ^js ctx)
(.release ^js pool browser))
(p/handle (fn [_ _]
(if result
(p/resolved result)
(p/rejected error))))))))))]
(when-let [pool (deref pool)]
(-> (.acquire ^js pool)
(p/then (partial on-acquire pool))))))

View File

@@ -8,13 +8,15 @@
(:require
[app.config :as cf]
[app.http.export :refer [export-handler]]
[app.http.export-frames :refer [export-frames-handler]]
[app.http.impl :as impl]
[lambdaisland.glogi :as log]
[promesa.core :as p]
[reitit.core :as r]))
(def routes
[["/export" {:handler export-handler}]])
[["/export-frames" {:handler export-frames-handler}]
["/export" {:handler export-handler}]])
(def instance (atom nil))

View File

@@ -0,0 +1,69 @@
;; 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) UXBOX Labs SL
(ns app.http.export-frames
(:require
["path" :as path]
[app.common.exceptions :as exc :include-macros true]
[app.common.spec :as us]
[app.renderer.pdf :as rp]
[app.util.shell :as sh]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[promesa.core :as p]))
(s/def ::name ::us/string)
(s/def ::file-id ::us/uuid)
(s/def ::page-id ::us/uuid)
(s/def ::frame-id ::us/uuid)
(s/def ::frame-ids (s/coll-of ::frame-id :kind vector?))
(s/def ::handler-params
(s/keys :req-un [::file-id ::page-id ::frame-ids]))
(defn- export-frame
[tdpath file-id page-id token frame-id spaths]
(p/let [spath (path/join tdpath (str frame-id ".pdf"))
result (rp/render {:name (str frame-id)
:suffix ""
:token token
:file-id file-id
:page-id page-id
:object-id frame-id
:scale 1
:save-path spath})]
(cons spath spaths)))
(defn- join-files
[tdpath file-id paths]
(let [output-path (path/join tdpath (str file-id ".pdf"))
paths-str (str/join " " paths)]
(-> (sh/run-cmd! (str "gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile='" output-path "' " paths-str))
(p/then (constantly output-path)))))
(defn- clean-tmp-data
[tdpath data]
(p/do!
(sh/rmdir! tdpath)
data))
(defn export-frames-handler
[{:keys [params cookies] :as request}]
(let [{:keys [name file-id page-id frame-ids]} (us/conform ::handler-params params)
token (.get ^js cookies "auth-token")]
(p/let [tdpath (sh/create-tmpdir! "pdfexport-")
data (-> (reduce (fn [promis frame-id]
(p/then promis (partial export-frame tdpath file-id page-id token frame-id)))
(p/future [])
frame-ids)
(p/then (partial join-files tdpath file-id))
(p/then sh/read-file)
(p/then (partial clean-tmp-data tdpath)))]
{:status 200
:body data
:headers {"content-type" "application/pdf"
"content-length" (.-length data)}})))

View File

@@ -29,7 +29,7 @@
:value token}))
(defn screenshot-object
[browser {:keys [file-id page-id object-id token scale type]}]
[{:keys [file-id page-id object-id token scale type]}]
(letfn [(handle [page]
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
uri (-> (u/uri (cf/get :public-uri))
@@ -55,7 +55,7 @@
:png (bw/screenshot dom {:omit-background? true :type type})
:jpeg (bw/screenshot dom {:omit-background? false :type type}))))))]
(bw/exec! browser handle)))
(bw/exec! handle)))
(s/def ::name ::us/string)
(s/def ::suffix ::us/string)
@@ -74,22 +74,16 @@
(defn render
[params]
(us/assert ::render-params params)
(let [browser @bw/instance]
(when-not browser
(ex/raise :type :internal
:code :browser-not-ready
:hint "browser cluster is not initialized yet"))
(p/let [content (screenshot-object browser params)]
{:content content
:filename (or (:filename params)
(str (:name params)
(:suffix params "")
(case (:type params)
:png ".png"
:jpeg ".jpg")))
:length (alength content)
:mime-type (case (:type params)
:png "image/png"
:jpeg "image/jpeg")})))
(p/let [content (screenshot-object params)]
{:content content
:filename (or (:filename params)
(str (:name params)
(:suffix params "")
(case (:type params)
:png ".png"
:jpeg ".jpg")))
:length (alength content)
:mime-type (case (:type params)
:png "image/png"
:jpeg "image/jpeg")}))

View File

@@ -26,7 +26,7 @@
:value token}))
(defn pdf-from-object
[browser {:keys [file-id page-id object-id token scale type]}]
[{:keys [file-id page-id object-id token scale type save-path]}]
(letfn [(handle [page]
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
uri (-> (u/uri (cf/get :public-uri))
@@ -39,12 +39,14 @@
(log/info :uri uri)
(let [options {:cookie cookie}]
(p/do!
(bw/configure-page! page options)
(bw/navigate! page uri)
(bw/wait-for page "#screenshot")
(bw/pdf page))))]
(bw/configure-page! page options)
(bw/navigate! page uri)
(bw/wait-for page "#screenshot")
(if save-path
(bw/pdf page {:save-path save-path})
(bw/pdf page)))))]
(bw/exec! browser handle)))
(bw/exec! handle)))
(s/def ::name ::us/string)
(s/def ::suffix ::us/string)
@@ -54,26 +56,21 @@
(s/def ::scale ::us/number)
(s/def ::token ::us/string)
(s/def ::filename ::us/string)
(s/def ::save-path ::us/string)
(s/def ::render-params
(s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id]
:opt-un [::filename]))
:opt-un [::filename ::save-path]))
(defn render
[params]
(us/assert ::render-params params)
(let [browser @bw/instance]
(when-not browser
(ex/raise :type :internal
:code :browser-not-ready
:hint "browser cluster is not initialized yet"))
(p/let [content (pdf-from-object browser params)]
{:content content
:filename (or (:filename params)
(str (:name params)
(:suffix params "")
".pdf"))
:length (alength content)
:mime-type "application/pdf"})))
(p/let [content (pdf-from-object params)]
{:content content
:filename (or (:filename params)
(str (:name params)
(:suffix params "")
".pdf"))
:length (alength content)
:mime-type "application/pdf"}))

View File

@@ -114,7 +114,7 @@
(defn- render-object
[browser {:keys [page-id file-id object-id token scale suffix type]}]
[{:keys [page-id file-id object-id token scale suffix type]}]
(letfn [(convert-to-ppm [pngpath]
(log/trace :fn :convert-to-ppm)
(let [basepath (path/dirname pngpath)
@@ -279,7 +279,7 @@
rctx {:cookie cookie
:uri (str uri)}]
(log/info :uri (:uri rctx))
(bw/exec! browser (partial handle rctx)))))
(bw/exec! (partial handle rctx)))))
(s/def ::name ::us/string)
(s/def ::suffix ::us/string)
@@ -298,18 +298,11 @@
(defn render
[params]
(us/assert ::render-params params)
(let [browser @bw/instance]
(when-not browser
(ex/raise :type :internal
:code :browser-not-ready
:hint "browser cluster is not initialized yet"))
(p/let [content (render-object browser params)]
{:content content
:filename (or (:filename params)
(str (:name params)
(:suffix params "")
".svg"))
:length (alength content)
:mime-type "image/svg+xml"})))
(p/let [content (render-object params)]
{:content content
:filename (or (:filename params)
(str (:name params)
(:suffix params "")
".svg"))
:length (alength content)
:mime-type "image/svg+xml"}))

View File

@@ -2,23 +2,23 @@
# yarn lockfile v1
"@babel/runtime-corejs3@^7.12.1":
version "7.14.0"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.0.tgz#6bf5fbc0b961f8e3202888cb2cd0fb7a0a9a3f66"
integrity sha512-0R0HTZWHLk6G8jIk0FtoX+AatCtKnswS98VhXwGImFc759PJRp4Tru0PQYZofyijTFUr+gT8Mu7sgXVJLQ0ceg==
"@babel/runtime-corejs3@^7.14.9":
version "7.15.3"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.15.3.tgz#28754263988198f2a928c09733ade2fb4d28089d"
integrity sha512-30A3lP+sRL6ml8uhoJSs+8jwpKzbw8CqBvDc1laeptxPm5FahumJxirigcbD2qTs71Sonvj1cyZB0OKGAmxQ+A==
dependencies:
core-js-pure "^3.0.0"
core-js-pure "^3.16.0"
regenerator-runtime "^0.13.4"
"@types/node@*":
version "15.6.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.2.tgz#c61d49f38af70da32424b5322eee21f97e627175"
integrity sha512-dxcOx8801kMo3KlU+C+/ctWrzREAH7YvoF3aoVpRdqgs+Kf7flp+PJDN/EX5bME3suDUZHsxes9hpvBmzYlWbA==
version "16.6.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
"@types/yauzl@^2.9.1":
version "2.9.1"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af"
integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==
version "2.9.2"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a"
integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==
dependencies:
"@types/node" "*"
@@ -60,11 +60,6 @@ assert@^1.1.1:
object-assign "^4.1.1"
util "0.10.3"
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -174,9 +169,9 @@ buffer-crc32@~0.2.3:
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buffer-xor@^1.0.3:
version "1.0.3"
@@ -271,10 +266,10 @@ cookies@~0.8.0:
depd "~2.0.0"
keygrip "~1.1.0"
core-js-pure@^3.0.0:
version "3.13.1"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.13.1.tgz#5d139d346780f015f67225f45ee2362a6bed6ba1"
integrity sha512-wVlh0IAi2t1iOEh16y4u1TRk6ubd4KvLE8dlMi+3QUI6SfKphQUh7tAwihGGSQ8affxEXpVIPpOdf9kjR4v4Pw==
core-js-pure@^3.16.0:
version "3.16.2"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.16.2.tgz#0ef4b79cabafb251ea86eb7d139b42bd98c533e8"
integrity sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw==
core-util-is@~1.0.0:
version "1.0.2"
@@ -329,7 +324,14 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
debug@4, debug@4.3.1, debug@^4.1.1:
debug@4, debug@^4.1.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
dependencies:
ms "2.1.2"
debug@4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
@@ -376,10 +378,10 @@ destroy@^1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
devtools-protocol@0.0.883894:
version "0.0.883894"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.883894.tgz#d403f2c75cd6d71c916aee8dde9258da988a4da9"
integrity sha512-33idhm54QJzf3Q7QofMgCvIVSd2o9H3kQPWaKT/fhoZh+digc+WSiMhbkeG3iN79WY4Hwr9G05NpbhEVrsOYAg==
devtools-protocol@0.0.901419:
version "0.0.901419"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd"
integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==
diffie-hellman@^5.0.0:
version "5.0.3"
@@ -484,6 +486,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
generic-pool@^3.8.2:
version "3.8.2"
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9"
integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==
get-stream@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
@@ -503,6 +510,18 @@ glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
has-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
has-tostringtag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
dependencies:
has-symbols "^1.0.2"
hash-base@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
@@ -618,9 +637,11 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
is-generator-function@^1.0.7:
version "1.0.9"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.9.tgz#e5f82c2323673e7fcad3d12858c83c4039f6399c"
integrity sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A==
version "1.0.10"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
dependencies:
has-tostringtag "^1.0.0"
isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
@@ -632,10 +653,10 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
jszip@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9"
integrity sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==
jszip@^3.7.0:
version "3.7.1"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
@@ -712,10 +733,10 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
luxon@^1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.27.0.tgz#ae10c69113d85dab8f15f5e8390d0cbeddf4f00f"
integrity sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA==
luxon@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133"
integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg==
md5.js@^1.3.4:
version "1.3.5"
@@ -739,17 +760,17 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
mime-db@1.48.0:
version "1.48.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
mime-db@1.49.0:
version "1.49.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
mime-types@^2.1.18, mime-types@~2.1.24:
version "2.1.31"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b"
integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==
version "2.1.32"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==
dependencies:
mime-db "1.48.0"
mime-db "1.49.0"
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
@@ -986,20 +1007,13 @@ punycode@^1.2.4:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
puppeteer-cluster@^0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/puppeteer-cluster/-/puppeteer-cluster-0.22.0.tgz#4ab214671f414f15ad6a94a4b61ed0b4172e86e6"
integrity sha512-hmydtMwfVM+idFIDzS8OXetnujHGre7RY3BGL+3njy9+r8Dcu3VALkZHfuBEPf6byKssTCgzxU1BvLczifXd5w==
dependencies:
debug "^4.1.1"
puppeteer@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-10.0.0.tgz#1b597c956103e2d989ca17f41ba4693b20a3640c"
integrity sha512-AxHvCb9IWmmP3gMW+epxdj92Gglii+6Z4sb+W+zc2hTTu10HF0yg6hGXot5O74uYkVqG3lfDRLfnRpi6WOwi5A==
puppeteer-core@^10.1.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-10.2.0.tgz#8d6606cf345fc0e421bc0612055579ea53234111"
integrity sha512-c1COxSnfynsE6Mtt+dW0t3TITjF9Ku4dnJbFMDDVhLQuMTYSpz4rkSP37qvzcSo3k02/Ac3GYWk0/ncp6DKZNA==
dependencies:
debug "4.3.1"
devtools-protocol "0.0.883894"
devtools-protocol "0.0.901419"
extract-zip "2.0.1"
https-proxy-agent "5.0.0"
node-fetch "2.6.1"
@@ -1074,9 +1088,9 @@ readline-sync@^1.4.7:
integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
regenerator-runtime@^0.13.4:
version "0.13.7"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
rimraf@3.0.2:
version "3.0.2"
@@ -1146,17 +1160,17 @@ shadow-cljs-jar@1.3.2:
resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
shadow-cljs@^2.14.2:
version "2.14.2"
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.14.2.tgz#dba651ea124028064aea6fa9a390f257cb6eede4"
integrity sha512-ficaYfBAATzJ6OGt/GbIl393+cqLchzNkdTrM2PY4ttbsAOyBfWd39t+PZcYpCqemXjkgfBdZt9DJda7WaHJGA==
shadow-cljs@^2.15.2:
version "2.15.4"
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.15.4.tgz#0d657fc8ab9a02d8980db5c49cb1622e8fc6fa52"
integrity sha512-xn8UsiVpOf2LTsQZLsCa910CcMCYdMRT6STAsgveOEIncC9cunGdqE7cTq69vTmIijVQmzf0A1nALidyzO3Hcw==
dependencies:
node-libs-browser "^2.2.1"
readline-sync "^1.4.7"
shadow-cljs-jar "1.3.2"
source-map-support "^0.4.15"
which "^1.3.1"
ws "^3.0.0"
ws "^7.4.6"
source-map-support@^0.4.15:
version "0.4.18"
@@ -1282,11 +1296,6 @@ type-is@^1.6.16:
media-typer "0.3.0"
mime-types "~2.1.24"
ultron@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
unbzip2-stream@1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a"
@@ -1354,14 +1363,10 @@ ws@7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
ws@^3.0.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==
dependencies:
async-limiter "~1.0.0"
safe-buffer "~5.1.0"
ultron "~1.1.0"
ws@^7.4.6:
version "7.5.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
xml-js@^1.6.11:
version "1.6.11"
@@ -1371,11 +1376,11 @@ xml-js@^1.6.11:
sax "^1.2.4"
xregexp@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.0.2.tgz#798aa7757836f39cdbdeeba3daf94d75f7a9dcc1"
integrity sha512-JPNfN40YMNSDxZrahMrmtNH1QqPJp0/qNeEJM2nnOlhcBdfCCjekPYFV2OnwKxwvpEYglH1RBotbpRRaEuCG8Q==
version "5.1.0"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-5.1.0.tgz#c87e7ae5ffa5fdc520f898a467dcba02b0d391e9"
integrity sha512-PynwUWtXnSZr8tpQlDPMZfPTyv78EYuA4oI959ukxcQ0a9O/lvndLVKy5wpImzzA26eMxpZmnAXJYiQA13AtWA==
dependencies:
"@babel/runtime-corejs3" "^7.12.1"
"@babel/runtime-corejs3" "^7.14.9"
xtend@^4.0.0:
version "4.0.2"

View File

@@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><rect rx="6" ry="6" x="10" width="6" height="6"/><path d="M0 3h14.5" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 165 B

View File

@@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><rect rx="0" ry="0" x="11" y="1" transform="rotate(45 13 3)" width="4" height="4"/><path d="M0 3h14.5" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 199 B

View File

@@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><path d="M0 3h14.5M11.7 0l1 1 1.6 2-2.6 3" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 139 B

View File

@@ -0,0 +1 @@
<svg viewBox="1863 1374 16 8" width="16" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M1879 1374h-12s-4 0-4 4 4 4 4 4h12" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 166 B

View File

@@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><rect rx="0" ry="0" x="10" width="6" height="6" fill="#070707"/><path d="M0 3h14.5" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 180 B

View File

@@ -0,0 +1 @@
<svg viewBox="1863 1407 16 8" width="16" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M1879 1407h-16v8h16" fill="none" stroke="#000"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -0,0 +1 @@
<svg width="16" height="6" xmlns="http://www.w3.org/2000/svg"><path d="M0 3h14.5" fill="none" stroke="#000"/><path d="M13 0l2.9 3L13 6V0z"/></svg>

After

Width:  |  Height:  |  Size: 147 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 500 500" width="500" height="500" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M374.8 238.3l-19.6 18.5 94.8 97.3-437.2.3V383l437.2.3-94.8 97.3 18.8 19 126.4-130.8zM126 260.9l19.6-18.6L50.8 145H488v-28.8L50.8 116l94.8-97.2L126.8-.4.4 130.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -131,6 +131,24 @@
}
}
.btn-text-dark {
@extend %btn;
background: $color-gray-60;
color: $color-gray-20;
svg {
fill: $color-gray-20;
}
&:hover {
background: $color-primary;
color: $color-gray-60;
svg {
fill: $color-gray-60;
}
}
}
.btn-gray {
@extend %btn;
background: $color-gray-30;
@@ -588,7 +606,6 @@ input.element-name {
box-sizing: border-box;
flex-shrink: 0;
}
}
&.column {
@@ -975,6 +992,14 @@ input[type=range]:focus::-ms-fill-upper {
}
}
&.tooltip-expand {
&:hover {
&::after {
min-width: 100%;
}
}
}
&.tooltip-bottom-left {
&:hover {
&::after {
@@ -1130,7 +1155,7 @@ input[type=range]:focus::-ms-fill-upper {
padding-left: 16px;
top: 16px;
right: 16px;
z-index: 13;
z-index: 1005;
display: flex;
align-items: center;

View File

@@ -88,3 +88,4 @@
@import "main/partials/color-bullet";
@import "main/partials/handoff";
@import "main/partials/exception-page";
@import "main/partials/share-link";

View File

@@ -53,8 +53,8 @@
.icon {
display: flex;
align-items: center;
width: 25px;
height: 25px;
width: 20px;
height: 20px;
margin-right: 7px;
}
}

View File

@@ -154,6 +154,10 @@
.modal-footer .action-buttons {
justify-content: space-around;
}
.fields-container {
margin-top: 1rem;
}
}
.confirm-dialog {
@@ -807,7 +811,7 @@
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
height: 100%;
width: 106%;
width: 115%;
}
}
}

View File

@@ -0,0 +1,141 @@
.share-link-dialog {
width: 475px;
background-color: $color-white;
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
height: unset;
padding: 16px 26px;
.btn-primary,
.btn-secondary,
.btn-warning {
width: 126px;
margin-bottom: 0px;
&:not(:last-child) {
margin-right: 10px;
}
}
.confirm-dialog {
display: flex;
flex-direction: column;
background-color: unset;
.description {
font-size: $fs14;
margin-bottom: 16px;
}
.actions {
display: flex;
justify-content: flex-end;
}
}
}
.modal-content {
padding: 26px;
&:first-child {
border-top: 0px;
}
.title {
display: flex;
justify-content: space-between;
h2 {
font-size: $fs18;
color: $color-black;
}
.modal-close-button {
margin-right: 0px;
}
}
.share-link-section {
margin-top: 12px;
label {
font-size: $fs11;
color: $color-black;
}
.hint {
padding-top: 10px;
font-size: $fs14;
color: $color-gray-40;
}
.help-icon {
cursor: pointer;
}
}
.view-mode,
.access-mode {
display: flex;
flex-direction: column;
.title {
color: $color-black;
font-weight: 400;
}
.items {
padding-left: 20px;
display: flex;
> .input-checkbox, > .input-radio {
display: flex;
user-select: none;
/* input { */
/* appearance: checkbox; */
/* } */
label {
display: flex;
align-items: center;
color: $color-black;
.hint {
margin-left: 5px;
color: $color-gray-30;
}
}
&.disabled {
label {
color: $color-gray-30;
}
}
}
}
}
.pages-selection {
padding-left: 20px;
max-height: 200px;
overflow-y: scroll;
user-select: none;
label {
color: $color-black;
}
}
.custom-input {
input {
padding: 0 40px 0 15px;
}
}
}
}

View File

@@ -1316,7 +1316,7 @@
&::after {
content: ' ';
background-color: $color-gray-20;
background-color: $color-gray-30;
}
&.active,
@@ -1436,5 +1436,57 @@
}
}
}
}
.cap-select {
background-color: transparent;
border: 1px solid transparent;
border-bottom-color: $color-gray-40;
color: $color-gray-10;
cursor: pointer;
font-size: $fs11;
margin: $x-small;
overflow: hidden;
padding: $x-small;
padding-right: 20px;
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
& .cap-select-button {
svg {
fill: $color-gray-10;
height: 11px;
position: absolute;
right: 5px;
top: 6px;
width: 11px;
}
}
&:hover {
border-color: $color-gray-40;
}
&:focus {
border-color: $color-primary;
}
}
.cap-select-dropdown {
right: 5px;
top: 30px;
z-index: 12;
min-width: 200px;
position: fixed;
& li.separator {
border-top: 1px solid $color-gray-10;
}
& li img {
width: 16px;
margin-right: $small;
}
}

View File

@@ -42,56 +42,64 @@
}
}
.view-options {
.icon {
align-items: center;
cursor: pointer;
display: flex;
justify-content: center;
.options-zone {
align-items: center;
display: flex;
// width: 384px;
justify-content: flex-end;
position: relative;
svg {
fill: $color-gray-30;
height: 30px;
width: 28px;
}
> * {
margin-left: $big;
}
&:hover {
> svg {
fill: $color-primary;
}
.btn-primary {
flex-shrink: 0;
}
.zoom-widget {
.dropdown {
top: 45px;
left: 25px;
}
}
.dropdown {
min-width: 260px;
left: 0px;
top: 40px;
}
.view-options-dropdown {
.view-options {
align-items: center;
cursor: pointer;
display: flex;
width: 90px;
span {
> span {
color: $color-gray-10;
font-size: $fs13;
margin-right: $x-small;
}
svg {
fill: $color-gray-10;
height: 12px;
width: 12px;
}
}
}
> .icon {
align-items: center;
cursor: pointer;
display: flex;
justify-content: center;
.file-menu {
.dropdown {
min-width: 100px;
right: 0px;
top: 40px;
svg {
fill: $color-gray-10;
height: 12px;
width: 12px;
}
&:hover {
> svg {
fill: $color-primary;
}
}
}
.dropdown {
min-width: 260px;
top: 45px;
left: -25px;
}
}
}
@@ -100,39 +108,50 @@
cursor: pointer;
display: flex;
padding: $x-small;
position: relative;
svg {
fill: $color-gray-20;
height: 20px;
margin-right: $small;
width: 20px;
}
.icon {
display: flex;
justify-content: center;
align-items: center;
span {
color: $color-gray-20;
margin-right: $x-small;
font-size: $fs14;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.frame-name {
color: $color-white;
svg {
fill: $color-gray-20;
height: 12px;
margin-right: $small;
width: 12px;
}
}
.show-thumbnails-button svg {
fill: $color-white;
height: 10px;
width: 10px;
.breadcrumb, .current-frame {
display: flex;
position: relative;
> span {
color: $color-gray-20;
margin-right: $x-small;
font-size: $fs14;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
> .dropdown {
top: 45px;
right: 10px;
}
}
.page-name {
color: $color-white;
}
.current-frame {
display: flex;
span {
color: $color-white;
margin-right: $x-small;
}
.counters {
margin-left: $size-3;
.counters {
color: $color-gray-20;
}
}
}
@@ -166,133 +185,6 @@
}
}
.options-zone {
align-items: center;
display: flex;
width: 384px;
justify-content: flex-end;
position: relative;
> * {
margin-left: $big;
}
.btn-share {
display: flex;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
cursor: pointer;
svg {
fill: $color-gray-20;
width: 20px;
height: 20px;
}
}
.btn-primary {
flex-shrink: 0;
}
}
.share-link-dropdown {
background-color: $color-white;
border-radius: $br-small;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25);
display: flex;
flex-direction: column;
left: -135px;
position: absolute;
padding: 1rem;
top: 45px;
width: 400px;
.share-link-title {
color: $color-black;
font-size: $fs15;
padding-bottom: 1rem;
}
.share-link-subtitle {
color: $color-gray-40;
padding-bottom: 1rem;
}
.share-link-buttons {
display: flex;
justify-content: center;
align-items: center;
.btn-warning,
.btn-primary {
width: 50%;
}
}
.share-link-input {
border: 1px solid $color-gray-20;
border-radius: 3px;
display: flex;
height: 40px;
justify-content: space-between;
margin-bottom: 1rem;
padding: 9px $small;
overflow: hidden;
.link {
&:before {
content: '';
position: absolute;
width: 50%;
background: linear-gradient(45deg, transparent, #ffffff);
height: 100%;
top: 0;
left: 0;
pointer-events: none;
margin-left: 50%;
}
overflow: hidden;
white-space: nowrap;
position: relative;
color: $color-gray-50;
line-height: 1.5;
user-select: all;
overflow: hidden;
}
.link-button {
color: $color-primary-dark;
cursor: pointer;
flex-shrink: 0;
font-size: $fs15;
&:hover {
color: $color-black;
}
}
}
&:before {
background-color: $color-white;
content: "";
height: 16px;
left: 53%;
position: absolute;
transform: rotate(45deg);
top: -5px;
width: 16px;
}
}
.zoom-dropdown {
left: 180px;
top: 40px;
}
.users-zone {
align-items: center;
cursor: pointer;

View File

@@ -1,4 +1,3 @@
.viewer-thumbnails {
grid-row: 1 / span 1;
grid-column: 1 / span 1;
@@ -9,6 +8,11 @@
flex-direction: column;
z-index: 12;
&.invisible {
visibility: hidden;
pointer-events: none;
}
&.expanded {
grid-row: 1 / span 2;
@@ -159,7 +163,7 @@
&:hover {
border-color: $color-primary;
border-width: 2px;
outline: 2px solid $color-primary;
}
}

View File

@@ -8,13 +8,13 @@
margin-left: $x-small;
}
.dropdown-button svg {
.icon svg {
fill: $color-gray-10;
height: 10px;
width: 10px;
}
.zoom-dropdown {
.dropdown {
position: absolute;
z-index: 12;
width: 210px;

View File

@@ -6,6 +6,7 @@
(ns app.config
(:require
[app.common.flags :as flags]
[app.common.spec :as us]
[app.common.uri :as u]
[app.common.version :as v]
@@ -53,10 +54,14 @@
:browser
:webworker))
(def default-flags
#{:registration :demo-users})
(defn- parse-flags
[global]
(let [flags (obj/get global "penpotFlags" "")]
(into #{} (map keyword) (str/words flags))))
(let [flags (obj/get global "penpotFlags" "")
flags (into #{} (map keyword) (str/words flags))]
(flags/parse default-flags flags)))
(defn- parse-version
[global]
@@ -68,26 +73,27 @@
(def default-theme "default")
(def default-language "en")
(def demo-warning (obj/get global "penpotDemoWarning" false))
(def feedback-enabled (obj/get global "penpotFeedbackEnabled" false))
(def allow-demo-users (obj/get global "penpotAllowDemoUsers" true))
(def google-client-id (obj/get global "penpotGoogleClientID" nil))
(def gitlab-client-id (obj/get global "penpotGitlabClientID" nil))
(def github-client-id (obj/get global "penpotGithubClientID" nil))
(def oidc-client-id (obj/get global "penpotOIDCClientID" nil))
(def login-with-ldap (obj/get global "penpotLoginWithLDAP" false))
(def registration-enabled (obj/get global "penpotRegistrationEnabled" true))
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
(def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes"))
(def analytics (obj/get global "penpotAnalyticsEnabled" false))
(def flags (delay (parse-flags global)))
(def flags (atom (parse-flags global)))
(def version (atom (parse-version global)))
(def target (atom (parse-target global)))
(def browser (atom (parse-browser)))
(def platform (atom (parse-platform)))
(def version (delay (parse-version global)))
(def target (delay (parse-target global)))
(def browser (delay (parse-browser)))
(def platform (delay (parse-platform)))
;; mantain for backward compatibility
(let [login-with-ldap (obj/get global "penpotLoginWithLDAP" false)
registration (obj/get global "penpotRegistrationEnabled" true)]
(when login-with-ldap
(swap! flags conj :login-with-ldap))
(when (false? registration)
(swap! flags disj :registration)))
(def public-uri
(let [uri (u/uri (or (obj/get global "penpotPublicURI")

View File

@@ -42,10 +42,13 @@
(if-let [conform (get-in match [:data :conform])]
(let [spath (get conform :path-params ::any)
squery (get conform :query-params ::any)]
(-> (dissoc match :params)
(assoc :path-params (us/conform spath (get match :path-params))
:query-params (us/conform squery (get match :query-params)))))
match)))
(try
(-> (dissoc match :params)
(assoc :path-params (us/conform spath (get match :path-params))
:query-params (us/conform squery (get match :query-params))))
(catch :default _
nil)))
match)))
(defn on-navigate
[router path]

View File

@@ -72,7 +72,7 @@
(update :workspace-drawing dissoc :comment)
(update-in [:comments id] assoc (:id comment) comment)))]
(ptk/reify ::create-thread
(ptk/reify ::create-comment-thread
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/mutation :create-comment-thread params)
@@ -94,6 +94,8 @@
[{:keys [id is-resolved] :as thread}]
(us/assert ::comment-thread thread)
(ptk/reify ::update-comment-thread
IDeref
(-deref [_] {:is-resolved is-resolved})
ptk/UpdateEvent
(update [_ state]
@@ -122,7 +124,7 @@
(defn update-comment
[{:keys [id content thread-id] :as comment}]
(us/assert ::comment comment)
(ptk/reify :update-comment
(ptk/reify ::update-comment
ptk/UpdateEvent
(update [_ state]
(d/update-in-when state [:comments thread-id id] assoc :content content))
@@ -135,7 +137,7 @@
(defn delete-comment-thread
[{:keys [id] :as thread}]
(us/assert ::comment-thread thread)
(ptk/reify :delete-comment-thread
(ptk/reify ::delete-comment-thread
ptk/UpdateEvent
(update [_ state]
(-> state
@@ -150,7 +152,7 @@
(defn delete-comment
[{:keys [id thread-id] :as comment}]
(us/assert ::comment comment)
(ptk/reify :delete-comment
(ptk/reify ::delete-comment
ptk/UpdateEvent
(update [_ state]
(d/update-in-when state [:comments thread-id] dissoc id))
@@ -212,7 +214,7 @@
(defn open-thread
[{:keys [id] :as thread}]
(us/assert ::comment-thread thread)
(ptk/reify ::open-thread
(ptk/reify ::open-comment-thread
ptk/UpdateEvent
(update [_ state]
(-> state
@@ -221,7 +223,7 @@
(defn close-thread
[]
(ptk/reify ::close-thread
(ptk/reify ::close-comment-thread
ptk/UpdateEvent
(update [_ state]
(-> state

View File

@@ -0,0 +1,46 @@
;; 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) UXBOX Labs SL
(ns app.main.data.common
"A general purpose events."
(:require
[app.main.repo :as rp]
[beicon.core :as rx]
[potok.core :as ptk]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHARE LINK
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn share-link-created
[link]
(ptk/reify ::share-link-created
ptk/UpdateEvent
(update [_ state]
(update state :share-links (fnil conj []) link))))
(defn create-share-link
[params]
(ptk/reify ::create-share-link
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/mutation! :create-share-link params)
(rx/map share-link-created)))))
(defn delete-share-link
[{:keys [id] :as link}]
(ptk/reify ::delete-share-link
ptk/UpdateEvent
(update [_ state]
(update state :share-links
(fn [links]
(filterv #(not= id (:id %)) links))))
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/mutation! :delete-share-link {:id id})
(rx/ignore)))))

View File

@@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.fonts :as df]
[app.main.data.media :as di]
[app.main.data.users :as du]
@@ -386,6 +387,9 @@
(us/assert ::us/email email)
(us/assert ::us/keyword role)
(ptk/reify ::invite-team-member
IDeref
(-deref [_] {:role role})
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [on-success on-error]
@@ -475,6 +479,10 @@
(us/assert ::us/uuid id)
(us/assert ::us/uuid team-id)
(ptk/reify ::move-project
IDeref
(-deref [_]
{:id id :team-id team-id})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
@@ -566,6 +574,10 @@
[{:keys [id name] :as params}]
(us/assert ::file params)
(ptk/reify ::rename-file
IDeref
(-deref [_]
{::ev/origin "dashboard" :id id :name name})
ptk/UpdateEvent
(update [_ state]
(-> state
@@ -585,6 +597,10 @@
[{:keys [id is-shared] :as params}]
(us/assert ::file params)
(ptk/reify ::set-file-shared
IDeref
(-deref [_]
{::ev/origin "dashboard" :id id :shared is-shared})
ptk/UpdateEvent
(update [_ state]
(-> state
@@ -663,12 +679,16 @@
(us/assert ::set-of-uuid ids)
(us/assert ::us/uuid project-id)
(ptk/reify ::move-files
IDeref
(-deref [_]
{:num-files (count ids)
:project-id project-id})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/mutation! :move-files {:ids ids :project-id project-id})
(rx/tap on-success)
(rx/catch on-error))))))
@@ -690,14 +710,14 @@
(defn go-to-files
([project-id]
(ptk/reify ::go-to-files
(ptk/reify ::go-to-files-1
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-files {:team-id team-id
:project-id project-id}))))))
([team-id project-id]
(ptk/reify ::go-to-files
(ptk/reify ::go-to-files-2
ptk/WatchEvent
(watch [_ _ _]
(rx/of (rt/nav :dashboard-files {:team-id team-id
@@ -719,13 +739,13 @@
(defn go-to-projects
([]
(ptk/reify ::go-to-projects
(ptk/reify ::go-to-projects-0
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
([team-id]
(ptk/reify ::go-to-projects
(ptk/reify ::go-to-projects-1
ptk/WatchEvent
(watch [_ _ _]
(du/set-current-team! team-id)

View File

@@ -71,18 +71,84 @@
;; --- EVENT TRANSLATION
(defmulti ^:private process-event ptk/type)
(derive :app.main.data.comments/create-comment ::generic-action)
(derive :app.main.data.comments/create-comment-thread ::generic-action)
(derive :app.main.data.comments/delete-comment ::generic-action)
(derive :app.main.data.comments/delete-comment-thread ::generic-action)
(derive :app.main.data.comments/open-comment-thread ::generic-action)
(derive :app.main.data.comments/update-comment ::generic-action)
(derive :app.main.data.comments/update-comment-thread ::generic-action)
(derive :app.main.data.comments/update-comment-thread-status ::generic-action)
(derive :app.main.data.dashboard/delete-team-member ::generic-action)
(derive :app.main.data.dashboard/duplicate-project ::generic-action)
(derive :app.main.data.dashboard/file-created ::generic-action)
(derive :app.main.data.dashboard/invite-team-member ::generic-action)
(derive :app.main.data.dashboard/leave-team ::generic-action)
(derive :app.main.data.dashboard/move-files ::generic-action)
(derive :app.main.data.dashboard/move-project ::generic-action)
(derive :app.main.data.dashboard/project-created ::generic-action)
(derive :app.main.data.dashboard/rename-file ::generic-action)
(derive :app.main.data.dashboard/set-file-shared ::generic-action)
(derive :app.main.data.dashboard/update-team-member-role ::generic-action)
(derive :app.main.data.dashboard/update-team-photo ::generic-action)
(derive :app.main.data.fonts/add-font ::generic-action)
(derive :app.main.data.fonts/delete-font ::generic-action)
(derive :app.main.data.fonts/delete-font-variant ::generic-action)
(derive :app.main.data.users/logout ::generic-action)
(derive :app.main.data.users/request-email-change ::generic-action)
(derive :app.main.data.users/update-password ::generic-action)
(derive :app.main.data.users/update-photo ::generic-action)
(derive :app.main.data.workspace.comments/open-comment-thread ::generic-action)
(derive :app.main.data.workspace.libraries/add-color ::generic-action)
(derive :app.main.data.workspace.libraries/add-media ::generic-action)
(derive :app.main.data.workspace.libraries/add-typography ::generic-action)
(derive :app.main.data.workspace.libraries/delete-color ::generic-action)
(derive :app.main.data.workspace.libraries/delete-media ::generic-action)
(derive :app.main.data.workspace.libraries/delete-typography ::generic-action)
(derive :app.main.data.workspace.persistence/attach-library ::generic-action)
(derive :app.main.data.workspace.persistence/detach-library ::generic-action)
(derive :app.main.data.workspace.persistence/set-file-shard ::generic-action)
(derive :app.main.data.workspace/create-page ::generic-action)
(derive :app.main.data.workspace/set-workspace-layout ::generic-action)
(defmulti process-event ptk/type)
(defmethod process-event :default [_] nil)
(defmethod process-event ::event
[event]
(let [data (deref event)]
(let [data (deref event)
origin (::origin data)]
(when (::name data)
(d/without-nils
{:type (::type data "action")
:name (::name data)
:context (::context data)
:props (dissoc data ::name ::type ::context)}))))
:props (-> data
(dissoc ::name)
(dissoc ::type)
(dissoc ::origin)
(dissoc ::context)
(cond-> origin (assoc :origin origin)))}))))
(defmethod process-event ::generic-action
[event]
(let [type (ptk/type event)
mdata (meta event)
data (if (satisfies? IDeref event)
(deref event)
{})
name (or (::name mdata)
(name type))]
{:type "action"
:name (name type)
:props (merge data (d/without-nils (::props mdata)))
:context (d/without-nils
{:event-origin (::origin mdata)
:event-namespace (namespace type)
:event-symbol (name type)})}))
(defmethod process-event :app.util.router/navigated
[event]
@@ -113,42 +179,6 @@
:profile-id (:id data)
:props (d/without-nils props)}))
(defmethod process-event :app.main.data.dashboard/project-created
[event]
(let [data (deref event)]
{:type "action"
:name "create-project"
:props {:id (:id data)
:team-id (:team-id data)}}))
(defmethod process-event :app.main.data.dashboard/file-created
[event]
(let [data (deref event)]
{:type "action"
:name "create-file"
:props {:id (:id data)
:project-id (:project-id data)}}))
(defmethod process-event :app.main.data.workspace/create-page
[event]
(let [data (deref event)]
{:type "action"
:name "create-page"
:props {:id (:id data)
:file-id (:file-id data)
:project-id (:project-id data)}}))
(defn- event->generic-action
[_ name]
{:type "action"
:name name
:props {}})
(defmethod process-event :app.main.data.users/logout
[event]
(event->generic-action event "signout"))
;; --- MAIN LOOP
(defn- append-to-buffer
@@ -164,7 +194,7 @@
(defn- persist-events
[events]
(if (seq events)
(let [uri (u/join cf/public-uri "events")
(let [uri (u/join cf/public-uri "api/audit/events")
params {:events events}]
(->> (http/send! {:uri uri
:method :post
@@ -203,8 +233,7 @@
ptk/EffectEvent
(effect [_ _ stream]
(let [events (methods process-event)
session (atom nil)
(let [session (atom nil)
profile (->> (rx/from-atom storage {:emit-current-value? true})
(rx/map :profile)
@@ -215,12 +244,9 @@
(rx/with-latest-from profile)
(rx/map (fn [result]
(let [event (aget result 0)
profile-id (aget result 1)
type (ptk/type event)
impl-fn (get events type)]
(when (fn? impl-fn)
(some-> (impl-fn event)
(update :profile-id #(or % profile-id)))))))
profile-id (aget result 1)]
(some-> (process-event event)
(update :profile-id #(or % profile-id))))))
(rx/filter :profile-id)
(rx/map (fn [event]
(let [session* (or @session (dt/now))
@@ -242,6 +268,6 @@
(defmethod ptk/resolve ::initialize
[_ params]
(if cf/analytics
(if (contains? @cf/flags :audit-log)
(initialize)
(ptk/data-event ::initialize params)))

View File

@@ -111,10 +111,13 @@
(:data content)})
(dissoc :content)))))))
(parse-mtype [mtype]
(case mtype
"application/vnd.oasis.opendocument.formula-template" "font/otf"
mtype))
(parse-mtype [ba]
(let [u8 (js/Uint8Array. ba 0 4)
sg (areduce u8 i ret "" (str ret (if (zero? i) "" " ") (.toString (aget u8 i) 8)))]
(case sg
"117 124 124 117" "font/otf"
"0 1 0 0" "font/ttf"
"167 117 106 106" "font/woff")))
(parse-font [{:keys [data] :as params}]
(try
@@ -128,7 +131,11 @@
(rx/map (fn [data]
{:data data
:name (.-name blob)
:type (parse-mtype (.-type blob))}))))]
:type (parse-mtype data)}))
(rx/mapcat (fn [{:keys [type] :as font}]
(if type
(rx/of font)
(rx/empty))))))]
(->> (rx/from blobs)
(rx/mapcat read-blob)
@@ -180,6 +187,9 @@
(defn add-font
[font]
(ptk/reify ::add-font
IDeref
(-deref [_] (select-keys font [:font-family :font-style :font-weight]))
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-fonts assoc (:id font) font))))

View File

@@ -24,6 +24,10 @@
;; --- COMMON SPECS
(defn is-authenticated?
[{:keys [id]}]
(and (uuid? id) (not= id uuid/zero)))
(s/def ::id ::us/uuid)
(s/def ::fullname ::us/string)
(s/def ::email ::us/email)

View File

@@ -14,24 +14,12 @@
[app.main.data.comments :as dcm]
[app.main.data.fonts :as df]
[app.main.repo :as rp]
[app.util.globals :as ug]
[app.util.router :as rt]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
;; --- General Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::project (s/keys :req-un [::id ::name]))
(s/def ::file (s/keys :req-un [::id ::name]))
(s/def ::page ::cp/page)
(s/def ::bundle
(s/keys :req-un [::project ::file ::page]))
;; --- Local State Initialization
(def ^:private
@@ -49,25 +37,24 @@
(declare fetch-bundle)
(declare bundle-fetched)
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::index ::us/integer)
(s/def ::token (s/nilable ::us/string))
(s/def ::page-id (s/nilable ::us/uuid))
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::section ::us/string)
(s/def ::initialize-params
(s/keys :req-un [::page-id ::file-id]
:opt-un [::token]))
(s/keys :req-un [::file-id]
:opt-un [::share-id ::page-id]))
(defn initialize
[{:keys [page-id file-id] :as params}]
[{:keys [file-id] :as params}]
(us/assert ::initialize-params params)
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :current-file-id file-id)
(assoc :current-page-id page-id)
(update :viewer-local
(fn [lstate]
(if (nil? lstate)
@@ -77,55 +64,72 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of (fetch-bundle params)
(fetch-comment-threads params)))))
(fetch-comment-threads params)))
;; --- Data Fetching
ptk/EffectEvent
(effect [_ _ _]
;; Set the window name, the window name is used on inter-tab
;; navigation; in other words: when a user opens a tab with a
;; name, if there are already opened tab with that name, the
;; browser just focus the opened tab instead of creating new
;; tab.
(let [name (str "viewer-" file-id)]
(unchecked-set ug/global "name" name)))))
(s/def ::fetch-bundle-params
(s/keys :req-un [::page-id ::file-id]
:opt-un [::token]))
(defn finalize
[_]
(ptk/reify ::finalize
ptk/UpdateEvent
(update [_ state]
(dissoc state :viewer))))
(defn fetch-bundle
[{:keys [page-id file-id token] :as params}]
(us/assert ::fetch-bundle-params params)
(ptk/reify ::fetch-file
ptk/WatchEvent
(watch [_ _ _]
(let [params (cond-> {:page-id page-id
:file-id file-id}
(string? token) (assoc :token token))]
(->> (rp/query :viewer-bundle params)
(rx/mapcat
(fn [{:keys [fonts] :as bundle}]
(rx/of (df/fonts-fetched fonts)
(bundle-fetched bundle)))))))))
(defn- extract-frames
[objects]
(defn select-frames
[{:keys [objects] :as page}]
(let [root (get objects uuid/zero)]
(into [] (comp (map #(get objects %))
(filter #(= :frame (:type %))))
(reverse (:shapes root)))))
;; --- Data Fetching
(s/def ::fetch-bundle-params
(s/keys :req-un [::page-id ::file-id]
:opt-un [::share-id]))
(defn fetch-bundle
[{:keys [file-id share-id] :as params}]
(us/assert ::fetch-bundle-params params)
(ptk/reify ::fetch-file
ptk/WatchEvent
(watch [_ _ _]
(let [params' (cond-> {:file-id file-id}
(uuid? share-id) (assoc :share-id share-id))]
(->> (rp/query :view-only-bundle params')
(rx/mapcat
(fn [{:keys [fonts] :as bundle}]
(rx/of (df/fonts-fetched fonts)
(bundle-fetched (merge bundle params))))))))))
(defn bundle-fetched
[{:keys [project file page share-token token libraries users] :as bundle}]
(us/verify ::bundle bundle)
(ptk/reify ::bundle-fetched
ptk/UpdateEvent
(update [_ state]
(let [objects (:objects page)
frames (extract-frames objects)]
[{:keys [project file share-links libraries users permissions] :as bundle}]
(let [pages (->> (get-in file [:data :pages])
(map (fn [page-id]
(let [data (get-in file [:data :pages-index page-id])]
[page-id (assoc data :frames (select-frames data))])))
(into {}))]
(ptk/reify ::bundle-fetched
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :viewer-libraries (d/index-by :id libraries))
(update :viewer-data assoc
:project project
:objects objects
:users (d/index-by :id users)
:file file
:page page
:frames frames
:token token
:share-token share-token))))))
(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
:file file}))))))
(defn fetch-comment-threads
[{:keys [file-id page-id] :as params}]
@@ -168,32 +172,6 @@
(->> (rp/query :comments {:thread-id thread-id})
(rx/map #(partial fetched %)))))))
(defn create-share-link
[]
(ptk/reify ::create-share-link
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)]
(->> (rp/mutation! :create-file-share-token {:file-id file-id
:page-id page-id})
(rx/map (fn [{:keys [token]}]
#(assoc-in % [:viewer-data :token] token))))))))
(defn delete-share-link
[]
(ptk/reify ::delete-share-link
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
token (get-in state [:viewer-data :token])
params {:file-id file-id
:page-id page-id
:token token}]
(->> (rp/mutation :delete-file-share-token params)
(rx/map (fn [_] #(update % :viewer-data dissoc :token))))))))
;; --- Zoom Management
(def increase-zoom
@@ -245,29 +223,32 @@
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route)
index (:index qparams)]
(when (pos? index)
(rx/of
(dcm/close-thread)
(rt/nav screen pparams (assoc qparams :index (dec index)))))))))
(rt/nav :viewer pparams (assoc qparams :index (dec index)))))))))
(def select-next-frame
(ptk/reify ::select-prev-frame
(ptk/reify ::select-next-frame
ptk/WatchEvent
(watch [_ state _]
(prn "select-next-frame")
(let [route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route)
qparams (:query-params route)
page-id (:page-id qparams)
index (:index qparams)
total (count (get-in state [:viewer-data :frames]))]
total (count (get-in state [:viewer :pages page-id :frames]))]
(when (< index (dec total))
(rx/of
(dcm/close-thread)
(rt/nav screen pparams (assoc qparams :index (inc index)))))))))
(rt/nav :viewer pparams (assoc qparams :index (inc index)))))))))
(s/def ::interactions-mode #{:hide :show :show-on-click})
@@ -309,7 +290,7 @@
(defn go-to-frame-by-index
[index]
(ptk/reify ::go-to-frame
(ptk/reify ::go-to-frame-by-index
ptk/WatchEvent
(watch [_ state _]
(let [route (:route state)
@@ -324,12 +305,15 @@
(ptk/reify ::go-to-frame
ptk/WatchEvent
(watch [_ state _]
(let [frames (get-in state [:viewer-data :frames])
(let [route (:route state)
qparams (:query-params route)
page-id (:page-id qparams)
frames (get-in state [:viewer :pages page-id :frames])
index (d/index-of-pred frames #(= (:id %) frame-id))]
(when index
(rx/of (go-to-frame-by-index index)))))))
(defn go-to-section
[section]
(ptk/reify ::go-to-section
@@ -340,13 +324,6 @@
qparams (:query-params route)]
(rx/of (rt/nav :viewer pparams (assoc qparams :section section)))))))
(defn set-current-frame [frame-id]
(ptk/reify ::set-current-frame
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-data :current-frame-id] frame-id))))
(defn deselect-all []
(ptk/reify ::deselect-all
ptk/UpdateEvent
@@ -376,7 +353,10 @@
(ptk/reify ::shift-select-to
ptk/UpdateEvent
(update [_ state]
(let [objects (get-in state [:viewer-data :objects])
(let [route (:route state)
qparams (:query-params route)
page-id (:page-id qparams)
objects (get-in state [:viewer :pages page-id :objects])
selection (-> state
(get-in [:viewer-local :selected] #{})
(conj id))]
@@ -389,8 +369,13 @@
(ptk/reify ::select-all
ptk/UpdateEvent
(update [_ state]
(let [objects (get-in state [:viewer-data :objects])
frame-id (get-in state [:viewer-data :current-frame-id])
(let [route (:route state)
qparams (:query-params route)
page-id (:page-id qparams)
index (:index qparams)
objects (get-in state [:viewer :pages page-id :objects])
frame-id (get-in state [:viewer :pages page-id :frames index :id])
selection (->> objects
(filter #(= (:frame-id (second %)) frame-id))
(map first)
@@ -405,18 +390,50 @@
(let [toggled? (contains? (get-in state [:viewer-local :collapsed]) id)]
(update-in state [:viewer-local :collapsed] (if toggled? disj conj) id)))))
(defn hover-shape [id hover?]
(defn hover-shape
[id hover?]
(ptk/reify ::hover-shape
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:viewer-local :hover] (when hover? id)))))
;; --- NAV
(defn go-to-dashboard
([] (go-to-dashboard nil))
([{:keys [team-id]}]
(ptk/reify ::go-to-dashboard
ptk/WatchEvent
[]
(ptk/reify ::go-to-dashboard
ptk/WatchEvent
(watch [_ state _]
(let [team-id (get-in state [:viewer :project :team-id])
params {:team-id team-id}]
(rx/of (rt/nav :dashboard-projects params))))))
(defn go-to-page
[page-id]
(ptk/reify ::go-to-page
ptk/WatchEvent
(watch [_ state _]
(let [team-id (or team-id (get-in state [:viewer-data :project :team-id]))]
(rx/of (rt/nav :dashboard-projects {:team-id team-id})))))))
(let [route (:route state)
pparams (:path-params route)
qparams (-> (:query-params route)
(assoc :index 0)
(assoc :page-id page-id))
rname (get-in route [:data :name])]
(rx/of (rt/nav rname pparams qparams))))))
(defn go-to-workspace
[page-id]
(ptk/reify ::go-to-workspace
ptk/WatchEvent
(watch [_ state _]
(let [project-id (get-in state [:viewer :project :id])
file-id (get-in state [:viewer :file :id])
pparams {:project-id project-id :file-id file-id}
qparams {:page-id page-id}]
(rx/of (rt/nav-new-window*
{:rname :workspace
:path-params pparams
:query-params qparams
:name (str "workspace-" file-id)}))))))

View File

@@ -20,6 +20,7 @@
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.events :as ev]
[app.main.data.messages :as dm]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
@@ -37,6 +38,7 @@
[app.main.repo :as rp]
[app.main.streams :as ms]
[app.main.worker :as uw]
[app.util.globals :as ug]
[app.util.http :as http]
[app.util.i18n :as i18n]
[app.util.router :as rt]
@@ -48,7 +50,6 @@
[potok.core :as ptk]))
;; (log/set-level! :trace)
;; --- Specs
(s/def ::shape-attrs ::cp/shape-attrs)
(s/def ::set-of-string
@@ -87,7 +88,7 @@
:snap-grid
:dynamic-alignment})
(def layout-names
(def layout-presets
{:assets
{:del #{:sitemap :layers :document-history }
:add #{:assets}}
@@ -121,22 +122,31 @@
:picked-color nil
:picked-color-select false})
(declare ensure-layout)
(defn initialize-layout
[layout-name]
(us/verify (s/nilable ::us/keyword) layout-name)
(ptk/reify ::initialize-layout
(defn ensure-layout
[lname]
(ptk/reify ::ensure-layout
ptk/UpdateEvent
(update [_ state]
(update state :workspace-layout
(fn [layout]
(or layout default-layout))))
(fn [stored]
(let [todel (get-in layout-presets [lname :del] #{})
toadd (get-in layout-presets [lname :add] #{})]
(-> stored
(set/difference todel)
(set/union toadd))))))))
(defn setup-layout
[lname]
(us/verify (s/nilable ::us/keyword) lname)
(ptk/reify ::setup-layout
ptk/UpdateEvent
(update [_ state]
(update state :workspace-layout #(or % default-layout)))
ptk/WatchEvent
(watch [_ _ _]
(if (and layout-name (contains? layout-names layout-name))
(rx/of (ensure-layout layout-name))
(if (and lname (contains? layout-presets lname))
(rx/of (ensure-layout lname))
(rx/of (ensure-layout :layers))))))
(defn initialize-file
@@ -171,7 +181,12 @@
(->> stream
(rx/filter #(= ::dwc/index-initialized %))
(rx/first)
(rx/map #(file-initialized bundle)))))))))))
(rx/map #(file-initialized bundle)))))))))
ptk/EffectEvent
(effect [_ _ _]
(let [name (str "workspace-" file-id)]
(unchecked-set ug/global "name" name)))))
(defn- file-initialized
[{:keys [file users project libraries] :as bundle}]
@@ -206,7 +221,6 @@
(dissoc state
:current-file-id
:current-project-id
:current-team-id
:workspace-data
:workspace-editor-state
:workspace-file
@@ -220,8 +234,10 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwn/finalize file-id)
::dwp/finalize))))
(rx/merge
(rx/of (dwn/finalize file-id))
(->> (rx/of ::dwp/finalize)
(rx/observe-on :async))))))
(defn initialize-page
[page-id]
@@ -275,7 +291,7 @@
(watch [it state _]
(let [pages (get-in state [:workspace-data :pages-index])
unames (dwc/retrieve-used-names pages)
name (dwc/generate-unique-name unames "Page")
name (dwc/generate-unique-name unames "Page-1")
rchange {:type :add-page
:id id
@@ -349,7 +365,6 @@
(when (= id (:current-page-id state))
go-to-file))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WORKSPACE File Actions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -358,6 +373,10 @@
[id name]
{:pre [(uuid? id) (string? name)]}
(ptk/reify ::rename-file
IDeref
(-deref [_]
{::ev/origin "workspace" :id id :name name})
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-file :name] name))
@@ -374,6 +393,9 @@
;; --- Viewport Sizing
(declare increase-zoom)
(declare decrease-zoom)
(declare set-zoom)
(declare zoom-to-fit-all)
(defn initialize-viewport
@@ -458,7 +480,6 @@
(update :height #(/ % hprop))
(assoc :left-offset left-offset))))))))))))
(defn start-panning []
(ptk/reify ::start-panning
ptk/WatchEvent
@@ -485,23 +506,32 @@
(-> state
(update :workspace-local dissoc :panning)))))
(defn start-zooming [pt]
(ptk/reify ::start-zooming
ptk/WatchEvent
(watch [_ state stream]
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-zooming)))]
(when-not (get-in state [:workspace-local :zooming])
(rx/concat
(rx/of #(-> % (assoc-in [:workspace-local :zooming] true)))
(->> stream
(rx/filter ms/pointer-event?)
(rx/filter #(= :delta (:source %)))
(rx/map :pt)
(rx/take-until stopper)
(rx/map (fn [delta]
(let [scale (+ 1 (/ (:y delta) 100))] ;; this number may be adjusted after user testing
(set-zoom pt scale)))))))))))
;; --- Toggle layout flag
(defn ensure-layout
[layout-name]
(assert (contains? layout-names layout-name)
(str "unexpected layout name: " layout-name))
(ptk/reify ::ensure-layout
(defn finish-zooming []
(ptk/reify ::finish-zooming
ptk/UpdateEvent
(update [_ state]
(update state :workspace-layout
(fn [stored]
(let [todel (get-in layout-names [layout-name :del] #{})
toadd (get-in layout-names [layout-name :add] #{})]
(-> stored
(set/difference todel)
(set/union toadd))))))))
(-> state
(update :workspace-local dissoc :zooming)))))
;; --- Toggle layout flag
(defn toggle-layout-flags
[& flags]
@@ -570,6 +600,16 @@
(update state :workspace-local
#(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01)))))))
(defn set-zoom
[center scale]
(ptk/reify ::set-zoom
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local
#(impl-update-zoom % center (fn [z] (-> (* z scale)
(max 0.01)
(min 200))))))))
(def reset-zoom
(ptk/reify ::reset-zoom
ptk/UpdateEvent
@@ -1059,6 +1099,9 @@
:group
(rx/of (dwc/select-shapes (into (d/ordered-set) [(last shapes)])))
:svg-raw
nil
(rx/of (dwc/start-edition-mode id)
(dwdp/start-path-edit id)))))))))
@@ -1090,7 +1133,7 @@
(defn align-objects
[axis]
(us/verify ::gal/align-axis axis)
(ptk/reify :align-objects
(ptk/reify ::align-objects
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
@@ -1121,7 +1164,7 @@
(defn distribute-objects
[axis]
(us/verify ::gal/dist-axis axis)
(ptk/reify :align-objects
(ptk/reify ::distribute-objects
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
@@ -1196,7 +1239,7 @@
(rx/of (rt/nav' :workspace pparams qparams))))))
([page-id]
(us/verify ::us/uuid page-id)
(ptk/reify ::go-to-page
(ptk/reify ::go-to-page-2
ptk/WatchEvent
(watch [_ state _]
(let [project-id (:current-project-id state)
@@ -1208,7 +1251,10 @@
(defn go-to-layout
[layout]
(us/verify ::layout-flag layout)
(ptk/reify ::go-to-layout
(ptk/reify ::set-workspace-layout
IDeref
(-deref [_] {:layout layout})
ptk/WatchEvent
(watch [_ state _]
(let [project-id (get-in state [:workspace-project :id])
@@ -1235,10 +1281,14 @@
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [current-file-id current-page-id]} state
params {:file-id (or file-id current-file-id)
:page-id (or page-id current-page-id)}]
pparams {:file-id (or file-id current-file-id)}
qparams {:page-id (or page-id current-page-id)
:index 0}]
(rx/of ::dwp/force-persist
(rt/nav-new-window :viewer params {:index 0})))))))
(rt/nav-new-window* {:rname :viewer
:path-params pparams
:query-params qparams
:name (str "viewer-" (:file-id pparams))})))))))
(defn go-to-dashboard
([] (go-to-dashboard nil))
@@ -1252,7 +1302,7 @@
(defn go-to-dashboard-fonts
[]
(ptk/reify ::go-to-dashboard
(ptk/reify ::go-to-dashboard-fonts
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]

View File

@@ -69,7 +69,7 @@
(defn show-palette
"Show the palette tool and change the library it uses"
[selected]
(ptk/reify ::change-palette-selected
(ptk/reify ::show-palette
ptk/UpdateEvent
(update [_ state]
(-> state

View File

@@ -71,7 +71,7 @@
(defn center-to-comment-thread
[{:keys [position] :as thread}]
(us/assert ::dcm/comment-thread thread)
(ptk/reify :center-to-comment-thread
(ptk/reify ::center-to-comment-thread
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local
@@ -89,7 +89,7 @@
(defn navigate
[thread]
(us/assert ::dcm/comment-thread thread)
(ptk/reify ::navigate
(ptk/reify ::open-comment-thread
ptk/WatchEvent
(watch [_ _ stream]
(let [pparams {:project-id (:project-id thread)

View File

@@ -68,19 +68,17 @@
(defn generate-unique-name
"A unique name generator"
([used basename]
(generate-unique-name used basename false))
([used basename prefix-first?]
(s/assert ::set-of-string used)
(s/assert ::us/string basename)
(let [[prefix initial] (extract-numeric-suffix basename)]
(loop [counter initial]
(let [candidate (if (and (= 1 counter) prefix-first?)
(str prefix)
(str prefix "-" counter))]
(if (contains? used candidate)
(recur (inc counter))
candidate))))))
[used basename]
(s/assert ::set-of-string used)
(s/assert ::us/string basename)
(if-not (contains? used basename)
basename
(let [[prefix initial] (extract-numeric-suffix basename)]
(loop [counter initial]
(let [candidate (str prefix "-" counter)]
(if (contains? used candidate)
(recur (inc counter))
candidate))))))
;; --- Shape attrs (Layers Sidebar)
@@ -144,6 +142,38 @@
:origin it
:save-undo? false}))))))))))
(defn undo-to-index
"Repeat undoing or redoing until dest-index is reached."
[dest-index]
(ptk/reify ::undo-to-index
ptk/WatchEvent
(watch [it state _]
(let [edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
(when-not (or (some? edition) (not-empty drawing))
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when (and (some? items)
(<= 0 dest-index (dec (count items))))
(let [changes (vec (apply concat
(cond
(< dest-index index)
(->> (subvec items (inc dest-index) (inc index))
(reverse)
(map :undo-changes))
(> dest-index index)
(->> (subvec items (inc index) (inc dest-index))
(map :redo-changes))
:else [])))]
(when (seq changes)
(rx/of (dwu/materialize-undo changes dest-index)
(dch/commit-changes {:redo-changes changes
:undo-changes []
:origin it
:save-undo? false})))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shapes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -54,7 +54,7 @@
(defn remove-frame-grid
[frame-id index]
(ptk/reify ::set-frame-grid
(ptk/reify ::remove-frame-grid
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) []))))))))

View File

@@ -182,7 +182,7 @@
shapes (shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [[group rchanges uchanges]
(prepare-create-group objects page-id shapes "Group" false)]
(prepare-create-group objects page-id shapes "Group-1" false)]
(rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges
:origin it})
@@ -221,7 +221,7 @@
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(prepare-create-group objects page-id shapes "Group" true))
(prepare-create-group objects page-id shapes "Group-1" true))
rchanges (d/concat rchanges
[{:type :mod-obj

View File

@@ -83,12 +83,15 @@
(defn add-color
[color]
(let [id (uuid/next)
color (assoc color
:id id
:name (default-color-name color))]
(let [id (uuid/next)
color (-> color
(assoc :id id)
(assoc :name (default-color-name color)))]
(us/assert ::cp/color color)
(ptk/reify ::add-color
IDeref
(-deref [_] color)
ptk/WatchEvent
(watch [it _ _]
(let [rchg {:type :add-color
@@ -211,6 +214,9 @@
(let [typography (update typography :id #(or % (uuid/next)))]
(us/assert ::cp/typography typography)
(ptk/reify ::add-typography
IDeref
(-deref [_] typography)
ptk/WatchEvent
(watch [it _ _]
(let [rchg {:type :add-typography
@@ -258,17 +264,20 @@
:undo-changes [uchg]
:origin it}))))))
(def add-component
"Add a new component to current file library, from the currently selected shapes."
(ptk/reify ::add-component
(defn- add-component2
"This is the second step of the component creation."
[selected]
(ptk/reify ::add-component2
IDeref
(-deref [_] {:num-shapes (count selected)})
ptk/WatchEvent
(watch [it state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
selected (cp/clean-loops objects selected)
shapes (dwg/shapes-for-grouping objects selected)]
shapes (dwg/shapes-for-grouping objects selected)]
(when-not (empty? shapes)
(let [[group rchanges uchanges]
(dwlh/generate-add-component shapes objects page-id file-id)]
@@ -278,6 +287,20 @@
:origin it})
(dwc/select-shapes (d/ordered-set (:id group)))))))))))
(defn add-component
"Add a new component to current file library, from the currently selected shapes.
This operation is made in two steps, first one for calculate the
shapes that will be part of the component and the second one with
the component creation."
[]
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (->> (wsh/lookup-selected state)
(cp/clean-loops objects))]
(rx/of (add-component2 selected))))))
(defn rename-component
"Rename the component with the given id, in the current file library."
[id new-name]
@@ -462,6 +485,31 @@
:undo-changes uchanges
:origin it}))))))
(def detach-selected-components
(ptk/reify ::detach-selected-components
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
local-library (dwlh/get-local-file state)
container (cp/get-container page-id :page local-library)
selected (->> state
(wsh/lookup-selected)
(cp/clean-loops objects))
[rchanges uchanges]
(reduce (fn [changes id]
(dwlh/concat-changes
changes
(dwlh/generate-detach-instance id container)))
dwlh/empty-changes
selected)]
(rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges
:origin it}))))))
(defn nav-to-component-file
[file-id]
(us/assert ::us/uuid file-id)

View File

@@ -129,7 +129,7 @@
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[(first shapes) [] []]
(dwg/prepare-create-group objects page-id shapes "Component" true))
(dwg/prepare-create-group objects page-id shapes "Component-1" true))
[new-shape new-shapes updated-shapes]
(make-component-shape group objects file-id)

View File

@@ -59,7 +59,8 @@
ptk/WatchEvent
(watch [_ state stream]
(let [wsession (get-in state [:ws file-id])
stoper (rx/filter #(= ::finalize %) stream)
stoper (->> stream
(rx/filter (ptk/type? ::finalize)))
interval (* 1000 60)]
(->> (rx/merge
;; Each 60 seconds send a keepalive message for maintain
@@ -106,7 +107,7 @@
(defn- handle-pointer-send
[file-id point]
(ptk/reify ::handle-pointer-update
(ptk/reify ::handle-pointer-send
ptk/EffectEvent
(effect [_ state _]
(let [ws (get-in state [:ws file-id])
@@ -122,11 +123,10 @@
(defn finalize
[file-id]
(ptk/reify ::finalize
ptk/WatchEvent
(watch [_ state _]
ptk/EffectEvent
(effect [_ state _]
(when-let [ws (get-in state [:ws file-id])]
(ws/-close ws))
(rx/of ::finalize))))
(ws/-close ws)))))
;; --- Handle: Presence

View File

@@ -179,7 +179,7 @@
:right (gpt/point 1 0)))
(defn finish-move-selected []
(ptk/reify ::move-selected
(ptk/reify ::finish-move-selected
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])]

View File

@@ -18,7 +18,7 @@
(defn end-path-event? [event]
(or (= (ptk/type event) ::common/finish-path)
(= (ptk/type event) :esc-pressed)
(= (ptk/type event) :app.main.data.workspace.path.shortcuts/esc-pressed)
(= :app.main.data.workspace.common/clear-edition-mode (ptk/type event))
(= :app.main.data.workspace/finalize-page (ptk/type event))
(= event :interrupt) ;; ESC

View File

@@ -20,7 +20,7 @@
;; Shortcuts format https://github.com/ccampbell/mousetrap
(defn esc-pressed []
(ptk/reify :esc-pressed
(ptk/reify ::esc-pressed
ptk/WatchEvent
(watch [_ state _]
;; Not interrupt when we're editing a path

View File

@@ -90,7 +90,7 @@
"Joins the head with the previous undo in one. This is done so when the user changes a
node handlers after adding it the undo merges both in one operation only"
[]
(ptk/reify ::add-undo-entry
(ptk/reify ::merge-head
ptk/UpdateEvent
(update [_ state]
(let [id (st/get-path-id state)

View File

@@ -12,6 +12,7 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.fonts :as df]
[app.main.data.media :as di]
[app.main.data.messages :as dm]
@@ -275,6 +276,10 @@
[id is-shared]
{:pre [(uuid? id) (boolean? is-shared)]}
(ptk/reify ::set-file-shared
IDeref
(-deref [_]
{::ev/origin "workspace" :id id :shared is-shared})
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-file :is-shared] is-shared))
@@ -313,7 +318,7 @@
(defn link-file-to-library
[file-id library-id]
(ptk/reify ::link-file-to-library
(ptk/reify ::attach-library
ptk/WatchEvent
(watch [_ _ _]
(let [fetched #(assoc-in %2 [:workspace-libraries (:id %1)] %1)
@@ -325,7 +330,7 @@
(defn unlink-file-from-library
[file-id library-id]
(ptk/reify ::unlink-file-from-library
(ptk/reify ::detach-library
ptk/UpdateEvent
(update [_ state]
(d/dissoc-in state [:workspace-libraries library-id]))

View File

@@ -114,7 +114,7 @@
(defn deselect-shape
[id]
(us/verify ::us/uuid id)
(ptk/reify ::select-shape
(ptk/reify ::deselect-shape
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :selected] disj id))))
@@ -219,17 +219,15 @@
lks/empty-linked-set)
selrect (get-in state [:workspace-local :selrect])
blocked? (fn [id] (get-in objects [id :blocked] false))]
(rx/merge
(when selrect
(->> (uw/ask! {:cmd :selection/query
:page-id page-id
:rect selrect
:include-frames? true
:full-frame? true})
(rx/map #(cp/clean-loops objects %))
(rx/map #(into initial-set (filter (comp not blocked?)) %))
(rx/map select-shapes))))))))
(when selrect
(->> (uw/ask! {:cmd :selection/query
:page-id page-id
:rect selrect
:include-frames? true
:full-frame? true})
(rx/map #(cp/clean-loops objects %))
(rx/map #(into initial-set (filter (comp not blocked?)) %))
(rx/map select-shapes)))))))
(defn select-inside-group
[group-id position]
@@ -383,6 +381,53 @@
(into [fch] sch)))
(defn clear-memorize-duplicated
[]
(ptk/reify ::clear-memorize-duplicated
ptk/UpdateEvent
(update [_ state]
(d/dissoc-in state [:workspace-local :duplicated]))))
(defn memorize-duplicated
"When duplicate an object, remember the operation during the following seconds.
If the user moves the duplicated object, and then duplicates it again, check
the displacement and apply it to the third copy. This is useful for doing
grids or cascades of cloned objects."
[id-original id-duplicated]
(ptk/reify ::memorize-duplicated
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :duplicated] {:id-original id-original
:id-duplicated id-duplicated}))
ptk/WatchEvent
(watch [_ _ stream]
(let [stoper (rx/filter (ptk/type? ::memorize-duplicated) stream)]
(->> (rx/timer 10000) ;; This time may be adjusted after some user testing.
(rx/take-until stoper)
(rx/map clear-memorize-duplicated))))))
(defn calc-duplicate-delta
[obj state objects]
(let [{:keys [id-original id-duplicated]}
(get-in state [:workspace-local :duplicated])]
(if (and (not= id-original (:id obj))
(not= id-duplicated (:id obj)))
;; The default is leave normal shapes in place, but put
;; new frames to the right of the original.
(if (= (:type obj) :frame)
(gpt/point (+ (:width obj) 50) 0)
(gpt/point 0 0))
(let [obj-original (get objects id-original)
obj-duplicated (get objects id-duplicated)
distance (gpt/subtract (gpt/point obj-duplicated)
(gpt/point obj-original))
new-pos (gpt/add (gpt/point obj-duplicated) distance)
delta (gpt/subtract new-pos (gpt/point obj))]
delta))))
(def duplicate-selected
(ptk/reify ::duplicate-selected
ptk/WatchEvent
@@ -390,7 +435,10 @@
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (wsh/lookup-selected state)
delta (gpt/point 0 0)
delta (if (= (count selected) 1)
(let [obj (get objects (first selected))]
(calc-duplicate-delta obj state objects))
(gpt/point 0 0))
unames (dwc/retrieve-used-names objects)
@@ -400,15 +448,20 @@
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %))
(reverse rchanges))
id-original (when (= (count selected) 1) (first selected))
selected (->> rchanges
(filter #(selected (:old-id %)))
(map #(get-in % [:obj :id]))
(into (d/ordered-set)))]
(into (d/ordered-set)))
id-duplicated (when (= (count selected) 1) (first selected))]
(rx/of (dch/commit-changes {:redo-changes rchanges
:undo-changes uchanges
:origin it})
(select-shapes selected))))))
(select-shapes selected)
(memorize-duplicated id-original id-duplicated))))))
(defn change-hover-state
[id value]

View File

@@ -91,7 +91,11 @@
:create-component {:tooltip (ds/meta "K")
:command (ds/c-mod "k")
:fn #(st/emit! dwl/add-component)}
:fn #(st/emit! (dwl/add-component))}
:detach-component {:tooltip (ds/meta-shift "K")
:command (ds/c-mod "shift+k")
:fn #(st/emit! dwl/detach-selected-components)}
:flip-vertical {:tooltip (ds/shift "V")
:command "shift+v"

View File

@@ -95,7 +95,11 @@
(d/parse-double))))))
(defn setup-stroke [shape]
(let [shape
(let [stroke-linecap (-> (or (get-in shape [:svg-attrs :stroke-linecap])
(get-in shape [:svg-attrs :style :stroke-linecap]))
((d/nilf str/trim))
((d/nilf keyword)))
shape
(cond-> shape
(uc/color? (get-in shape [:svg-attrs :stroke]))
(-> (update :svg-attrs dissoc :stroke)
@@ -113,8 +117,16 @@
(get-in shape [:svg-attrs :style :stroke-width])
(-> (update-in [:svg-attrs :style] dissoc :stroke-width)
(assoc :stroke-width (-> (get-in shape [:svg-attrs :style :stroke-width])
(d/parse-double)))))]
(if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width)
(d/parse-double))))
(and stroke-linecap (= (:type shape) :path))
(-> (update-in [:svg-attrs :style] dissoc :stroke-linecap)
(cond->
(#{:round :square} stroke-linecap)
(assoc :stroke-cap-start stroke-linecap
:stroke-cap-end stroke-linecap))))]
(if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end)
(merge {:stroke-style :svg} shape)
shape)))
@@ -331,7 +343,7 @@
(let [{:keys [tag attrs]} element-data
attrs (usvg/format-styles attrs)
element-data (cond-> element-data (map? element-data) (assoc :attrs attrs))
name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag)) true)
name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag)))
att-refs (usvg/find-attr-references attrs)
references (usvg/find-def-references (:defs svg-data) att-refs)

View File

@@ -11,6 +11,7 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.pages :as cp]
[app.common.spec :as us]
[app.main.data.workspace.changes :as dch]
@@ -222,7 +223,7 @@
root
transformed-root)))]
(reduce set-child
(update-in modif-tree [(:id shape) :modifiers] #(merge % modifiers))
(assoc-in modif-tree [(:id shape) :modifiers] modifiers)
children)))
(defn- check-delta
@@ -281,7 +282,7 @@
(defn start-resize
"Enter mouse resize mode, until mouse button is released."
[handler ids shape]
(letfn [(resize [shape initial layout [point lock? point-snap]]
(letfn [(resize [shape initial layout [point lock? center? point-snap]]
(let [{:keys [width height]} (:selrect shape)
{:keys [rotation]} shape
rotation (or rotation 0)
@@ -315,17 +316,34 @@
scalev)
;; Resize origin point given the selected handler
origin (handler-resize-origin (:selrect shape) handler)
shape-center (gsh/center-shape shape)
shape-transform (:transform shape (gmt/matrix))
shape-transform-inverse (:transform-inverse shape (gmt/matrix))
shape-center (gsh/center-shape shape)
;; If we want resize from center, displace the shape
;; so it is still centered after resize.
displacement (when center?
(-> shape-center
(gpt/subtract origin)
(gpt/multiply scalev)
(gpt/add origin)
(gpt/subtract shape-center)
(gpt/multiply (gpt/point -1 -1))
(gpt/transform shape-transform)))
;; Resize origin point given the selected handler
origin (-> (handler-resize-origin (:selrect shape) handler)
(gsh/transform-point-center shape-center shape-transform))]
origin (cond-> (gsh/transform-point-center origin shape-center shape-transform)
(some? displacement)
(gpt/add displacement))
displacement (when (some? displacement)
(gmt/translate-matrix displacement))]
(rx/of (set-modifiers ids
{:resize-vector scalev
{:displacement displacement
:resize-vector scalev
:resize-origin origin
:resize-transform shape-transform
:resize-scale-text scale-text
@@ -334,9 +352,9 @@
;; Unifies the instantaneous proportion lock modifier
;; activated by Shift key and the shapes own proportion
;; lock flag that can be activated on element options.
(normalize-proportion-lock [[point shift?]]
(normalize-proportion-lock [[point shift? alt?]]
(let [proportion-lock? (:proportion-lock shape)]
[point (or proportion-lock? shift?)]))]
[point (or proportion-lock? shift?) alt?]))]
(reify
ptk/UpdateEvent
(update [_ state]
@@ -358,11 +376,11 @@
(rx/concat
(rx/of (dch/update-shapes text-shapes-ids #(assoc % :grow-type :fixed)))
(->> ms/mouse-position
(rx/with-latest vector ms/mouse-position-shift)
(rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt)
(rx/map normalize-proportion-lock)
(rx/switch-map (fn [[point :as current]]
(->> (snap/closest-snap-point page-id resizing-shapes layout zoom point)
(rx/map #(conj current %)))))
(rx/switch-map (fn [[point _ _ :as current]]
(->> (snap/closest-snap-point page-id resizing-shapes layout zoom point)
(rx/map #(conj current %)))))
(rx/mapcat (partial resize shape initial-position layout))
(rx/take-until stoper))
(rx/of (apply-modifiers ids)
@@ -493,7 +511,7 @@
(defn- start-move-duplicate
[from-position]
(ptk/reify ::start-move-selected
(ptk/reify ::start-move-duplicate
ptk/WatchEvent
(watch [_ _ stream]
(->> stream
@@ -521,10 +539,18 @@
layout (get state :workspace-layout)
zoom (get-in state [:workspace-local :zoom] 1)
fix-axis (fn [[position shift?]]
(let [delta (gpt/to-vec from-position position)]
(if shift?
(if (> (mth/abs (:x delta)) (mth/abs (:y delta)))
(gpt/point (:x delta) 0)
(gpt/point 0 (:y delta)))
delta)))
position (->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(gpt/to-vec from-position %)))
(rx/with-latest-from ms/mouse-position-shift)
(rx/map #(fix-axis %)))
snap-delta (rx/concat
;; We send the nil first so the stream is not waiting for the first value

View File

@@ -44,7 +44,8 @@
(defn- calculate-dimensions
[{:keys [objects] :as data} vport]
(let [shapes (cp/select-toplevel-shapes objects {:include-frames? true})
(let [shapes (cp/select-toplevel-shapes objects {:include-frames? true
:include-frame-children? false})
to-finite (fn [val fallback] (if (not (mth/finite? val)) fallback val))
rect (cond->> (gsh/selection-rect shapes)
(some? vport)
@@ -131,7 +132,8 @@
(mf/defc page-svg
{::mf/wrap [mf/memo]}
[{:keys [data width height thumbnails? embed?] :as props}]
[{:keys [data width height thumbnails? embed? include-metadata?] :as props
:or {embed? false include-metadata? false}}]
(let [objects (:objects data)
root (get objects uuid/zero)
shapes
@@ -158,35 +160,36 @@
(mf/deps objects)
#(shape-wrapper-factory objects))]
[:& (mf/provider embed/context) {:value embed?}
[:svg {:view-box vbox
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns"
:style {:width "100%"
:height "100%"
:background background-color}}
[:& (mf/provider use/include-metadata-ctx) {:value include-metadata?}
[:svg {:view-box vbox
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
:style {:width "100%"
:height "100%"
:background background-color}}
[:& use/export-page {:options (:options data)}]
[:& ff/fontfaces-style {:shapes root-children}]
(for [item shapes]
(let [frame? (= (:type item) :frame)]
(cond
(and frame? thumbnails? (some? (:thumbnail item)))
[:image {:xlinkHref (:thumbnail item)
:x (:x item)
:y (:y item)
:width (:width item)
:height (:height item)
;; DEBUG
;; :style {:filter "sepia(1)"}
}]
frame?
[:& frame-wrapper {:shape item
:key (:id item)}]
:else
[:& shape-wrapper {:shape item
:key (:id item)}])))]]))
[:& use/export-page {:options (:options data)}]
[:& ff/fontfaces-style {:shapes root-children}]
(for [item shapes]
(let [frame? (= (:type item) :frame)]
(cond
(and frame? thumbnails? (some? (:thumbnail item)))
[:image {:xlinkHref (:thumbnail item)
:x (:x item)
:y (:y item)
:width (:width item)
:height (:height item)
;; DEBUG
;; :style {:filter "sepia(1)"}
}]
frame?
[:& frame-wrapper {:shape item
:key (:id item)}]
:else
[:& shape-wrapper {:shape item
:key (:id item)}])))]]]))
(mf/defc frame-svg
{::mf/wrap [mf/memo]}
@@ -197,6 +200,8 @@
frame-id (:id frame)
include-metadata? (mf/use-ctx use/include-metadata-ctx)
modifier-ids (concat [frame-id] (cp/get-children frame-id objects))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
objects (reduce update-fn objects modifier-ids)
@@ -214,9 +219,9 @@
:width width
:height height
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns"}
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")}
[:& wrapper {:shape frame :view-box vbox}]]))
(mf/defc component-svg
@@ -229,6 +234,8 @@
group-id (:id group)
include-metadata? (mf/use-ctx use/include-metadata-ctx)
modifier-ids (concat [group-id] (cp/get-children group-id objects))
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
objects (reduce update-fn objects modifier-ids)
@@ -246,10 +253,11 @@
:width width
:height height
:version "1.1"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns "http://www.w3.org/2000/svg"
:xmlns:penpot "https://penpot.app/xmlns"}
[:& wrapper {:shape group :view-box vbox}]]))
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")}
[:> shape-container {:shape group}
[:& wrapper {:shape group :view-box vbox}]]]))
(mf/defc component-symbol
[{:keys [id data] :as props}]
@@ -287,20 +295,21 @@
(let [data (obj/get props "data")
children (obj/get props "children")
embed? (obj/get props "embed?")]
embed? (obj/get props "embed?")
include-metadata? (obj/get props "include-metadata?")]
[:& (mf/provider embed/context) {:value embed?}
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot "https://penpot.app/xmlns"
:style {:width "100vw"
:height "100vh"
:display (when-not (some? children) "none")}}
[:& (mf/provider use/include-metadata-ctx) {:value include-metadata?}
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot (when include-metadata? "https://penpot.app/xmlns")
:style {:width "100vw"
:height "100vh"
:display (when-not (some? children) "none")}}
[:defs
(for [[component-id component-data] (:components data)]
[:& component-symbol {:id component-id
:key (str component-id)
:data component-data}])]
[:defs
(for [[component-id component-data] (:components data)]
[:& component-symbol {:id component-id
:key (str component-id)
:data component-data}])]
children]]))
children]]]))

View File

@@ -38,6 +38,9 @@
(def threads-ref
(l/derived :comment-threads st/state))
(def share-links
(l/derived :share-links st/state))
;; ---- Dashboard refs
(def dashboard-local
@@ -110,6 +113,7 @@
:edit-path
:tooltip
:panning
:zooming
:picking-color?
:transform
:hover
@@ -286,8 +290,17 @@
;; ---- Viewer refs
(def viewer-file
(l/derived :viewer-file st/state))
(def viewer-project
(l/derived :viewer-file st/state))
(def viewer-data
(l/derived :viewer-data st/state))
(l/derived :viewer st/state))
(def viewer-state
(l/derived :viewer st/state))
(def viewer-local
(l/derived :viewer-local st/state))

View File

@@ -63,7 +63,7 @@
(->> (rx/of data)
(rx/map
(fn [data]
(let [elem (mf/element exports/page-svg #js {:data data :embed? true})]
(let [elem (mf/element exports/page-svg #js {:data data :embed? true :include-metadata? true})]
(rds/renderToStaticMarkup elem)))))))
(defn render-components
@@ -82,5 +82,5 @@
(->> (rx/of data)
(rx/map
(fn [data]
(let [elem (mf/element exports/components-sprite-svg #js {:data data :embed? true})]
(let [elem (mf/element exports/components-sprite-svg #js {:data data :embed? true :include-metadata? true})]
(rds/renderToStaticMarkup elem))))))))

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