diff --git a/.circleci/config.yml b/.circleci/config.yml index 50e60873e2..4354b1f645 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,6 +47,13 @@ jobs: clj-kondo --version clj-kondo --parallel --lint src/ + - run: + name: frontend styles prettier + working_directory: "./frontend" + command: | + yarn install + yarn run lint-scss + - run: name: backend lint working_directory: "./backend" @@ -74,21 +81,27 @@ jobs: node target/tests.js environment: - JAVA_HOME: /usr/lib/jvm/openjdk16 - PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin + PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin + + # - run: + # working_directory: "./common" + # name: common tests (cljs) + # command: | + # yarn install + # yarn run compile-test + # node target/test.js + # + # environment: + # PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin - run: working_directory: "./common" - name: common tests + name: common tests (clj) command: | - yarn install - clojure -M:dev:shadow-cljs compile test - node target/tests.js clojure -X:dev:test environment: - JAVA_HOME: /usr/lib/jvm/openjdk16 - PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin + PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin - save_cache: paths: diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 4ba5b0442a..5fc3808459 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -10,7 +10,6 @@ {:analyze-call {app.common.data/export hooks.export/export potok.core/reify hooks.export/potok-reify - cljs.core/specify! hooks.export/clojure-specify app.util.services/defmethod hooks.export/service-defmethod }} diff --git a/.gitignore b/.gitignore index f34bc1676f..330eadd832 100644 --- a/.gitignore +++ b/.gitignore @@ -1,47 +1,50 @@ -figwheel_server.log -*.penpot -*.jar *-init.clj +*.jar +*.penpot +.calva +.clj-kondo +.cpcache .lein-deps-sum .lein-failures -.lein-repl-history .lein-plugins/ -.repl +.lein-repl-history +.lsp .nrepl-port -.cpcache +.nyc_output .rebel_readline_history -node_modules -/clj-profiler/ -/vendor/**/target -/cd.md -/backend/target/ -/backend/resources/public/media -/backend/resources/public/assets +.repl +/.clj-kondo/.cache +/_dump +/backend/- /backend/assets/ /backend/dist/ /backend/logs/ -/backend/- -/telemetry/ -/frontend/npm-debug.log -/frontend/target/ -/frontend/dist/ -/frontend/out/ -/frontend/.shadow-cljs -/frontend/resources/public/* -/frontend/resources/fonts/experiments -/exporter/target -/exporter/.shadow-cljs -/docker/images/bundle* -/common/.shadow-cljs -/common/target -/.clj-kondo/.cache +/backend/resources/public/assets +/backend/resources/public/media +/backend/target/ /bundle* -/media +/cd.md +/clj-profiler/ +/common/.shadow-cljs +/common/coverage +/common/target /deploy -/web -/_dump +/docker/images/bundle* +/exporter/.shadow-cljs +/exporter/target +/frontend/.shadow-cljs +/frontend/cypress/videos/*/ +/frontend/dist/ +/frontend/npm-debug.log +/frontend/out/ +/frontend/resources/fonts/experiments +/frontend/resources/public/* +/frontend/target/ +/media +/telemetry/ +/vendor/**/target /vendor/svgclean/bundle*.js - -.calva -.clj-kondo -.lsp +/web +clj-profiler/ +figwheel_server.log +node_modules diff --git a/CHANGES.md b/CHANGES.md index 888a2dc045..e94e1f1787 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,15 +1,95 @@ # CHANGELOG -## :rocket: Next +## 1.11.0-beta ### :boom: Breaking changes + ### :sparkles: New features + +- Add an option to hide artboards names on the viewport [Taiga #2034](https://tree.taiga.io/project/penpot/issue/2034) +- Limit pasted object position to container boundaries [Taiga #2449](https://tree.taiga.io/project/penpot/us/2449) +- Add new options for zoom widget in workspace and viewer mode [Taiga #896](https://tree.taiga.io/project/penpot/us/896) +- Allow decimals on stroke width and positions [Taiga #2035](https://tree.taiga.io/project/penpot/issue/2035) +- Ability to ignore background when exporting an artboard [Taiga #1395](https://tree.taiga.io/project/penpot/us/1395) +- Show color hex or name on hover [Taiga #2413](https://tree.taiga.io/project/penpot/us/2413) +- Add shortcut to create artboard from selected objects [Taiga #2412](https://tree.taiga.io/project/penpot/us/2412) +- Add shortcut for opacity [Taiga #2442](https://tree.taiga.io/project/penpot/us/2442) +- Setting fill automatically for new texts [Taiga #2441](https://tree.taiga.io/project/penpot/us/2441) +- Add shortcut to move action [Github #1213](https://github.com/penpot/penpot/issues/1213) +- Add alt as mod key to add stroke color from library menu [Taiga #2207](https://tree.taiga.io/project/penpot/us/2207) +- Add detach in bulk option to context menu [Taiga #2210](https://tree.taiga.io/project/penpot/us/2210) +- Add penpot look and feel to multiuser cursors [Taiga #1387](https://tree.taiga.io/project/penpot/us/1387) +- Add actions to go to main component context menu option [Taiga #2053](https://tree.taiga.io/project/penpot/us/2053) +- Add contrast between component select color and shape select color [Taiga #2121](https://tree.taiga.io/project/penpot/issue/2121) +- Add animations in interactions [Taiga #2244](https://tree.taiga.io/project/penpot/us/2244) +- Add performance improvements on .penpot file import process [Taiga #2497](https://tree.taiga.io/project/penpot/us/2497) +- On team settings set color of members count to black [Taiga #2607](https://tree.taiga.io/project/penpot/us/2607) + ### :bug: Bugs fixed + +- Fix remove gradient if any when applying color from library [Taiga #2299](https://tree.taiga.io/project/penpot/issue/2299) +- Fix Enter as key action to exit edit path [Taiga #2444](https://tree.taiga.io/project/penpot/issue/2444) +- Fix add fill color from palette to groups and components [Taiga #2313](https://tree.taiga.io/project/penpot/issue/2313) +- Fix default project name in all languages [Taiga #2280](https://tree.taiga.io/project/penpot/issue/2280) +- Fix line-height and letter-spacing inputs to allow negative values [Taiga #2381](https://tree.taiga.io/project/penpot/issue/2381) +- Fix typo in Handoff tooltip [Taiga #2428](https://tree.taiga.io/project/penpot/issue/2428) +- Fix crash when pressing Shift+1 on empty file [#1435](https://github.com/penpot/penpot/issues/1435) +- Fix masked group resize strange behavior [Taiga #2317](https://tree.taiga.io/project/penpot/issue/2317) +- Fix problems when exporting all artboards [Taiga #2234](https://tree.taiga.io/project/penpot/issue/2234) +- Fix problems with team management [#1353](https://github.com/penpot/penpot/issues/1353) +- Fix problem when importing in shared libraries [#1362](https://github.com/penpot/penpot/issues/1362) +- Fix problem with join nodes [#1422](https://github.com/penpot/penpot/issues/1422) +- After team onboarding importing a file will import into the team drafts [Taiga #2408](https://tree.taiga.io/project/penpot/issue/2408) +- Fix problem exporting shapes from handoff mode [Taiga #2386](https://tree.taiga.io/project/penpot/issue/2386) +- Fix lock/hide elements in context menu when multiples shapes selected [Taiga #2340](https://tree.taiga.io/project/penpot/issue/2340) +- Fix problem with booleans [Taiga #2356](https://tree.taiga.io/project/penpot/issue/2356) +- Fix line-height/letter-spacing inputs behaviour [Taiga #2331](https://tree.taiga.io/project/penpot/issue/2331) +- Fix dotted style in strokes [Taiga #2312](https://tree.taiga.io/project/penpot/issue/2312) +- Fix problem when resizing texts inside groups [Taiga #2310](https://tree.taiga.io/project/penpot/issue/2310) +- Fix problem with multiple exports [Taiga #2468](https://tree.taiga.io/project/penpot/issue/2468) +- Allow import to continue from recoverable failures [#1412](https://github.com/penpot/penpot/issues/1412) +- Improved behaviour on text options when not text is selected [Taiga #2390](https://tree.taiga.io/project/penpot/issue/2390) +- Fix decimal numbers in export viewbox [Taiga #2290](https://tree.taiga.io/project/penpot/issue/2290) +- Right click over artboard name to open its menu [Taiga #1679](https://tree.taiga.io/project/penpot/issue/1679) +- Make the default session cookue use SameSite=Lax instead of Strict (causes some issues in latest versions of Chrome) +- Fix "open in new tab" on dashboard [Taiga #2235](https://tree.taiga.io/project/penpot/issue/2355) +- Changing pages while comments activated will not close the panel [#1350](https://github.com/penpot/penpot/issues/1350) +- Fix navigate comments in right sidebar [Taiga #2163](https://tree.taiga.io/project/penpot/issue/2163) +- Fix keep name of component equal to the shape name [Taiga #2341](https://tree.taiga.io/project/penpot/issue/2341) +- Fix lossing changes when changing selection and an input was already changed [Taiga #2329](https://tree.taiga.io/project/penpot/issue/2329), [Taiga #2330](https://tree.taiga.io/project/penpot/issue/2330) +- Fix blur input field when click on viewport [Taiga #2164](https://tree.taiga.io/project/penpot/issue/2164) +- Fix default page id in workspace [Taiga #2205](https://tree.taiga.io/project/penpot/issue/2205) +- Fix problem when importing a file with grids [Taiga #2314](https://tree.taiga.io/project/penpot/issue/2314) +- Fix problem with imported svgs with filters [Taiga #2478](https://tree.taiga.io/project/penpot/issue/2478) +- Fix issues when updating selrect in paths [Taiga #2366](https://tree.taiga.io/project/penpot/issue/2366) +- Fix scroll jumps in handoff mode [Taiga #2383](https://tree.taiga.io/project/penpot/issue/2383) +- Fix handoff text with opacity [Taiga #2384](https://tree.taiga.io/project/penpot/issue/2384) +- Restored rules color [Taiga #2460](https://tree.taiga.io/project/penpot/issue/2460) +- Fix thumbnail not taking frame blending mode [Taiga #2301](https://tree.taiga.io/project/penpot/issue/2301) +- Fix import/export with SVG edge cases [Taiga #2389](https://tree.taiga.io/project/penpot/issue/2389) +- Avoid modifying component when moving into a group [Taiga #2534](https://tree.taiga.io/project/penpot/issue/2534) +- Show correctly group types label in handoff [Taiga #2482](https://tree.taiga.io/project/penpot/issue/2482) +- Display view mode buttons always centered in viewer [#Taiga 2466](https://tree.taiga.io/project/penpot/issue/2466) +- Fix default profile image generation issue [Taiga #2601](https://tree.taiga.io/project/penpot/issue/2601) +- Fix edit blur attributes for multiselection [Taiga #2625](https://tree.taiga.io/project/penpot/issue/2625) +- Fix auto hide header in viewer full screen [Taiga #2632](https://tree.taiga.io/project/penpot/issue/2632) +- Fix zoom in/out after fit or fill [Taiga #2630](https://tree.taiga.io/project/penpot/issue/2630) +- Normalize zoom levels in workspace and viewer [Taiga #2631](https://tree.taiga.io/project/penpot/issue/2631) +- Avoid empty names in projects, files and pages [Taiga #2594](https://tree.taiga.io/project/penpot/issue/2594) +- Fix "move to" menu when duplicated team or project names [Taiga #2655](https://tree.taiga.io/project/penpot/issue/2655) +- Fix ungroup a component leaves an asterisk in layers [Taiga #2694](https://tree.taiga.io/project/penpot/issue/2694) + ### :arrow_up: Deps updates + +- Update devenv docker image dependencies. + ### :heart: Community contributions by (Thank you!) +- Spelling fixes (by @jsoref) [#1340](https://github.com/penpot/penpot/pull/1340) +- Explain folders in components (by @candideu) [Penpot-docs #42](https://github.com/penpot/penpot-docs/pull/42) +- Readability improvements of user guide (by @PaulSchulz) [Penpot-docs #50](https://github.com/penpot/penpot-docs/pull/50) -# 1.10.4-beta +## 1.10.4-beta ### :sparkles: Enhacements @@ -21,8 +101,7 @@ - Minor fix on how file changes log is persisted. - Fix many issues on error reporting. - -# 1.10.3-beta +## 1.10.3-beta ### :sparkles: Enhacements @@ -33,7 +112,7 @@ - Fix unexpected exception on saving pages with default grids [#2409](https://tree.taiga.io/project/penpot/issue/2409) - Fix react warnings on setting size 1 on row and column grids. -- Fix minor issues on ZMQ logging listener (used in error reporting service). +- Fix minor issues on ZMQ logging listener (used in error reporting service) - Remove "ALPHA" from the code. - Fix value and nil handling on numeric-input component. This fixes many issues related to typography, components, etc. renaming. - Fix NPE on email complains processing. @@ -44,8 +123,7 @@ - Update log4j2 dependency. - -# 1.10.2-beta +## 1.10.2-beta ### :bug: Bugs fixed @@ -57,14 +135,12 @@ - Update log4j2 dependency. - -# 1.10.1-beta +## 1.10.1-beta ### :bug: Bugs fixed - Fix problems with team management [#1353](https://github.com/penpot/penpot/issues/1353) - ## 1.10.0-beta ### :boom: Breaking changes @@ -76,18 +152,19 @@ ### :sparkles: New features -- Enhance corner radius behavior [Taiga #2190](https://tree.taiga.io/project/penpot/issue/2190). -- Allow preserve scroll position in interactions [Taiga #2250](https://tree.taiga.io/project/penpot/us/2250). +- Allow ungroup groups in bulk [Taiga #2211](https://tree.taiga.io/project/penpot/us/2211) +- Enhance corner radius behavior [Taiga #2190](https://tree.taiga.io/project/penpot/issue/2190) +- Allow preserve scroll position in interactions [Taiga #2250](https://tree.taiga.io/project/penpot/us/2250) - Add new onboarding modals. ### :bug: Bugs fixed -- Fix problem with exporting before the document is saved [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189). -- Fix undo stacking when changing color from color-picker [Taiga #2191](https://tree.taiga.io/project/penpot/issue/2191). -- Fix pages dropdown in viewer [Taiga #2087](https://tree.taiga.io/project/penpot/issue/2087). -- Fix problem when exporting texts with gradients or opacity [Taiga #2200](https://tree.taiga.io/project/penpot/issue/2200). -- Fix problem with view mode comments [Taiga #2226](https://tree.taiga.io/project/penpot/issue/2226). -- Disallow to create a component when already has one [Taiga #2237](https://tree.taiga.io/project/penpot/issue/2237). +- Fix problem with exporting before the document is saved [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189) +- Fix undo stacking when changing color from color-picker [Taiga #2191](https://tree.taiga.io/project/penpot/issue/2191) +- Fix pages dropdown in viewer [Taiga #2087](https://tree.taiga.io/project/penpot/issue/2087) +- Fix problem when exporting texts with gradients or opacity [Taiga #2200](https://tree.taiga.io/project/penpot/issue/2200) +- Fix problem with view mode comments [Taiga #2226](https://tree.taiga.io/project/penpot/issue/2226) +- Disallow to create a component when already has one [Taiga #2237](https://tree.taiga.io/project/penpot/issue/2237) - Add ellipsis in long labels for input fields [Taiga #2224](https://tree.taiga.io/project/penpot/issue/2224) - Fix problem with text rendering on export [Taiga #2223](https://tree.taiga.io/project/penpot/issue/2223) - Fix problem when flattening booleans losing styles [Taiga #2217](https://tree.taiga.io/project/penpot/issue/2217) @@ -100,61 +177,66 @@ - Add placeholder to create shareable link - Fix project files count not refreshing correctly after import [Taiga #2216](https://tree.taiga.io/project/penpot/issue/2216) - Remove button after import process finish [Taiga #2215](https://tree.taiga.io/project/penpot/issue/2215) +- Fix problem with styles in the viewer [Taiga #2467](https://tree.taiga.io/project/penpot/issue/2467) +- Fix default state in viewer [Taiga #2465](https://tree.taiga.io/project/penpot/issue/2465) +- Fix division by zero in bool operation [Taiga #2349](https://tree.taiga.io/project/penpot/issue/2349) ### :heart: Community contributions by (Thank you!) - To the translation community for the hard work on making penpot available on so many languages. +- Guide to integrate with Azure Directory (by @skrzyneckik) [Penpot-docs #33](https://github.com/penpot/penpot-docs/pull/33) +- Improve libraries section readability (by @PaulSchulz) [Penpot-docs #39](https://github.com/penpot/penpot-docs/pull/39) ## 1.9.0-alpha ### :boom: Breaking changes - Some stroke-caps can change behaviour. -- Text display bug fix could potentialy make some texts jump a line. +- Text display bug fix could potentially make some texts jump a line. ### :sparkles: New features -- Add boolean shapes: intersections, unions, difference and exclusions[Taiga #748](https://tree.taiga.io/project/penpot/us/748). -- Add advanced prototyping [Taiga #244](https://tree.taiga.io/project/penpot/us/244). -- Add multiple flows [Taiga #2091](https://tree.taiga.io/project/penpot/us/2091). +- Add boolean shapes: intersections, unions, difference and exclusions[Taiga #748](https://tree.taiga.io/project/penpot/us/748) +- Add advanced prototyping [Taiga #244](https://tree.taiga.io/project/penpot/us/244) +- Add multiple flows [Taiga #2091](https://tree.taiga.io/project/penpot/us/2091) - Change order of the teams menu so it's in the joined time order. ### :bug: Bugs fixed -- Enhance duplicating prototype connections behaviour [Taiga #2093](https://tree.taiga.io/project/penpot/us/2093). -- Ignore constraints in horizontal or vertical flip [Taiga #2038](https://tree.taiga.io/project/penpot/issue/2038). -- Fix color and typographies refs lost when duplicated file [Taiga #2165](https://tree.taiga.io/project/penpot/issue/2165). -- Fix problem with overflow dropdown on stroke-cap [#1216](https://github.com/penpot/penpot/issues/1216). -- Fix menu context for single element nested in components [#1186](https://github.com/penpot/penpot/issues/1186). -- Fix error screen when operations over comments fail [#1219](https://github.com/penpot/penpot/issues/1219). -- Fix undo problem when changing typography/color from library [#1230](https://github.com/penpot/penpot/issues/1230). -- Fix problem with text margin while rendering [#1231](https://github.com/penpot/penpot/issues/1231). -- Fix problem with masked texts on exporting [Taiga #2116](https://tree.taiga.io/project/penpot/issue/2116). -- Fix text editor enter behaviour with centered texts [Taiga #2126](https://tree.taiga.io/project/penpot/issue/2126). -- Fix residual stroke on imported svg [Taiga #2125](https://tree.taiga.io/project/penpot/issue/2125). -- Add links for terms of service and privacy policy in register checkbox [Taiga #2020](https://tree.taiga.io/project/penpot/issue/2020). -- Allow three character hex and web colors in color picker hex input [#1184](https://github.com/penpot/penpot/issues/1184). -- Allow lowercase search for fonts [#1180](https://github.com/penpot/penpot/issues/1180). -- Fix group renaming problem [Taiga #1969](https://tree.taiga.io/project/penpot/issue/1969). -- Fix export group with shadows on children [Taiga #2036](https://tree.taiga.io/project/penpot/issue/2036). -- Fix zoom context menu in viewer [Taiga #2041](https://tree.taiga.io/project/penpot/issue/2041). -- Fix stroke caps adjustments in relation with stroke size [Taiga #2123](https://tree.taiga.io/project/penpot/issue/2123). -- Fix problem duplicating paths [Taiga #2147](https://tree.taiga.io/project/penpot/issue/2147). -- Fix problem inheriting attributes from SVG root when importing [Taiga #2124](https://tree.taiga.io/project/penpot/issue/2124). -- Fix problem with lines and inside/outside stroke [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146). -- Add stroke width in selection calculation [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146). -- Fix shift+wheel to horizontal scrolling in MacOS [#1217](https://github.com/penpot/penpot/issues/1217). -- Fix path stroke is not working properly with high thickness [Taiga #2154](https://tree.taiga.io/project/penpot/issue/2154). -- Fix bug with transformation operations [Taiga #2155](https://tree.taiga.io/project/penpot/issue/2155). -- Fix bug in firefox when a text box is inside a mask [Taiga #2152](https://tree.taiga.io/project/penpot/issue/2152). +- Enhance duplicating prototype connections behaviour [Taiga #2093](https://tree.taiga.io/project/penpot/us/2093) +- Ignore constraints in horizontal or vertical flip [Taiga #2038](https://tree.taiga.io/project/penpot/issue/2038) +- Fix color and typographies refs lost when duplicated file [Taiga #2165](https://tree.taiga.io/project/penpot/issue/2165) +- Fix problem with overflow dropdown on stroke-cap [#1216](https://github.com/penpot/penpot/issues/1216) +- Fix menu context for single element nested in components [#1186](https://github.com/penpot/penpot/issues/1186) +- Fix error screen when operations over comments fail [#1219](https://github.com/penpot/penpot/issues/1219) +- Fix undo problem when changing typography/color from library [#1230](https://github.com/penpot/penpot/issues/1230) +- Fix problem with text margin while rendering [#1231](https://github.com/penpot/penpot/issues/1231) +- Fix problem with masked texts on exporting [Taiga #2116](https://tree.taiga.io/project/penpot/issue/2116) +- Fix text editor enter behaviour with centered texts [Taiga #2126](https://tree.taiga.io/project/penpot/issue/2126) +- Fix residual stroke on imported svg [Taiga #2125](https://tree.taiga.io/project/penpot/issue/2125) +- Add links for terms of service and privacy policy in register checkbox [Taiga #2020](https://tree.taiga.io/project/penpot/issue/2020) +- Allow three character hex and web colors in color picker hex input [#1184](https://github.com/penpot/penpot/issues/1184) +- Allow lowercase search for fonts [#1180](https://github.com/penpot/penpot/issues/1180) +- Fix group renaming problem [Taiga #1969](https://tree.taiga.io/project/penpot/issue/1969) +- Fix export group with shadows on children [Taiga #2036](https://tree.taiga.io/project/penpot/issue/2036) +- Fix zoom context menu in viewer [Taiga #2041](https://tree.taiga.io/project/penpot/issue/2041) +- Fix stroke caps adjustments in relation with stroke size [Taiga #2123](https://tree.taiga.io/project/penpot/issue/2123) +- Fix problem duplicating paths [Taiga #2147](https://tree.taiga.io/project/penpot/issue/2147) +- Fix problem inheriting attributes from SVG root when importing [Taiga #2124](https://tree.taiga.io/project/penpot/issue/2124) +- Fix problem with lines and inside/outside stroke [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146) +- Add stroke width in selection calculation [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146) +- Fix shift+wheel to horizontal scrolling in MacOS [#1217](https://github.com/penpot/penpot/issues/1217) +- Fix path stroke is not working properly with high thickness [Taiga #2154](https://tree.taiga.io/project/penpot/issue/2154) +- Fix bug with transformation operations [Taiga #2155](https://tree.taiga.io/project/penpot/issue/2155) +- Fix bug in firefox when a text box is inside a mask [Taiga #2152](https://tree.taiga.io/project/penpot/issue/2152) - Fix problem with stroke inside/outside [Taiga #2186](https://tree.taiga.io/project/penpot/issue/2186) - Fix masks export area [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189) -- Fix paste in place in arboards [Taiga #2188](https://tree.taiga.io/project/penpot/issue/2188) +- Fix paste in place in artboards [Taiga #2188](https://tree.taiga.io/project/penpot/issue/2188) - Fix font size input stuck on selection change [Taiga #2184](https://tree.taiga.io/project/penpot/issue/2184) - Fix stroke cut on shapes export [Taiga #2171](https://tree.taiga.io/project/penpot/issue/2171) - Fix no color when boolean with an SVG [Taiga #2193](https://tree.taiga.io/project/penpot/issue/2193) -- Fix unlink color styles at strokes [Taiga #2206](https://tree.taiga.io/project/penpot/issue/2206). +- Fix unlink color styles at strokes [Taiga #2206](https://tree.taiga.io/project/penpot/issue/2206) ### :arrow_up: Deps updates @@ -163,13 +245,11 @@ - To the translation community for the hard work on making penpot available on so many languages. - - ## 1.8.4-alpha ### :bug: Bugs fixed -- Fix problem importing components [Taiga #2151](https://tree.taiga.io/project/penpot/issue/2151). +- Fix problem importing components [Taiga #2151](https://tree.taiga.io/project/penpot/issue/2151) ## 1.8.3-alpha @@ -181,18 +261,17 @@ ### :bug: Bugs fixed -- Fix problem with masking images in viewer [#1238](https://github.com/penpot/penpot/issues/1238). +- Fix problem with masking images in viewer [#1238](https://github.com/penpot/penpot/issues/1238) ## 1.8.1-alpha ### :bug: Bugs fixed -- Fix project renaming issue (and some other related to the same underlying bug). +- Fix project renaming issue (and some other related to the same underlying bug) - Fix internal exception on audit log persistence layer. - Set proper environment variable on docker images for chrome executable. - Fix internal metrics on websocket connections. - ## 1.8.0-alpha ### :boom: Breaking changes @@ -203,25 +282,25 @@ ### :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). +- 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). +- 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) @@ -234,11 +313,12 @@ - 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). - +- eduayme [#1129](https://github.com/penpot/penpot/pull/1129) ## 1.7.4-alpha @@ -247,14 +327,12 @@ - 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 @@ -263,8 +341,8 @@ ### :bug: Bugs fixed -- Add scroll bar to Teams menu [Taiga #1894](https://tree.taiga.io/project/penpot/issue/1894). -- Fix repeated names when duplicating artboards or groups [Taiga #1892](https://tree.taiga.io/project/penpot/issue/1892). +- Add scroll bar to Teams menu [Taiga #1894](https://tree.taiga.io/project/penpot/issue/1894) +- Fix repeated names when duplicating artboards or groups [Taiga #1892](https://tree.taiga.io/project/penpot/issue/1892) - Fix properly messages lifecycle on navigate. - Fix handling repeated names on duplicate object trees. - Fix group naming on group creation. @@ -278,7 +356,6 @@ - soultipsy [#1100](https://github.com/penpot/penpot/pull/1100) - ## 1.7.1-alpha ### :bug: Bugs fixed @@ -288,19 +365,18 @@ - Fix issue on undo page deletion. - Fix some issues related to constraints. - ## 1.7.0-alpha ### :sparkles: New features -- Allow nested asset groups [Taiga #1716](https://tree.taiga.io/project/penpot/us/1716). -- Allow to ungroup assets [Taiga #1719](https://tree.taiga.io/project/penpot/us/1719). -- Allow to rename assets groups [Taiga #1721](https://tree.taiga.io/project/penpot/us/1721). -- Component constraints (left, right, left and right, center, scale...) [Taiga #1125](https://tree.taiga.io/project/penpot/us/1125). -- Export elements to PDF [Taiga #519](https://tree.taiga.io/project/penpot/us/519). -- Memorize collapse state of assets in panel [Taiga #1718](https://tree.taiga.io/project/penpot/us/1718). -- Headers button sets and menus review [Taiga #1663](https://tree.taiga.io/project/penpot/us/1663). -- Preserve components if possible, when pasted into a different file [Taiga #1063](https://tree.taiga.io/project/penpot/issue/1063). +- Allow nested asset groups [Taiga #1716](https://tree.taiga.io/project/penpot/us/1716) +- Allow to ungroup assets [Taiga #1719](https://tree.taiga.io/project/penpot/us/1719) +- Allow to rename assets groups [Taiga #1721](https://tree.taiga.io/project/penpot/us/1721) +- Component constraints (left, right, left and right, center, scale...) [Taiga #1125](https://tree.taiga.io/project/penpot/us/1125) +- Export elements to PDF [Taiga #519](https://tree.taiga.io/project/penpot/us/519) +- Memorize collapse state of assets in panel [Taiga #1718](https://tree.taiga.io/project/penpot/us/1718) +- Headers button sets and menus review [Taiga #1663](https://tree.taiga.io/project/penpot/us/1663) +- Preserve components if possible, when pasted into a different file [Taiga #1063](https://tree.taiga.io/project/penpot/issue/1063) - Add the ability to offload file data to a cheaper storage when file becomes inactive. - Import/Export Penpot files from dashboard. - Double click won't make a shape a path until you change a node [Taiga #1796](https://tree.taiga.io/project/penpot/us/1796) @@ -309,21 +385,20 @@ ### :bug: Bugs fixed - Process numeric input changes only if the value actually changed. -- Remove unnecesary redirect from history when user goes to workspace from dashboard [Taiga #1820](https://tree.taiga.io/project/penpot/issue/1820). -- Detach shapes from deleted assets [Taiga #1850](https://tree.taiga.io/project/penpot/issue/1850). -- Fix tooltip position on view application [Taiga #1819](https://tree.taiga.io/project/penpot/issue/1819). -- Fix dashboard navigation on moving file to other team [Taiga #1817](https://tree.taiga.io/project/penpot/issue/1817). -- Fix workspace header presence styles and invalid link [Taiga #1813](https://tree.taiga.io/project/penpot/issue/1813). -- Fix color-input wrong behavior (on workspace page color) [Taiga #1795](https://tree.taiga.io/project/penpot/issue/1795). -- Fix file contextual menu in shared libraries at dashboard [Taiga #1865](https://tree.taiga.io/project/penpot/issue/1865). +- Remove unnecessary redirect from history when user goes to workspace from dashboard [Taiga #1820](https://tree.taiga.io/project/penpot/issue/1820) +- Detach shapes from deleted assets [Taiga #1850](https://tree.taiga.io/project/penpot/issue/1850) +- Fix tooltip position on view application [Taiga #1819](https://tree.taiga.io/project/penpot/issue/1819) +- Fix dashboard navigation on moving file to other team [Taiga #1817](https://tree.taiga.io/project/penpot/issue/1817) +- Fix workspace header presence styles and invalid link [Taiga #1813](https://tree.taiga.io/project/penpot/issue/1813) +- Fix color-input wrong behavior (on workspace page color) [Taiga #1795](https://tree.taiga.io/project/penpot/issue/1795) +- Fix file contextual menu in shared libraries at dashboard [Taiga #1865](https://tree.taiga.io/project/penpot/issue/1865) - Fix problem with color picker and fonts [#1049](https://github.com/penpot/penpot/issues/1049) - Fix negative values in blur [Taiga #1815](https://tree.taiga.io/project/penpot/issue/1815) - Fix problem when editing color in group [Taiga #1816](https://tree.taiga.io/project/penpot/issue/1816) - Fix resize/rotate with mouse buttons different than left [#1060](https://github.com/penpot/penpot/issues/1060) -- Fix header partialy visible on fullscreen viewer mode [Taiga #1875](https://tree.taiga.io/project/penpot/issue/1875) +- Fix header partially visible on fullscreen viewer mode [Taiga #1875](https://tree.taiga.io/project/penpot/issue/1875) - Fix dynamic alignment enabled with hidden objects [#1063](https://github.com/penpot/penpot/issues/1063) - ## 1.6.5-alpha ### :bug: Bugs fixed @@ -334,8 +409,8 @@ ### :sparkles: Minor improvements -- Decrease default bulk buffers on storage tasks. -- Reduce file_change preserve interval to 24h. +- Decrease default bulk buffers on storage tasks. +- Reduce file_change preserve interval to 24h. ### :bug: Bugs fixed @@ -348,7 +423,6 @@ - Properly handle nil values on `update-shapes` function. - Replace frame term usage by artboard on viewer app. - ## 1.6.3-alpha ### :bug: Bugs fixed @@ -375,42 +449,39 @@ - Minor fix on previous commit. - Minor improvements on svg uploading on libraries. - ## 1.6.1-alpha ### :bug: Bugs fixed - Add safety check on reg-objects change impl. -- Fix custom fonts embbedding issue. +- Fix custom fonts embedding issue. - Fix dashboard ordering issue. - Fix problem when creating a component with empty data. - Fix problem with moving shapes into frames. - Fix problems with mov-objects. -- Fix unexpected excetion related to rounding integers. +- Fix unexpected exception related to rounding integers. - Fix wrong type usage on libraries changes. - Improve editor lifecycle management. - Make the navigation async by default. - ## 1.6.0-alpha ### :sparkles: New features -- Add improved workspace font selector [Taiga US #292](https://tree.taiga.io/project/penpot/us/292). +- Add improved workspace font selector [Taiga US #292](https://tree.taiga.io/project/penpot/us/292) - Add option to interactively scale text [Taiga #1527](https://tree.taiga.io/project/penpot/us/1527) - Add performance improvements on dashboard data loading. - Add performance improvements to indexes handling on workspace. -- Add the ability to upload/use custom fonts (and automatically generate all needed webfonts) [Taiga US #292](https://tree.taiga.io/project/penpot/us/292). +- Add the ability to upload/use custom fonts (and automatically generate all needed webfonts) [Taiga US #292](https://tree.taiga.io/project/penpot/us/292) - Transform shapes to path on double click - Translate automatic names of new files and projects. -- Use shift instead of ctrl/cmd to keep aspect ratio [Taiga 1697](https://tree.taiga.io/project/penpot/issue/1697). -- New translations: Portuguese (Brazil) and Romanias. - +- Use shift instead of ctrl/cmd to keep aspect ratio [Taiga 1697](https://tree.taiga.io/project/penpot/issue/1697) +- New translations: Portuguese (Brazil) and Romanias. ### :bug: Bugs fixed -- Remove interactions when the destination artboard is deleted [Taiga #1656](https://tree.taiga.io/project/penpot/issue/1656). -- Fix problem with fonts that ends with numbers [#940](https://github.com/penpot/penpot/issues/940). +- Remove interactions when the destination artboard is deleted [Taiga #1656](https://tree.taiga.io/project/penpot/issue/1656) +- Fix problem with fonts that ends with numbers [#940](https://github.com/penpot/penpot/issues/940) - Fix problem with imported SVG on editing paths [#971](https://github.com/penpot/penpot/issues/971) - Fix problem with color picker positioning - Fix order on color palette [#961](https://github.com/penpot/penpot/issues/961) @@ -424,7 +495,6 @@ - Update exporter dependencies (puppeteer), that fixes some unexpected exceptions. - Update string manipulation library. - ### :boom: Breaking changes - The OIDC setting `PENPOT_OIDC_SCOPES` has changed the default semantics. Before this @@ -435,7 +505,6 @@ - Translations: Portuguese (Brazil) and Romanias. - ## 1.5.4-alpha ### :bug: Bugs fixed @@ -443,7 +512,6 @@ - Fix issues on group rendering. - Fix problem with text editing auto-height [Taiga #1683](https://tree.taiga.io/project/penpot/issue/1683) - ## 1.5.3-alpha ### :bug: Bugs fixed @@ -467,7 +535,6 @@ - Increase default team invitation token expiration to 48h. - Fix wrong error message when an expired token is used. - ## 1.5.0-alpha ### :sparkles: New features @@ -513,7 +580,6 @@ - madmath03 (by [Monogramm](https://github.com/Monogramm)) [#807](https://github.com/penpot/penpot/pull/807) - zzkt [#814](https://github.com/penpot/penpot/pull/814) - ## 1.4.1-alpha ### :bug: Bugs fixed @@ -525,7 +591,6 @@ - Fix incorrect state management of user lang selection. - Fix email validation usability issue on team invitation lightbox. - ## 1.4.0-alpha ### :sparkles: New features @@ -534,7 +599,7 @@ - Add http caching layer on top of Query RPC. - Add layer opacity and blend mode to shapes [Taiga #937](https://tree.taiga.io/project/penpot/us/937) - Add more chinese translations [#726](https://github.com/penpot/penpot/pull/726) -- Add native support for text-direction (RTL, LTR & auto). +- Add native support for text-direction (RTL, LTR & auto) - Add several enhancements in shape selection [Taiga #1195](https://tree.taiga.io/project/penpot/us/1195) - Add thumbnail in memory caching mechanism. - Add turkish translation strings [#759](https://github.com/penpot/penpot/pull/759), [#794](https://github.com/penpot/penpot/pull/794) @@ -542,13 +607,12 @@ - Hide viewer navbar on fullscreen [Taiga 1375](https://tree.taiga.io/project/penpot/us/1375) - Import SVG will create Penpot's shapes [Taiga #1006](https://tree.taiga.io/project/penpot/us/1066) - Improve french translations [#731](https://github.com/penpot/penpot/pull/731) -- Reimplement workspace presence (remove database state). +- Reimplement workspace presence (remove database state) - Remember last visited team when you re-enter the application [Taiga #1376](https://tree.taiga.io/project/penpot/us/1376) - Rename artboard with double click on the title [Taiga #1392](https://tree.taiga.io/project/penpot/us/1392) - Replace Slate-Editor with DraftJS [Taiga #1346](https://tree.taiga.io/project/penpot/us/1346) - Set proper page title [Taiga #1377](https://tree.taiga.io/project/penpot/us/1377) - ### :bug: Bugs fixed - Disable buttons in view mode for users without permissions [Taiga #1328](https://tree.taiga.io/project/penpot/issue/1328) @@ -589,15 +653,13 @@ - The LDAP configuration variables interpolation starts using `:` (example `:username`) instead of `$`. The main reason is avoid - unnecesary conflict with bash interpolation. - + unnecessary conflict with bash interpolation. ### :arrow_up: Deps updates - Update backend to JDK16. - Update exporter nodejs to v14.16.0 - ### :heart: Community contributions by (Thank you!) - iblueer [#726](https://github.com/penpot/penpot/pull/726) @@ -605,27 +667,25 @@ - girafic [#748](https://github.com/penpot/penpot/pull/748) - mbrksntrk [#794](https://github.com/penpot/penpot/pull/794) - ## 1.3.0-alpha ### :sparkles: New features - Add emailcatcher and ldap test containers to devenv. [#506](https://github.com/penpot/penpot/pull/506) - Add major refactor of internal pubsub/redis code; improves scalability and performance [#640](https://github.com/penpot/penpot/pull/640) -- Add more chinese transtions [#687](https://github.com/penpot/penpot/pull/687) +- Add more chinese translations [#687](https://github.com/penpot/penpot/pull/687) - Add more presets for artboard [#654](https://github.com/penpot/penpot/pull/654) - Add optional loki integration [#645](https://github.com/penpot/penpot/pull/645) - Add proper http session lifecycle handling. - Allow to set border radius of each rect corner individually - Bounce & Complaint handling [#635](https://github.com/penpot/penpot/pull/635) - Disable groups interactions when holding "Ctrl" key (deep selection) -- New action in context menu to "edit" some shapes (binded to key "Enter") - +- New action in context menu to "edit" some shapes (bound to key "Enter") ### :bug: Bugs fixed - Add more improvements to french translation strings [#591](https://github.com/penpot/penpot/pull/591) -- Add some missing database indexes (mainly improves performance on large databases on file-update rpc method, and some background tasks). +- Add some missing database indexes (mainly improves performance on large databases on file-update rpc method, and some background tasks) - Disables filters in masking elements (issue with Firefox rendering) - Drawing tool will have priority over resize/rotate handlers [Taiga #1225](https://tree.taiga.io/project/penpot/issue/1225) - Fix broken bounding box on editing paths [Taiga #1254](https://tree.taiga.io/project/penpot/issue/1254) @@ -639,16 +699,14 @@ - Have language change notification written in the new language [Taiga #1205](https://tree.taiga.io/project/penpot/issue/1205) - Hide register screen when registration is disabled [#598](https://github.com/penpot/penpot/issues/598) - Properly handle errors on github, gitlab and ldap auth backends. -- Properly mark profile auth backend (on first register/ auth with 3rd party auth provider). +- Properly mark profile auth backend (on first register/ auth with 3rd party auth provider) - Refactor LDAP auth backend. - ### :heart: Community contributions by (Thank you!) - girafic [#538](https://github.com/penpot/penpot/pull/654) - arkhi [#591](https://github.com/penpot/penpot/pull/591) - ## 1.2.0-alpha ### :sparkles: New features @@ -663,7 +721,6 @@ - Show a pixel grid when zoom greater than 800% [#519](https://github.com/penpot/penpot/discussions/519) - Fix behavior of select all command when there are objects outside frames [Taiga #1209](https://tree.taiga.io/project/penpot/issue/1209) - ### :bug: Bugs fixed - Fix 404 when access shared link [#615](https://github.com/penpot/penpot/issues/615) @@ -699,7 +756,6 @@ - Improved MacOS shortcuts and helpers - Small changes to shape creation - ## 1.0.0-alpha Initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc3d2b7fda..f716777a15 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,9 +19,9 @@ If you found a bug, please report it, as far as possible with: - a browser and the browser version used - a dev tools console exception stack trace (if it is available) -If you found a bug that you consider better discuse in private (for +If you found a bug that you consider better discuss in private (for example: security bugs), consider first send an email to -`info@penpot.app`. +`support@penpot.app`. **We don't have formal bug bounty program for security reports; this is an open source application and your contribution will be recognized @@ -54,7 +54,7 @@ We will use the `easy fix` mark for tag for indicate issues that are easy for beginners. -## Commit Message Guidelines ## +## Commit Guidelines ## We have very precise rules over how our git commit messages can be formatted. @@ -78,7 +78,6 @@ Where type is: - :ambulance: `:ambulance:` a commit that fixes critical bug - :books: `:books:` a commit that improves or adds documentation - :construction: `:construction:`: a wip commit -- :construction_worker: `:construction_worker:` a commit with CI related stuff - :boom: `:boom:` a commit with breaking changes - :wrench: `:wrench:` a commit for config updates - :zap: `:zap:` a commit with performance improvements @@ -91,13 +90,14 @@ More info: - https://gist.github.com/parmentf/035de27d6ed1dce0b36a - https://gist.github.com/rxaviers/7360908 -The subject should be: - -- Use the imperative mood. -- Capitalize the first letter. -- Don't put a period at the end of the subject line. -- Put a blank line between the subject line and the body. +Each commit should have: +- A concise subject using imperative mood. +- The subject should have capitalized the first letter and without + period at the end. +- A blank line between the subject line and the body. +- An entry on the CHANGES.md file if applicable, referencing the + github or taiga issue/user-story using the these same rules. ## Code of conduct ## diff --git a/README.md b/README.md index ebe1760c42..61db4de9a0 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,9 @@ You can ask and answer questions, have open-ended conversations, and follow alon ✉️ [Mail us](mailto:info@penpot.app) -💬 [Github discussions](https://github.com/penpot/penpot/discussions) +💬 [GitHub discussions](https://github.com/penpot/penpot/discussions) -🐞 [Github issues](mailto:info@penpot.apphttps://github.com/penpot/penpot/issues) +🐞 [GitHub issues](https://github.com/penpot/penpot/issues) ✍️️ [Gitter](https://gitter.im/penpot/community) @@ -81,7 +81,7 @@ You can ask and answer questions, have open-ended conversations, and follow alon You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project. Would you like to know more about Penpot? We recommend you to visit our youtube channel and learn more about the functionalities and possibilities of Penpot with our video tutorials. -🎞️ [Youtube channel](https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g) +🎞️ [YouTube channel](https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g) ## License ## diff --git a/backend/build.clj b/backend/build.clj new file mode 100644 index 0000000000..9db6eea2f0 --- /dev/null +++ b/backend/build.clj @@ -0,0 +1,36 @@ +(ns build + (:refer-clojure :exclude [compile]) + (:require + [clojure.tools.build.api :as b] + [clojure.java.io])) + +(def class-dir "target/classes") +(def basis (b/create-basis {:project "deps.edn"})) +(def jar-file "target/penpot.jar") + +(defn clean [_] + (b/delete {:path "target"})) + +(defn jar [_] + (b/copy-dir + {:src-dirs ["src" "resources"] + :target-dir class-dir}) + + (b/compile-clj + {:basis basis + :src-dirs ["src"] + :class-dir class-dir}) + + (b/uber + {:class-dir class-dir + :uber-file jar-file + :main 'clojure.main + :exclude [#"goog.*" #"^javasist.*"] + :basis basis})) + +(defn compile [_] + (b/javac + {:src-dirs ["dev/java"] + :class-dir class-dir + :basis basis + :javac-opts ["-source" "11" "-target" "11"]})) diff --git a/backend/deps.edn b/backend/deps.edn index cebf952830..f2c71a40de 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -6,67 +6,71 @@ org.zeromq/jeromq {:mvn/version "0.5.2"} com.taoensso/nippy {:mvn/version "3.1.1"} - com.github.luben/zstd-jni {:mvn/version "1.5.0-4"} + com.github.luben/zstd-jni {:mvn/version "1.5.1-1"} + org.clojure/data.fressian {:mvn/version "1.0.0"} - ;; NOTE: don't upgrade to latest version, breaking change is - ;; introduced on 0.10.0 that suffixes counters with _total if they - ;; are not already has this suffix. - io.prometheus/simpleclient {:mvn/version "0.9.0"} - io.prometheus/simpleclient_hotspot {:mvn/version "0.9.0"} - io.prometheus/simpleclient_jetty {:mvn/version "0.9.0" + io.prometheus/simpleclient {:mvn/version "0.14.1"} + io.prometheus/simpleclient_hotspot {:mvn/version "0.14.1"} + io.prometheus/simpleclient_jetty {:mvn/version "0.14.1" :exclusions [org.eclipse.jetty/jetty-server org.eclipse.jetty/jetty-servlet]} - io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"} + io.prometheus/simpleclient_httpserver {:mvn/version "0.14.1"} - io.lettuce/lettuce-core {:mvn/version "6.1.5.RELEASE"} + io.lettuce/lettuce-core {:mvn/version "6.1.6.RELEASE"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} - info.sunng/ring-jetty9-adapter {:mvn/version "0.15.2"} - com.github.seancorfield/next.jdbc {:mvn/version "1.2.709"} + funcool/yetti {:git/tag "v4.0" :git/sha "59ed2a7" + :git/url "https://github.com/funcool/yetti.git" + :exclusions [org.slf4j/slf4j-api]} + + com.github.seancorfield/next.jdbc {:mvn/version "1.2.761"} metosin/reitit-ring {:mvn/version "0.5.15"} - org.postgresql/postgresql {:mvn/version "42.2.23"} - com.zaxxer/HikariCP {:mvn/version "5.0.0"} + org.postgresql/postgresql {:mvn/version "42.3.1"} + com.zaxxer/HikariCP {:mvn/version "5.0.1"} funcool/datoteka {:mvn/version "2.0.0"} - buddy/buddy-core {:mvn/version "1.10.1"} - buddy/buddy-hashers {:mvn/version "1.8.1"} - buddy/buddy-sign {:mvn/version "3.4.1"} + buddy/buddy-hashers {:mvn/version "1.8.158"} + buddy/buddy-sign {:mvn/version "3.4.333"} - org.jsoup/jsoup {:mvn/version "1.14.2"} + org.jsoup/jsoup {:mvn/version "1.14.3"} org.im4java/im4java {:mvn/version "1.4.0"} org.lz4/lz4-java {:mvn/version "1.8.0"} org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} integrant/integrant {:mvn/version "0.8.0"} - io.sentry/sentry {:mvn/version "5.1.2"} + io.sentry/sentry {:mvn/version "5.5.2"} ;; Pretty Print specs pretty-spec/pretty-spec {:mvn/version "0.1.4"} - software.amazon.awssdk/s3 {:mvn/version "2.17.40"}} + software.amazon.awssdk/s3 {:mvn/version "2.17.111"}} - :paths ["src" "resources"] + :paths ["src" "resources" "target/classes"] :aliases {:dev {:extra-deps {com.bhauman/rebel-readline {:mvn/version "RELEASE"} org.clojure/tools.namespace {:mvn/version "RELEASE"} org.clojure/test.check {:mvn/version "RELEASE"} - org.clojure/data.csv {:mvn/version "1.0.0"} - com.clojure-goes-fast/clj-async-profiler {:mvn/version "0.5.1"} - - criterium/criterium {:mvn/version "RELEASE"} + clojure-humanize/clojure-humanize {:mvn/version "0.2.2"} + org.clojure/data.csv {:mvn/version "RELEASE"} + com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"} mockery/mockery {:mvn/version "RELEASE"}} :extra-paths ["test" "dev"]} + :build + {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.7.4" :git/sha "ac442da"}} + :ns-default build} + :kaocha - {:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.887"}} + {:extra-deps {lambdaisland/kaocha {:mvn/version "RELEASE"}} :main-opts ["-m" "kaocha.runner"]} :test - {:extra-deps {io.github.cognitect-labs/test-runner - {:git/url "https://github.com/cognitect-labs/test-runner.git" - :git/sha "dd6da11611eeb87f08780a30ac8ea6012d4c05ce"}} + {:extra-paths ["test"] + :extra-deps + {io.github.cognitect-labs/test-runner + {:git/tag "v0.5.0" :git/sha "b3fd0d2"}} :exec-fn cognitect.test-runner.api/test} :outdated diff --git a/backend/dev/user.clj b/backend/dev/user.clj index d65cd01cd9..5e7fdcb94b 100644 --- a/backend/dev/user.clj +++ b/backend/dev/user.clj @@ -7,12 +7,17 @@ (ns user (:require [app.common.exceptions :as ex] + [app.common.geom.matrix :as gmt] + [app.common.perf :as perf] + [app.common.transit :as t] [app.config :as cfg] [app.main :as main] [app.util.blob :as blob] + [app.util.fressian :as fres] [app.util.json :as json] [app.util.time :as dt] - [app.util.transit :as t] + [clj-async-profiler.core :as prof] + [clojure.contrib.humanize :as hum] [clojure.java.io :as io] [clojure.pprint :refer [pprint print-table]] [clojure.repl :refer :all] @@ -22,31 +27,14 @@ [clojure.test :as test] [clojure.tools.namespace.repl :as repl] [clojure.walk :refer [macroexpand-all]] - [criterium.core :refer [quick-bench bench with-progress-reporting]] + [datoteka.core] [integrant.core :as ig])) (repl/disable-reload! (find-ns 'integrant.core)) +(set! *warn-on-reflection* true) (defonce system nil) -;; --- Benchmarking Tools - -(defmacro run-quick-bench - [& exprs] - `(with-progress-reporting (quick-bench (do ~@exprs) :verbose))) - -(defmacro run-quick-bench' - [& exprs] - `(quick-bench (do ~@exprs))) - -(defmacro run-bench - [& exprs] - `(with-progress-reporting (bench (do ~@exprs) :verbose))) - -(defmacro run-bench' - [& exprs] - `(bench (do ~@exprs))) - ;; --- Development Stuff (defn- run-tests @@ -91,11 +79,13 @@ (defn compression-bench [data] - (print-table - [{:v1 (alength (blob/encode data {:version 1})) - :v2 (alength (blob/encode data {:version 2})) - :v3 (alength (blob/encode data {:version 3}))}])) - + (let [humanize (fn [v] (hum/filesize v :binary true :format " %.4f "))] + (print-table + [{:v1 (humanize (alength (blob/encode data {:version 1}))) + :v2 (humanize (alength (blob/encode data {:version 2}))) + :v3 (humanize (alength (blob/encode data {:version 3}))) + :v4 (humanize (alength (blob/encode data {:version 4}))) + }]))) (defonce debug-tap (do diff --git a/backend/resources/error-list.tmpl b/backend/resources/error-list.tmpl deleted file mode 100644 index 360734e142..0000000000 --- a/backend/resources/error-list.tmpl +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - penpot - error report {{id}} - - - - - - -
- -
- - - diff --git a/backend/resources/error-report.tmpl b/backend/resources/error-report.tmpl deleted file mode 100644 index ad663f604b..0000000000 --- a/backend/resources/error-report.tmpl +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - penpot - error report {{id}} - - - - - - -
-
-
-
CONTEXT:
-
-
{{context}}
-
-
- - {% if params %} -
-
PARAMS:
-
-
{{params}}
-
-
- {% endif %} - - - {% if data %} -
-
ERROR DATA:
-
-
{{data}}
-
-
- {% endif %} - - {% if spec-problems %} -
-
SPEC PROBLEMS:
-
-
{{spec-problems}}
-
-
- {% endif %} - - {% if trace %} -
-
TRACE:
-
-
{{trace}}
-
-
- {% endif %} -
-
- - - diff --git a/backend/resources/log4j2.xml b/backend/resources/log4j2.xml index 96750c0456..d2a045c36c 100644 --- a/backend/resources/log4j2.xml +++ b/backend/resources/log4j2.xml @@ -2,7 +2,7 @@ - + diff --git a/backend/resources/templates/base.tmpl b/backend/resources/templates/base.tmpl new file mode 100644 index 0000000000..7f8709dd9f --- /dev/null +++ b/backend/resources/templates/base.tmpl @@ -0,0 +1,18 @@ + + + + + + + {% block title %}{% endblock %} + + + + + {% block content %} + {% endblock %} + + + diff --git a/backend/resources/templates/debug.tmpl b/backend/resources/templates/debug.tmpl new file mode 100644 index 0000000000..a3044dba54 --- /dev/null +++ b/backend/resources/templates/debug.tmpl @@ -0,0 +1,32 @@ +{% extends "templates/base.tmpl" %} + +{% block title %} +Debug Main Page +{% endblock %} + +{% block content %} + +
+
+

Download file data:

+ Given an FILE-ID, downloads the file data as file. The file data is encoded using transit. +
+ + + +
+
+ +
+

Upload File Data:

+ Create a new file on your draft projects using the file downloaded from the previous section. +
+ + +
+
+
+{% endblock %} diff --git a/backend/resources/templates/error-list.tmpl b/backend/resources/templates/error-list.tmpl new file mode 100644 index 0000000000..66835867a7 --- /dev/null +++ b/backend/resources/templates/error-list.tmpl @@ -0,0 +1,18 @@ +{% extends "templates/base.tmpl" %} + +{% block title %} +penpot - error list +{% endblock %} + +{% block content %} + +
+ +
+{% endblock %} diff --git a/backend/resources/templates/error-report.tmpl b/backend/resources/templates/error-report.tmpl new file mode 100644 index 0000000000..a3fcc158d0 --- /dev/null +++ b/backend/resources/templates/error-report.tmpl @@ -0,0 +1,98 @@ +{% extends "templates/base.tmpl" %} + +{% block title %} +penpot - error report {{id}} +{% endblock %} + +{% block content %} + +
+
+
+
CONTEXT:
+ +
+

{{hint}}

+
+ +
+
{{context}}
+
+
+ + {% if params %} +
+
REQUEST PARAMS:
+
+
{{params}}
+
+
+ {% endif %} + + {% if data %} +
+
ERROR DATA:
+
+
{{data}}
+
+
+ {% endif %} + + {% if spec-explain %} +
+
SPEC EXPLAIN:
+
+
{{spec-explain}}
+
+
+ {% endif %} + + {% if spec-problems %} +
+
SPEC PROBLEMS:
+
+
{{spec-problems}}
+
+
+ {% endif %} + + {% if spec-value %} +
+
SPEC VALUE:
+
+
{{spec-value}}
+
+
+ {% endif %} + + {% if trace %} +
+
TRACE:
+
+
{{trace}}
+
+
+ {% endif %} +
+
+{% endblock %} diff --git a/backend/resources/templates/styles.css b/backend/resources/templates/styles.css new file mode 100644 index 0000000000..60db4b548e --- /dev/null +++ b/backend/resources/templates/styles.css @@ -0,0 +1,150 @@ +* { + font-family: "JetBrains Mono", monospace; + font-size: 12px; +} + +body { + margin: 0px; + padding: 0px; +} + +pre { + margin: 0px; + line-height: 16px; +} + +desc { + display: flex; + margin-bottom: 10px; + font-size: 10px; + color: #666; +} + +input[type=text], input[type=submit] { + padding: 3px; +} + +main { + margin: 20px; +} + +nav { + position: fixed; + width: 100vw; + top: 0; + left: 0; + padding: 5px 20px; + display: flex; + background: #e3e3e3; +} + +nav > h1 { + padding: 0px; + margin: 0px; + font-size: 11px; +} + +nav > div { + text-transform: uppercase; + font-weight: bold; +} + +nav > div:not(:last-child) { + margin-right: 10px; +} + +.table { + margin-top: 25px; + display: flex; + flex-direction: column; +} + +.table-row { + display: flex; + padding-bottom: 15px; + /* width: 100%; */ + /* border: 1px solid red; */ +} + +.table-key { + font-weight: 600; + width: 60px; + padding: 4px; + + padding-top: 40px; + margin-top: -40px; +} + +.table-val { + font-weight: 200; + color: #333; + padding: 4px; +} + +.multiline { + margin-top: 15px; + flex-direction: column; +} + +.multiline .table-key { + margin-bottom: 10px; + border-bottom: 1px dashed #dddddd; + /* padding: 4px; */ + width: unset; +} + +.index { + margin-top: 40px; +} + +.index > section { + padding: 10px; + background-color: #e3e3e3; +} + +.index > section:not(:last-child) { + margin-bottom: 10px; +} + + +.index > section > h2 { + margin-top: 0px; +} + +.horizontal-list { + margin: 20px; + margin-top: 40px; +} + +.horizontal-list ul { + display: flex; + margin: 0px; + padding: 0px; + flex-direction: column; + flex-wrap: wrap; + height: calc(100vh - 75px); + justify-content: flex-start; +} + +.horizontal-list li { + list-style: none; + padding: 0px; + margin: 0px; + line-height: 18px; + min-width: 210px; + margin: 0px 20px; + cursor: pointer; + display: flex; + justify-content: center; + border-radius: 3px; +} + +.horizontal-list li:hover { + background-color: #e9e9e9; +} + +.horizontal-list li > a { + text-decoration: none; + color: inherit; +} + diff --git a/backend/scripts/build b/backend/scripts/build index 865fed48b0..d2b7b5966a 100755 --- a/backend/scripts/build +++ b/backend/scripts/build @@ -1,79 +1,20 @@ -#!/usr/bin/env bb +#!/usr/bin/env bash -;; 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 +CURRENT_VERSION=$1; -(ns build - (:require - [clojure.string :as str] - [clojure.java.io :as io] - [clojure.pprint :refer [pprint]] - [babashka.fs :as fs] - [babashka.process :refer [$ check]])) +set -ex -(defn split-cp - [data] - (str/split data #":")) +rm -rf target; +mkdir -p target/classes; +mkdir -p target/dist; +echo "$CURRENT_VERSION" > target/classes/version.txt; -(def classpath - (->> ($ clojure -Spath) - (check) - (:out) - (slurp) - (split-cp) - (map str/trim))) +clojure -T:build jar; +mv target/penpot.jar target/dist/penpot.jar +cp scripts/run.template.sh target/dist/run.sh; +cp scripts/manage.template.sh target/dist/manage.sh; +chmod +x target/dist/run.sh; +chmod +x target/dist/manage.sh; -(def classpath-jars - (let [xfm (filter #(str/ends-with? % ".jar"))] - (into #{} xfm classpath))) -(def classpath-paths - (let [xfm (comp (remove #(str/ends-with? % ".jar")) - (filter #(.isDirectory (io/file %))))] - (into #{} xfm classpath))) -(def version - (or (first *command-line-args*) "%version%")) - -;; Clean previous dist -(-> ($ rm -rf "./target/dist") check) - -;; Create a new dist -(-> ($ mkdir -p "./target/dist/deps") check) - -;; Copy all jar deps into dist -(run! (fn [item] (-> ($ cp ~item "./target/dist/deps/") check)) classpath-jars) - -;; Create the application jar -(spit "./target/dist/version.txt" version) - -(-> ($ jar cvf "./target/dist/deps/app.jar" -C ~(first classpath-paths) ".") check) -(-> ($ jar uvf "./target/dist/deps/app.jar" -C "./target/dist" "version.txt") check) -(run! (fn [item] - (-> ($ jar uvf "./target/dist/deps/app.jar" -C ~item ".") check)) - (rest classpath-paths)) - -;; Copy logging configuration -(-> ($ cp "./resources/log4j2.xml" "./target/dist/") check) - -;; Create classpath file -(let [jars (->> (into ["app.jar"] classpath-jars) - (map fs/file-name) - (map #(fs/path "deps" %)) - (map str))] - (spit "./target/dist/classpath" (str/join ":" jars))) - -;; Copy run script template -(-> ($ cp "./scripts/run.template.sh" "./target/dist/run.sh") check) - -;; Copy run script template -(-> ($ cp "./scripts/manage.template.sh" "./target/dist/manage.sh") check) - -;; Add exec permisions to scripts. -(-> ($ chmod +x "./target/dist/run.sh") check) -(-> ($ chmod +x "./target/dist/manage.sh") check) - -nil diff --git a/backend/scripts/manage.template.sh b/backend/scripts/manage.template.sh index 31f261b6b8..f3469f5975 100644 --- a/backend/scripts/manage.template.sh +++ b/backend/scripts/manage.template.sh @@ -16,4 +16,4 @@ if [ -f ./environ ]; then source ./environ fi -exec $JAVA_CMD $JVM_OPTS -classpath $(cat classpath) -Dlog4j2.configurationFile=./log4j2.xml clojure.main -m app.cli.manage "$@" +exec $JAVA_CMD $JVM_OPTS -jar penpot.jar -m app.cli.manage "$@" diff --git a/backend/scripts/repl b/backend/scripts/repl index 3ca39aa9c5..22bebe8c73 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -1,19 +1,25 @@ #!/usr/bin/env bash -export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS" +# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot" +# export PENPOT_DATABASE_USERNAME="penpot" +# export PENPOT_DATABASE_PASSWORD="penpot" +# export PENPOT_DATABASE_READONLY=true +# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot_pre" +# export PENPOT_DATABASE_USERNAME="penpot_pre" +# export PENPOT_DATABASE_PASSWORD="penpot_pre" +# export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS" export OPTIONS=" - -A:jmx-remote:dev \ + -A:dev \ -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ - -J-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory \ -J-Dlog4j2.configurationFile=log4j2-devenv.xml \ - -J-XX:+UseShenandoahGC \ + -J-XX:+UseZGC \ -J-XX:-OmitStackTraceInFastThrow \ - -J-Xms50m -J-Xmx512m"; - -# export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions"; -# export OPTIONS="$OPTIONS -J-XX:-TieredCompilation -J-XX:CompileThreshold=10000"; + -J-Xms50m -J-Xmx1024m \ + -J-Djdk.attach.allowAttachSelf \ + -J-XX:+UnlockDiagnosticVMOptions \ + -J-XX:+DebugNonSafepoints"; export OPTIONS_EVAL="nil" # export OPTIONS_EVAL="(set! *warn-on-reflection* true)" diff --git a/backend/scripts/run.template.sh b/backend/scripts/run.template.sh index 2742fe9fed..33c8eda2ed 100644 --- a/backend/scripts/run.template.sh +++ b/backend/scripts/run.template.sh @@ -17,4 +17,4 @@ if [ -f ./environ ]; then fi set -x -exec $JAVA_CMD $JVM_OPTS -classpath "$(cat classpath)" -Dlog4j2.configurationFile=./log4j2.xml "$@" clojure.main -m app.main +exec $JAVA_CMD $JVM_OPTS "$@" -jar penpot.jar -m app.main diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 615401b67c..71ee82fbeb 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -42,6 +42,7 @@ (def defaults {:http-server-port 6060 + :http-server-host "localhost" :host "devenv" :tenant "dev" :database-uri "postgresql://postgres/penpot" @@ -132,6 +133,7 @@ (s/def ::oidc-roles-attr ::us/keyword) (s/def ::host ::us/string) (s/def ::http-server-port ::us/integer) +(s/def ::http-server-host ::us/string) (s/def ::http-session-idle-max-age ::dt/duration) (s/def ::http-session-updater-batch-max-age ::dt/duration) (s/def ::http-session-updater-batch-max-size ::us/integer) @@ -221,6 +223,7 @@ ::oidc-roles-attr ::oidc-roles ::host + ::http-server-host ::http-server-port ::http-session-idle-max-age ::http-session-updater-batch-max-age diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index bcbf372b24..a4976b47b0 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -96,8 +96,8 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def initsql - (str "SET statement_timeout = 200000;\n" - "SET idle_in_transaction_session_timeout = 200000;")) + (str "SET statement_timeout = 300000;\n" + "SET idle_in_transaction_session_timeout = 300000;")) (defn- create-datasource-config [{:keys [metrics read-only] :or {read-only false} :as cfg}] diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index e413207a38..d550281998 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -12,91 +12,78 @@ [app.common.spec :as us] [app.http.doc :as doc] [app.http.errors :as errors] - [app.http.debug :as debug] [app.http.middleware :as middleware] [app.metrics :as mtx] [clojure.spec.alpha :as s] [integrant.core :as ig] [reitit.ring :as rr] - [ring.adapter.jetty9 :as jetty]) + [yetti.adapter :as yt]) (:import org.eclipse.jetty.server.Server - org.eclipse.jetty.server.handler.ErrorHandler org.eclipse.jetty.server.handler.StatisticsHandler)) -(declare router-handler) +(declare wrap-router) (s/def ::handler fn?) (s/def ::router some?) -(s/def ::ws (s/map-of ::us/string fn?)) (s/def ::port ::us/integer) +(s/def ::host ::us/string) (s/def ::name ::us/string) (defmethod ig/pre-init-spec ::server [_] (s/keys :req-un [::port] - :opt-un [::ws ::name ::mtx/metrics ::router ::handler])) + :opt-un [::name ::mtx/metrics ::router ::handler ::host])) (defmethod ig/prep-key ::server [_ cfg] (merge {:name "http"} (d/without-nils cfg))) +(defn- instrument-metrics + [^Server server metrics] + (let [stats (doto (StatisticsHandler.) + (.setHandler (.getHandler server)))] + (.setHandler server stats) + (mtx/instrument-jetty! (:registry metrics) stats) + server)) + (defmethod ig/init-key ::server - [_ {:keys [handler router ws port name metrics] :as opts}] - (l/info :msg "starting http server" :port port :name name) - (let [pre-start (fn [^Server server] - (let [handler (doto (ErrorHandler.) - (.setShowStacks true) - (.setServer server))] - (.setErrorHandler server ^ErrorHandler handler) - (when metrics - (let [stats (StatisticsHandler.)] - (.setHandler ^StatisticsHandler stats (.getHandler server)) - (.setHandler server stats) - (mtx/instrument-jetty! (:registry metrics) stats))))) - - options (merge - {:port port - :h2c? true - :join? false - :allow-null-path-info true - :configurator pre-start} - (when (seq ws) - {:websockets ws})) - - handler (cond - (fn? handler) handler - (some? router) (router-handler router) - :else (ex/raise :type :internal - :code :invalid-argument - :hint "Missing `handler` or `router` option.")) - - server (jetty/run-jetty handler options)] - (assoc opts :server server))) + [_ {:keys [handler router port name metrics host] :as opts}] + (l/info :msg "starting http server" :port port :host host :name name) + (let [options {:http/port port :http/host host} + handler (cond + (fn? handler) handler + (some? router) (wrap-router router) + :else (ex/raise :type :internal + :code :invalid-argument + :hint "Missing `handler` or `router` option.")) + server (-> (yt/server handler options) + (cond-> metrics (instrument-metrics metrics)))] + (assoc opts :server (yt/start! server)))) (defmethod ig/halt-key! ::server [_ {:keys [server name port] :as opts}] - (l/info :msg "stoping http server" - :name name - :port port) - (jetty/stop-server server)) + (l/info :msg "stoping http server" :name name :port port) + (yt/stop! server)) -(defn- router-handler +(defn- wrap-router [router] - (let [handler (rr/ring-handler router - (rr/routes - (rr/create-resource-handler {:path "/"}) - (rr/create-default-handler)) - {:middleware [middleware/server-timing]})] + (let [default (rr/routes + (rr/create-resource-handler {:path "/"}) + (rr/create-default-handler)) + options {:middleware [middleware/server-timing]} + handler (rr/ring-handler router default options)] (fn [request] (try (handler request) (catch Throwable e (l/with-context (errors/get-error-context request e) - (l/error :hint (ex-message e) :cause e) + (l/error :hint "unexpected error processing request" + :query-string (:query-string request) + :cause e) {:status 500 :body "internal server error"})))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Http Main Handler (Router) +;; Http Router ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (s/def ::rpc map?) @@ -105,16 +92,17 @@ (s/def ::storage map?) (s/def ::assets map?) (s/def ::feedback fn?) +(s/def ::ws fn?) (s/def ::audit-http-handler fn?) (s/def ::debug map?) (defmethod ig/pre-init-spec ::router [_] - (s/keys :req-un [::rpc ::session ::mtx/metrics + (s/keys :req-un [::rpc ::session ::mtx/metrics ::ws ::oauth ::storage ::assets ::feedback ::debug ::audit-http-handler])) (defmethod ig/init-key ::router - [_ {:keys [session rpc oauth metrics assets feedback debug] :as cfg}] + [_ {:keys [ws session rpc oauth metrics assets feedback debug] :as cfg}] (rr/router [["/metrics" {:get (:handler metrics)}] ["/assets" {:middleware [[middleware/format-response-body] @@ -125,27 +113,39 @@ ["/by-file-media-id/:id" {:get (:file-objects-handler assets)}] ["/by-file-media-id/:id/thumbnail" {:get (:file-thumbnails-handler assets)}]] - ["/dbg" {:middleware [[middleware/params] + ["/dbg" {:middleware [[middleware/multipart-params] + [middleware/params] [middleware/keyword-params] [middleware/format-response-body] [middleware/errors errors/handle] [middleware/cookies] [(:middleware session)]]} + ["" {:get (:index debug)}] ["/error-by-id/:id" {:get (:retrieve-error debug)}] ["/error/:id" {:get (:retrieve-error debug)}] ["/error" {:get (:retrieve-error-list debug)}] - ["/file/data/:id" {:get (:retrieve-file-data debug)}] - ["/file/changes/:id" {:get (:retrieve-file-changes debug)}]] + ["/file/data" {:get (:retrieve-file-data debug) + :post (:upload-file-data debug)}] + ["/file/changes" {:get (:retrieve-file-changes debug)}]] ["/webhooks" ["/sns" {:post (:sns-webhook cfg)}]] + ["/ws/notifications" + {:middleware [[middleware/params] + [middleware/keyword-params] + [middleware/format-response-body] + [middleware/errors errors/handle] + [middleware/cookies] + [(:middleware session)]] + :get ws}] + ["/api" {:middleware [[middleware/cors] - [middleware/etag] - [middleware/format-response-body] [middleware/params] [middleware/multipart-params] [middleware/keyword-params] + [middleware/format-response-body] + [middleware/etag] [middleware/parse-request-body] [middleware/errors errors/handle] [middleware/cookies]]} diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj index 7117c6466b..11cfe28a88 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -173,14 +173,14 @@ (defn- process-report [cfg {:keys [type profile-id] :as report}] - (l/trace :action "procesing report" :report (pr-str report)) + (l/trace :action "processing report" :report (pr-str report)) (cond ;; In this case we receive a bounce/complaint notification without ;; confirmed identity, we just emit a warning but do nothing about ;; it because this is not a normal case. All notifications should ;; come with profile identity. (nil? profile-id) - (l/warn :msg "a notification without identity recevied from AWS" + (l/warn :msg "a notification without identity received from AWS" :report (pr-str report)) (= "bounce" type) diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 1ec0668869..2098cd084f 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -9,26 +9,21 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] - [app.common.transit :as t] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.rpc.mutations.files :as m.files] [app.rpc.queries.profile :as profile] [app.util.blob :as blob] - [app.util.json :as json] [app.util.template :as tmpl] [app.util.time :as dt] [clojure.java.io :as io] [clojure.pprint :as ppr] - [clojure.spec.alpha :as s] [cuerdas.core :as str] + [datoteka.core :as fs] [integrant.core :as ig])) -(def sql:retrieve-range-of-changes - "select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn") - -(def sql:retrieve-single-change - "select revn, changes, data from file_change where file_id=? and revn = ?") +;; (selmer.parser/cache-off!) (defn authorized? [pool {:keys [profile-id]}] @@ -37,66 +32,104 @@ admins (or (cf/get :admins) #{})] (contains? admins (:email profile))))) -(defn prepare-response - [body] - (when-not body - (ex/raise :type :not-found - :code :enpty-data - :hint "empty response")) - - {:status 200 - :headers {"content-type" "application/transit+json"} - :body body}) - -(defn retrieve-file-data +(defn index [{:keys [pool]} request] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) - (let [id (some-> (get-in request [:path-params :id]) uuid/uuid) - revn (some-> (get-in request [:params :revn]) d/parse-integer)] - (when-not id - (ex/raise :type :validation - :code :missing-arguments)) + {:status 200 + :headers {"content-type" "text/html"} + :body (-> (io/resource "templates/debug.tmpl") + (tmpl/render {}))}) - (if (integer? revn) - (let [fchange (db/exec-one! pool [sql:retrieve-single-change id revn])] - (prepare-response (some-> fchange :data blob/decode))) - (let [file (db/get-by-id pool :file id)] - (prepare-response (some-> file :data blob/decode)))))) +(def sql:retrieve-range-of-changes + "select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn") -(defn retrieve-file-changes - [{:keys [pool]} {:keys [params path-params profile-id] :as request}] +(def sql:retrieve-single-change + "select revn, changes, data from file_change where file_id=? and revn = ?") + +(defn prepare-response + [{:keys [params] :as request} body] + (when-not body + (ex/raise :type :not-found + :code :enpty-data + :hint "empty response")) + + (cond-> {:status 200 + :headers {"content-type" "application/transit+json"} + :body body} + (contains? params :download) + (update :headers assoc "content-disposition" "attachment"))) + +(defn retrieve-file-data + [{:keys [pool]} {:keys [params] :as request}] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) - (let [id (some-> (get-in request [:path-params :id]) uuid/uuid) - revn (get-in request [:params :revn] "latest")] + (let [file-id (some-> (get-in request [:params :file-id]) uuid/uuid) + revn (some-> (get-in request [:params :revn]) d/parse-integer)] + (when-not file-id + (ex/raise :type :validation + :code :missing-arguments)) - (when (or (not id) (not revn)) + (let [data (if (integer? revn) + (some-> (db/exec-one! pool [sql:retrieve-single-change file-id revn]) :data) + (some-> (db/get-by-id pool :file file-id) :data))] + (if (contains? params :download) + (-> (prepare-response request data) + (update :headers assoc "content-type" "application/octet-stream")) + (prepare-response request (some-> data blob/decode)))))) + +(defn upload-file-data + [{:keys [pool]} {:keys [profile-id params] :as request}] + (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id) + data (some-> params :file :tempfile fs/slurp-bytes blob/decode)] + + (if (and data project-id) + (let [fname (str "imported-file-" (dt/now))] + (m.files/create-file pool {:id (uuid/next) + :name fname + :project-id project-id + :profile-id profile-id + :data data}) + {:status 200 + :body "OK"}) + {:status 500 + :body "error"}))) + +(defn retrieve-file-changes + [{:keys [pool]} request] + (when-not (authorized? pool request) + (ex/raise :type :authentication + :code :only-admins-allowed)) + + (let [file-id (some-> (get-in request [:params :id]) uuid/uuid) + revn (or (get-in request [:params :revn]) "latest")] + + (when (or (not file-id) (not revn)) (ex/raise :type :validation :code :invalid-arguments :hint "missing arguments")) (cond (d/num-string? revn) - (let [item (db/exec-one! pool [sql:retrieve-single-change id (d/parse-integer revn)])] - (prepare-response (some-> item :changes blob/decode vec))) + (let [item (db/exec-one! pool [sql:retrieve-single-change file-id (d/parse-integer revn)])] + (prepare-response request (some-> item :changes blob/decode vec))) (str/includes? revn ":") (let [[start end] (->> (str/split revn #":") (map str/trim) (map d/parse-integer)) - items (db/exec! pool [sql:retrieve-range-of-changes id start end])] - (prepare-response (some->> items - (map :changes) - (map blob/decode) - (mapcat identity) - (vec)))) - + items (db/exec! pool [sql:retrieve-range-of-changes file-id start end])] + (prepare-response request + (some->> items + (map :changes) + (map blob/decode) + (mapcat identity) + (vec)))) :else (ex/raise :type :validation :code :invalid-arguments)))) @@ -115,14 +148,19 @@ (render-template [report] (binding [ppr/*print-right-margin* 300] - (let [context (dissoc report :trace :cause :params :data :spec-prob :spec-problems :error :explain) + (let [context (dissoc report + :trace :cause :params :data :spec-problems + :spec-explain :spec-value :error :explain :hint) params {:context (with-out-str (ppr/pprint context)) - :data (:data report) - :trace (or (:cause report) - (:trace report) - (some-> report :error :trace)) - :params (:params report)}] - (-> (io/resource "error-report.tmpl") + :hint (:hint report) + :spec-explain (:spec-explain report) + :spec-problems (:spec-problems report) + :spec-value (:spec-value report) + :data (:data report) + :trace (or (:trace report) + (some-> report :error :trace)) + :params (:params report)}] + (-> (io/resource "templates/error-report.tmpl") (tmpl/render params))))) ] @@ -154,12 +192,14 @@ {:status 200 :headers {"content-type" "text/html; charset=utf-8" "x-robots-tag" "noindex"} - :body (-> (io/resource "error-list.tmpl") + :body (-> (io/resource "templates/error-list.tmpl") (tmpl/render {:items items}))})) (defmethod ig/init-key ::handlers - [_ {:keys [pool] :as cfg}] - {:retrieve-file-data (partial retrieve-file-data cfg) + [_ cfg] + {:index (partial index cfg) + :retrieve-file-data (partial retrieve-file-data cfg) :retrieve-file-changes (partial retrieve-file-changes cfg) :retrieve-error (partial retrieve-error cfg) - :retrieve-error-list (partial retrieve-error-list cfg)}) + :retrieve-error-list (partial retrieve-error-list cfg) + :upload-file-data (partial upload-file-data cfg)}) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 99992a9677..9e27ce9f11 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -12,7 +12,8 @@ [app.common.uuid :as uuid] [clojure.pprint] [clojure.spec.alpha :as s] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [expound.alpha :as expound])) (defn- parse-client-ip [{:keys [headers] :as request}] @@ -27,16 +28,22 @@ {:id (uuid/next) :path (:uri request) :method (:request-method request) - :hint (or (:hint data) (ex-message error)) - :params (l/stringify-data (:params request)) - :spec-problems (some-> data ::s/problems) - :data (some-> data (dissoc ::s/problems)) + :hint (ex-message error) + :params (:params request) + + :spec-problems (some->> data ::s/problems (take 10) seq vec) + :spec-value (some->> data ::s/value) + :data (some-> data (dissoc ::s/problems ::s/value ::s/spec)) :ip-addr (parse-client-ip request) :profile-id (:profile-id request)} - (let [headers (:headers request)] {:user-agent (get headers "user-agent") - :frontend-version (get headers "x-frontend-version" "unknown")})))) + :frontend-version (get headers "x-frontend-version" "unknown")}) + + (when (and data (::s/problems data)) + {:spec-explain (binding [s/*explain-out* expound/printer] + (with-out-str + (s/explain-out (update data ::s/problems #(take 10 %)))))})))) (defmulti handle-exception (fn [err & _rest] @@ -54,18 +61,25 @@ (defmethod handle-exception :validation [err _] - (let [edata (ex-data err)] - {:status 400 :body (dissoc edata ::s/problems)})) + (let [data (ex-data err) + explain (binding [s/*explain-out* expound/printer] + (with-out-str + (s/explain-out (update data ::s/problems #(take 10 %)))))] + {:status 400 + :body (-> data + (dissoc ::s/problems) + (dissoc ::s/value) + (assoc :explain explain))})) (defmethod handle-exception :assertion [error request] (let [edata (ex-data error)] (l/with-context (get-error-context request error) - (l/error :hint (ex-message error) :cause error)) + (l/error ::l/raw (ex-message error) :cause error)) {:status 500 :body {:type :server-error :code :assertion - :data (dissoc edata ::s/problems)}})) + :data (dissoc edata ::s/problems ::s/value ::s/spec)}})) (defmethod handle-exception :not-found [err _] @@ -84,7 +98,7 @@ (handle-exception (:handling edata) request) (do (l/with-context (get-error-context request error) - (l/error :hint (ex-message error) :cause error)) + (l/error ::l/raw (ex-message error) :cause error)) {:status 500 :body {:type :server-error @@ -97,10 +111,7 @@ (let [state (.getSQLState ^java.sql.SQLException error)] (l/with-context (get-error-context request error) - (l/error :hint "psql exception" - :error-message (ex-message error) - :state state - :cause error)) + (l/error ::l/raw (ex-message error) :cause error)) (cond (= state "57014") diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index de3343dbca..3845fe8398 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -9,14 +9,15 @@ [app.common.logging :as l] [app.common.transit :as t] [app.config :as cf] - [app.metrics :as mtx] [app.util.json :as json] [buddy.core.codecs :as bc] [buddy.core.hash :as bh] + [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]] - [ring.middleware.params :refer [wrap-params]])) + [ring.middleware.params :refer [wrap-params]] + [yetti.adapter :as yt])) (defn wrap-server-timing [handler] @@ -35,52 +36,77 @@ (t/read! reader))) (parse-json [body] - (json/read body)) - - (parse [type body] - (try - (case type - :json (parse-json body) - :transit (parse-transit body)) - (catch Exception e - (let [data {:type :parse - :hint "unable to parse request body" - :message (ex-message e)}] - {:status 400 - :headers {"content-type" "application/transit+json"} - :body (t/encode-str data {:type :json-verbose})}))))] - + (json/read body))] (fn [{:keys [headers body] :as request}] - (let [ctype (get headers "content-type")] - (handler - (case ctype - "application/transit+json" - (let [params (parse :transit body)] - (-> request - (assoc :body-params params) - (update :params merge params))) + (try + (let [ctype (get headers "content-type")] + (handler (case ctype + "application/transit+json" + (let [params (parse-transit body)] + (-> request + (assoc :body-params params) + (update :params merge params))) - "application/json" - (let [params (parse :json body)] - (-> request - (assoc :body-params params) - (update :params merge params))) + "application/json" + (let [params (parse-json body)] + (-> request + (assoc :body-params params) + (update :params merge params))) - request)))))) + request))) + (catch Exception e + (let [data {:type :validation + :code :unable-to-parse-request-body + :hint "malformed params"}] + (l/error :hint (ex-message e) :cause e) + {:status 400 + :headers {"content-type" "application/transit+json"} + :body (t/encode-str data {:type :json-verbose})})))))) (def parse-request-body {:name ::parse-request-body :compile (constantly wrap-parse-request-body)}) +(defn buffered-output-stream + "Returns a buffered output stream that ignores flush calls. This is + needed because transit-java calls flush very aggresivelly on each + object write." + [^java.io.OutputStream os ^long chunk-size] + (proxy [java.io.BufferedOutputStream] [os (int chunk-size)] + ;; Explicitly do not forward flush + (flush []) + (close [] + (proxy-super flush) + (proxy-super close)))) + +(def ^:const buffer-size (:http/output-buffer-size yt/base-defaults)) + +(defn- transit-streamable-body + [data opts] + (reify rp/StreamableResponseBody + (write-body-to-stream [_ _ output-stream] + ;; Use the same buffer as jetty output buffer size + (try + (with-open [bos (buffered-output-stream output-stream buffer-size)] + (let [tw (t/writer bos opts)] + (t/write! tw data))) + (catch Throwable cause + (l/warn :hint "unexpected error on encoding response" + :cause cause)))))) + (defn- impl-format-response-body - [response _request] - (let [body (:body response) - opts {:type :json}] + [response {:keys [query-params] :as request}] + (let [body (:body response) + opts {:type (if (contains? query-params "transit_verbose") :json-verbose :json)}] + (cond + (:ws response) + response + (coll? body) (-> response (update :headers assoc "content-type" "application/transit+json") - (assoc :body (t/encode body opts))) + (assoc :body (transit-streamable-body body opts))) (nil? body) (assoc response :status 204 :body "") @@ -111,11 +137,6 @@ {:name ::errors :compile (constantly wrap-errors)}) -(def metrics - {:name ::metrics - :wrap (fn [handler] - (mtx/wrap-counter handler {:id "http__requests_counter" - :help "Absolute http requests counter."}))}) (def cookies {:name ::cookies :compile (constantly wrap-cookies)}) @@ -138,24 +159,18 @@ (defn wrap-etag [handler] - (letfn [(generate-etag [{:keys [body] :as response}] - (str "W/\"" (-> body bh/blake2b-128 bc/bytes->hex) "\"")) - (get-match [{:keys [headers] :as request}] - (get headers "if-none-match"))] - (fn [request] - (let [response (handler request)] - (if (= :get (:request-method request)) - (let [etag (generate-etag response) - match (get-match request) - response (update response :headers #(assoc % "ETag" etag))] - (cond-> response - (and (string? match) - (= :get (:request-method request)) - (= etag match)) - (-> response - (assoc :body "") - (assoc :status 304)))) - response))))) + (letfn [(encode [data] + (when (string? data) + (str "W/\"" (-> data bh/blake2b-128 bc/bytes->hex) "\"")))] + (fn [{method :request-method headers :headers :as request}] + (cond-> (handler request) + (= :get method) + (as-> $ (if-let [etag (-> $ :body meta :etag encode)] + (cond-> (update $ :headers assoc "etag" etag) + (= etag (get headers "if-none-match")) + (-> (assoc :body "") + (assoc :status 304))) + $)))))) (def etag {:name ::etag diff --git a/backend/src/app/http/oauth.clj b/backend/src/app/http/oauth.clj index 1e0b67498f..c116836a60 100644 --- a/backend/src/app/http/oauth.clj +++ b/backend/src/app/http/oauth.clj @@ -130,7 +130,7 @@ (when-not (set/subset? provider-roles profile-roles) (ex/raise :type :internal :code :unable-to-auth - :hint "not enought permissions")))) + :hint "not enough permissions")))) (cond-> info (some? (:invitation-token state)) @@ -268,14 +268,29 @@ (defn- discover-oidc-config [{:keys [base-uri] :as opts}] + (let [discovery-uri (u/join base-uri ".well-known/openid-configuration") - response (http/send! {:method :get :uri (str discovery-uri)})] - (when (= 200 (:status response)) + response (ex/try (http/send! {:method :get :uri (str discovery-uri)}))] + (cond + (ex/exception? response) + (do + (l/warn :hint "unable to discover oidc configuration" + :discover-uri (str discovery-uri) + :cause response) + nil) + + (= 200 (:status response)) (let [data (json/read-str (:body response))] - (assoc opts - :token-uri (get data "token_endpoint") - :auth-uri (get data "authorization_endpoint") - :user-uri (get data "userinfo_endpoint")))))) + {:token-uri (get data "token_endpoint") + :auth-uri (get data "authorization_endpoint") + :user-uri (get data "userinfo_endpoint")}) + + :else + (do + (l/warn :hint "unable to discover OIDC configuration" + :uri (str discovery-uri) + :response-status-code (:status response)) + nil)))) (defn- obfuscate-string [s] @@ -299,17 +314,23 @@ (if (and (string? (:base-uri opts)) (string? (:client-id opts)) (string? (:client-secret opts))) - (if (and (string? (:token-uri opts)) - (string? (:user-uri opts)) - (string? (:auth-uri opts))) - (do - (l/info :action "initialize" :provider "oidc" :method "static" - :opts (pr-str (update opts :client-secret obfuscate-string))) - (assoc-in cfg [:providers "oidc"] opts)) - (let [opts (discover-oidc-config opts)] - (l/info :action "initialize" :provider "oidc" :method "discover" - :opts (pr-str (update opts :client-secret obfuscate-string))) - (assoc-in cfg [:providers "oidc"] opts))) + (do + (l/debug :hint "initialize oidc provider" :name "generic-oidc" + :opts (update opts :client-secret obfuscate-string)) + (if (and (string? (:token-uri opts)) + (string? (:user-uri opts)) + (string? (:auth-uri opts))) + (do + (l/debug :hint "initialized with user provided configuration") + (assoc-in cfg [:providers "oidc"] opts)) + (do + (l/debug :hint "trying to discover oidc provider configuration using BASE_URI") + (if-let [opts' (discover-oidc-config opts)] + (do + (l/debug :hint "discovered opts" :additional-opts opts') + (assoc-in cfg [:providers "oidc"] (merge opts opts'))) + + cfg)))) cfg))) (defn- initialize-google-provider diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index f341f91da2..90e3d217d2 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -58,9 +58,7 @@ (assoc response :cookies {cookie-name {:path "/" :http-only true :value id - :same-site (cond (not secure?) :lax - cors? :none - :else :strict) + :same-site (if cors? :none :lax) :secure secure?}}))) (defn- clear-cookies @@ -74,7 +72,7 @@ (do (a/>!! (::events-ch cfg) id) (l/set-context! {:profile-id profile-id}) - (handler (assoc request :profile-id profile-id))) + (handler (assoc request :profile-id profile-id :session-id id))) (handler request)))) ;; --- STATE INIT: SESSION diff --git a/backend/src/app/http/websocket.clj b/backend/src/app/http/websocket.clj new file mode 100644 index 0000000000..af7d63e40e --- /dev/null +++ b/backend/src/app/http/websocket.clj @@ -0,0 +1,145 @@ +;; 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.websocket + "A penpot notification service for file cooperative edition." + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.spec :as us] + [app.db :as db] + [app.metrics :as mtx] + [app.util.websocket :as ws] + [app.worker :as wrk] + [clojure.core.async :as a] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [yetti.websocket :as yws])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; WEBSOCKET HANDLER +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare send-presence!) + +(defmulti handle-message + (fn [_wsp message] (:type message))) + +(defmethod handle-message :connect + [wsp _] + (let [{:keys [msgbus file-id team-id session-id ::ws/output-ch]} @wsp + sub-ch (a/chan (a/dropping-buffer 32))] + + (swap! wsp assoc :sub-ch sub-ch) + + ;; Start a subscription forwarding goroutine + (a/go-loop [] + (when-let [val (a/! output-ch val)) + (recur))) + + (a/go + (a/ (merge cfg params) + (assoc :profile-id profile-id) + (assoc :team-id (:team-id file)) + (assoc ::ws/metrics metrics))] + + (when-not profile-id + (ex/raise :type :authentication + :hint "Authentication required.")) + + (when-not file + (ex/raise :type :not-found + :code :object-not-found)) + + (when-not (yws/upgrade-request? req) + (ex/raise :type :validation + :code :websocket-request-expected + :hint "this endpoint only accepts websocket connections")) + + (->> (ws/handler handle-message cfg) + (yws/upgrade req)))))) + +(def ^:private + sql:retrieve-file + "select f.id as id, + p.team_id as team_id + from file as f + join project as p on (p.id = f.project_id) + where f.id = ?") + +(defn- retrieve-file + [conn id] + (db/exec-one! conn [sql:retrieve-file id])) + diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index e312ebba23..d8c5d33f7a 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -150,7 +150,7 @@ (defmethod ig/init-key ::collector [_ cfg] (when (contains? cf/flags :audit-log) - (l/info :msg "intializing audit log collector") + (l/info :msg "initializing audit log collector") (let [input (a/chan 512 event-xform) buffer (aa/batch input {:max-batch-size 100 :max-batch-age (* 10 1000) ; 10s @@ -159,7 +159,7 @@ (when-let [[_type events] (a/otf [data] @@ -326,7 +324,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn configure-assets-storage - "Given storage map, returns a storage configured with the apropriate + "Given storage map, returns a storage configured with the appropriate backend for assets." [storage conn] (-> storage diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj index b1d0033e60..57e1ba531e 100644 --- a/backend/src/app/metrics.clj +++ b/backend/src/app/metrics.clj @@ -26,27 +26,57 @@ (declare instrument) (declare create-registry) (declare create) +(declare handler) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Defaults +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(def default-metrics + {:profile-register + {: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} + + :websocket-active-connections + {:name "websocket_active_connections" + :help "Active websocket connections gauge" + :type :gauge} + + :websocket-messages-total + {:name "websocket_message_total" + :help "Counter of processed messages." + :labels ["op"] + :type :counter} + + :websocket-session-timing + {:name "websocket_session_timing" + :help "Websocket session timing (seconds)." + :quantiles [] + :type :summary}}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Entry Point ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- handler - [registry _request] - (let [samples (.metricFamilySamples ^CollectorRegistry registry) - writer (StringWriter.)] - (TextFormat/write004 writer samples) - {:headers {"content-type" TextFormat/CONTENT_TYPE_004} - :body (.toString writer)})) - -(s/def ::definitions - (s/map-of keyword? map?)) - -(defmethod ig/pre-init-spec ::metrics [_] - (s/keys :opt-un [::definitions])) - (defmethod ig/init-key ::metrics - [_ {:keys [definitions] :as cfg}] + [_ _] (l/info :action "initialize metrics") (let [registry (create-registry) definitions (reduce-kv (fn [res k v] @@ -54,7 +84,7 @@ (create) (assoc res k))) {} - definitions)] + default-metrics)] {:handler (partial handler registry) :definitions definitions :registry registry})) @@ -64,6 +94,14 @@ (s/def ::metrics (s/keys :req-un [::registry ::handler])) +(defn- handler + [registry _request] + (let [samples (.metricFamilySamples ^CollectorRegistry registry) + writer (StringWriter.)] + (TextFormat/write004 writer samples) + {:headers {"content-type" TextFormat/CONTENT_TYPE_004} + :body (.toString writer)})) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index d1afda81ee..9ea2129f7b 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -202,6 +202,9 @@ {:name "0064-mod-audit-log-table" :fn (mg/resource "app/migrations/sql/0064-mod-audit-log-table.sql")} + + {:name "0065-add-trivial-spelling-fixes" + :fn (mg/resource "app/migrations/sql/0065-add-trivial-spelling-fixes.sql")} ]) diff --git a/backend/src/app/migrations/sql/0035-add-storage-tables.sql b/backend/src/app/migrations/sql/0035-add-storage-tables.sql index d1ec7f9d46..a5d64894f4 100644 --- a/backend/src/app/migrations/sql/0035-add-storage-tables.sql +++ b/backend/src/app/migrations/sql/0035-add-storage-tables.sql @@ -22,7 +22,7 @@ CREATE TABLE storage_data ( CREATE INDEX storage_data__id__idx ON storage_data(id); -- Table used for store inflight upload ids, for later recheck and --- delete possible staled files that exists on the phisical storage +-- delete possible staled files that exists on the physical storage -- but does not exists in the 'storage_object' table. CREATE TABLE storage_pending ( diff --git a/backend/src/app/migrations/sql/0062-fix-metadata-media.sql b/backend/src/app/migrations/sql/0062-fix-metadata-media.sql index 5b98236961..02aa86a2c0 100644 --- a/backend/src/app/migrations/sql/0062-fix-metadata-media.sql +++ b/backend/src/app/migrations/sql/0062-fix-metadata-media.sql @@ -1,4 +1,4 @@ --- Fix problem with content-type inconherence +-- Fix problem with content-type incoherence UPDATE storage_object so SET metadata = jsonb_set(metadata, '{~:content-type}', to_jsonb(fmo.mtype)) diff --git a/backend/src/app/migrations/sql/0065-add-trivial-spelling-fixes.sql b/backend/src/app/migrations/sql/0065-add-trivial-spelling-fixes.sql new file mode 100644 index 0000000000..aa746afab0 --- /dev/null +++ b/backend/src/app/migrations/sql/0065-add-trivial-spelling-fixes.sql @@ -0,0 +1,2 @@ +ALTER INDEX file__modified_at__has_media_trimed__idx RENAME TO file__modified_at__has_media_trimmed__idx; +ALTER INDEX media_bject__file_id__idx RENAME TO media_object__file_id__idx; diff --git a/backend/src/app/migrations/sql/XXXX-drop-obsolete-tables.sql b/backend/src/app/migrations/sql/XXXX-drop-obsolete-tables.sql index 50258305c7..0dcd2aaa6b 100644 --- a/backend/src/app/migrations/sql/XXXX-drop-obsolete-tables.sql +++ b/backend/src/app/migrations/sql/XXXX-drop-obsolete-tables.sql @@ -1,5 +1,5 @@ --- This is a second migration but it should be applied when manual ---- migration intervention is alteady executed. +--- migration intervention is already executed. ALTER TABLE file_media_object ALTER COLUMN media_id SET NOT NULL; DROP TABLE file_media_thumbnail; diff --git a/backend/src/app/msgbus.clj b/backend/src/app/msgbus.clj index dec1335167..285f185c74 100644 --- a/backend/src/app/msgbus.clj +++ b/backend/src/app/msgbus.clj @@ -243,7 +243,7 @@ (recur)) (a/close! rcv-ch))) - ;; Asyncrhonous message processing loop;x + ;; Asynchronous message processing loop;x (a/go-loop [] (if-let [{:keys [topic message]} (a/ #(handler cfg %) - (wrap-session) - (wrap-keyword-params) - (wrap-cookies) - (wrap-params)))) - -(s/def ::file-id ::us/uuid) -(s/def ::session-id ::us/uuid) - -(s/def ::websocket-handler-params - (s/keys :req-un [::file-id ::session-id])) - -(defn- handler - [{:keys [pool] :as cfg} {:keys [profile-id params] :as req}] - (let [params (us/conform ::websocket-handler-params params) - file (retrieve-file pool (:file-id params)) - cfg (merge cfg params - {:profile-id profile-id - :team-id (:team-id file)})] - (cond - (not profile-id) - {:error {:code 403 :message "Authentication required"}} - - (not file) - {:error {:code 404 :message "File does not exists"}} - - :else - (websocket cfg)))) - -(def ^:private - sql:retrieve-file - "select f.id as id, - p.team_id as team_id - from file as f - join project as p on (p.id = f.project_id) - where f.id = ?") - -(defn- retrieve-file - [conn id] - (db/exec-one! conn [sql:retrieve-file id])) - - -;; --- WEBSOCKET INIT - -(declare handle-connect) - -(defn- ws-send - [conn data] - (try - (when (jetty/connected? conn) - (jetty/send! conn data) - true) - (catch java.lang.NullPointerException _e - false))) - -(defn websocket - [{:keys [file-id team-id msgbus executor] :as cfg}] - (let [rcv-ch (a/chan 32) - out-ch (a/chan 32) - mtx-aconn (:mtx-active-connections cfg) - mtx-messages (:mtx-messages cfg) - mtx-sessions (:mtx-sessions cfg) - created-at (dt/now) - ws-send (mtx/wrap-counter ws-send mtx-messages ["send"])] - - (letfn [(on-connect [conn] - ((::mtx/fn mtx-aconn) {:cmd :inc :by 1}) - ;; A subscription channel should use a lossy buffer - ;; because we can't penalize normal clients when one - ;; slow client is connected to the room. - (let [sub-ch (a/chan (a/dropping-buffer 128)) - cfg (assoc cfg - :conn conn - :rcv-ch rcv-ch - :out-ch out-ch - :sub-ch sub-ch)] - - (l/trace :event "connect" :session (:session-id cfg)) - - ;; Forward all messages from out-ch to the websocket - ;; connection - (a/go-loop [] - (let [val (a/! out-ch val)) - (recur)) - - ;; When timeout channel is signaled, we need to send a ping - ;; message to the output channel. TODO: we need to make this - ;; more smart. - (= port timeout) - (do - (a/>! out-ch {:type :ping}) - (recur)))))) - -(defn send-presence - ([cfg] (send-presence cfg :presence)) - ([{:keys [msgbus session-id profile-id file-id]} type] - (a/go - (a/> rev :changes blob/decode (cp/process-changes data))) + (db/update! conn :file + {:deleted-at nil + :revn revn + :data (blob/encode data)} + {:id id}))) + + nil))) diff --git a/backend/src/app/rpc/mutations/management.clj b/backend/src/app/rpc/mutations/management.clj index 76d997feee..322fbdacd7 100644 --- a/backend/src/app/rpc/mutations/management.clj +++ b/backend/src/app/rpc/mutations/management.clj @@ -62,7 +62,7 @@ (= :image (:type form))) (update-in [:metadata :id] #(get index % %)))) - ;; A function responsible to analize all file data and + ;; A function responsible to analyze all file data and ;; replace the old :component-file reference with the new ;; ones, using the provided file-index (relink-shapes [data] @@ -294,7 +294,7 @@ ;; move all files to the project (db/exec-one! conn [sql:move-files project-id fids]) - ;; delete posible broken relations on moved files + ;; delete possible broken relations on moved files (db/exec-one! conn [sql:delete-broken-relations pids]) nil))) @@ -329,7 +329,7 @@ {:team-id team-id} {:id project-id}) - ;; delete posible broken relations on moved files + ;; delete possible broken relations on moved files (db/exec-one! conn [sql:delete-broken-relations pids]) nil))) diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index cd621763e3..ab1a5a405f 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -108,7 +108,7 @@ :code :email-domain-is-not-allowed))) ;; Don't allow proceed in preparing registration if the profile is - ;; already reported as spamer. + ;; already reported as spammer. (when (eml/has-bounce-reports? pool (:email params)) (ex/raise :type :validation :code :email-has-permanent-bounces @@ -177,7 +177,7 @@ ::audit/profile-id (:id profile)})) ;; If auth backend is different from "penpot" means user is - ;; registring using third party auth mechanism; in this case + ;; registering using third party auth mechanism; in this case ;; we need to mark this session as logged. (not= "penpot" (:auth-backend profile)) (with-meta (profile/strip-private-attrs profile) @@ -370,6 +370,7 @@ (declare validate-password!) (declare update-profile-password!) +(declare invalidate-profile-session!) (s/def ::update-profile-password (s/keys :req-un [::profile-id ::password ::old-password])) @@ -378,10 +379,18 @@ {::rlimit/permits (cf/get :rlimit-password)} [{:keys [pool] :as cfg} {:keys [password] :as params}] (db/with-atomic [conn pool] - (let [profile (validate-password! conn params)] + (let [profile (validate-password! conn params) + session-id (:app.rpc/session-id params)] (update-profile-password! conn (assoc profile :password password)) + (invalidate-profile-session! conn (:id profile) session-id) nil))) +(defn- invalidate-profile-session! + "Removes all sessions except the current one." + [conn profile-id session-id] + (let [sql "delete from http_session where profile_id = ? and id != ?"] + (:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id])))) + (defn- validate-password! [conn {:keys [profile-id old-password] :as params}] (let [profile (db/get-by-id conn :profile profile-id)] @@ -396,7 +405,6 @@ {:password (derive-password password)} {:id id})) - ;; --- MUTATION: Update Photo (declare update-profile-photo) @@ -438,7 +446,7 @@ ;; --- MUTATION: Request Email Change (declare request-email-change) -(declare change-email-inmediatelly) +(declare change-email-immediately) (s/def ::request-email-change (s/keys :req-un [::email])) @@ -454,9 +462,9 @@ (if (or (cf/get :smtp-enabled) (contains? cf/flags :smtp)) (request-email-change cfg params) - (change-email-inmediatelly cfg params))))) + (change-email-immediately cfg params))))) -(defn- change-email-inmediatelly +(defn- change-email-immediately [{:keys [conn]} {:keys [profile email] :as params}] (when (not= email (:email profile)) (check-profile-existence! conn params)) @@ -639,7 +647,7 @@ (let [rows (db/exec! conn [sql:owned-teams profile-id])] ;; If we found owned teams with more than one profile we don't ;; allow delete profile until the user properly transfer ownership - ;; or explictly removes all participants from the team. + ;; or explicitly removes all participants from the team. (when (some #(> (:num-profiles %) 1) rows) (ex/raise :type :validation :code :owner-teams-with-people diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index ef65882157..e6cc7288b3 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -164,7 +164,7 @@ (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 +;; should raise a specific exception for signal that this action is ;; not allowed. (sv/defmethod ::delete-team @@ -201,8 +201,8 @@ (let [perms (teams/get-permissions conn profile-id team-id) ;; We retrieve all team members instead of query the ;; database for a single member. This is just for - ;; convenience, if this bocomes a bottleneck or problematic, - ;; we will change it to more efficient fetch mechanims. + ;; convenience, if this becomes a bottleneck or problematic, + ;; we will change it to more efficient fetch mechanisms. members (teams/retrieve-team-members conn team-id) member (d/seek #(= member-id (:id %)) members) diff --git a/backend/src/app/rpc/mutations/verify_token.clj b/backend/src/app/rpc/mutations/verify_token.clj index 97458b5ca9..1065803165 100644 --- a/backend/src/app/rpc/mutations/verify_token.clj +++ b/backend/src/app/rpc/mutations/verify_token.clj @@ -134,7 +134,7 @@ ;; If the session does not matches the invited member, replace ;; the session with a new one matching the invited member. - ;; This techinique should be considered secure because the + ;; This technique should be considered secure because the ;; user clicking the link he already has access to the email ;; account. (with-meta @@ -179,7 +179,7 @@ ::audit/profile-id member-id})) ;; In this case, we wait until frontend app redirect user to - ;; registeration page, the user is correctly registered and the + ;; registration page, the user is correctly registered and the ;; register mutation call us again with the same token to finally ;; create the corresponding team-profile relation from the first ;; condition of this if. diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 7caff964b4..8e22f66371 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -6,6 +6,8 @@ (ns app.rpc.queries.files (:require + [app.common.data :as d] + [app.common.pages :as cp] [app.common.pages.migrations :as pmg] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -84,8 +86,8 @@ (let [perms (get-permissions conn profile-id file-id) ldata (retrieve-share-link conn file-id share-id)] - ;; NOTE: in a future when share-link becomes more powerfull and - ;; will allow us specify which parts of the app is availabel, we + ;; NOTE: in a future when share-link becomes more powerful and + ;; will allow us specify which parts of the app is available, we ;; will probably need to tweak this function in order to expose ;; this flags to the frontend. (cond @@ -165,6 +167,7 @@ f.created_at, f.modified_at, f.name, + f.revn, f.is_shared from file as f where f.project_id = ? @@ -214,11 +217,66 @@ (some-> (retrieve-file cfg id) (assoc :permissions perms))))) -(s/def ::page - (s/keys :req-un [::profile-id ::file-id])) +(declare trim-file-data) -(defn remove-thumbnails-frames - "Removes from data the children for frames that have a thumbnail set up" +(s/def ::page-id ::us/uuid) +(s/def ::object-id ::us/uuid) + +(s/def ::trimmed-file + (s/keys :req-un [::profile-id ::id ::object-id ::page-id])) + +(sv/defmethod ::trimmed-file + "Retrieve a file by its ID and trims all unnecesary content from + it. It is mainly used for rendering a concrete object, so we don't + need force download all shapes when only a small subset is + necesseary." + [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] + (db/with-atomic [conn pool] + (let [cfg (assoc cfg :conn conn) + perms (get-permissions conn profile-id id)] + (check-read-permissions! perms) + (some-> (retrieve-file cfg id) + (trim-file-data params) + (assoc :permissions perms))))) + +(defn- trim-file-data + [file {:keys [page-id object-id]}] + (let [page (get-in file [:data :pages-index page-id]) + objects (->> (:objects page) + (cp/get-object-with-children object-id) + (map #(dissoc % :thumbnail))) + + objects (d/index-by :id objects) + page (assoc page :objects objects)] + + (-> file + (update :data assoc :pages-index {page-id page}) + (update :data assoc :pages [page-id])))) + +(declare strip-frames-with-thumbnails) + +(s/def ::strip-frames-with-thumbnails ::us/boolean) + +(s/def ::page + (s/keys :req-un [::profile-id ::file-id] + :opt-un [::strip-frames-with-thumbnails])) + +(sv/defmethod ::page + "Retrieves the first page of the file. Used mainly for render + thumbnails on dashboard." + [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}] + (db/with-atomic [conn pool] + (check-read-permissions! conn profile-id file-id) + + (let [cfg (assoc cfg :conn conn) + file (retrieve-file cfg file-id) + page-id (get-in file [:data :pages 0])] + (cond-> (get-in file [:data :pages-index page-id]) + (true? (:strip-frames-with-thumbnails props)) + (strip-frames-with-thumbnails))))) + +(defn strip-frames-with-thumbnails + "Remove unnecesary shapes from frames that have thumbnail." [data] (let [filter-shape? (fn [objects [id shape]] @@ -227,7 +285,7 @@ (= frame-id uuid/zero) (not (some? (get-in objects [frame-id :thumbnail])))))) - ;; We need to remove from the attribute :shapes its childrens because + ;; We need to remove from the attribute :shapes its children because ;; they will not be sent in the data remove-frame-children (fn [[id shape]] @@ -244,22 +302,12 @@ (update data :objects update-objects))) -(sv/defmethod ::page - [{:keys [pool] :as cfg} {:keys [profile-id file-id strip-thumbnails]}] - (db/with-atomic [conn pool] - (check-read-permissions! conn profile-id file-id) - - (let [cfg (assoc cfg :conn conn) - file (retrieve-file cfg file-id) - page-id (get-in file [:data :pages 0])] - (cond-> (get-in file [:data :pages-index page-id]) - strip-thumbnails - (remove-thumbnails-frames))))) ;; --- Query: Shared Library Files (def ^:private sql:team-shared-files "select f.id, + f.revn, f.project_id, f.created_at, f.modified_at, @@ -330,6 +378,7 @@ (def sql:team-recent-files "with recent_files as ( select f.id, + f.revn, f.project_id, f.created_at, f.modified_at, diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj index a3ca758f5e..5c4c338767 100644 --- a/backend/src/app/rpc/queries/profile.clj +++ b/backend/src/app/rpc/queries/profile.clj @@ -37,7 +37,6 @@ (sv/defmethod ::profile {:auth false} [{:keys [pool] :as cfg} {:keys [profile-id] :as params}] - ;; We need to return the anonymous profile object in two cases, when ;; no profile-id is in session, and when db call raises not found. In all other ;; cases we need to reraise the exception. @@ -111,6 +110,6 @@ ;; --- Attrs Helpers (defn strip-private-attrs - "Only selects a publicy visible profile attrs." + "Only selects a publicly visible profile attrs." [row] (dissoc row :password :deleted-at)) diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index 9d35f9e071..b36675c3a3 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -65,7 +65,7 @@ (ex/raise :type :not-found :code :object-not-found)) - ;; When we have only profile, we need to check read permissiones + ;; When we have only profile, we need to check read permissions ;; on file. (when (and profile-id (not slink)) (files/check-read-permissions! conn profile-id file-id)) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 19274cfb22..08ea82a9b3 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -3,8 +3,11 @@ #_:clj-kondo/ignore (:require [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.pages :as cp] [app.common.pages.migrations :as pmg] + [app.common.pages.spec :as spec] [app.common.uuid :as uuid] [app.config :as cfg] [app.db :as db] @@ -13,8 +16,11 @@ [app.rpc.queries.profile :as prof] [app.srepl.dev :as dev] [app.util.blob :as blob] + [app.util.time :as dt] [clojure.pprint :refer [pprint]] - [cuerdas.core :as str])) + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [expound.alpha :as expound])) (defn update-file ([system id f] (update-file system id f false)) @@ -33,8 +39,8 @@ {:id (:id file)})) (update file :data blob/decode))))) -(defn update-file-raw - [id data] +(defn reset-file-data + [system id data] (db/with-atomic [conn (:app.db/pool system)] (db/update! conn :file {:data data} @@ -42,36 +48,13 @@ (defn get-file [system id] - (with-open [conn (db/open (:app.db/pool system))] - (let [file (db/get-by-id conn :file id)] - (-> file - (update :data app.util.blob/decode) - (update :data pmg/migrate-data))))) - - -;; Examples: -;; (def backup (update-file #uuid "1586e1f0-3e02-11eb-b1d2-556a2f641513" identity)) -;; (def x (update-file -;; #uuid "1586e1f0-3e02-11eb-b1d2-556a2f641513" -;; (fn [{:keys [data] :as file}] -;; (update-in data [:pages-index #uuid "878278c0-3ef0-11eb-9d67-8551e7624f43" :objects] dissoc nil)))) - -;; Migrate - -(defn update-file-data-blob-format - [system] - (db/with-atomic [conn (:app.db/pool system)] - (doseq [id (->> (db/exec! conn ["select id from file;"]) (map :id))] - (let [{:keys [data]} (db/get-by-id conn :file id {:columns [:id :data]})] - (prn "Updating file:" id) - (db/update! conn :file - {:data (-> (blob/decode data) - (blob/encode {:version 2}))} - {:id id}))))) - + (-> (:app.db/pool system) + (db/get-by-id :file id) + (update :data app.util.blob/decode) + (update :data pmg/migrate-data))) (defn duplicate-file - "This is a raw version of duplication of file just only for forensic analisys" + "This is a raw version of duplication of file just only for forensic analysis" [system file-id email] (db/with-atomic [conn (:app.db/pool system)] (when-let [profile (some->> (prof/retrieve-profile-data-by-email conn (str/lower email)) @@ -82,3 +65,87 @@ :project-id (:default-project-id profile))] (db/insert! conn :file params) (:id file)))))) + +(defn verify-files + [system {:keys [age sleep chunk-size max-chunks stop-on-error? verbose?] + :or {sleep 1000 + age "72h" + chunk-size 10 + verbose? false + stop-on-error? true + max-chunks ##Inf}}] + + (letfn [(retrieve-chunk [conn cursor] + (let [sql (str "select id, name, modified_at, data from file " + " where modified_at > ? and deleted_at is null " + " order by modified_at asc limit ?") + age (if cursor + cursor + (-> (dt/now) (dt/minus age)))] + (seq (db/exec! conn [sql age chunk-size])))) + + (validate-item [{:keys [id data modified-at] :as file}] + (let [data (blob/decode data) + valid? (s/valid? ::spec/data data)] + + (l/debug :hint "validated file" + :file-id id + :age (-> (dt/diff modified-at (dt/now)) + (dt/truncate :minutes) + (str) + (subs 2) + (str/lower)) + :valid valid?) + + (when (and (not valid?) verbose?) + (let [edata (-> (s/explain-data ::spec/data data) + (update ::s/problems #(take 5 %)))] + (binding [s/*explain-out* expound/printer] + (l/warn ::l/raw (with-out-str (s/explain-out edata)))))) + + (when (and (not valid?) stop-on-error?) + (throw (ex-info "penpot/abort" {}))) + + valid?)) + + (validate-chunk [chunk] + (loop [items chunk + success 0 + errored 0] + + (if-let [item (first items)] + (if (validate-item item) + (recur (rest items) (inc success) errored) + (recur (rest items) success (inc errored))) + [(:modified-at (last chunk)) + success + errored]))) + + (fmt-result [ns ne] + {:total (+ ns ne) + :errors ne + :success ns}) + + ] + + (try + (db/with-atomic [conn (:app.db/pool system)] + (loop [cursor nil + chunks 0 + success 0 + errors 0] + (if (< chunks max-chunks) + (if-let [chunk (retrieve-chunk conn cursor)] + (let [[cursor success' errors'] (validate-chunk chunk)] + (Thread/sleep (inst-ms (dt/duration sleep))) + (recur cursor + (inc chunks) + (+ success success') + (+ errors errors'))) + (fmt-result success errors)) + (fmt-result success errors)))) + (catch Throwable cause + (when (not= "penpot/abort" (ex-message cause)) + (throw cause)) + :error)))) + diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index a06c228e5a..947d69776d 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -188,7 +188,7 @@ object)) (defn clone-object - "Creates a clone of the provided object using backend basded efficient + "Creates a clone of the provided object using backend based efficient method. Always clones objects to the configured default." [{:keys [pool conn] :as storage} object] (us/assert ::storage storage) @@ -323,18 +323,18 @@ returning *;") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Garbage Collection: Analize touched objects +;; Garbage Collection: Analyze touched objects ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; This task is part of the garbage collection of storage objects and -;; is responsible on analizing the touched objects and mark them for deletion +;; is responsible on analyzing the touched objects and mark them for deletion ;; if corresponds. ;; ;; When file_media_object is deleted, the depending storage_object are ;; marked as touched. This means that some files that depend on a ;; concrete storage_object are no longer exists and maybe this -;; storage_object is no longer necessary and can be ellegible for -;; elimination. This task peridically analizes touched objects and +;; storage_object is no longer necessary and can be eligible for +;; elimination. This task periodically analyzes touched objects and ;; mark them as freeze (means that has other references and the object ;; is still valid) or deleted (no more references to this object so is ;; ready to be deleted). @@ -408,7 +408,7 @@ ;; For this situations we need to write a "log" of inserted files that ;; are checked in some time in future. If physical file exists but the ;; database refence does not exists means that leaked file is found -;; and is inmediatelly deleted. The responsability of this task is +;; and is immediately deleted. The responsibility of this task is ;; check that write log for possible leaked files. (def recheck-min-age (dt/duration {:hours 1})) diff --git a/backend/src/app/tasks/file_media_gc.clj b/backend/src/app/tasks/file_media_gc.clj index 2b8a133841..39d194f685 100644 --- a/backend/src/app/tasks/file_media_gc.clj +++ b/backend/src/app/tasks/file_media_gc.clj @@ -6,7 +6,7 @@ (ns app.tasks.file-media-gc "A maintenance task that is responsible to purge the unused media - objects from files. A file is ellegible to be garbage collected + objects from files. A file is eligible to be garbage collected after some period of inactivity (the default threshold is 72h)." (:require [app.common.logging :as l] @@ -107,7 +107,7 @@ :thumbnail-id (:thumbnail-id mobj)) ;; NOTE: deleting the file-media-object in the database - ;; automatically marks as toched the referenced storage + ;; automatically marks as touched the referenced storage ;; objects. The touch mechanism is needed because many files can ;; point to the same storage objects and we can't just delete ;; them. diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj index a3d06dcdd5..9d2e44146f 100644 --- a/backend/src/app/tasks/objects_gc.clj +++ b/backend/src/app/tasks/objects_gc.clj @@ -129,7 +129,7 @@ (doseq [{:keys [id] :as profile} profiles] (l/trace :action "delete object" :table table :id id) - ;; Mark the owned teams as deleted; this enables them to be procesed + ;; Mark the owned teams as deleted; this enables them to be processed ;; in the same transaction in the "team" table step. (db/exec-one! conn [sql:mark-owned-teams-deleted id max-age]) diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj index f9441be124..7f24fe7c0e 100644 --- a/backend/src/app/tasks/telemetry.clj +++ b/backend/src/app/tasks/telemetry.clj @@ -5,7 +5,7 @@ ;; Copyright (c) UXBOX Labs SL (ns app.tasks.telemetry - "A task that is reponsible to collect anonymous statistical + "A task that is responsible to collect anonymous statistical information about the current instance and send it to the telemetry server." (:require @@ -19,10 +19,8 @@ [clojure.spec.alpha :as s] [integrant.core :as ig])) -(declare handler) -(declare acquire-lock) -(declare release-all-locks) (declare retrieve-stats) +(declare send!) (s/def ::version ::us/string) (s/def ::uri ::us/string) @@ -34,49 +32,48 @@ (s/keys :req-un [::db/pool ::version ::uri ::sprops])) (defmethod ig/init-key ::handler - [_ {:keys [pool] :as cfg}] + [_ {:keys [pool sprops version] :as cfg}] (fn [_] - (db/with-atomic [conn pool] - (try - (acquire-lock conn) - (handler (assoc cfg :conn conn)) - (finally - (release-all-locks conn)))))) + (let [instance-id (:instance-id sprops)] + (-> (retrieve-stats pool version) + (assoc :instance-id instance-id) + (send! cfg))))) -(defn- acquire-lock - [conn] - (db/exec-one! conn ["select pg_advisory_lock(87562985867332);"])) - -(defn- release-all-locks - [conn] - (db/exec-one! conn ["select pg_advisory_unlock_all();"])) - -(defn- handler - [{:keys [sprops] :as cfg}] - (let [instance-id (:instance-id sprops) - data (retrieve-stats cfg) - data (assoc data :instance-id instance-id) - response (http/send! {:method :post - :uri (:uri cfg) - :headers {"content-type" "application/json"} - :body (json/write-str data)})] +(defn- send! + [data cfg] + (let [response (http/send! {:method :post + :uri (:uri cfg) + :headers {"content-type" "application/json"} + :body (json/write-str data)})] (when (> (:status response) 206) (ex/raise :type :internal :code :invalid-response - :context {:status (:status response) - :body (:body response)})))) + :response-status (:status response) + :response-body (:body response))))) -(defn retrieve-num-teams +(defn- retrieve-num-teams [conn] (-> (db/exec-one! conn ["select count(*) as count from team;"]) :count)) -(defn retrieve-num-projects +(defn- retrieve-num-projects [conn] (-> (db/exec-one! conn ["select count(*) as count from project;"]) :count)) -(defn retrieve-num-files +(defn- retrieve-num-files [conn] - (-> (db/exec-one! conn ["select count(*) as count from project;"]) :count)) + (-> (db/exec-one! conn ["select count(*) as count from file;"]) :count)) + +(defn- retrieve-num-users + [conn] + (-> (db/exec-one! conn ["select count(*) as count from profile;"]) :count)) + +(defn- retrieve-num-fonts + [conn] + (-> (db/exec-one! conn ["select count(*) as count from team_font_variant;"]) :count)) + +(defn- retrieve-num-comments + [conn] + (-> (db/exec-one! conn ["select count(*) as count from comment;"]) :count)) (def sql:team-averages "with projects_by_team as ( @@ -98,7 +95,6 @@ select t.id, count(tp.profile_id) as num_users from team as t left join team_profile_rel as tp on(tp.team_id = t.id) - where t.is_default = false group by 1 ) select (select avg(num_projects)::integer from projects_by_team) as avg_projects_on_team, @@ -110,20 +106,20 @@ (select avg(num_users)::integer from users_by_team) as avg_users_on_team, (select max(num_users)::integer from users_by_team) as max_users_on_team;") -(defn retrieve-team-averages +(defn- retrieve-team-averages [conn] (->> [sql:team-averages] (db/exec-one! conn))) -(defn retrieve-jvm-stats +(defn- retrieve-jvm-stats [] (let [^Runtime runtime (Runtime/getRuntime)] {:jvm-heap-current (.totalMemory runtime) :jvm-heap-max (.maxMemory runtime) :jvm-cpus (.availableProcessors runtime)})) -(defn- retrieve-stats - [{:keys [conn version]}] +(defn retrieve-stats + [conn version] (let [referer (if (cfg/get :telemetry-with-taiga) "taiga" (cfg/get :telemetry-referer))] @@ -131,7 +127,10 @@ :referer referer :total-teams (retrieve-num-teams conn) :total-projects (retrieve-num-projects conn) - :total-files (retrieve-num-files conn)} + :total-files (retrieve-num-files conn) + :total-users (retrieve-num-users conn) + :total-fonts (retrieve-num-fonts conn) + :total-comments (retrieve-num-comments conn)} (d/merge (retrieve-team-averages conn) (retrieve-jvm-stats)) diff --git a/backend/src/app/util/blob.clj b/backend/src/app/util/blob.clj index 42539b9342..f4daebc354 100644 --- a/backend/src/app/util/blob.clj +++ b/backend/src/app/util/blob.clj @@ -10,6 +10,7 @@ (:require [app.common.transit :as t] [app.config :as cf] + [app.util.fressian :as fres] [taoensso.nippy :as n]) (:import java.io.ByteArrayInputStream @@ -21,23 +22,28 @@ net.jpountz.lz4.LZ4FastDecompressor net.jpountz.lz4.LZ4Compressor)) +(set! *warn-on-reflection* true) + (def lz4-factory (LZ4Factory/fastestInstance)) (declare decode-v1) (declare decode-v2) (declare decode-v3) +(declare decode-v4) (declare encode-v1) (declare encode-v2) (declare encode-v3) +(declare encode-v4) (defn encode ([data] (encode data nil)) ([data {:keys [version]}] - (let [version (or version (cf/get :default-blob-version 1))] + (let [version (or version (cf/get :default-blob-version 3))] (case (long version) 1 (encode-v1 data) 2 (encode-v2 data) 3 (encode-v3 data) + 4 (encode-v4 data) (throw (ex-info "unsupported version" {:version version})))))) (defn decode @@ -51,6 +57,7 @@ 1 (decode-v1 data ulen) 2 (decode-v2 data ulen) 3 (decode-v3 data ulen) + 4 (decode-v4 data ulen) (throw (ex-info "unsupported version" {:version version})))))) ;; --- IMPL @@ -122,3 +129,26 @@ (Zstd/decompressByteArray ^bytes udata 0 ulen ^bytes cdata 6 (- (alength cdata) 6)) (t/decode udata {:type :json}))) + +(defn- encode-v4 + [data] + (let [data (fres/encode data) + dlen (alength ^bytes data) + mlen (Zstd/compressBound dlen) + cdata (byte-array mlen) + clen (Zstd/compressByteArray ^bytes cdata 0 mlen + ^bytes data 0 dlen + 0)] + (with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4)) + ^DataOutputStream dos (DataOutputStream. baos)] + (.writeShort dos (short 4)) ;; version number + (.writeInt dos (int dlen)) + (.write dos ^bytes cdata (int 0) clen) + (.toByteArray baos)))) + +(defn- decode-v4 + [^bytes cdata ^long ulen] + (let [udata (byte-array ulen)] + (Zstd/decompressByteArray ^bytes udata 0 ulen + ^bytes cdata 6 (- (alength cdata) 6)) + (fres/decode udata))) diff --git a/backend/src/app/util/emails.clj b/backend/src/app/util/emails.clj index 0ff6b3eb66..7136a288cb 100644 --- a/backend/src/app/util/emails.clj +++ b/backend/src/app/util/emails.clj @@ -206,7 +206,7 @@ :content html}]))})) (s/def ::priority #{:high :low}) -(s/def ::to (s/or :sigle ::us/email +(s/def ::to (s/or :single ::us/email :multi (s/coll-of ::us/email))) (s/def ::from ::us/email) (s/def ::reply-to ::us/email) diff --git a/backend/src/app/util/fressian.clj b/backend/src/app/util/fressian.clj new file mode 100644 index 0000000000..d81c92db28 --- /dev/null +++ b/backend/src/app/util/fressian.clj @@ -0,0 +1,281 @@ +;; 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.util.fressian + (:require + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [clojure.data.fressian :as fres]) + (:import + app.common.geom.matrix.Matrix + app.common.geom.point.Point + clojure.lang.Ratio + java.io.ByteArrayInputStream + java.io.ByteArrayOutputStream + java.time.Instant + java.time.OffsetDateTime + org.fressian.Reader + org.fressian.StreamingWriter + org.fressian.Writer + org.fressian.handlers.ReadHandler + org.fressian.handlers.WriteHandler)) + +;; --- MISC + +(set! *warn-on-reflection* true) + +(defn str->bytes + ([^String s] + (str->bytes s "UTF-8")) + ([^String s, ^String encoding] + (.getBytes s encoding))) + +(defn write-named + [tag ^Writer w s] + (.writeTag w tag 2) + (.writeObject w (namespace s) true) + (.writeObject w (name s) true)) + +(defn write-list-like + ([^Writer w tag o] + (.writeTag w tag 1) + (.writeList w o))) + +(defn read-list-like + [^Reader rdr build-fn] + (build-fn (.readObject rdr))) + +(defn write-map-like + "Writes a map as Fressian with the tag 'map' and all keys cached." + [^Writer w tag m] + (.writeTag w tag 1) + (.beginClosedList ^StreamingWriter w) + (loop [items (seq m)] + (when-let [^clojure.lang.MapEntry item (first items)] + (.writeObject w (.key item) true) + (.writeObject w (.val item)) + (recur (rest items)))) + (.endList ^StreamingWriter w)) + +(defn read-map-like + [^Reader rdr] + (let [kvs ^java.util.List (.readObject rdr)] + (if (< (.size kvs) 16) + (clojure.lang.PersistentArrayMap. (.toArray kvs)) + (clojure.lang.PersistentHashMap/create (seq kvs))))) + +(def write-handlers + { Character + {"char" + (reify WriteHandler + (write [_ w ch] + (.writeTag w "char" 1) + (.writeInt w (int ch))))} + + app.common.geom.point.Point + {"penpot/point" + (reify WriteHandler + (write [_ w o] + (.writeTag ^Writer w "penpot/point" 1) + (.writeList ^Writer w (java.util.List/of (.-x ^Point o) (.-y ^Point o)))))} + + app.common.geom.matrix.Matrix + {"penpot/matrix" + (reify WriteHandler + (write [_ w o] + (.writeTag ^Writer w "penpot/matrix" 1) + (.writeList ^Writer w (java.util.List/of (.-a ^Matrix o) + (.-b ^Matrix o) + (.-c ^Matrix o) + (.-d ^Matrix o) + (.-e ^Matrix o) + (.-f ^Matrix o)))))} + + Instant + {"java/instant" + (reify WriteHandler + (write [_ w ch] + (.writeTag w "java/instant" 1) + (.writeInt w (.toEpochMilli ^Instant ch))))} + + OffsetDateTime + {"java/instant" + (reify WriteHandler + (write [_ w ch] + (.writeTag w "java/instant" 1) + (.writeInt w (.toEpochMilli ^Instant (.toInstant ^OffsetDateTime ch)))))} + + Ratio + {"ratio" + (reify WriteHandler + (write [_ w n] + (.writeTag w "ratio" 2) + (.writeObject w (.numerator ^Ratio n)) + (.writeObject w (.denominator ^Ratio n))))} + + clojure.lang.IPersistentMap + {"clj/map" + (reify WriteHandler + (write [_ w d] + (write-map-like w "clj/map" d)))} + + clojure.lang.Keyword + {"clj/keyword" + (reify WriteHandler + (write [_ w s] + (write-named "clj/keyword" w s)))} + + clojure.lang.BigInt + {"bigint" + (reify WriteHandler + (write [_ w d] + (let [^BigInteger bi (if (instance? clojure.lang.BigInt d) + (.toBigInteger ^clojure.lang.BigInt d) + d)] + (.writeTag w "bigint" 1) + (.writeBytes w (.toByteArray bi)))))} + + ;; Persistent set + clojure.lang.IPersistentSet + {"clj/set" + (reify WriteHandler + (write [_ w o] + (write-list-like w "clj/set" o)))} + + ;; Persistent vector + clojure.lang.IPersistentVector + {"clj/vector" + (reify WriteHandler + (write [_ w o] + (write-list-like w "clj/vector" o)))} + + ;; Persistent list + clojure.lang.IPersistentList + {"clj/list" + (reify WriteHandler + (write [_ w o] + (write-list-like w "clj/list" o)))} + + ;; Persistent seq & lazy seqs + clojure.lang.ISeq + {"clj/seq" + (reify WriteHandler + (write [_ w o] + (write-list-like w "clj/seq" o)))} + }) + + +(def read-handlers + {"bigint" + (reify ReadHandler + (read [_ rdr _ _] + (let [^bytes bibytes (.readObject rdr)] + (bigint (BigInteger. bibytes))))) + + "byte" + (reify ReadHandler + (read [_ rdr _ _] + (byte (.readObject rdr)))) + + "penpot/matrix" + (reify ReadHandler + (read [_ rdr _ _] + (let [^java.util.List x (.readObject rdr)] + (Matrix. (.get x 0) (.get x 1) (.get x 2) (.get x 3) (.get x 4) (.get x 5))))) + + "penpot/point" + (reify ReadHandler + (read [_ rdr _ _] + (let [^java.util.List x (.readObject rdr)] + (Point. (.get x 0) (.get x 1))))) + + "char" + (reify ReadHandler + (read [_ rdr _ _] + (char (.readObject rdr)))) + + "java/instant" + (reify ReadHandler + (read [_ rdr _ _] + (Instant/ofEpochMilli (.readInt rdr)))) + + + "clj/ratio" + (reify ReadHandler + (read [_ rdr _ _] + (Ratio. (biginteger (.readObject rdr)) + (biginteger (.readObject rdr))))) + + "clj/keyword" + (reify ReadHandler + (read [_ rdr _ _] + (keyword (.readObject rdr) (.readObject rdr)))) + + "clj/map" + (reify ReadHandler + (read [_ rdr _ _] + (read-map-like rdr))) + + "clj/set" + (reify ReadHandler + (read [_ rdr _ _] + (read-list-like rdr set))) + + "clj/vector" + (reify ReadHandler + (read [_ rdr _ _] + (read-list-like rdr vec))) + + "clj/list" + (reify ReadHandler + (read [_ rdr _ _] + (read-list-like rdr #(apply list %)))) + + "clj/seq" + (reify ReadHandler + (read [_ rdr _ _] + (read-list-like rdr sequence))) + }) + +(def write-handler-lookup + (-> write-handlers + fres/associative-lookup + fres/inheritance-lookup)) + +(def read-handler-lookup + (-> read-handlers + (fres/associative-lookup))) + +;; --- Low-Level Api + +(defn reader + [istream] + (fres/create-reader istream :handlers read-handler-lookup)) + +(defn writer + [ostream] + (fres/create-writer ostream :handlers write-handler-lookup)) + +(defn read! + [reader] + (fres/read-object reader)) + +(defn write! + [writer data] + (fres/write-object writer data)) + +;; --- High-Level Api + +(defn encode + [data] + (with-open [out (ByteArrayOutputStream.)] + (write! (writer out) data) + (.toByteArray out))) + +(defn decode + [data] + (with-open [input (ByteArrayInputStream. ^bytes data)] + (read! (reader input)))) diff --git a/backend/src/app/util/migrations.clj b/backend/src/app/util/migrations.clj index 7b7fb8a44e..7f802730d3 100644 --- a/backend/src/app/util/migrations.clj +++ b/backend/src/app/util/migrations.clj @@ -20,7 +20,7 @@ ;; --- Implementation (defn- registered? - "Check if concrete migration is already registred." + "Check if concrete migration is already registered." [pool modname stepname] (let [sql "select * from migrations where module=? and step=?" rows (jdbc/execute! pool [sql modname stepname])] diff --git a/backend/src/app/util/time.clj b/backend/src/app/util/time.clj index 9d5d3d4d75..ed8f809e06 100644 --- a/backend/src/app/util/time.clj +++ b/backend/src/app/util/time.clj @@ -17,6 +17,8 @@ java.time.ZonedDateTime java.time.format.DateTimeFormatter java.time.temporal.TemporalAmount + java.time.temporal.TemporalUnit + java.time.temporal.ChronoUnit java.util.Date org.apache.logging.log4j.core.util.CronExpression)) @@ -54,15 +56,30 @@ :else (obj->duration ms-or-obj))) -(defn duration-between - {:deprecated true} - [t1 t2] - (Duration/between t1 t2)) - (defn diff [t1 t2] (Duration/between t1 t2)) +(defn truncate + [o unit] + (let [unit (if (instance? TemporalUnit unit) + unit + (case unit + :nanos ChronoUnit/NANOS + :millis ChronoUnit/MILLIS + :micros ChronoUnit/MICROS + :seconds ChronoUnit/SECONDS + :minutes ChronoUnit/MINUTES))] + (cond + (instance? Instant o) + (.truncatedTo ^Instant o ^TemporalUnit unit) + + (instance? Duration o) + (.truncatedTo ^Duration o ^TemporalUnit unit) + + :else + (throw (IllegalArgumentException. "only instant and duration allowed"))))) + (s/def ::duration (s/conformer (fn [v] diff --git a/backend/src/app/util/websocket.clj b/backend/src/app/util/websocket.clj new file mode 100644 index 0000000000..994070797e --- /dev/null +++ b/backend/src/app/util/websocket.clj @@ -0,0 +1,202 @@ +;; 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.util.websocket + "A general protocol implementation on top of websockets." + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.transit :as t] + [app.metrics :as mtx] + [app.util.time :as dt] + [clojure.core.async :as a] + [yetti.websocket :as yws]) + (:import + java.nio.ByteBuffer)) + +(declare decode-beat) +(declare encode-beat) +(declare process-heartbeat) +(declare process-input) +(declare process-output) +(declare ws-ping!) +(declare ws-send!) + +(defmacro call-mtx + [definitions name & args] + `(when-let [mtx-fn# (some-> ~definitions ~name ::mtx/fn)] + (mtx-fn# ~@args))) + +(def noop (constantly nil)) + +(defn handler + "A WebSocket upgrade handler factory. Returns a handler that can be + used to upgrade to websocket connection. This handler implements the + basic custom protocol on top of websocket connection with all the + borring stuff already handled (lifecycle, heartbeat,...). + + The provided function should have the `(fn [ws msg])` signature. + + It also accepts some options that allows you parametrize the + protocol behavior. The options map will be used as-as for the + initial data of the `ws` data structure" + ([handle-message] (handler handle-message {})) + ([handle-message {:keys [::input-buff-size + ::output-buff-size + ::idle-timeout + ::metrics] + :or {input-buff-size 64 + output-buff-size 64 + idle-timeout 30000} + :as options}] + (fn [_] + (let [input-ch (a/chan input-buff-size) + output-ch (a/chan output-buff-size) + pong-ch (a/chan (a/sliding-buffer 6)) + close-ch (a/chan) + options (-> options + (assoc ::input-ch input-ch) + (assoc ::output-ch output-ch) + (assoc ::close-ch close-ch) + (dissoc ::metrics)) + + terminated (atom false) + created-at (dt/now) + + on-terminate + (fn [& _args] + (when (compare-and-set! terminated false true) + (call-mtx metrics :connections {:cmd :dec :by 1}) + (call-mtx metrics :sessions {:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)}) + + (a/close! close-ch) + (a/close! pong-ch) + (a/close! output-ch) + (a/close! input-ch))) + + on-error + (fn [_ error] + (on-terminate) + (when-not (or (instance? org.eclipse.jetty.websocket.api.exceptions.WebSocketTimeoutException error) + (instance? java.nio.channels.ClosedChannelException error)) + (l/error :hint (ex-message error) :cause error))) + + on-connect + (fn [conn] + (call-mtx metrics :connections {:cmd :inc :by 1}) + + (let [wsp (atom (assoc options ::conn conn))] + ;; Handle heartbeat + (yws/idle-timeout! conn (dt/duration idle-timeout)) + (-> @wsp + (assoc ::pong-ch pong-ch) + (assoc ::on-close on-terminate) + (process-heartbeat)) + + ;; Forward all messages from output-ch to the websocket + ;; connection + (a/go-loop [] + (when-let [val (a/!! pong-ch buffer))] + + {:on-connect on-connect + :on-error on-error + :on-close on-terminate + :on-text on-message + :on-pong on-pong})))) + +(defn- ws-send! + [conn s] + (let [ch (a/chan 1)] + (yws/send! conn s (fn [e] + (when e (a/offer! ch e)) + (a/close! ch))) + ch)) + +(defn- ws-ping! + [conn s] + (let [ch (a/chan 1)] + (yws/ping! conn s (fn [e] + (when e (a/offer! ch e)) + (a/close! ch))) + ch)) + +(defn- encode-beat + [n] + (doto (ByteBuffer/allocate 8) + (.putLong n) + (.rewind))) + +(defn- decode-beat + [^ByteBuffer buffer] + (when (= 8 (.capacity buffer)) + (.rewind buffer) + (.getLong buffer))) + +(defn- process-input + [wsp handler] + (let [{:keys [::input-ch ::output-ch ::close-ch]} @wsp] + (a/go + (a/! output-ch {:type :error :error (ex-data val)}) + + (ex/exception? val) + (a/>! output-ch {:type :error :error {:message (ex-message val)}}) + + (map? val) + (a/>! output-ch (cond-> val (:request-id request) (assoc :request-id (:request-id request))))) + + (recur)))))) + (a/= (count issued) max-missed-heartbeats) + (on-close conn -1 "heartbeat-timeout") + (recur (inc i))))))) + + (a/go-loop [] + (when-let [buffer (a/ th/*system* :app.worker/registry :telemetry) + prof (th/create-profile* 1 {:is-active true})] + + ;; run the task + (task-fn nil) + + (t/is (:called? @mock)) + (let [[data] (-> @mock :call-args)] + (t/is (contains? data :total-fonts)) + (t/is (contains? data :total-users)) + (t/is (contains? data :total-projects)) + (t/is (contains? data :total-files)) + (t/is (contains? data :total-teams)) + (t/is (contains? data :total-comments)) + (t/is (contains? data :instance-id)) + (t/is (contains? data :jvm-cpus)) + (t/is (contains? data :jvm-heap-max)) + (t/is (contains? data :max-users-on-team)) + (t/is (contains? data :avg-users-on-team)) + (t/is (contains? data :max-files-on-project)) + (t/is (contains? data :avg-files-on-project)) + (t/is (contains? data :max-projects-on-team)) + (t/is (contains? data :avg-files-on-project)) + (t/is (contains? data :version)))))) diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj index f503e5d663..9161296d18 100644 --- a/backend/test/app/test_helpers.clj +++ b/backend/test/app/test_helpers.clj @@ -57,6 +57,7 @@ :app.http/server :app.http/router :app.notifications/handler + :app.loggers.sentry/reporter :app.http.oauth/google :app.http.oauth/gitlab :app.http.oauth/github diff --git a/common/deps.edn b/common/deps.edn index 172c11c5e1..10b2415878 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -1,38 +1,39 @@ {:deps {org.clojure/clojure {:mvn/version "1.10.3"} - org.clojure/data.json {:mvn/version "2.3.1"} + org.clojure/data.json {:mvn/version "2.4.0"} org.clojure/tools.cli {:mvn/version "1.0.206"} - metosin/jsonista {:mvn/version "0.3.3"} - org.clojure/clojurescript {:mvn/version "1.10.844"} + metosin/jsonista {:mvn/version "0.3.5"} + org.clojure/clojurescript {:mvn/version "1.10.914"} ;; Logging - org.clojure/tools.logging {:mvn/version "1.2.3"} - org.apache.logging.log4j/log4j-api {:mvn/version "2.17.0"} - org.apache.logging.log4j/log4j-core {:mvn/version "2.17.0"} - org.apache.logging.log4j/log4j-web {:mvn/version "2.17.0"} - org.apache.logging.log4j/log4j-jul {:mvn/version "2.17.0"} - org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.17.0"} + org.apache.logging.log4j/log4j-api {:mvn/version "2.17.1"} + org.apache.logging.log4j/log4j-core {:mvn/version "2.17.1"} + org.apache.logging.log4j/log4j-web {:mvn/version "2.17.1"} + org.apache.logging.log4j/log4j-jul {:mvn/version "2.17.1"} + org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.17.1"} org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"} - selmer/selmer {:mvn/version "1.12.40"} - expound/expound {:mvn/version "0.8.9"} + selmer/selmer {:mvn/version "1.12.49"} + criterium/criterium {:mvn/version "0.4.6"} + + expound/expound {:mvn/version "0.9.0"} com.cognitect/transit-clj {:mvn/version "1.0.324"} com.cognitect/transit-cljs {:mvn/version "0.8.269"} - java-http-clj/java-http-clj {:mvn/version "0.4.2"} + java-http-clj/java-http-clj {:mvn/version "0.4.3"} - funcool/promesa {:mvn/version "6.0.1"} - funcool/cuerdas {:mvn/version "2021.05.29-0"} + funcool/promesa {:mvn/version "6.0.2"} + funcool/cuerdas {:mvn/version "2022.01.14-391"} - lambdaisland/uri {:mvn/version "1.4.70" + lambdaisland/uri {:mvn/version "1.12.89" :exclusions [org.clojure/data.json]} frankiesardo/linked {:mvn/version "1.3.0"} danlentz/clj-uuid {:mvn/version "0.1.9"} - commons-io/commons-io {:mvn/version "2.8.0"} + commons-io/commons-io {:mvn/version "2.11.0"} com.sun.mail/jakarta.mail {:mvn/version "2.0.1"} ;; exception printing - fipp/fipp {:mvn/version "0.6.24"} + fipp/fipp {:mvn/version "0.6.25"} io.aviso/pretty {:mvn/version "1.1.1"} environ/environ {:mvn/version "1.2.0"}} :paths ["src"] @@ -42,25 +43,17 @@ {org.clojure/tools.namespace {:mvn/version "RELEASE"} org.clojure/test.check {:mvn/version "RELEASE"} org.clojure/tools.deps.alpha {:mvn/version "RELEASE"} - thheller/shadow-cljs {:mvn/version "2.12.6"} + com.bhauman/rebel-readline {:mvn/version "RELEASE"} + thheller/shadow-cljs {:mvn/version "2.16.12"} criterium/criterium {:mvn/version "RELEASE"} mockery/mockery {:mvn/version "RELEASE"}} :extra-paths ["test" "dev"]} - :repl - {:extra-deps - {com.bhauman/rebel-readline {:mvn/version "RELEASE"}} - :main-opts ["-m" "rebel-readline.main"]} - - :kaocha - {:extra-deps {lambdaisland/kaocha {:mvn/version "RELEASE"}} - :main-opts ["-m" "kaocha.runner"]} - :test {:extra-paths ["test"] - :extra-deps {io.github.cognitect-labs/test-runner - {:git/url "https://github.com/cognitect-labs/test-runner.git" - :sha "705ad25bbf0228b1c38d0244a36001c2987d7337"}} + :extra-deps + {io.github.cognitect-labs/test-runner + {:git/tag "v0.5.0" :git/sha "b3fd0d2"}} :exec-fn cognitect.test-runner.api/test} :shadow-cljs diff --git a/common/package.json b/common/package.json index 4344351821..b989def6c1 100644 --- a/common/package.json +++ b/common/package.json @@ -6,7 +6,14 @@ "dependencies": { "luxon": "^1.27.0" }, + "scripts": { + "compile-and-watch-test": "clojure -M:dev:shadow-cljs watch test", + "compile-test": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", + "run-test": "node target/test.js", + "test": "yarn run compile-test && yarn run run-test" + }, "devDependencies": { + "shadow-cljs": "2.16.12", "source-map-support": "^0.5.19", "ws": "^7.4.6" } diff --git a/common/scripts/repl b/common/scripts/repl new file mode 100755 index 0000000000..4570f636f4 --- /dev/null +++ b/common/scripts/repl @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS" +export OPTIONS="-A:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m"; +export OPTIONS_EVAL="nil" +# export OPTIONS_EVAL="(set! *warn-on-reflection* true)" + +set -ex +exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main diff --git a/common/shadow-cljs.edn b/common/shadow-cljs.edn index e6bcd7175c..f6ff2bbf27 100644 --- a/common/shadow-cljs.edn +++ b/common/shadow-cljs.edn @@ -6,12 +6,29 @@ :builds {:test {:target :node-test - :output-to "target/tests.js" + :output-to "target/test.js" + :output-dir "target/test/" :ns-regexp "^app.common.*-test$" - ;; :autorun true + :autorun true :compiler-options {:output-feature-set :es-next :output-wrapper false - :warnings {:fn-deprecated false}}}}} + :source-map true + :source-map-include-sources-content true + :source-map-detail-level :all + :warnings {:fn-deprecated false}}} + + :bench + {:target :node-script + :output-to "target/bench.js" + :output-dir "target/bench/" + :main bench/main + :devtools {:autoload false} + + :compiler-options + {:output-feature-set :es-next + :output-wrapper false}}} + } + diff --git a/common/src/app/common/attrs.cljc b/common/src/app/common/attrs.cljc index ce6af91313..febb784461 100644 --- a/common/src/app/common/attrs.cljc +++ b/common/src/app/common/attrs.cljc @@ -9,7 +9,7 @@ ;; Extract some attributes of a list of shapes. ;; For each attribute, if the value is the same in all shapes, -;; wll take this value. If there is any shape that is different, +;; will take this value. If there is any shape that is different, ;; the value of the attribute will be the keyword :multiple. ;; ;; If some shape has the value nil in any attribute, it's diff --git a/common/src/app/common/colors.cljc b/common/src/app/common/colors.cljc new file mode 100644 index 0000000000..f8acee0d3f --- /dev/null +++ b/common/src/app/common/colors.cljc @@ -0,0 +1,18 @@ +;; 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.colors + (:refer-clojure :exclude [test])) + +(def black "#000000") +(def canvas "#E8E9EA") +(def default-layout "#DE4762") +(def gray-20 "#B1B2B5") +(def gray-30 "#7B7D85") +(def info "#59B9E2") +(def test "#fabada") +(def white "#FFFFFF") + diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index a23dfe101d..ad167a3211 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -157,7 +157,7 @@ "Return a map without the keys provided in the `keys` parameter." [data keys] - (when data + (when (map? data) (persistent! (reduce #(dissoc! %1 %2) (transient data) keys)))) @@ -201,7 +201,7 @@ "Maps a function to each pair of values that can be combined inside the function without repetition. - Optional parmeters: + Optional parameters: `pred?` A predicate that if not satisfied won't process the pair `target?` A collection that will be used as seed to be stored @@ -497,7 +497,7 @@ (keyword (str prefix kw)))) (defn tap - "Simpilar to the tap in rxjs but for plain collections" + "Similar to the tap in rxjs but for plain collections" [f coll] (f coll) coll) diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index 3371dbbd94..dbc63a8ba7 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -12,26 +12,23 @@ (s/def ::type keyword?) (s/def ::code keyword?) -(s/def ::mesage string?) (s/def ::hint string?) (s/def ::cause #?(:clj #(instance? Throwable %) :cljs #(instance? js/Error %))) + (s/def ::error-params (s/keys :req-un [::type] :opt-un [::code ::hint - ::mesage ::cause])) (defn error - [& {:keys [message hint cause] :as params}] + [& {:keys [hint cause ::data] :as params}] (s/assert ::error-params params) - (let [message (or message hint "") - payload (-> params - (dissoc :cause) - (dissoc :message) - (assoc :hint message))] - (ex-info message payload cause))) + (let [payload (-> params + (dissoc :cause ::data) + (merge data))] + (ex-info hint payload cause))) (defmacro raise [& args] diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 4cf883a975..0d6c96ff4a 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -12,7 +12,6 @@ [app.common.geom.shapes :as gsh] [app.common.pages.changes :as ch] [app.common.pages.init :as init] - [app.common.pages.spec :as spec] [app.common.spec :as us] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -21,15 +20,14 @@ (def conjv (fnil conj [])) (def conjs (fnil conj #{})) -;; This flag controls if we should execute spec validation after every commit -(def verify-on-commit? true) - (defn- commit-change ([file change] (commit-change file change nil)) - ([file change {:keys [add-container?] - :or {add-container? false}}] + ([file change {:keys [add-container? + fail-on-spec?] + :or {add-container? false + fail-on-spec? false}}] (let [component-id (:current-component-id file) change (cond-> change (and add-container? (some? component-id)) @@ -39,11 +37,20 @@ (assoc :page-id (:current-page-id file) :frame-id (:current-frame-id file)))] - (when verify-on-commit? - (us/assert ::spec/change change)) - (-> file - (update :changes conjv change) - (update :data ch/process-changes [change] verify-on-commit?))))) + (when fail-on-spec? + (us/verify :app.common.pages.spec/change change)) + + (let [valid? (us/valid? :app.common.pages.spec/change change)] + #?(:cljs + (when-not valid? (.warn js/console "Invalid shape" (clj->js change)))) + + (cond-> file + valid? + (-> (update :changes conjv change) + (update :data ch/process-changes [change] false)) + + (not valid?) + (update :errors conjv change)))))) (defn- lookup-objects ([file] @@ -51,20 +58,21 @@ (get-in file [:data :components (:current-component-id file) :objects]) (get-in file [:data :pages-index (:current-page-id file) :objects])))) -(defn- lookup-shape [file shape-id] +(defn lookup-shape [file shape-id] (-> (lookup-objects file) (get shape-id))) (defn- commit-shape [file obj] - (let [parent-id (-> file :parent-stack peek)] - (-> file - (commit-change - {:type :add-obj - :id (:id obj) - :obj obj - :parent-id parent-id} + (let [parent-id (-> file :parent-stack peek) + change {:type :add-obj + :id (:id obj) + :obj obj + :parent-id parent-id} - {:add-container? true})))) + fail-on-spec? (or (= :group (:type obj)) + (= :frame (:type obj)))] + + (commit-change file change {:add-container? true :fail-on-spec? fail-on-spec?}))) (defn setup-rect-selrect [obj] (let [rect (select-keys obj [:x :y :width :height]) @@ -321,16 +329,11 @@ (update :parent-stack pop)))) (defn create-shape [file type data] - (let [frame-id (:current-frame-id file) - frame (when-not (= frame-id root-frame) - (lookup-shape file frame-id)) - obj (-> (init/make-minimal-shape type) + (let [obj (-> (init/make-minimal-shape type) (merge data) (check-name file :type) (setup-selrect) - (d/without-nils)) - obj (cond-> obj - frame (gsh/translate-from-frame frame))] + (d/without-nils))] (-> file (commit-shape obj) (assoc :last-id (:id obj)) @@ -426,35 +429,36 @@ (defn add-interaction [file from-id interaction-src] - (assert (some? (lookup-shape file from-id)) (str "Cannot locate shape with id " from-id)) + (let [shape (lookup-shape file from-id)] + (if (nil? shape) + file + (let [{:keys [event-type action-type]} (read-classifier interaction-src) + {:keys [delay]} (read-event-opts interaction-src) + {:keys [destination overlay-pos-type overlay-position url + close-click-outside background-overlay preserve-scroll]} + (read-action-opts interaction-src) - (let [{:keys [event-type action-type]} (read-classifier interaction-src) - {:keys [delay]} (read-event-opts interaction-src) - {:keys [destination overlay-pos-type overlay-position url - close-click-outside background-overlay preserve-scroll]} - (read-action-opts interaction-src) + interactions (-> shape + :interactions + (conjv + (d/without-nils {:event-type event-type + :action-type action-type + :delay delay + :destination destination + :overlay-pos-type overlay-pos-type + :overlay-position overlay-position + :url url + :close-click-outside close-click-outside + :background-overlay background-overlay + :preserve-scroll preserve-scroll})))] + (commit-change + file + {:type :mod-obj + :page-id (:current-page-id file) + :id from-id - interactions (-> (lookup-shape file from-id) - :interactions - (conjv - (d/without-nils {:event-type event-type - :action-type action-type - :delay delay - :destination destination - :overlay-pos-type overlay-pos-type - :overlay-position overlay-position - :url url - :close-click-outside close-click-outside - :background-overlay background-overlay - :preserve-scroll preserve-scroll})))] - (commit-change - file - {:type :mod-obj - :page-id (:current-page-id file) - :id from-id - - :operations - [{:type :set :attr :interactions :val interactions}]}))) + :operations + [{:type :set :attr :interactions :val interactions}]}))))) (defn generate-changes [file] diff --git a/common/src/app/common/geom/align.cljc b/common/src/app/common/geom/align.cljc index ad0a35adc3..11c8422c6c 100644 --- a/common/src/app/common/geom/align.cljc +++ b/common/src/app/common/geom/align.cljc @@ -6,7 +6,9 @@ (ns app.common.geom.align (:require + [app.common.data :as d] [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :refer [get-children]] [clojure.spec.alpha :as s])) ;; --- Alignment @@ -15,23 +17,13 @@ (declare calc-align-pos) -;; TODO: revisit on how to reuse code and dont have this function -;; duplicated because the implementation right now differs from the -;; original function. - -;; Duplicated from pages/helpers to remove cyclic dependencies -(defn- get-children [id objects] - (let [shapes (vec (get-in objects [id :shapes]))] - (if shapes - (into shapes (mapcat #(get-children % objects)) shapes) - []))) - (defn- recursive-move "Move the shape and all its recursive children." [shape dpoint objects] - (let [children-ids (get-children (:id shape) objects) - children (map #(get objects %) children-ids)] - (map #(gsh/move % dpoint) (cons shape children)))) + (->> (get-children (:id shape) objects) + (map (d/getf objects)) + (cons shape) + (map #(gsh/move % dpoint)))) (defn align-to-rect "Move the shape so that it is aligned with the given rectangle @@ -81,7 +73,7 @@ "Distribute equally the space between shapes in the given axis. If there is no space enough, it does nothing. It takes into account the form of the shape and the rotation, what is distributed is - the wrapping recangles of the shapes. If any shape is a group, + the wrapping rectangles of the shapes. If any shape is a group, move also all of its recursive children." [shapes axis objects] (let [coord (if (= axis :horizontal) :x :y) @@ -119,7 +111,7 @@ (mapcat #(recursive-move %1 {coord %2 other-coord 0} objects) sorted-shapes deltas))))) -;; Adjusto to viewport +;; Adjust to viewport (defn adjust-to-viewport ([viewport srect] (adjust-to-viewport viewport srect nil)) diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index c04c05bb3b..b03cba0d4e 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -14,7 +14,12 @@ ;; --- Matrix Impl -(defrecord Matrix [a b c d e f] +(defrecord Matrix [^double a + ^double b + ^double c + ^double d + ^double e + ^double f] Object (toString [_] (str "matrix(" a "," b "," c "," d "," e "," f ")"))) @@ -36,15 +41,29 @@ (apply matrix params))) (defn multiply - ([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f} - {m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}] - (Matrix. - (+ (* m1a m2a) (* m1c m2b)) - (+ (* m1b m2a) (* m1d m2b)) - (+ (* m1a m2c) (* m1c m2d)) - (+ (* m1b m2c) (* m1d m2d)) - (+ (* m1a m2e) (* m1c m2f) m1e) - (+ (* m1b m2e) (* m1d m2f) m1f))) + ([^Matrix m1 ^Matrix m2] + (let [m1a (.-a m1) + m1b (.-b m1) + m1c (.-c m1) + m1d (.-d m1) + m1e (.-e m1) + m1f (.-f m1) + + m2a (.-a m2) + m2b (.-b m2) + m2c (.-c m2) + m2d (.-d m2) + m2e (.-e m2) + m2f (.-f m2)] + + (Matrix. + (+ (* m1a m2a) (* m1c m2b)) + (+ (* m1b m2a) (* m1d m2b)) + (+ (* m1a m2c) (* m1c m2d)) + (+ (* m1b m2c) (* m1d m2d)) + (+ (* m1a m2e) (* m1c m2f) m1e) + (+ (* m1b m2e) (* m1d m2f) m1f)))) + ([m1 m2 & others] (reduce multiply (multiply m1 m2) others))) @@ -53,13 +72,8 @@ values), combine them. Quicker than multiplying them, for this precise case." ([{m1e :e m1f :f} {m2e :e m2f :f}] - (Matrix. - 1 - 0 - 0 - 1 - (+ m1e m2e) - (+ m1f m2f))) + (Matrix. 1 0 0 1 (+ m1e m2e) (+ m1f m2f))) + ([m1 m2 & others] (reduce add-translate (add-translate m1 m2) others))) diff --git a/common/src/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc index b73a050e61..21fecc68b3 100644 --- a/common/src/app/common/geom/point.cljc +++ b/common/src/app/common/geom/point.cljc @@ -187,14 +187,14 @@ (defn round "Change the precision of the point coordinates." ([point] (round point 0)) - ([{:keys [x y] :as p} decimanls] + ([{:keys [x y] :as p} decimals] (assert (point? p)) - (assert (number? decimanls)) - (Point. (mth/precision x decimanls) - (mth/precision y decimanls)))) + (assert (number? decimals)) + (Point. (mth/precision x decimals) + (mth/precision y decimals)))) (defn transform - "Transform a point applying a matrix transfomation." + "Transform a point applying a matrix transformation." [{:keys [x y] :as p} {:keys [a b c d e f]}] (assert (point? p)) (Point. (+ (* x a) (* y c) e) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 91a7043ba2..6df3f748ea 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -10,6 +10,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes.bool :as gsb] [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.constraints :as gct] [app.common.geom.shapes.intersect :as gin] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.rect :as gpr] @@ -56,8 +57,7 @@ rotation of each shape. Mainly used for multiple selection." [shapes] (->> shapes - (gtr/transform-shape) - (map (comp gpr/points->selrect :points)) + (map (comp gpr/points->selrect :points gtr/transform-shape)) (gpr/join-selrects))) (defn translate-to-frame @@ -150,6 +150,7 @@ (d/export gpr/points->rect) (d/export gpr/center->rect) (d/export gpr/join-rects) +(d/export gpr/contains-selrect?) (d/export gtr/move) (d/export gtr/absolute-move) @@ -163,7 +164,12 @@ (d/export gtr/rotation-modifiers) (d/export gtr/merge-modifiers) (d/export gtr/transform-shape) -(d/export gtr/calc-child-modifiers) +(d/export gtr/transform-selrect) +(d/export gtr/modifiers->transform) +(d/export gtr/empty-modifiers?) + +;; Constratins +(d/export gct/calc-child-modifiers) ;; PATHS (d/export gsp/content->selrect) @@ -178,3 +184,4 @@ ;; Bool (d/export gsb/update-bool-selrect) +(d/export gsb/calc-bool-content) diff --git a/common/src/app/common/geom/shapes/bool.cljc b/common/src/app/common/geom/shapes/bool.cljc index 93b7ccc72c..4d5bdb4010 100644 --- a/common/src/app/common/geom/shapes/bool.cljc +++ b/common/src/app/common/geom/shapes/bool.cljc @@ -6,21 +6,31 @@ (ns app.common.geom.shapes.bool (:require + [app.common.data :as d] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.rect :as gpr] [app.common.geom.shapes.transforms :as gtr] [app.common.path.bool :as pb] [app.common.path.shapes-to-path :as stp])) +(defn calc-bool-content + [shape objects] + + (let [extract-content-xf + (comp (map (d/getf objects)) + (filter (comp not :hidden)) + (map #(stp/convert-to-path % objects)) + (map :content)) + + shapes-content + (into [] extract-content-xf (:shapes shape))] + (pb/content-bool (:bool-type shape) shapes-content))) + (defn update-bool-selrect "Calculates the selrect+points for the boolean shape" [shape children objects] - (let [content (->> children - (map #(stp/convert-to-path % objects)) - (mapv :content) - (pb/content-bool (:bool-type shape))) - + (let [content (calc-bool-content shape objects) [points selrect] (if (empty? content) (let [selrect (gtr/selection-rect children) @@ -29,4 +39,6 @@ (gsp/content->points+selrect shape content))] (-> shape (assoc :selrect selrect) - (assoc :points points)))) + (assoc :points points) + (assoc :bool-content content)))) + diff --git a/common/src/app/common/geom/shapes/common.cljc b/common/src/app/common/geom/shapes/common.cljc index 00eec4386a..aa0a655ec3 100644 --- a/common/src/app/common/geom/shapes/common.cljc +++ b/common/src/app/common/geom/shapes/common.cljc @@ -50,6 +50,22 @@ :width width :height height}) +(defn make-centered-selrect + "Creates a rect given a center and a width and height" + [center width height] + (let [x1 (- (:x center) (/ width 2.0)) + y1 (- (:y center) (/ height 2.0)) + x2 (+ x1 width) + y2 (+ y1 height)] + {:x x1 + :y y1 + :x1 x1 + :x2 x2 + :y1 y1 + :y2 y2 + :width width + :height height})) + (defn transform-points ([points matrix] (transform-points points nil matrix)) diff --git a/common/src/app/common/geom/shapes/constraints.cljc b/common/src/app/common/geom/shapes/constraints.cljc new file mode 100644 index 0000000000..4ead68b4e0 --- /dev/null +++ b/common/src/app/common/geom/shapes/constraints.cljc @@ -0,0 +1,182 @@ +;; 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.geom.shapes.constraints + (:require + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.transforms :as gtr] + [app.common.math :as mth] + [app.common.pages.spec :as spec])) + +;; Auxiliary methods to work in an specifica axis +(defn get-delta-start [axis rect tr-rect] + (if (= :x axis) + (- (:x1 tr-rect) (:x1 rect)) + (- (:y1 tr-rect) (:y1 rect)))) + +(defn get-delta-end [axis rect tr-rect] + (if (= :x axis) + (- (:x2 tr-rect) (:x2 rect)) + (- (:y2 tr-rect) (:y2 rect)))) + +(defn get-delta-size [axis rect tr-rect] + (if (= :x axis) + (- (:width tr-rect) (:width rect)) + (- (:height tr-rect) (:height rect)))) + +(defn get-delta-center [axis center tr-center] + (if (= :x axis) + (- (:x tr-center) (:x center)) + (- (:y tr-center) (:y center)))) + +(defn get-displacement + ([axis delta] + (get-displacement axis delta 0 0)) + + ([axis delta init-x init-y] + (if (= :x axis) + (gpt/point (+ init-x delta) init-y) + (gpt/point init-x (+ init-y delta))))) + +(defn get-scale [axis scale] + (if (= :x axis) + (gpt/point scale 1) + (gpt/point 1 scale))) + +(defn get-size [axis rect] + (if (= :x axis) + (:width rect) + (:height rect))) + +;; Constraint function definitions + +(defmulti constraint-modifier (fn [type & _] type)) + +(defmethod constraint-modifier :start + [_ axis parent _ _ transformed-parent-rect] + + (let [parent-rect (:selrect parent) + delta-start (get-delta-start axis parent-rect transformed-parent-rect)] + (if-not (mth/almost-zero? delta-start) + {:displacement (get-displacement axis delta-start)} + {}))) + +(defmethod constraint-modifier :end + [_ axis parent _ _ transformed-parent-rect] + (let [parent-rect (:selrect parent) + delta-end (get-delta-end axis parent-rect transformed-parent-rect)] + (if-not (mth/almost-zero? delta-end) + {:displacement (get-displacement axis delta-end)} + {}))) + +(defmethod constraint-modifier :fixed + [_ axis parent child _ transformed-parent-rect] + (let [parent-rect (:selrect parent) + child-rect (:selrect child) + + delta-start (get-delta-start axis parent-rect transformed-parent-rect) + delta-size (get-delta-size axis parent-rect transformed-parent-rect) + child-size (get-size axis child-rect) + child-center (gco/center-rect child-rect)] + (if (or (not (mth/almost-zero? delta-start)) + (not (mth/almost-zero? delta-size))) + + {:displacement (get-displacement axis delta-start) + :resize-origin (-> (get-displacement axis delta-start (:x1 child-rect) (:y1 child-rect)) + (gtr/transform-point-center child-center (:transform child (gmt/matrix)))) + :resize-vector (get-scale axis (/ (+ child-size delta-size) child-size))} + {}))) + +(defmethod constraint-modifier :center + [_ axis parent _ _ transformed-parent-rect] + (let [parent-rect (:selrect parent) + parent-center (gco/center-rect parent-rect) + transformed-parent-center (gco/center-rect transformed-parent-rect) + delta-center (get-delta-center axis parent-center transformed-parent-center)] + (if-not (mth/almost-zero? delta-center) + {:displacement (get-displacement axis delta-center)} + {}))) + +(defmethod constraint-modifier :scale + [_ axis _ _ modifiers _] + (let [{:keys [resize-vector resize-vector-2 displacement]} modifiers] + (cond-> {} + (and (some? resize-vector) + (not (mth/close? (axis resize-vector) 1))) + (assoc :resize-origin (:resize-origin modifiers) + :resize-vector (if (= :x axis) + (gpt/point (:x resize-vector) 1) + (gpt/point 1 (:y resize-vector)))) + + (and (= :y axis) (some? resize-vector-2) + (not (mth/close? (:y resize-vector-2) 1))) + (assoc :resize-origin (:resize-origin-2 modifiers) + :resize-vector (gpt/point 1 (:y resize-vector-2))) + + (some? displacement) + (assoc :displacement + (get-displacement axis (-> (gpt/point 0 0) + (gpt/transform displacement) + (gpt/transform (:resize-transform-inverse modifiers (gmt/matrix))) + axis)))))) + +(defmethod constraint-modifier :default [_ _ _ _ _] + {}) + +(def const->type+axis + {:left :start + :top :start + :right :end + :bottom :end + :leftright :fixed + :topbottom :fixed + :center :center + :scale :scale}) + +(defn calc-child-modifiers + [parent child modifiers ignore-constraints transformed-parent-rect] + (let [constraints-h + (if-not ignore-constraints + (:constraints-h child (spec/default-constraints-h child)) + :scale) + + constraints-v + (if-not ignore-constraints + (:constraints-v child (spec/default-constraints-v child)) + :scale) + + modifiers-h (constraint-modifier (constraints-h const->type+axis) :x parent child modifiers transformed-parent-rect) + modifiers-v (constraint-modifier (constraints-v const->type+axis) :y parent child modifiers transformed-parent-rect)] + + ;; Build final child modifiers. Apply transform again to the result, to get the + ;; real modifiers that need to be applied to the child, including rotation as needed. + (cond-> {} + (or (contains? modifiers-h :displacement) + (contains? modifiers-v :displacement)) + (assoc :displacement (cond-> (gpt/point (get-in modifiers-h [:displacement :x] 0) + (get-in modifiers-v [:displacement :y] 0)) + (some? (:resize-transform modifiers)) + (gpt/transform (:resize-transform modifiers)) + + :always + (gmt/translate-matrix))) + + (:resize-vector modifiers-h) + (assoc :resize-origin (:resize-origin modifiers-h) + :resize-vector (gpt/point (get-in modifiers-h [:resize-vector :x] 1) + (get-in modifiers-h [:resize-vector :y] 1))) + + (:resize-vector modifiers-v) + (assoc :resize-origin-2 (:resize-origin modifiers-v) + :resize-vector-2 (gpt/point (get-in modifiers-v [:resize-vector :x] 1) + (get-in modifiers-v [:resize-vector :y] 1))) + + (:resize-transform modifiers) + (assoc :resize-transform (:resize-transform modifiers) + :resize-transform-inverse (:resize-transform-inverse modifiers))))) + diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 14360639ff..7b8925dc01 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -147,7 +147,7 @@ (not= wn 0)))) ;; A intersects with B -;; Three posible cases: +;; Three possible cases: ;; 1) A is inside of B ;; 2) B is inside of A ;; 3) A intersects B @@ -196,8 +196,8 @@ [point {:keys [cx cy rx ry transform]}] (let [center (gpt/point cx cy) - transform (gmt/transform-in center transform) - {px :x py :y} (gpt/transform point transform) + transform (when (some? transform) (gmt/transform-in center transform)) + {px :x py :y} (if (some? transform) (gpt/transform point transform) point) ;; Ellipse inequality formula ;; https://en.wikipedia.org/wiki/Ellipse#Shifted_ellipse v (+ (/ (mth/sq (- px cx)) @@ -207,11 +207,11 @@ (<= v 1))) (defn intersects-line-ellipse? - "Checks wether a single line intersects with the given ellipse" + "Checks whether a single line intersects with the given ellipse" [[{x1 :x y1 :y} {x2 :x y2 :y}] {:keys [cx cy rx ry]}] ;; Given the ellipse inequality after inserting the line parametric equations - ;; we resolve t and gives us a cuadratic formula + ;; we resolve t and gives us a quadratic formula ;; The result of this quadratic will give us a value of T that needs to be ;; between 0-1 to be in the segment @@ -256,10 +256,10 @@ "Checks if a set of lines intersect with an ellipse in any point" [rect-lines {:keys [cx cy transform] :as ellipse-data}] (let [center (gpt/point cx cy) - transform (gmt/transform-in center transform)] + transform (when (some? transform) (gmt/transform-in center transform))] (some (fn [[p1 p2]] - (let [p1 (gpt/transform p1 transform) - p2 (gpt/transform p2 transform)] + (let [p1 (if (some? transform) (gpt/transform p1 transform) p1) + p2 (if (some? transform) (gpt/transform p2 transform) p2)] (intersects-line-ellipse? [p1 p2] ellipse-data))) rect-lines))) (defn overlaps-ellipse? @@ -284,7 +284,7 @@ (intersects-lines-ellipse? rect-lines ellipse-data)))) (defn overlaps? - "General case to check for overlaping between shapes and a rectangle" + "General case to check for overlapping between shapes and a rectangle" [shape rect] (let [stroke-width (/ (or (:stroke-width shape) 0) 2) rect (-> rect diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index 80d601bb12..38d2e56807 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -22,7 +22,7 @@ (mth/almost-zero? (- a b))) (defn calculate-opposite-handler - "Given a point and its handler, gives the symetric handler" + "Given a point and its handler, gives the symmetric handler" [point handler] (let [handler-vector (gpt/to-vec point handler)] (gpt/add point (gpt/negate handler-vector)))) @@ -119,7 +119,9 @@ ;; normalize value d (mth/sqrt (+ (* x x) (* y y)))] - (gpt/point (/ x d) (/ y d)))) + (if (mth/almost-zero? d) + (gpt/point 0 0) + (gpt/point (/ x d) (/ y d))))) (defn curve-windup [curve t] @@ -179,7 +181,7 @@ (and (mth/almost-zero? d) (mth/almost-zero? a)) [(/ (- c) b)] - ;; Cuadratic + ;; Quadratic (mth/almost-zero? d) [(/ (+ (- b) sqrt-b2-4ac) (* 2 a)) @@ -279,11 +281,19 @@ (filterv #(and (>= % 0) (<= % 1))))))) (defn command->point - ([command] (command->point command nil)) - ([{params :params} coord] - (let [prefix (if coord (name coord) "") - xkey (keyword (str prefix "x")) - ykey (keyword (str prefix "y")) + ([command] + (command->point command nil)) + + ([command coord] + (let [params (:params command) + xkey (case coord + :c1 :c1x + :c2 :c2x + :x) + ykey (case coord + :c1 :c1y + :c2 :c2y + :y) x (get params xkey) y (get params ykey)] (when (and (some? x) (some? y)) @@ -322,7 +332,7 @@ (command->point command :c1) (command->point command :c2)]] (->> (curve-extremities curve) - (map #(curve-values curve %))))) + (mapv #(curve-values curve %))))) []) selrect (gpr/points->selrect points)] (-> selrect @@ -361,25 +371,25 @@ (update :height #(if (mth/almost-zero? %) 1 %))))) (defn move-content [content move-vec] - (let [set-tr (fn [params px py] - (let [tr-point (-> (gpt/point (get params px) (get params py)) - (gpt/add move-vec))] - (assoc params - px (:x tr-point) - py (:y tr-point)))) + (let [dx (:x move-vec) + dy (:y move-vec) + + set-tr + (fn [params px py] + (-> params + (update px + dx) + (update py + dy))) transform-params (fn [{:keys [x c1x c2x] :as params}] (cond-> params - (not (nil? x)) (set-tr :x :y) - (not (nil? c1x)) (set-tr :c1x :c1y) - (not (nil? c2x)) (set-tr :c2x :c2y)))] + (some? x) (set-tr :x :y) + (some? c1x) (set-tr :c1x :c1y) + (some? c2x) (set-tr :c2x :c2y)))] - (->> content - (mapv (fn [cmd] - (cond-> cmd - (map? cmd) - (update :params transform-params))))))) + (into [] + (map #(update % :params transform-params)) + content))) (defn transform-content [content transform] @@ -393,11 +403,13 @@ transform-params (fn [{:keys [x c1x c2x] :as params}] (cond-> params - (not (nil? x)) (set-tr :x :y) - (not (nil? c1x)) (set-tr :c1x :c1y) - (not (nil? c2x)) (set-tr :c2x :c2y)))] + (some? x) (set-tr :x :y) + (some? c1x) (set-tr :c1x :c1y) + (some? c2x) (set-tr :c2x :c2y)))] - (mapv #(update % :params transform-params) content))) + (into [] + (map #(update % :params transform-params)) + content))) (defn segments->content ([segments] @@ -675,12 +687,10 @@ (curve-roots c2' :y))) - - (defn ray-line-intersect [point [a b :as line]] - ;; If the ray is paralell to the line there will be no crossings + ;; If the ray is parallel to the line there will be no crossings (let [ray-line [point (gpt/point (inc (:x point)) (:y point))] ;; Rays fail when fall just in a vertex so we move a bit upward ;; because only want to use this for insideness @@ -707,20 +717,19 @@ [[l1-t] [l2-t]]))) (defn ray-curve-intersect - [ray-line c2] + [ray-line curve] - (let [;; ray-line [point (gpt/point (inc (:x point)) (:y point))] - curve-ts (->> (line-curve-crossing ray-line c2) - (filterv #(let [curve-v (curve-values c2 %) - curve-tg (curve-tangent c2 %) + (let [curve-ts (->> (line-curve-crossing ray-line curve) + (filterv #(let [curve-v (curve-values curve %) + curve-tg (curve-tangent curve %) curve-tg-angle (gpt/angle curve-tg) ray-t (get-line-tval ray-line curve-v)] (and (> ray-t 0) (> (mth/abs (- curve-tg-angle 180)) 0.01) (> (mth/abs (- curve-tg-angle 0)) 0.01)) )))] (->> curve-ts - (mapv #(vector (curve-values c2 %) - (curve-windup c2 %)))))) + (mapv #(vector (curve-values curve %) + (curve-windup curve %)))))) (defn line-curve-intersect [l1 c2] @@ -816,32 +825,58 @@ (->> content (some inside-border?)))) -(defn is-point-in-content? - [point content] - (let [selrect (content->selrect content) - ray-line [point (gpt/point (inc (:x point)) (:y point))] +(defn close-content + [content] + (into [] + (comp (filter sp/is-closed?) + (mapcat :data)) + (->> content + (sp/close-subpaths) + (sp/get-subpaths)))) - closed-content - (into [] - (comp (filter sp/is-closed?) - (mapcat :data)) - (->> content - (sp/close-subpaths) - (sp/get-subpaths))) + +(defn ray-overlaps? + [ray-point {selrect :selrect}] + (and (>= (:y ray-point) (:y1 selrect)) + (<= (:y ray-point) (:y2 selrect)))) + +(defn content->geom-data + [content] + + (->> content + (close-content) + (filter #(not= (= :line-to (:command %)) + (= :curve-to (:command %)))) + (mapv (fn [segment] + {:command (:command segment) + :segment segment + :geom (if (= :line-to (:command segment)) + (command->line segment) + (command->bezier segment)) + :selrect (command->selrect segment)})))) + +(defn is-point-in-geom-data? + [point content-geom] + + (let [ray-line [point (gpt/point (inc (:x point)) (:y point))] cast-ray - (fn [cmd] - (case (:command cmd) - :line-to (ray-line-intersect point (command->line cmd)) - :curve-to (ray-curve-intersect ray-line (command->bezier cmd)) - #_:else []))] + (fn [data] + (case (:command data) + :line-to + (ray-line-intersect point (:geom data)) - (and (gpr/contains-point? selrect point) - (->> closed-content - (mapcat cast-ray) - (map second) - (reduce +) - (not= 0))))) + :curve-to + (ray-curve-intersect ray-line (:geom data)) + + #_:default []))] + + (->> content-geom + (filter (partial ray-overlaps? point)) + (mapcat cast-ray) + (map second) + (reduce +) + (not= 0)))) (defn split-line-to "Given a point and a line-to command will create a two new line-to commands diff --git a/common/src/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index 047781a70f..740c91677b 100644 --- a/common/src/app/common/geom/shapes/rect.cljc +++ b/common/src/app/common/geom/shapes/rect.cljc @@ -121,3 +121,19 @@ (or (< px x2) (s= px x2)) (or (> py y1) (s= py y1)) (or (< py y2) (s= py y2))))) + +(defn contains-selrect? + "Check if a selrect sr2 is contained inside sr1" + [sr1 sr2] + (and (>= (:x1 sr2) (:x1 sr1)) + (<= (:x2 sr2) (:x2 sr1)) + (>= (:y1 sr2) (:y1 sr1)) + (<= (:y2 sr2) (:y2 sr1)))) + +(defn round-selrect + [selrect] + (-> selrect + (update :x mth/round) + (update :y mth/round) + (update :width mth/round) + (update :height mth/round))) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index bfb6a751f8..01de122190 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -14,31 +14,37 @@ [app.common.geom.shapes.path :as gpa] [app.common.geom.shapes.rect :as gpr] [app.common.math :as mth] - [app.common.pages.spec :as spec] [app.common.spec :as us] [app.common.text :as txt])) +(def ^:dynamic *skip-adjust* false) + ;; --- Relative Movement -(defn- move-selrect [selrect {dx :x dy :y}] - (-> selrect - (d/update-when :x + dx) - (d/update-when :y + dy) - (d/update-when :x1 + dx) - (d/update-when :y1 + dy) - (d/update-when :x2 + dx) - (d/update-when :y2 + dy))) +(defn- move-selrect [selrect pt] + (when (and (some? selrect) (some? pt)) + (let [dx (.-x pt) + dy (.-y pt) + {:keys [x y x1 y1 x2 y2 width height]} selrect] + {:x (if (some? x) (+ dx x) x) + :y (if (some? y) (+ dy y) y) + :x1 (if (some? x1) (+ dx x1) x1) + :y1 (if (some? y1) (+ dy y1) y1) + :x2 (if (some? x2) (+ dx x2) x2) + :y2 (if (some? y2) (+ dy y2) y2) + :width width + :height height}))) (defn- move-points [points move-vec] (->> points (mapv #(gpt/add % move-vec)))) (defn move - "Move the shape relativelly to its current + "Move the shape relatively to its current position applying the provided delta." - [shape {dx :x dy :y}] - (let [dx (d/check-num dx) - dy (d/check-num dy) + [{:keys [type] :as shape} {dx :x dy :y}] + (let [dx (d/check-num dx) + dy (d/check-num dy) move-vec (gpt/point dx dy)] (-> shape @@ -46,9 +52,8 @@ (update :points move-points move-vec) (d/update-when :x + dx) (d/update-when :y + dy) - (cond-> (= :path (:type shape)) - (update :content gpa/move-content move-vec))))) - + (cond-> (= :bool type) (update :bool-content gpa/move-content move-vec)) + (cond-> (= :path type) (update :content gpa/move-content move-vec))))) ;; --- Absolute Movement @@ -71,7 +76,7 @@ :else scale)) (defn- calculate-skew-angle - "Calculates the skew angle of the paralelogram given by the points" + "Calculates the skew angle of the parallelogram given by the points" [[p1 _ p3 p4]] (let [v1 (gpt/to-vec p3 p4) v2 (gpt/to-vec p4 p1)] @@ -83,13 +88,13 @@ (- 90 (gpt/angle-with-other v1 v2))))) (defn- calculate-height - "Calculates the height of a paralelogram given by the points" + "Calculates the height of a parallelogram given by the points" [[p1 _ _ p4]] (-> (gpt/to-vec p4 p1) (gpt/length))) (defn- calculate-width - "Calculates the width of a paralelogram given by the points" + "Calculates the width of a parallelogram given by the points" [[p1 p2 _ _]] (-> (gpt/to-vec p1 p2) (gpt/length))) @@ -154,11 +159,12 @@ (defn transform-point-center "Transform a point around the shape center" [point center matrix] - (gpt/transform - point - (gmt/multiply (gmt/translate-matrix center) - matrix - (gmt/translate-matrix (gpt/negate center))))) + (when point + (gpt/transform + point + (gmt/multiply (gmt/translate-matrix center) + matrix + (gmt/translate-matrix (gpt/negate center)))))) (defn transform-rect "Transform a rectangles and changes its attributes" @@ -170,9 +176,11 @@ (defn calculate-adjust-matrix "Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that - after applying them the end result is the `shape-pathn-temp`. + after applying them the end result is the `shape-path-temp`. This is compose of three transformations: skew, resize and rotation" - ([points-temp points-rec] (calculate-adjust-matrix points-temp points-rec false false)) + ([points-temp points-rec] + (calculate-adjust-matrix points-temp points-rec false false)) + ([points-temp points-rec flip-x flip-y] (let [center (gco/center-points points-temp) @@ -210,63 +218,78 @@ stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix) ;; This is the inverse to be able to remove the transformation - stretch-matrix-inverse (-> (gmt/matrix) - (gmt/scale (gpt/point (/ 1 w3) (/ 1 h3))) - (gmt/skew (- skew-angle) 0) - (gmt/rotate (- rotation-angle)))] + stretch-matrix-inverse + (gmt/multiply (gmt/scale-matrix (gpt/point (/ 1 w3) (/ 1 h3))) + (gmt/skew-matrix (- skew-angle) 0) + (gmt/rotate-matrix (- rotation-angle)))] [stretch-matrix stretch-matrix-inverse rotation-angle]))) -(defn- apply-transform - "Given a new set of points transformed, set up the rectangle so it keeps - its properties. We adjust de x,y,width,height and create a custom transform" - [shape transform round-coords?] - (let [points (-> shape :points (gco/transform-points transform)) - center (gco/center-points points) +(defn is-rotated? + [[a b _c _d]] + ;; true if either a-b or c-d are parallel to the axis + (not (mth/close? (:y a) (:y b)))) - ;; Reverse the current transformation stack to get the base rectangle - tr-inverse (:transform-inverse shape (gmt/matrix)) +(defn- adjust-rotated-transform + [{:keys [transform transform-inverse flip-x flip-y]} points] + (let [center (gco/center-points points) - points-temp (gco/transform-points points center tr-inverse) + points-temp (cond-> points + (some? transform-inverse) + (gco/transform-points center transform-inverse)) points-temp-dim (calculate-dimensions points-temp) ;; This rectangle is the new data for the current rectangle. We want to change our rectangle ;; to have this width, height, x, y - rect-shape (-> (gco/make-centered-rect - center - (:width points-temp-dim) - (:height points-temp-dim)) - (update :width max 1) - (update :height max 1)) + new-width (max 1 (:width points-temp-dim)) + new-height (max 1 (:height points-temp-dim)) + selrect (gco/make-centered-selrect center new-width new-height) - rect-points (gpr/rect->points rect-shape) + rect-points (gpr/rect->points selrect) + [matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points flip-x flip-y)] - [matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape)) + [selrect + (if transform (gmt/multiply transform matrix) matrix) + (if transform-inverse (gmt/multiply matrix-inverse transform-inverse) matrix-inverse)])) - rect-shape (cond-> rect-shape - round-coords? - (-> (update :x mth/round) - (update :y mth/round) - (update :width mth/round) - (update :height mth/round))) +(defn- apply-transform + "Given a new set of points transformed, set up the rectangle so it keeps + its properties. We adjust de x,y,width,height and create a custom transform" + [shape transform-mtx round-coords?] - shape (cond - (= :path (:type shape)) - (-> shape - (update :content #(gpa/transform-content % transform))) + (let [points' (:points shape) + points (gco/transform-points points' transform-mtx) + bool? (= (:type shape) :bool) + path? (= (:type shape) :path) + rotated? (is-rotated? points) - :else - (-> shape - (merge rect-shape))) + [selrect transform transform-inverse] + (if (not rotated?) + [(gpr/points->selrect points) nil nil] + (adjust-rotated-transform shape points)) + selrect (cond-> selrect + round-coords? gpr/round-selrect) + + ;; Redondear los points? base-rotation (or (:rotation shape) 0) - modif-rotation (or (get-in shape [:modifiers :rotation]) 0)] + modif-rotation (or (get-in shape [:modifiers :rotation]) 0) + rotation (mod (+ base-rotation modif-rotation) 360)] - (as-> shape $ - (update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix)) - (update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix)))) - (assoc $ :points (into [] points)) - (assoc $ :selrect (gpr/rect->selrect rect-shape)) - (assoc $ :rotation (mod (+ base-rotation modif-rotation) 360))))) + (-> shape + (cond-> bool? + (update :bool-content gpa/transform-content transform-mtx)) + (cond-> path? + (update :content gpa/transform-content transform-mtx)) + (cond-> (not path?) + (-> (merge (select-keys selrect [:x :y :width :height])))) + (cond-> transform + (-> (assoc :transform transform) + (assoc :transform-inverse transform-inverse))) + (cond-> (not transform) + (dissoc :transform :transform-inverse)) + (assoc :selrect selrect) + (assoc :points points) + (assoc :rotation rotation)))) (defn- update-group-viewbox "Updates the viewbox for groups imported from SVG's" @@ -299,7 +322,7 @@ (gpr/rect->points) (gco/transform-points shape-center (:transform group (gmt/matrix)))) - ;; Calculte the new selrect + ;; Calculate the new selrect new-selrect (gpr/points->selrect base-points)] ;; Updates the shape and the applytransform-rect will update the other properties @@ -342,6 +365,9 @@ ;; tells if the resize vectors must be applied to text shapes ;; or not. +(defn empty-modifiers? [modifiers] + (empty? (dissoc modifiers :ignore-geometry?))) + (defn resize-modifiers [shape attr value] (us/assert map? shape) @@ -395,53 +421,54 @@ (->> modifiers (reduce set-modifier objects)))) -(defn- modifiers->transform - [center modifiers] - (let [ds-modifier (:displacement modifiers (gmt/matrix)) - {res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1)) - {res-x-2 :x res-y-2 :y} (:resize-vector-2 modifiers (gpt/point 1 1)) +(defn modifiers->transform + ([modifiers] + (modifiers->transform nil modifiers)) - ;; Normalize x/y vector coordinates because scale by 0 is infinite - res-x (normalize-scale res-x) - res-y (normalize-scale res-y) - resize (gpt/point res-x res-y) + ([center modifiers] + (let [displacement (:displacement modifiers) + resize-v1 (:resize-vector modifiers) + resize-v2 (:resize-vector-2 modifiers) + origin-1 (:resize-origin modifiers (gpt/point)) + origin-2 (:resize-origin-2 modifiers (gpt/point)) - res-x-2 (normalize-scale res-x-2) - res-y-2 (normalize-scale res-y-2) - resize-2 (gpt/point res-x-2 res-y-2) + ;; Normalize x/y vector coordinates because scale by 0 is infinite + resize-1 (when (some? resize-v1) + (gpt/point (normalize-scale (:x resize-v1)) + (normalize-scale (:y resize-v1)))) - origin (:resize-origin modifiers (gpt/point 0 0)) - origin-2 (:resize-origin-2 modifiers (gpt/point 0 0)) + resize-2 (when (some? resize-v2) + (gpt/point (normalize-scale (:x resize-v2)) + (normalize-scale (:y resize-v2)))) - resize-transform (:resize-transform modifiers (gmt/matrix)) - resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) - rt-modif (or (:rotation modifiers) 0) - center (gpt/transform center ds-modifier) + resize-transform (:resize-transform modifiers (gmt/matrix)) + resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix)) - transform (-> (gmt/matrix) + rt-modif (:rotation modifiers)] - ;; Applies the current resize transformation - (gmt/translate origin) - (gmt/multiply resize-transform) - (gmt/scale resize) - (gmt/multiply resize-transform-inverse) - (gmt/translate (gpt/negate origin)) + (cond-> (gmt/matrix) + (some? resize-1) + (-> (gmt/translate origin-1) + (gmt/multiply resize-transform) + (gmt/scale resize-1) + (gmt/multiply resize-transform-inverse) + (gmt/translate (gpt/negate origin-1))) - (gmt/translate origin-2) - (gmt/multiply resize-transform) - (gmt/scale resize-2) - (gmt/multiply resize-transform-inverse) - (gmt/translate (gpt/negate origin-2)) + (some? resize-2) + (-> (gmt/translate origin-2) + (gmt/multiply resize-transform) + (gmt/scale resize-2) + (gmt/multiply resize-transform-inverse) + (gmt/translate (gpt/negate origin-2))) - ;; Applies the stacked transformations - (gmt/translate center) - (gmt/multiply (gmt/rotate-matrix rt-modif)) - (gmt/translate (gpt/negate center)) + (some? displacement) + (gmt/multiply displacement) - ;; Displacement - (gmt/multiply ds-modifier))] - transform)) + (some? rt-modif) + (-> (gmt/translate center) + (gmt/multiply (gmt/rotate-matrix rt-modif)) + (gmt/translate (gpt/negate center))))))) (defn- set-flip [shape modifiers] (let [rx (or (get-in modifiers [:resize-vector :x]) @@ -463,7 +490,7 @@ modifiers (dissoc modifiers :displacement)] (-> shape (assoc :modifiers modifiers) - (cond-> (empty? modifiers) + (cond-> (empty-modifiers? modifiers) (dissoc :modifiers)))) shape))) @@ -485,205 +512,77 @@ %))) shape)) +(defn apply-modifiers + [shape modifiers round-coords?] + (let [center (gco/center-shape shape) + transform (modifiers->transform center modifiers)] + (apply-transform shape transform round-coords?))) + (defn transform-shape ([shape] (transform-shape shape nil)) - ([shape {:keys [round-coords?] - :or {round-coords? true}}] - (let [shape (apply-displacement shape) - center (gco/center-shape shape) - modifiers (:modifiers shape)] - (if (and modifiers center) - (let [transform (modifiers->transform center modifiers)] - (-> shape - (set-flip modifiers) - (apply-transform transform round-coords?) - (apply-text-resize modifiers) - (dissoc :modifiers))) - shape)))) + ([shape {:keys [round-coords?] :or {round-coords? true}}] + (let [modifiers (:modifiers shape)] + (cond + (nil? modifiers) + shape -(defn calc-child-modifiers - "Given the modifiers to apply to the parent, calculate the corresponding - modifiers for the child, depending on the child constraints." - [parent child parent-modifiers ignore-constraints] - (let [parent-rect (:selrect parent) - child-rect (:selrect child) + (empty-modifiers? modifiers) + (dissoc shape :modifiers) - ;; Apply the modifiers to the parent's selrect, to check the difference with - ;; the original, and calculate child transformations from this. - ;; - ;; Note that a shape's selrect is always "horizontal" (i.e. without applying - ;; the shape transform, that may include some rotation and skew). Thus, to - ;; apply the modifiers, we first apply to them the transform-inverse. - parent-displacement (-> (gpt/point 0 0) - (gpt/transform (get parent-modifiers :displacement (gmt/matrix))) - (gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix))) - (gmt/translate-matrix)) - parent-origin (-> (:resize-origin parent-modifiers) - ((d/nilf transform-point-center) - (gco/center-shape parent) - (:resize-transform-inverse parent-modifiers (gmt/matrix)))) - parent-origin-2 (-> (:resize-origin-2 parent-modifiers) - ((d/nilf transform-point-center) - (gco/center-shape parent) - (:resize-transform-inverse parent-modifiers (gmt/matrix)))) - parent-vector (get parent-modifiers :resize-vector (gpt/point 1 1)) - parent-vector-2 (get parent-modifiers :resize-vector-2 (gpt/point 1 1)) + :else + (let [shape (apply-displacement shape) + modifiers (:modifiers shape)] + (cond-> shape + (not (empty-modifiers? modifiers)) + (-> (set-flip modifiers) + (apply-modifiers modifiers round-coords?) + (apply-text-resize modifiers)) - transformed-parent-rect (-> parent-rect - (gpr/rect->points) - (gco/transform-points parent-displacement) - (gco/transform-points parent-origin (gmt/scale-matrix parent-vector)) - (gco/transform-points parent-origin-2 (gmt/scale-matrix parent-vector-2)) - (gpr/points->selrect)) + :always + (dissoc :modifiers))))))) - ;; Calculate the modifiers in the horizontal and vertical directions - ;; depending on the child constraints. - constraints-h (if-not ignore-constraints - (get child :constraints-h (spec/default-constraints-h child)) - :scale) - constraints-v (if-not ignore-constraints - (get child :constraints-v (spec/default-constraints-v child)) - :scale) +(defn transform-selrect + [selrect {:keys [displacement resize-transform-inverse resize-vector resize-origin resize-vector-2 resize-origin-2]}] + + ;; FIXME: Improve Performance + (let [resize-transform-inverse (or resize-transform-inverse (gmt/matrix)) - modifiers-h (case constraints-h - :left - (let [delta-left (- (:x1 transformed-parent-rect) (:x1 parent-rect))] + displacement + (when (some? displacement) + (gmt/multiply resize-transform-inverse displacement) + #_(-> (gpt/point 0 0) + (gpt/transform displacement) + (gpt/transform resize-transform-inverse) + (gmt/translate-matrix))) - (if-not (mth/almost-zero? delta-left) - {:displacement (gpt/point delta-left 0)} ;; we convert to matrix below - {})) + resize-origin + (when (some? resize-origin) + (transform-point-center resize-origin (gco/center-selrect selrect) resize-transform-inverse)) - :right - (let [delta-right (- (:x2 transformed-parent-rect) (:x2 parent-rect))] - (if-not (mth/almost-zero? delta-right) - {:displacement (gpt/point delta-right 0)} - {})) + resize-origin-2 + (when (some? resize-origin-2) + (transform-point-center resize-origin-2 (gco/center-selrect selrect) resize-transform-inverse))] - :leftright - (let [delta-left (- (:x1 transformed-parent-rect) (:x1 parent-rect)) - delta-width (- (:width transformed-parent-rect) (:width parent-rect))] - (if (or (not (mth/almost-zero? delta-left)) - (not (mth/almost-zero? delta-width))) - {:displacement (gpt/point delta-left 0) - :resize-origin (-> (gpt/point (+ (:x1 child-rect) delta-left) - (:y1 child-rect)) - (transform-point-center - (gco/center-rect child-rect) - (:transform child (gmt/matrix)))) - :resize-vector (gpt/point (/ (+ (:width child-rect) delta-width) - (:width child-rect)) 1)} - {})) + (if (and (nil? displacement) (nil? resize-origin) (nil? resize-origin-2)) + selrect - :center - (let [parent-center (gco/center-rect parent-rect) - transformed-parent-center (gco/center-rect transformed-parent-rect) - delta-center (- (:x transformed-parent-center) (:x parent-center))] - (if-not (mth/almost-zero? delta-center) - {:displacement (gpt/point delta-center 0)} - {})) + (cond-> selrect + :always + (gpr/rect->points) - :scale - (cond-> {} - (and (:resize-vector parent-modifiers) - (not (mth/close? (:x (:resize-vector parent-modifiers)) 1))) - (assoc :resize-origin (:resize-origin parent-modifiers) - :resize-vector (gpt/point (:x (:resize-vector parent-modifiers)) 1)) + (some? displacement) + (gco/transform-points displacement) - ;; resize-vector-2 is always for vertical modifiers, so no need to - ;; check it here. + (some? resize-origin) + (gco/transform-points resize-origin (gmt/scale-matrix resize-vector)) - (:displacement parent-modifiers) - (assoc :displacement - (gpt/point (-> (gpt/point 0 0) - (gpt/transform (:displacement parent-modifiers)) - (gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix))) - (:x)) - 0))) - {}) + (some? resize-origin-2) + (gco/transform-points resize-origin-2 (gmt/scale-matrix resize-vector-2)) - modifiers-v (case constraints-v - :top - (let [delta-top (- (:y1 transformed-parent-rect) (:y1 parent-rect))] - (if-not (mth/almost-zero? delta-top) - {:displacement (gpt/point 0 delta-top)} ;; we convert to matrix below - {})) - - :bottom - (let [delta-bottom (- (:y2 transformed-parent-rect) (:y2 parent-rect))] - (if-not (mth/almost-zero? delta-bottom) - {:displacement (gpt/point 0 delta-bottom)} - {})) - - :topbottom - (let [delta-top (- (:y1 transformed-parent-rect) (:y1 parent-rect)) - delta-height (- (:height transformed-parent-rect) (:height parent-rect))] - (if (or (not (mth/almost-zero? delta-top)) - (not (mth/almost-zero? delta-height))) - {:displacement (gpt/point 0 delta-top) - :resize-origin (-> (gpt/point (:x1 child-rect) - (+ (:y1 child-rect) delta-top)) - (transform-point-center - (gco/center-rect child-rect) - (:transform child (gmt/matrix)))) - :resize-vector (gpt/point 1 (/ (+ (:height child-rect) delta-height) - (:height child-rect)))} - {})) - - :center - (let [parent-center (gco/center-rect parent-rect) - transformed-parent-center (gco/center-rect transformed-parent-rect) - delta-center (- (:y transformed-parent-center) (:y parent-center))] - (if-not (mth/almost-zero? delta-center) - {:displacement (gpt/point 0 delta-center)} - {})) - - :scale - (cond-> {} - (and (:resize-vector parent-modifiers) - (not (mth/close? (:y (:resize-vector parent-modifiers)) 1))) - (assoc :resize-origin (:resize-origin parent-modifiers) - :resize-vector (gpt/point 1 (:y (:resize-vector parent-modifiers)))) - - ;; If there is a resize-vector-2, this means that we come from a recursive - ;; call, and the resize-vector has no vertical data, so we may override it. - (and (:resize-vector-2 parent-modifiers) - (not (mth/close? (:y (:resize-vector-2 parent-modifiers)) 1))) - (assoc :resize-origin (:resize-origin-2 parent-modifiers) - :resize-vector (gpt/point 1 (:y (:resize-vector-2 parent-modifiers)))) - - (:displacement parent-modifiers) - (assoc :displacement - (gpt/point 0 (-> (gpt/point 0 0) - (gpt/transform (:displacement parent-modifiers)) - (gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix))) - (:y))))) - {})] - - ;; Build final child modifiers. Apply transform again to the result, to get the - ;; real modifiers that need to be applied to the child, including rotation as needed. - (cond-> {} - (or (:displacement modifiers-h) (:displacement modifiers-v)) - (assoc :displacement (gmt/translate-matrix - (-> (gpt/point (get (:displacement modifiers-h) :x 0) - (get (:displacement modifiers-v) :y 0)) - (gpt/transform - (:resize-transform parent-modifiers (gmt/matrix)))))) - - (:resize-vector modifiers-h) - (assoc :resize-origin (:resize-origin modifiers-h) - :resize-vector (gpt/point (get (:resize-vector modifiers-h) :x 1) - (get (:resize-vector modifiers-h) :y 1))) - - (:resize-vector modifiers-v) - (assoc :resize-origin-2 (:resize-origin modifiers-v) - :resize-vector-2 (gpt/point (get (:resize-vector modifiers-v) :x 1) - (get (:resize-vector modifiers-v) :y 1))) - - (:resize-transform parent-modifiers) - (assoc :resize-transform (:resize-transform parent-modifiers) - :resize-transform-inverse (:resize-transform-inverse parent-modifiers))))) + :always + (gpr/points->selrect))))) (defn selection-rect @@ -691,6 +590,5 @@ rotation of each shape. Mainly used for multiple selection." [shapes] (->> shapes - (transform-shape) - (map (comp gpr/points->selrect :points)) + (map (comp gpr/points->selrect :points transform-shape)) (gpr/join-selrects))) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index b9940f1ea2..aea40b9356 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -168,21 +168,22 @@ `(write-log! ~(or logger (str *ns*)) ~level ~cause - ~(dissoc props :level :cause ::logger ::raw)) + (or ~raw ~(dissoc props :level :cause ::logger ::raw))) (let [props (dissoc props :level :cause ::logger ::async ::raw) logger (or logger (str *ns*)) logger-sym (gensym "log") level-sym (gensym "log")] `(let [~logger-sym (get-logger ~logger) ~level-sym (get-level ~level)] - (if (enabled? ~logger-sym ~level-sym) + (when (enabled? ~logger-sym ~level-sym) ~(if async - `(let [cdata# (ThreadContext/getImmutableContext)] - (send-off logging-agent - (fn [_#] - (with-context (into {:cause ~cause} cdata#) - (->> (or ~raw (build-map-message ~props)) - (write-log! ~logger-sym ~level-sym ~cause)))))) + `(->> (ThreadContext/getImmutableContext) + (send-off logging-agent + (fn [_# cdata#] + (with-context (into {} cdata#) + (->> (or ~raw (build-map-message ~props)) + (write-log! ~logger-sym ~level-sym ~cause)))))) + `(let [message# (or ~raw (build-map-message ~props))] (write-log! ~logger-sym ~level-sym ~cause message#)))))))) @@ -297,7 +298,7 @@ #?(:cljs (defn default-handler - [{:keys [message level logger-name]}] + [{:keys [message level logger-name exception] :as params}] (let [header-styles (str "font-weight: 600; color: " (level->color level)) normal-styles (str "font-weight: 300; color: " (get colors :gray6)) level-name (level->short-name level) @@ -318,7 +319,13 @@ (js/console.error v)))) (js/console.groupEnd message)) (let [message (str header "%c" (pr-str message))] - (js/console.log message header-styles normal-styles)))))))) + (js/console.log message header-styles normal-styles))))) + + (when exception + (when-let [data (ex-data exception)] + (js/console.error "cause data:" (pr-str data))) + (js/console.error (.-stack exception)))))) + #?(:cljs (defn record->map diff --git a/common/src/app/common/media.cljc b/common/src/app/common/media.cljc index cbf1fb8262..54106046a1 100644 --- a/common/src/app/common/media.cljc +++ b/common/src/app/common/media.cljc @@ -78,7 +78,7 @@ (re-seq #"(?i)(?:extra\s*bold|ultra\s*bold)" variant) 800 (re-seq #"(?i)(?:bold)" variant) 700 (re-seq #"(?i)(?:extra\s*black|ultra\s*black)" variant) 950 - (re-seq #"(?i)(?:black|heavy)" variant) 900 + (re-seq #"(?i)(?:black|heavy|solid)" variant) 900 :else 400)) (defn parse-font-style diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index 00725454ae..4c6248b86a 100644 --- a/common/src/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -69,6 +69,7 @@ (d/export helpers/compact-path) (d/export helpers/compact-name) (d/export helpers/unframed-shape?) +(d/export helpers/children-seq) ;; Indices (d/export indices/calculate-z-index) @@ -94,7 +95,6 @@ (s/def ::color ::spec/color) (s/def ::data ::spec/data) (s/def ::media-object ::spec/media-object) -(s/def ::minimal-shape ::spec/minimal-shape) (s/def ::page ::spec/page) (s/def ::recent-color ::spec/recent-color) (s/def ::shape-attrs ::spec/shape-attrs) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 295489f284..f8539e038e 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -40,7 +40,9 @@ (defmulti process-operation (fn [_ op] (:type op))) (defn process-changes - ([data items] (process-changes data items true)) + ([data items] + (process-changes data items true)) + ([data items verify?] ;; When verify? false we spec the schema validation. Currently used to make just ;; 1 validation even if the changes are applied twice @@ -152,29 +154,37 @@ ;; reg-objects operation "regenerates" the geometry and selrect of the parent groups (defmethod process-change :reg-objects [data {:keys [page-id component-id shapes]}] + ;; FIXME: Improve performance (letfn [(reg-objects [objects] - (reduce #(d/update-when %1 %2 update-group %1) objects - (sequence (comp - (mapcat #(cons % (cph/get-parents % objects))) - (map #(get objects %)) - (filter #(contains? #{:group :bool} (:type %))) - (map :id) - (distinct)) - shapes))) + (let [lookup (d/getf objects) + update-fn #(d/update-when %1 %2 update-group %1) + xform (comp + (mapcat #(cons % (cph/get-parents % objects))) + (map lookup) + (filter #(contains? #{:group :bool} (:type %))) + (map :id) + (distinct))] + + (->> (sequence xform shapes) + (reduce update-fn objects)))) + (set-mask-selrect [group children] (let [mask (first children)] (-> group - (merge (select-keys mask [:selrect :points])) - (assoc :x (-> mask :selrect :x) - :y (-> mask :selrect :y) - :width (-> mask :selrect :width) - :height (-> mask :selrect :height) - :flip-x (-> mask :flip-x) - :flip-y (-> mask :flip-y))))) + (assoc :selrect (-> mask :selrect)) + (assoc :points (-> mask :points)) + (assoc :x (-> mask :selrect :x)) + (assoc :y (-> mask :selrect :y)) + (assoc :width (-> mask :selrect :width)) + (assoc :height (-> mask :selrect :height)) + (assoc :flip-x (-> mask :flip-x)) + (assoc :flip-y (-> mask :flip-y))))) + (update-group [group objects] - (let [children (->> group :shapes (map #(get objects %)))] + (let [lookup (d/getf objects) + children (->> group :shapes (map lookup))] (cond - ;; If the group is empty we don't make any changes. Should be removed by a later process + ;; If the group is empty we don't make any changes. Will be removed by a later process (empty? children) group @@ -292,7 +302,7 @@ (reduce update-parent-id $ shapes) ;; Analyze the old parents and clear the old links - ;; only if the new parrent is different form old + ;; only if the new parent is different form old ;; parent. (reduce (partial remove-from-old-parent cpindex) $ shapes) @@ -470,4 +480,3 @@ (ex/raise :type :not-implemented :code :operation-not-implemented :context {:type (:type op)})) - diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index d999822bd7..ddc5dcbaba 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -12,7 +12,8 @@ ;; Auxiliary functions to help create a set of changes (undo + redo) -(defn empty-changes [origin page-id] +(defn empty-changes + [origin page-id] (let [changes {:redo-changes [] :undo-changes [] :origin origin}] @@ -46,29 +47,33 @@ (update :undo-changes d/preconj del-change))))) (defn change-parent - [changes parent-id shapes] - (assert (contains? (meta changes) ::objects) "Call (with-objects) first to use this function") + ([changes parent-id shapes] (change-parent changes parent-id shapes nil)) + ([changes parent-id shapes index] + (assert (contains? (meta changes) ::objects) "Call (with-objects) first to use this function") - (let [objects (::objects (meta changes)) - set-parent-change - {:type :mov-objects - :parent-id parent-id - :page-id (::page-id (meta changes)) - :shapes (->> shapes (mapv :id))} + (let [objects (::objects (meta changes)) + set-parent-change + (cond-> {:type :mov-objects + :parent-id parent-id + :page-id (::page-id (meta changes)) + :shapes (->> shapes (mapv :id))} - mk-undo-change - (fn [change-set shape] - (d/preconj - change-set - {:type :mov-objects - :page-id (::page-id (meta changes)) - :parent-id (:parent-id shape) - :shapes [(:id shape)] - :index (cp/position-on-parent (:id shape) objects)}))] + (some? index) + (assoc :index index)) - (-> changes - (update :redo-changes conj set-parent-change) - (update :undo-changes #(reduce mk-undo-change % shapes))))) + mk-undo-change + (fn [change-set shape] + (d/preconj + change-set + {:type :mov-objects + :page-id (::page-id (meta changes)) + :parent-id (:parent-id shape) + :shapes [(:id shape)] + :index (cp/position-on-parent (:id shape) objects)}))] + + (-> changes + (update :redo-changes conj set-parent-change) + (update :undo-changes #(reduce mk-undo-change % shapes)))))) (defn- generate-operation "Given an object old and new versions and an attribute will append into changes @@ -165,3 +170,11 @@ (update :undo-changes #(as-> % $ (reduce add-undo-change-parent $ ids) (reduce add-undo-change-shape $ ids)))))) + + +(defn move-page + [chdata index prev-index] + (let [page-id (::page-id (meta chdata))] + (-> chdata + (update :redo-changes conj {:type :mov-page :id page-id :index index}) + (update :undo-changes conj {:type :mov-page :id page-id :index prev-index})))) diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc index 53c7157833..6356f4bbec 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -6,10 +6,11 @@ (ns app.common.pages.common (:require + [app.common.colors :as clr] [app.common.uuid :as uuid])) (def file-version 12) -(def default-color "#b1b2b5") ;; $color-gray-20 +(def default-color clr/gray-20) (def root uuid/zero) (def component-sync-attrs @@ -19,6 +20,7 @@ :fill-color-gradient :fill-group :fill-color-ref-file :fill-group :fill-color-ref-id :fill-group + :hide-fill-on-export :fill-group :content :content-group :hidden :visibility-group :blocked :modifiable-group diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 5bc912c752..4fd822eca4 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -46,13 +46,14 @@ (defn get-root-shape "Get the root shape linked to a component for this shape, if any" [shape objects] - (if-not (:shape-ref shape) - nil - (if (:component-root? shape) - shape - (if-let [parent-id (:parent-id shape)] - (get-root-shape (get objects parent-id) objects) - nil)))) + + (cond + (some? (:component-root? shape)) + shape + + (some? (:shape-ref shape)) + (recur (get objects (:parent-id shape)) + objects))) (defn make-container [page-or-component type] @@ -98,35 +99,10 @@ [component] (get-in component [:objects (:id component)])) -;; Implemented with transient for performance -(defn get-children - "Retrieve all children ids recursively for a given object. The - children's order will be breadth first." - [id objects] - (loop [result (transient []) - pending (transient []) - next id] - (let [children (get-in objects [next :shapes] []) - [result pending] - ;; Iterate through children and add them to the result - ;; also add them in pending to check for their children - (loop [result result - pending pending - current (first children) - children (rest children)] - (if current - (recur (conj! result current) - (conj! pending current) - (first children) - (rest children)) - [result pending])) - - ;; If we have still pending, advance the iterator - length (count pending)] - (if (pos? length) - (let [next (get pending (dec length))] - (recur result (pop! pending) next)) - (persistent! result))))) +(defn get-children [id objects] + (if-let [shapes (-> (get objects id) :shapes (some-> vec))] + (into shapes (mapcat #(get-children % objects)) shapes) + [])) (defn get-children-objects "Retrieve all children objects recursively for a given object" @@ -175,6 +151,7 @@ (defn clean-loops "Clean a list of ids from circular references." [objects ids] + (let [parent-selected? (fn [id] (let [parents (get-parents id objects)] @@ -481,3 +458,10 @@ (and (not= (:type shape) :frame) (= (:frame-id shape) uuid/zero))) +(defn children-seq + "Creates a sequence of shapes through the objects tree" + [shape objects] + (let [getter (partial get objects)] + (tree-seq #(d/not-empty? (get shape :shapes)) + #(->> (get % :shapes) (map getter)) + shape))) diff --git a/common/src/app/common/pages/init.cljc b/common/src/app/common/pages/init.cljc index a37584c235..8e3b5bb92f 100644 --- a/common/src/app/common/pages/init.cljc +++ b/common/src/app/common/pages/init.cljc @@ -6,6 +6,7 @@ (ns app.common.pages.init (:require + [app.common.colors :as clr] [app.common.data :as d] [app.common.exceptions :as ex] [app.common.pages.common :refer [file-version default-color]] @@ -32,9 +33,10 @@ (def default-frame-attrs {:frame-id uuid/zero - :fill-color "#ffffff" + :fill-color clr/white :fill-opacity 1 - :shapes []}) + :shapes [] + :hide-fill-on-export false}) (def ^:private minimal-shapes [{:type :rect @@ -44,7 +46,7 @@ :stroke-style :none :stroke-alignment :center :stroke-width 0 - :stroke-color "#000000" + :stroke-color clr/black :stroke-opacity 0 :rx 0 :ry 0} @@ -58,7 +60,7 @@ :stroke-style :none :stroke-alignment :center :stroke-width 0 - :stroke-color "#000000" + :stroke-color clr/black :stroke-opacity 0} {:type :path @@ -66,17 +68,17 @@ :stroke-style :solid :stroke-alignment :center :stroke-width 2 - :stroke-color "#000000" + :stroke-color clr/black :stroke-opacity 1} {:type :frame :name "Artboard-1" - :fill-color "#ffffff" + :fill-color clr/white :fill-opacity 1 :stroke-style :none :stroke-alignment :center :stroke-width 0 - :stroke-color "#000000" + :stroke-color clr/black :stroke-opacity 0} {:type :text diff --git a/common/src/app/common/pages/spec.cljc b/common/src/app/common/pages/spec.cljc index 8ce65e409e..59ded8f62d 100644 --- a/common/src/app/common/pages/spec.cljc +++ b/common/src/app/common/pages/spec.cljc @@ -173,13 +173,13 @@ ;; Page Data related (s/def :internal.shape/blocked boolean?) (s/def :internal.shape/collapsed boolean?) -(s/def :internal.shape/content any?) (s/def :internal.shape/fill-color string?) (s/def :internal.shape/fill-opacity ::us/safe-number) (s/def :internal.shape/fill-color-gradient (s/nilable ::gradient)) (s/def :internal.shape/fill-color-ref-file (s/nilable uuid?)) (s/def :internal.shape/fill-color-ref-id (s/nilable uuid?)) +(s/def :internal.shape/hide-fill-on-export boolean?) (s/def :internal.shape/font-family string?) (s/def :internal.shape/font-size ::us/safe-integer) @@ -256,12 +256,37 @@ (s/def :internal.shape/transform ::matrix) (s/def :internal.shape/transform-inverse ::matrix) +(s/def :internal.shape/opacity ::us/safe-number) +(s/def :internal.shape/blend-mode + #{:normal + :darken + :multiply + :color-burn + :lighten + :screen + :color-dodge + :overlay + :soft-light + :hard-light + :difference + :exclusion + :hue + :saturation + :color + :luminosity}) + (s/def ::shape-attrs - (s/keys :opt-un [:internal.shape/selrect + (s/keys :opt-un [::id + ::type + ::name + ::component-id + ::component-file + ::component-root? + ::shape-ref + :internal.shape/selrect :internal.shape/points :internal.shape/blocked :internal.shape/collapsed - :internal.shape/content :internal.shape/fill-color :internal.shape/fill-opacity :internal.shape/fill-color-gradient @@ -307,22 +332,60 @@ ::cti/interactions :internal.shape/masked-group? :internal.shape/shadow - :internal.shape/blur])) + :internal.shape/blur + :internal.shape/opacity + :internal.shape/blend-mode])) +(s/def :internal.shape.text/type #{"root" "paragraph-set" "paragraph"}) +(s/def :internal.shape.text/children + (s/coll-of :internal.shape.text/content + :kind vector? + :min-count 1)) -;; shapes-group is handled differently +(s/def :internal.shape.text/text string?) +(s/def :internal.shape.text/key string?) -(s/def ::minimal-shape - (s/keys :req-un [::type ::name] - :opt-un [::id])) +(s/def :internal.shape.text/content + (s/nilable + (s/or :text-container + (s/keys :req-un [:internal.shape.text/type + :internal.shape.text/children] + :opt-un [:internal.shape.text/key]) + :text-content + (s/keys :req-un [:internal.shape.text/text])))) + +(s/def :internal.shape.path/command keyword?) +(s/def :internal.shape.path/params + (s/nilable (s/map-of keyword? any?))) + +(s/def :internal.shape.path/command-item + (s/keys :req-un [:internal.shape.path/command] + :opt-un [:internal.shape.path/params])) + +(s/def :internal.shape.path/content + (s/coll-of :internal.shape.path/command-item :kind vector?)) + +(defmulti shape-spec :type) + +(defmethod shape-spec :default [_] + (s/spec ::shape-attrs)) + +(defmethod shape-spec :text [_] + (s/and ::shape-attrs + (s/keys :opt-un [:internal.shape.text/content]))) + +(defmethod shape-spec :path [_] + (s/and ::shape-attrs + (s/keys :opt-un [:internal.shape.path/content]))) + +(defmethod shape-spec :frame [_] + (s/and ::shape-attrs + (s/keys :opt-un [:internal.shape/hide-fill-on-export]))) (s/def ::shape - (s/and ::minimal-shape ::shape-attrs - (s/keys :opt-un [::id - ::component-id - ::component-file - ::component-root? - ::shape-ref]))) + (s/and (s/multi-spec shape-spec :type) + #(contains? % :name) + #(contains? % :type))) (s/def :internal.page/objects (s/map-of uuid? ::shape)) @@ -332,7 +395,6 @@ ::cto/options :internal.page/objects])) - (s/def ::recent-color (s/keys :opt-un [:internal.color/value :internal.color/color diff --git a/common/src/app/common/path/bool.cljc b/common/src/app/common/path/bool.cljc index 0c13503ae4..8b6a66cf5a 100644 --- a/common/src/app/common/path/bool.cljc +++ b/common/src/app/common/path/bool.cljc @@ -91,55 +91,55 @@ :else [[] []])) -(defn split - [seg-1 seg-2] - (let [r1 (gsp/command->selrect seg-1) - r2 (gsp/command->selrect seg-2)] - (if (not (gpr/overlaps-rects? r1 r2)) - [[seg-1] [seg-2]] - (let [[ts-seg-1 ts-seg-2] (split-ts seg-1 seg-2)] - [(-> (split-command seg-1 ts-seg-1) (add-previous (:prev seg-1))) - (-> (split-command seg-2 ts-seg-2) (add-previous (:prev seg-2)))])))) - (defn content-intersect-split - [content-a content-b] + [content-a content-b sr-a sr-b] - (let [cache (atom {})] - (letfn [(split-cache [seg-1 seg-2] - (cond - (contains? @cache [seg-1 seg-2]) - (first (get @cache [seg-1 seg-2])) + (let [command->selrect (memoize gsp/command->selrect)] - (contains? @cache [seg-2 seg-1]) - (second (get @cache [seg-2 seg-1])) + (letfn [(overlap-segment-selrect? + [segment selrect] + (if (= :move-to (:command segment)) + false + (let [r1 (command->selrect segment)] + (gpr/overlaps-rects? r1 selrect)))) - :else - (let [value (split seg-1 seg-2)] - (swap! cache assoc [seg-1 seg-2] value) - (first value)))) + (overlap-segments? + [seg-1 seg-2] + (if (or (= :move-to (:command seg-1)) + (= :move-to (:command seg-2))) + false + (let [r1 (command->selrect seg-1) + r2 (command->selrect seg-2)] + (gpr/overlaps-rects? r1 r2)))) + + (split + [seg-1 seg-2] + (if (not (overlap-segments? seg-1 seg-2)) + [seg-1] + (let [[ts-seg-1 _] (split-ts seg-1 seg-2)] + (-> (split-command seg-1 ts-seg-1) + (add-previous (:prev seg-1)))))) (split-segment-on-content - [segment content] + [segment content content-sr] - (loop [current (first content) - content (rest content) - result [segment]] - - (if (nil? current) - result - (let [result (->> result (into [] (mapcat #(split-cache % current))))] - (recur (first content) - (rest content) - result))))) + (if (overlap-segment-selrect? segment content-sr) + (->> content + (filter #(overlap-segments? segment %)) + (reduce + (fn [result current] + (into [] (mapcat #(split % current)) result)) + [segment])) + [segment])) (split-content - [content-a content-b] + [content-a content-b sr-b] (into [] - (mapcat #(split-segment-on-content % content-b)) + (mapcat #(split-segment-on-content % content-b sr-b)) content-a))] - [(split-content content-a content-b) - (split-content content-b content-a)]))) + [(split-content content-a content-b sr-b) + (split-content content-b content-a sr-a)]))) (defn is-segment? [cmd] @@ -147,7 +147,7 @@ (contains? #{:line-to :curve-to} (:command cmd)))) (defn contains-segment? - [segment content] + [segment content content-sr content-geom] (let [point (case (:command segment) :line-to (-> (gsp/command->line segment) @@ -156,11 +156,13 @@ :curve-to (-> (gsp/command->bezier segment) (gsp/curve-values 0.5)))] - (or (gsp/is-point-in-content? point content) - (gsp/is-point-in-border? point content)))) + (and (gpr/contains-point? content-sr point) + (or + (gsp/is-point-in-geom-data? point content-geom) + (gsp/is-point-in-border? point content))))) (defn inside-segment? - [segment content] + [segment content-sr content-geom] (let [point (case (:command segment) :line-to (-> (gsp/command->line segment) (gsp/line-values 0.5)) @@ -168,7 +170,8 @@ :curve-to (-> (gsp/command->bezier segment) (gsp/curve-values 0.5)))] - (gsp/is-point-in-content? point content))) + (and (gpr/contains-point? content-sr point) + (gsp/is-point-in-geom-data? point content-geom)))) (defn overlap-segment? "Finds if the current segment is overlapping against other @@ -209,52 +212,9 @@ (d/seek overlap-single?) (some?)))) -(defn create-union [content-a content-a-split content-b content-b-split] - ;; Pick all segments in content-a that are not inside content-b - ;; Pick all segments in content-b that are not inside content-a - (let [content - (concat - (->> content-a-split (filter #(not (contains-segment? % content-b)))) - (->> content-b-split (filter #(not (contains-segment? % content-a))))) - - ;; Overlapping segments should be added when they are part of the border - border-content - (->> content-b-split - (filter #(and (contains-segment? % content-a) - (overlap-segment? % content-a-split) - (not (inside-segment? % content)))))] - - ;; Ensure that the output is always a vector - (d/concat-vec content border-content))) - -(defn create-difference [content-a content-a-split content-b content-b-split] - ;; Pick all segments in content-a that are not inside content-b - ;; Pick all segments in content b that are inside content-a - ;; removing overlapping - (d/concat-vec - (->> content-a-split (filter #(not (contains-segment? % content-b)))) - - ;; Reverse second content so we can have holes inside other shapes - (->> content-b-split - (filter #(and (contains-segment? % content-a) - (not (overlap-segment? % content-a-split))))))) - -(defn create-intersection [content-a content-a-split content-b content-b-split] - ;; Pick all segments in content-a that are inside content-b - ;; Pick all segments in content-b that are inside content-a - (d/concat-vec - (->> content-a-split (filter #(contains-segment? % content-b))) - (->> content-b-split (filter #(contains-segment? % content-a))))) - - -(defn create-exclusion [content-a content-b] - ;; Pick all segments - (d/concat-vec content-a content-b)) - - (defn fix-move-to [content] - ;; Remove the field `:prev` and makes the necesaries `move-to` + ;; Remove the field `:prev` and makes the necessaries `move-to` ;; then clean the subpaths (loop [current (first content) @@ -273,27 +233,90 @@ (gsp/command->point current) (conj result (dissoc current :prev))))))) +(defn create-union [content-a content-a-split content-b content-b-split sr-a sr-b] + ;; Pick all segments in content-a that are not inside content-b + ;; Pick all segments in content-b that are not inside content-a + (let [content-a-geom (gsp/content->geom-data content-a) + content-b-geom (gsp/content->geom-data content-b) + + content + (concat + (->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom)))) + (->> content-b-split (filter #(not (contains-segment? % content-a sr-a content-a-geom))))) + + content-geom (gsp/content->geom-data content) + + content-sr (gsp/content->selrect (fix-move-to content)) + + ;; Overlapping segments should be added when they are part of the border + border-content + (->> content-b-split + (filter #(and (contains-segment? % content-a sr-a content-a-geom) + (overlap-segment? % content-a-split) + (not (inside-segment? % content-sr content-geom)))))] + + ;; Ensure that the output is always a vector + (d/concat-vec content border-content))) + +(defn create-difference [content-a content-a-split content-b content-b-split sr-a sr-b] + ;; Pick all segments in content-a that are not inside content-b + ;; Pick all segments in content b that are inside content-a + ;; removing overlapping + (let [content-a-geom (gsp/content->geom-data content-a) + content-b-geom (gsp/content->geom-data content-b)] + (d/concat-vec + (->> content-a-split (filter #(not (contains-segment? % content-b sr-b content-b-geom)))) + + ;; Reverse second content so we can have holes inside other shapes + (->> content-b-split + (filter #(and (contains-segment? % content-a sr-a content-a-geom) + (not (overlap-segment? % content-a-split)))))))) + +(defn create-intersection [content-a content-a-split content-b content-b-split sr-a sr-b] + ;; Pick all segments in content-a that are inside content-b + ;; Pick all segments in content-b that are inside content-a + (let [content-a-geom (gsp/content->geom-data content-a) + content-b-geom (gsp/content->geom-data content-b)] + (d/concat-vec + (->> content-a-split (filter #(contains-segment? % content-b sr-b content-b-geom))) + (->> content-b-split (filter #(contains-segment? % content-a sr-a content-a-geom)))))) + + +(defn create-exclusion [content-a content-b] + ;; Pick all segments + (d/concat-vec content-a content-b)) + (defn content-bool-pair [bool-type content-a content-b] - (let [content-a (-> content-a (close-paths) (add-previous)) + (let [;; We need to reverse the second path when making a difference/intersection/exclude + ;; and both shapes are in the same direction + should-reverse? (and (not= :union bool-type) + (= (ups/clockwise? content-b) + (ups/clockwise? content-a))) + + content-a (-> content-a + (close-paths) + (add-previous)) content-b (-> content-b (close-paths) - (cond-> (ups/clockwise? content-b) - (ups/reverse-content)) + (cond-> should-reverse? (ups/reverse-content)) (add-previous)) + sr-a (gsp/content->selrect content-a) + sr-b (gsp/content->selrect content-b) + ;; Split content in new segments in the intersection with the other path - [content-a-split content-b-split] (content-intersect-split content-a content-b) + [content-a-split content-b-split] (content-intersect-split content-a content-b sr-a sr-b) content-a-split (->> content-a-split add-previous (filter is-segment?)) content-b-split (->> content-b-split add-previous (filter is-segment?)) bool-content (case bool-type - :union (create-union content-a content-a-split content-b content-b-split) - :difference (create-difference content-a content-a-split content-b content-b-split) - :intersection (create-intersection content-a content-a-split content-b content-b-split) + :union (create-union content-a content-a-split content-b content-b-split sr-a sr-b) + :difference (create-difference content-a content-a-split content-b content-b-split sr-a sr-b) + :intersection (create-intersection content-a content-a-split content-b content-b-split sr-a sr-b) :exclude (create-exclusion content-a-split content-b-split))] (->> (fix-move-to bool-content) diff --git a/common/src/app/common/path/commands.cljc b/common/src/app/common/path/commands.cljc index 80737db8cf..a79b346760 100644 --- a/common/src/app/common/path/commands.cljc +++ b/common/src/app/common/path/commands.cljc @@ -17,8 +17,8 @@ (command->point command)))) ([command] - (when-not (nil? command) - (let [{{:keys [x y]} :params} command] + (when command + (let [{:keys [x y]} (:params command)] (gpt/point x y))))) @@ -147,7 +147,7 @@ []))))) (defn opposite-index - "Calculate sthe opposite index given a prefix and an index" + "Calculates the opposite index given a prefix and an index" [content index prefix] (let [point (if (= prefix :c2) diff --git a/common/src/app/common/path/subpaths.cljc b/common/src/app/common/path/subpaths.cljc index aed3171950..7501cba90b 100644 --- a/common/src/app/common/path/subpaths.cljc +++ b/common/src/app/common/path/subpaths.cljc @@ -126,7 +126,7 @@ (pt= (:from subpath) (:to subpath))) (defn close-subpaths - "Searches a path for posible supaths that can create closed loops and merge them" + "Searches a path for possible supaths that can create closed loops and merge them" [content] (let [subpaths (get-subpaths content) closed-subpaths diff --git a/common/src/app/common/perf.cljc b/common/src/app/common/perf.cljc new file mode 100644 index 0000000000..4ec78df503 --- /dev/null +++ b/common/src/app/common/perf.cljc @@ -0,0 +1,122 @@ +;; 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.perf + (:require + #?(:clj [criterium.core :as cri]) + [app.common.math :as mth] + [app.common.uuid :as uuid] + [cuerdas.core :as str])) + +(defn timestamp [] + #?(:cljs (js/performance.now) + :clj (. System (nanoTime)))) + +(defonce measures (atom {})) + +(defn start + ([] + (start (uuid/next))) + + ([key] + (swap! measures assoc key (timestamp)) + key)) + +(defn measure + [key] + (- (timestamp) (get @measures key))) + +(def s-to-ns (* 1000 1000 1000)) + +(def default-jvm-bench-options + {:max-gc-attempts 100 + :samples 10 + :target-execution-time (* 1 s-to-ns) + :warmup-jit-period (* 1 s-to-ns) + :tail-quantile 0.025 + :overhead 0 + :bootstrap-size 500}) + +(defn scale-time + "Determine a scale factor and unit for displaying a time." + [measurement] + (cond + (> measurement 60) [(/ 60) "min"] + (< measurement 1e-6) [1e9 "ns"] + (< measurement 1e-3) [1e6 "µs"] + (< measurement 1) [1e3 "ms"] + :else [1 "sec"])) + +(defn format-time + [value] + (let [[scale unit] (scale-time value)] + (str/format "%s%s" (mth/precision (* scale value) 2) unit))) + +(defn benchmark + "A helper function for perform a unitari benchmark on JS/CLJS. It + uses browser native api so it only suitable to be executed in + browser." + [& {:keys [f name target samples] + :or {name "unnamed" + samples 10 + target 1} + :as options}] + #?(:cljs + (let [max-iterations (or (:max-iterations options) 100000) + + exec-and-measure + (fn [] + (let [t0 (js/performance.now) + x (f) + t1 (js/performance.now)] + (when-not x + (throw (ex-info "missing return value" {}))) + + (/ (- t1 t0) 1000))) + + calculate-iterations + (fn [single-duration minimum] + (let [result (mth/floor (/ (* samples target) (max single-duration minimum)))] + (min result max-iterations))) + + iterations + (atom (calculate-iterations (exec-and-measure) 0.0001))] + + (println "=> benchmarking:" name) + (println "--> WARM: " @iterations) + (loop [i 0 t 0] + (if (< i @iterations) + (let [measure (exec-and-measure)] + (recur (inc i) (+ t measure))) + (do + (reset! iterations (calculate-iterations (/ t @iterations) 0.00001))))) + + (println "--> BENCH:" @iterations) + ;; benchmarking + (loop [i 0 t 0] + (if (< i @iterations) + (recur (inc i) (+ t (exec-and-measure))) + (let [mean (/ t @iterations)] + (println "--> TOTAL:" (format-time t)) + (println "--> MEAN: " (format-time mean))))) + nil) + + :clj + (do + (println "=> benchmarking:" name) + (let [result (cri/benchmark* f (assoc default-jvm-bench-options + :samples samples + :target-execution-time (* target s-to-ns) + :warmup-jit-period (* target s-to-ns))) + iterations (:execution-count result) + total (:total-time result) + mean (first (:sample-mean result))] + + (println "--> BENCH:" iterations) + (println "--> TOTAL:" (format-time total)) + (println "--> MEAN: " (format-time mean))) + nil))) + diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index e674a32b5a..85c4a6912c 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -14,7 +14,7 @@ ;; NOTE: don't remove this, causes exception on advanced build ;; because of some strange interaction with cljs.spec.alpha and - ;; modules spliting. + ;; modules splitting. [app.common.exceptions :as ex] [app.common.geom.point :as gpt] [app.common.uuid :as uuid] @@ -31,6 +31,8 @@ (def max-safe-int (int 1e6)) (def min-safe-int (int -1e6)) +(def valid? s/valid?) + ;; --- Conformers (defn uuid-conformer @@ -215,8 +217,7 @@ (ex/raise :type :assertion :code :spec-validation :hint hint - :ctx ctx - ::s/problems (::s/problems data))))) + ::ex/data (merge ctx data))))) (defmacro assert "Development only assertion macro." @@ -256,7 +257,7 @@ (let [data (s/explain-data spec data)] (throw (ex/error :type :validation :code :spec-validation - ::s/problems (::s/problems data))))) + ::ex/data data)))) result)) (defmacro instrument! diff --git a/common/src/app/common/text.cljc b/common/src/app/common/text.cljc index f4c450f1a1..a48f525d4f 100644 --- a/common/src/app/common/text.cljc +++ b/common/src/app/common/text.cljc @@ -6,6 +6,7 @@ (ns app.common.text (:require + [app.common.colors :as clr] [app.common.data :as d] [app.common.transit :as t] [clojure.walk :as walk] @@ -25,7 +26,7 @@ :text-transform "none" :text-align "left" :text-decoration "none" - :fill-color nil + :fill-color clr/black :fill-opacity 1}) (def typography-fields @@ -247,6 +248,3 @@ {:blocks (reduce #(conj %1 (build-block %2)) [] (node-seq #(= (:type %) "paragraph") root)) :entityMap {}})) - - - diff --git a/common/src/app/common/transit.cljc b/common/src/app/common/transit.cljc index 7f1eddda54..4333051ee7 100644 --- a/common/src/app/common/transit.cljc +++ b/common/src/app/common/transit.cljc @@ -72,7 +72,15 @@ (fn [v] (into {} v)))) (def matrix-read-handler - (t/read-handler gmt/map->Matrix)) + (t/read-handler (fn [data] + #?(:cljs (gmt/map->Matrix data) + :clj (let [{:keys [a b c d e f]} data] + (gmt/matrix (double a) + (double b) + (double c) + (double d) + (double e) + (double f))))))) ;; --- ORDERED SET @@ -144,19 +152,28 @@ ;; --- Low-Level Api +#?(:clj + (def read-handlers + (t/read-handler-map +read-handlers+))) + +#?(:clj + (def write-handlers + (t/write-handler-map +write-handlers+))) + #?(:clj (defn reader ([istream] (reader istream nil)) ([istream {:keys [type] :or {type :json}}] - (t/reader istream type {:handlers +read-handlers+})))) + (t/reader istream type {:handlers read-handlers})))) #?(:clj (defn writer ([ostream] (writer ostream nil)) ([ostream {:keys [type] :or {type :json}}] - (t/writer ostream type {:handlers +write-handlers+})))) + (t/writer ostream type {:handlers write-handlers})))) + #?(:clj (defn read! [reader] diff --git a/common/src/app/common/types/interactions.cljc b/common/src/app/common/types/interactions.cljc index 838dbff9cf..5903662c34 100644 --- a/common/src/app/common/types/interactions.cljc +++ b/common/src/app/common/types/interactions.cljc @@ -42,6 +42,46 @@ (s/def ::event-opts (s/multi-spec event-opts-spec ::event-type)) +;; -- Animation options + +(s/def ::animation-type #{:dissolve + :slide + :push}) +(s/def ::duration ::us/safe-integer) +(s/def ::easing #{:linear + :ease + :ease-in + :ease-out + :ease-in-out}) +(s/def ::way #{:in + :out}) +(s/def ::direction #{:right + :left + :up + :down}) +(s/def ::offset-effect ::us/boolean) + +(defmulti animation-spec :animation-type) + +(defmethod animation-spec :dissolve [_] + (s/keys :req-un [::duration + ::easing])) + +(defmethod animation-spec :slide [_] + (s/keys :req-un [::duration + ::easing + ::way + ::direction + ::offset-effect])) + +(defmethod animation-spec :push [_] + (s/keys :req-un [::duration + ::easing + ::direction])) + +(s/def ::animation + (s/multi-spec animation-spec ::animation-type)) + ;; -- Options depending on action type (s/def ::action-type #{:navigate @@ -69,24 +109,29 @@ (defmulti action-opts-spec :action-type) (defmethod action-opts-spec :navigate [_] - (s/keys :opt-un [::destination ::preserve-scroll])) + (s/keys :opt-un [::destination + ::preserve-scroll + ::animation])) (defmethod action-opts-spec :open-overlay [_] (s/keys :req-un [::overlay-position ::overlay-pos-type] :opt-un [::destination ::close-click-outside - ::background-overlay])) + ::background-overlay + ::animation])) (defmethod action-opts-spec :toggle-overlay [_] (s/keys :req-un [::overlay-position ::overlay-pos-type] :opt-un [::destination ::close-click-outside - ::background-overlay])) + ::background-overlay + ::animation])) (defmethod action-opts-spec :close-overlay [_] - (s/keys :opt-un [::destination])) + (s/keys :opt-un [::destination + ::animation])) (defmethod action-opts-spec :prev-screen [_] (s/keys :req-un [])) @@ -114,13 +159,15 @@ (def default-interaction {:event-type :click :action-type :navigate - :destination nil}) + :destination nil + :preserve-scroll false}) (def default-delay 600) ;; -- Helpers for interaction (declare calc-overlay-pos-initial) +(declare allowed-animation?) (defn set-event-type [interaction event-type shape] @@ -140,42 +187,46 @@ (assoc interaction :event-type event-type)))) - (defn set-action-type [interaction action-type] (us/verify ::interaction interaction) (us/verify ::action-type action-type) - (if (= (:action-type interaction) action-type) - interaction - (case action-type + (let [new-interaction + (if (= (:action-type interaction) action-type) + interaction + (case action-type + :navigate + (assoc interaction + :action-type action-type + :destination (get interaction :destination) + :preserve-scroll (get interaction :preserve-scroll false)) - :navigate - (assoc interaction - :action-type action-type - :destination (get interaction :destination) - :preserve-scroll false) + (:open-overlay :toggle-overlay) + (let [overlay-pos-type (get interaction :overlay-pos-type :center) + overlay-position (get interaction :overlay-position (gpt/point 0 0))] + (assoc interaction + :action-type action-type + :overlay-pos-type overlay-pos-type + :overlay-position overlay-position)) - (:open-overlay :toggle-overlay) - (let [overlay-pos-type (get interaction :overlay-pos-type :center) - overlay-position (get interaction :overlay-position (gpt/point 0 0))] - (assoc interaction - :action-type action-type - :overlay-pos-type overlay-pos-type - :overlay-position overlay-position)) + :close-overlay + (assoc interaction + :action-type action-type + :destination (get interaction :destination)) - :close-overlay - (assoc interaction - :action-type action-type - :destination (get interaction :destination)) + :prev-screen + (assoc interaction + :action-type action-type) - :prev-screen - (assoc interaction - :action-type action-type) + :open-url + (assoc interaction + :action-type action-type + :url (get interaction :url ""))))] - :open-url - (assoc interaction - :action-type action-type - :url (get interaction :url ""))))) + (cond-> new-interaction + (not (allowed-animation? action-type + (-> new-interaction :animation :animation-type))) + (dissoc :animation-type :animation)))) (defn has-delay [interaction] @@ -198,10 +249,6 @@ (and (has-destination interaction) (some? (:destination interaction)))) -(defn has-preserve-scroll - [interaction] - (= (:action-type interaction) :navigate)) - (defn set-destination [interaction destination] (us/verify ::interaction interaction) @@ -216,6 +263,10 @@ (assoc :overlay-pos-type :center :overlay-position (gpt/point 0 0)))) +(defn has-preserve-scroll + [interaction] + (= (:action-type interaction) :navigate)) + (defn set-preserve-scroll [interaction preserve-scroll] (us/verify ::interaction interaction) @@ -288,7 +339,7 @@ (defn- calc-overlay-pos-initial [destination shape objects overlay-pos-type] - (if (= overlay-pos-type :manual) + (if (and (= overlay-pos-type :manual) (some? destination)) (let [dest-frame (get objects destination) overlay-size (:selrect dest-frame) orig-frame (if (= (:type shape) :frame) @@ -338,6 +389,129 @@ :manual (gpt/add (:overlay-position interaction) frame-offset))))) +(defn has-animation? + [interaction] + (#{:navigate :open-overlay :close-overlay :toggle-overlay} (:action-type interaction))) + +(defn allow-push? + [action-type] + ; Push animation is not allowed for overlay actions + (= :navigate action-type)) + +(defn allowed-animation? + [action-type animation-type] + ; Some specific combinations are forbidden, but may occur if the action type + ; is changed from a type that allows the animation to another one that doesn't. + ; Currently the only case is an overlay action with push animation. + (or (not= animation-type :push) + (allow-push? action-type))) + +(defn set-animation-type + [interaction animation-type] + (us/verify ::interaction interaction) + (us/verify (s/nilable ::animation-type) animation-type) + (assert (has-animation? interaction)) + (assert (allowed-animation? (:action-type interaction) animation-type)) + (if (= (-> interaction :animation :animation-type) animation-type) + interaction + (if (nil? animation-type) + (dissoc interaction :animation) + (cond-> interaction + :always + (update :animation assoc :animation-type animation-type) + + (= animation-type :dissolve) + (update :animation assoc + :duration (get-in interaction [:animation :duration] 300) + :easing (get-in interaction [:animation :easing] :linear)) + + (= animation-type :slide) + (update :animation assoc + :duration (get-in interaction [:animation :duration] 300) + :easing (get-in interaction [:animation :easing] :linear) + :way (get-in interaction [:animation :way] :in) + :direction (get-in interaction [:animation :direction] :right) + :offset-effect (get-in interaction [:animation :offset-effect] false)) + + (= animation-type :push) + (update :animation assoc + :duration (get-in interaction [:animation :duration] 300) + :easing (get-in interaction [:animation :easing] :linear) + :direction (get-in interaction [:animation :direction] :right)))))) + +(defn has-duration? + [interaction] + (#{:dissolve :slide :push} (-> interaction :animation :animation-type))) + +(defn set-duration + [interaction duration] + (us/verify ::interaction interaction) + (us/verify ::duration duration) + (assert (has-duration? interaction)) + (update interaction :animation assoc :duration duration)) + +(defn has-easing? + [interaction] + (#{:dissolve :slide :push} (-> interaction :animation :animation-type))) + +(defn set-easing + [interaction easing] + (us/verify ::interaction interaction) + (us/verify ::easing easing) + (assert (has-easing? interaction)) + (update interaction :animation assoc :easing easing)) + +(defn has-way? + [interaction] + ; Way is ignored in slide animations of overlay actions + (and (= (:action-type interaction) :navigate) + (= (-> interaction :animation :animation-type) :slide))) + +(defn set-way + [interaction way] + (us/verify ::interaction interaction) + (us/verify ::way way) + (assert (has-way? interaction)) + (update interaction :animation assoc :way way)) + +(defn has-direction? + [interaction] + (#{:slide :push} (-> interaction :animation :animation-type))) + +(defn set-direction + [interaction direction] + (us/verify ::interaction interaction) + (us/verify ::direction direction) + (assert (has-direction? interaction)) + (update interaction :animation assoc :direction direction)) + +(defn invert-direction + [animation] + (us/verify (s/nilable ::animation) animation) + (case (:direction animation) + :right + (assoc animation :direction :left) + :left + (assoc animation :direction :right) + :up + (assoc animation :direction :down) + :down + (assoc animation :direction :up) + animation)) + +(defn has-offset-effect? + [interaction] + ; Offset-effect is ignored in slide animations of overlay actions + (and (= (:action-type interaction) :navigate) + (= (-> interaction :animation :animation-type) :slide))) + +(defn set-offset-effect + [interaction offset-effect] + (us/verify ::interaction interaction) + (us/verify ::offset-effect offset-effect) + (assert (has-offset-effect? interaction)) + (update interaction :animation assoc :offset-effect offset-effect)) + ;; -- Helpers for interactions (defn add-interaction diff --git a/common/src/app/common/types/page_options.cljc b/common/src/app/common/types/page_options.cljc index 968be643c8..4901b87222 100644 --- a/common/src/app/common/types/page_options.cljc +++ b/common/src/app/common/types/page_options.cljc @@ -29,12 +29,12 @@ :artboard-grid/color])) (s/def :artboard-grid/column - (s/keys :req-un [:artboard-grid/size - :artboard-grid/color + (s/keys :req-un [:artboard-grid/color] + :opt-un [:artboard-grid/size + :artboard-grid/type + :artboard-grid/item-length :artboard-grid/margin - :artboard-grid/gutter] - :opt-un [:artboard-grid/type - :artboard-grid/item-length])) + :artboard-grid/gutter])) (s/def :artboard-grid/row :artboard-grid/column) diff --git a/common/src/app/common/version.cljc b/common/src/app/common/version.cljc index cb79885cfc..348052c40a 100644 --- a/common/src/app/common/version.cljc +++ b/common/src/app/common/version.cljc @@ -14,7 +14,7 @@ (defn parse [data] (cond - (= data "%version%") + (str/starts-with? data "%") {:full "develop" :branch "develop" :base "0.0.0" diff --git a/common/test/app/common/data_test.cljc b/common/test/app/common/data_test.cljc new file mode 100644 index 0000000000..4cf9c661f2 --- /dev/null +++ b/common/test/app/common/data_test.cljc @@ -0,0 +1,53 @@ +;; 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.data-test + (:require + [app.common.data :as d] + [clojure.test :as t])) + +(t/deftest concat-vec + (t/is (= [] (d/concat-vec))) + (t/is (= [1] (d/concat-vec [1]))) + (t/is (= [1] (d/concat-vec #{1}))) + (t/is (= [1 2] (d/concat-vec [1] #{2}))) + (t/is (= [1 2] (d/concat-vec '(1) [2])))) + +(t/deftest concat-set + (t/is (= #{} (d/concat-set))) + (t/is (= #{1 2} + (d/concat-set [1] [2])))) + +(t/deftest remove-at-index + (t/is (= [1 2 3 4] + (d/remove-at-index [1 2 3 4 5] 4))) + + + (t/is (= [1 2 3 4] + (d/remove-at-index [5 1 2 3 4] 0))) + + (t/is (= [1 2 3 4] + (d/remove-at-index [1 5 2 3 4] 1))) + ) + +(t/deftest with-next + (t/is (= [[0 1] [1 2] [2 3] [3 4] [4 nil]] + (d/with-next (range 5))))) + +(t/deftest with-prev + (t/is (= [[0 nil] [1 0] [2 1] [3 2] [4 3]] + (d/with-prev (range 5))))) + +(t/deftest with-prev-next + (t/is (= [[0 nil 1] [1 0 2] [2 1 3] [3 2 4] [4 3 nil]] + (d/with-prev-next (range 5))))) + +(t/deftest join + (t/is (= [[1 :a] [1 :b] [2 :a] [2 :b] [3 :a] [3 :b]] + (d/join [1 2 3] [:a :b]))) + (t/is (= [1 10 100 2 20 200 3 30 300] + (d/join [1 2 3] [1 10 100] *)))) + diff --git a/common/test/app/common/geom_shapes_test.cljc b/common/test/app/common/geom_shapes_test.cljc index 2b6a6fc57c..a7b190e00e 100644 --- a/common/test/app/common/geom_shapes_test.cljc +++ b/common/test/app/common/geom_shapes_test.cljc @@ -175,15 +175,16 @@ :x :y :width :height :x1 :y1 :x2 :y2)) :rect :path)) - (t/testing "Transform shape with invalid selrect fails gracefuly" + (t/testing "Transform shape with invalid selrect fails gracefully" (t/are [type selrect] (let [modifiers {:displacement (gmt/matrix)} shape-before (-> (create-test-shape type {:modifiers modifiers}) (assoc :selrect selrect)) shape-after (gsh/transform-shape shape-before {:round-coords? false})] - (= (:selrect shape-before) (:selrect shape-after))) + (= (:selrect shape-before) + (:selrect shape-after))) - :rect {:x 0 :y 0 :width ##Inf :height ##Inf} - :path {:x 0 :y 0 :width ##Inf :height ##Inf} + :rect {:x 0.0 :y 0.0 :x1 0.0 :y1 0.0 :x2 ##Inf :y2 ##Inf :width ##Inf :height ##Inf} + :path {:x 0.0 :y 0.0 :x1 0.0 :y1 0.0 :x2 ##Inf :y2 ##Inf :width ##Inf :height ##Inf} :rect nil :path nil))) diff --git a/common/test/app/common/geom_test.cljc b/common/test/app/common/geom_test.cljc index fce01a0613..8a4b7af2ff 100644 --- a/common/test/app/common/geom_test.cljc +++ b/common/test/app/common/geom_test.cljc @@ -71,21 +71,21 @@ (t/deftest matrix-constructors-test (let [m (gmt/matrix)] - (t/is (= (str m) "matrix(1,0,0,1,0,0)"))) + (t/is (= (str m) "matrix(1.0,0.0,0.0,1.0,0.0,0.0)"))) (let [m (gmt/matrix 1 1 1 2 2 2)] - (t/is (= (str m) "matrix(1,1,1,2,2,2)")))) + (t/is (= (str m) "matrix(1.0,1.0,1.0,2.0,2.0,2.0)")))) (t/deftest matrix-translate-test (let [m (-> (gmt/matrix) (gmt/translate (gpt/point 2 10)))] - (t/is (= (str m) "matrix(1,0,0,1,2,10)")))) + (t/is (= (str m) "matrix(1.0,0.0,0.0,1.0,2.0,10.0)")))) (t/deftest matrix-scale-test (let [m (-> (gmt/matrix) (gmt/scale (gpt/point 2)))] - (t/is (= (str m) "matrix(2,0,0,2,0,0)")))) + (t/is (= (str m) "matrix(2.0,0.0,0.0,2.0,0.0,0.0)")))) (t/deftest matrix-rotate-test (let [m (-> (gmt/matrix) (gmt/rotate 10))] - (t/is (= (str m) "matrix(0.984807753012208,0.17364817766693033,-0.17364817766693033,0.984807753012208,0,0)")))) + (t/is (= (str m) "matrix(0.984807753012208,0.17364817766693033,-0.17364817766693033,0.984807753012208,0.0,0.0)")))) diff --git a/common/test/app/common/pages_helpers_test.cljc b/common/test/app/common/pages_helpers_test.cljc new file mode 100644 index 0000000000..058ff6422e --- /dev/null +++ b/common/test/app/common/pages_helpers_test.cljc @@ -0,0 +1,59 @@ +;; 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.pages-helpers-test + (:require + [clojure.test :as t] + [clojure.pprint :refer [pprint]] + [app.common.pages.helpers :as cph])) + +(t/deftest insert-at-index + ;; insert different object + (t/is (= (cph/insert-at-index [:a :b] 1 [:c :d]) + [:a :c :d :b])) + + ;; insert on the start + (t/is (= (cph/insert-at-index [:a :b] 0 [:c]) + [:c :a :b])) + + ;; insert on the end 1 + (t/is (= (cph/insert-at-index [:a :b] 2 [:c]) + [:a :b :c])) + + ;; insert on the end with not existing index + (t/is (= (cph/insert-at-index [:a :b] 10 [:c]) + [:a :b :c])) + + ;; insert existing in a contiguos index + (t/is (= (cph/insert-at-index [:a :b] 1 [:a]) + [:a :b])) + + ;; insert existing in the same index + (t/is (= (cph/insert-at-index [:a :b] 0 [:a]) + [:a :b])) + + ;; insert existing in other index case 1 + (t/is (= (cph/insert-at-index [:a :b :c] 2 [:a]) + [:b :a :c])) + + ;; insert existing in other index case 2 + (t/is (= (cph/insert-at-index [:a :b :c :d] 0 [:d]) + [:d :a :b :c])) + + ;; insert existing in other index case 3 + (t/is (= (cph/insert-at-index [:a :b :c :d] 1 [:a]) + [:a :b :c :d])) + + ) + + +(t/deftest parse-path-name + (t/is (= ["foo" "bar"] (cph/parse-path-name "foo/bar"))) + (t/is (= ["" "foo"] (cph/parse-path-name "foo"))) + (t/is (= ["" "foo"] (cph/parse-path-name "/foo"))) + (t/is (= ["" ""] (cph/parse-path-name ""))) + (t/is (= ["" ""] (cph/parse-path-name nil))) + ) diff --git a/common/test/app/common/pages_migrations_test.cljc b/common/test/app/common/pages_migrations_test.cljc index ab697886eb..12950ca0a7 100644 --- a/common/test/app/common/pages_migrations_test.cljc +++ b/common/test/app/common/pages_migrations_test.cljc @@ -71,7 +71,7 @@ :components {} :version 7} - expct (-> data + expect (-> data (update-in [:pages-index page-id :objects] dissoc (uuid/custom 1 2) (uuid/custom 1 3) @@ -84,8 +84,8 @@ res (cpm/migrate-data data)] ;; (pprint res) - ;; (pprint expct) + ;; (pprint expect) - (t/is (= (dissoc expct :version) + (t/is (= (dissoc expect :version) (dissoc res :version))) )) diff --git a/common/test/app/common/types_interactions_test.cljc b/common/test/app/common/types_interactions_test.cljc new file mode 100644 index 0000000000..f5df8c448d --- /dev/null +++ b/common/test/app/common/types_interactions_test.cljc @@ -0,0 +1,574 @@ +;; 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.types-interactions-test + (:require + [clojure.test :as t] + [clojure.pprint :refer [pprint]] + [app.common.exceptions :as ex] + [app.common.pages.init :as cpi] + [app.common.types.interactions :as cti] + [app.common.uuid :as uuid] + [app.common.geom.point :as gpt])) + +(t/deftest set-event-type + (let [interaction cti/default-interaction + shape (cpi/make-minimal-shape :rect) + frame (cpi/make-minimal-shape :frame)] + + (t/testing "Set event type unchanged" + (let [new-interaction + (cti/set-event-type interaction :click shape)] + (t/is (= :click (:event-type new-interaction))))) + + (t/testing "Set event type changed" + (let [new-interaction + (cti/set-event-type interaction :mouse-press shape)] + (t/is (= :mouse-press (:event-type new-interaction))))) + + (t/testing "Set after delay on non-frame" + (let [result (ex/try + (cti/set-event-type interaction :after-delay shape))] + (t/is (ex/exception? result)))) + + (t/testing "Set after delay on frame" + (let [new-interaction + (cti/set-event-type interaction :after-delay frame)] + (t/is (= :after-delay (:event-type new-interaction))) + (t/is (= 600 (:delay new-interaction))))) + + (t/testing "Set after delay with previous data" + (let [interaction (assoc interaction :delay 300) + new-interaction + (cti/set-event-type interaction :after-delay frame)] + (t/is (= :after-delay (:event-type new-interaction))) + (t/is (= 300 (:delay new-interaction))))))) + + +(t/deftest set-action-type + (let [interaction cti/default-interaction] + + (t/testing "Set action type unchanged" + (let [new-interaction + (cti/set-action-type interaction :navigate)] + (t/is (= :navigate (:action-type new-interaction))))) + + (t/testing "Set action type changed" + (let [new-interaction + (cti/set-action-type interaction :prev-screen)] + (t/is (= :prev-screen (:action-type new-interaction))))) + + (t/testing "Set action type navigate" + (let [interaction {:event-type :click + :action-type :prev-screen} + new-interaction + (cti/set-action-type interaction :navigate)] + (t/is (= :navigate (:action-type new-interaction))) + (t/is (nil? (:destination new-interaction))) + (t/is (= false (:preserve-scroll new-interaction))))) + + (t/testing "Set action type navigate with previous data" + (let [destination (uuid/next) + interaction {:event-type :click + :action-type :prev-screen + :destination destination + :preserve-scroll true} + new-interaction + (cti/set-action-type interaction :navigate)] + (t/is (= :navigate (:action-type new-interaction))) + (t/is (= destination (:destination new-interaction))) + (t/is (= true (:preserve-scroll new-interaction))))) + + (t/testing "Set action type open-overlay" + (let [new-interaction + (cti/set-action-type interaction :open-overlay)] + (t/is (= :open-overlay (:action-type new-interaction))) + (t/is (= :center (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))) + + (t/testing "Set action type open-overlay with previous data" + (let [interaction (assoc interaction :overlay-pos-type :top-left + :overlay-position (gpt/point 100 200)) + new-interaction + (cti/set-action-type interaction :open-overlay)] + (t/is (= :open-overlay (:action-type new-interaction))) + (t/is (= :top-left (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 100 200) (:overlay-position new-interaction))))) + + (t/testing "Set action type toggle-overlay" + (let [new-interaction + (cti/set-action-type interaction :toggle-overlay)] + (t/is (= :toggle-overlay (:action-type new-interaction))) + (t/is (= :center (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))) + + (t/testing "Set action type toggle-overlay with previous data" + (let [interaction (assoc interaction :overlay-pos-type :top-left + :overlay-position (gpt/point 100 200)) + new-interaction + (cti/set-action-type interaction :toggle-overlay)] + (t/is (= :toggle-overlay (:action-type new-interaction))) + (t/is (= :top-left (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 100 200) (:overlay-position new-interaction))))) + + (t/testing "Set action type close-overlay" + (let [new-interaction + (cti/set-action-type interaction :close-overlay)] + (t/is (= :close-overlay (:action-type new-interaction))) + (t/is (nil? (:destination new-interaction))))) + + (t/testing "Set action type close-overlay with previous data" + (let [destination (uuid/next) + interaction (assoc interaction :destination destination) + new-interaction + (cti/set-action-type interaction :close-overlay)] + (t/is (= :close-overlay (:action-type new-interaction))) + (t/is (= destination (:destination new-interaction))))) + + (t/testing "Set action type prev-screen" + (let [new-interaction + (cti/set-action-type interaction :prev-screen)] + (t/is (= :prev-screen (:action-type new-interaction))))) + + (t/testing "Set action type open-url" + (let [new-interaction + (cti/set-action-type interaction :open-url)] + (t/is (= :open-url (:action-type new-interaction))) + (t/is (= "" (:url new-interaction))))) + + (t/testing "Set action type open-url with previous data" + (let [interaction (assoc interaction :url "https://example.com") + new-interaction + (cti/set-action-type interaction :open-url)] + (t/is (= :open-url (:action-type new-interaction))) + (t/is (= "https://example.com" (:url new-interaction))))))) + + +(t/deftest option-delay + (let [frame (cpi/make-minimal-shape :frame) + i1 cti/default-interaction + i2 (cti/set-event-type i1 :after-delay frame)] + + (t/testing "Has delay" + (t/is (not (cti/has-delay i1))) + (t/is (cti/has-delay i2))) + + (t/testing "Set delay" + (let [new-interaction (cti/set-delay i2 1000)] + (t/is (= 1000 (:delay new-interaction))))))) + + +(t/deftest option-destination + (let [destination (uuid/next) + i1 cti/default-interaction + i2 (cti/set-action-type i1 :prev-screen) + i3 (cti/set-action-type i1 :open-overlay)] + + (t/testing "Has destination" + (t/is (cti/has-destination i1)) + (t/is (not (cti/has-destination i2)))) + + (t/testing "Set destination" + (let [new-interaction (cti/set-destination i1 destination)] + (t/is (= destination (:destination new-interaction))) + (t/is (nil? (:overlay-pos-type new-interaction))) + (t/is (nil? (:overlay-position new-interaction))))) + + (t/testing "Set destination of overlay" + (let [new-interaction (cti/set-destination i3 destination)] + (t/is (= destination (:destination new-interaction))) + (t/is (= :center (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))))) + + +(t/deftest option-preserve-scroll + (let [i1 cti/default-interaction + i2 (cti/set-action-type i1 :prev-screen)] + + (t/testing "Has preserve-scroll" + (t/is (cti/has-preserve-scroll i1)) + (t/is (not (cti/has-preserve-scroll i2)))) + + (t/testing "Set preserve-scroll" + (let [new-interaction (cti/set-preserve-scroll i1 true)] + (t/is (= true (:preserve-scroll new-interaction))))))) + + +(t/deftest option-url + (let [i1 cti/default-interaction + i2 (cti/set-action-type i1 :open-url)] + + (t/testing "Has url" + (t/is (not (cti/has-url i1))) + (t/is (cti/has-url i2))) + + (t/testing "Set url" + (let [new-interaction (cti/set-url i2 "https://example.com")] + (t/is (= "https://example.com" (:url new-interaction))))))) + + +(t/deftest option-overlay-opts + (let [base-frame (-> (cpi/make-minimal-shape :frame) + (assoc-in [:selrect :width] 100) + (assoc-in [:selrect :height] 100)) + overlay-frame (-> (cpi/make-minimal-shape :frame) + (assoc-in [:selrect :width] 30) + (assoc-in [:selrect :height] 20)) + objects {(:id base-frame) base-frame + (:id overlay-frame) overlay-frame} + + i1 cti/default-interaction + i2 (cti/set-action-type i1 :open-overlay) + i3 (-> i1 + (cti/set-action-type :open-overlay) + (cti/set-destination (:id overlay-frame)))] + + (t/testing "Has overlay options" + (t/is (not (cti/has-overlay-opts i1))) + (t/is (cti/has-overlay-opts i2))) + + (t/testing "Set overlay-pos-type without destination" + (let [new-interaction (cti/set-overlay-pos-type i2 :top-right base-frame objects)] + (t/is (= :top-right (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))) + + (t/testing "Set overlay-pos-type with destination and auto" + (let [new-interaction (cti/set-overlay-pos-type i3 :bottom-right base-frame objects)] + (t/is (= :bottom-right (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 0 0) (:overlay-position new-interaction))))) + + (t/testing "Set overlay-pos-type with destination and manual" + (let [new-interaction (cti/set-overlay-pos-type i3 :manual base-frame objects)] + (t/is (= :manual (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 35 40) (:overlay-position new-interaction))))) + + (t/testing "Toggle overlay-pos-type" + (let [new-interaction (cti/toggle-overlay-pos-type i3 :center base-frame objects) + new-interaction-2 (cti/toggle-overlay-pos-type new-interaction :center base-frame objects) + new-interaction-3 (cti/toggle-overlay-pos-type new-interaction-2 :top-right base-frame objects)] + (t/is (= :manual (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 35 40) (:overlay-position new-interaction))) + (t/is (= :center (:overlay-pos-type new-interaction-2))) + (t/is (= (gpt/point 0 0) (:overlay-position new-interaction-2))) + (t/is (= :top-right (:overlay-pos-type new-interaction-3))) + (t/is (= (gpt/point 0 0) (:overlay-position new-interaction-3))))) + + (t/testing "Set overlay-position" + (let [new-interaction (cti/set-overlay-position i3 (gpt/point 50 60))] + (t/is (= :manual (:overlay-pos-type new-interaction))) + (t/is (= (gpt/point 50 60) (:overlay-position new-interaction))))) + + (t/testing "Set close-click-outside" + (let [new-interaction (cti/set-close-click-outside i3 true)] + (t/is (not (:close-click-outside i3))) + (t/is (:close-click-outside new-interaction)))) + + (t/testing "Set background-overlay" + (let [new-interaction (cti/set-background-overlay i3 true)] + (t/is (not (:background-overlay i3))) + (t/is (:background-overlay new-interaction)))))) + + +(t/deftest animation-checks + (let [i1 cti/default-interaction + i2 (cti/set-action-type i1 :open-overlay) + i3 (cti/set-action-type i1 :toggle-overlay) + i4 (cti/set-action-type i1 :close-overlay) + i5 (cti/set-action-type i1 :prev-screen) + i6 (cti/set-action-type i1 :open-url)] + + (t/testing "Has animation?" + (t/is (cti/has-animation? i1)) + (t/is (cti/has-animation? i2)) + (t/is (cti/has-animation? i3)) + (t/is (cti/has-animation? i4)) + (t/is (not (cti/has-animation? i5))) + (t/is (not (cti/has-animation? i6)))) + + (t/testing "Valid push?" + (t/is (cti/allow-push? (:action-type i1))) + (t/is (not (cti/allow-push? (:action-type i2)))) + (t/is (not (cti/allow-push? (:action-type i3)))) + (t/is (not (cti/allow-push? (:action-type i4)))) + (t/is (not (cti/allow-push? (:action-type i5)))) + (t/is (not (cti/allow-push? (:action-type i6))))))) + + +(t/deftest set-animation-type + (let [i1 cti/default-interaction + i2 (cti/set-animation-type i1 :dissolve)] + + (t/testing "Set animation type nil" + (let [new-interaction + (cti/set-animation-type i1 nil)] + (t/is (nil? (-> new-interaction :animation :animation-type))))) + + (t/testing "Set animation type unchanged" + (let [new-interaction + (cti/set-animation-type i2 :dissolve)] + (t/is (= :dissolve (-> new-interaction :animation :animation-type))))) + + (t/testing "Set animation type changed" + (let [new-interaction + (cti/set-animation-type i2 :slide)] + (t/is (= :slide (-> new-interaction :animation :animation-type))))) + + (t/testing "Set animation type reset" + (let [new-interaction + (cti/set-animation-type i2 nil)] + (t/is (nil? (-> new-interaction :animation))))) + + (t/testing "Set animation type dissolve" + (let [new-interaction + (cti/set-animation-type i1 :dissolve)] + (t/is (= :dissolve (-> new-interaction :animation :animation-type))) + (t/is (= 300 (-> new-interaction :animation :duration))) + (t/is (= :linear (-> new-interaction :animation :easing))))) + + (t/testing "Set animation type dissolve with previous data" + (let [interaction (assoc i1 :animation {:animation-type :slide + :duration 1000 + :easing :ease-out + :way :out + :direction :left + :offset-effect true}) + new-interaction + (cti/set-animation-type interaction :dissolve)] + (t/is (= :dissolve (-> new-interaction :animation :animation-type))) + (t/is (= 1000 (-> new-interaction :animation :duration))) + (t/is (= :ease-out (-> new-interaction :animation :easing))))) + + (t/testing "Set animation type slide" + (let [new-interaction + (cti/set-animation-type i1 :slide)] + (t/is (= :slide (-> new-interaction :animation :animation-type))) + (t/is (= 300 (-> new-interaction :animation :duration))) + (t/is (= :linear (-> new-interaction :animation :easing))) + (t/is (= :in (-> new-interaction :animation :way))) + (t/is (= :right (-> new-interaction :animation :direction))) + (t/is (= false (-> new-interaction :animation :offset-effect))))) + + (t/testing "Set animation type slide with previous data" + (let [interaction (assoc i1 :animation {:animation-type :dissolve + :duration 1000 + :easing :ease-out + :way :out + :direction :left + :offset-effect true}) + new-interaction + (cti/set-animation-type interaction :slide)] + (t/is (= :slide (-> new-interaction :animation :animation-type))) + (t/is (= 1000 (-> new-interaction :animation :duration))) + (t/is (= :ease-out (-> new-interaction :animation :easing))) + (t/is (= :out (-> new-interaction :animation :way))) + (t/is (= :left (-> new-interaction :animation :direction))) + (t/is (= true (-> new-interaction :animation :offset-effect))))) + + (t/testing "Set animation type push" + (let [new-interaction + (cti/set-animation-type i1 :push)] + (t/is (= :push (-> new-interaction :animation :animation-type))) + (t/is (= 300 (-> new-interaction :animation :duration))) + (t/is (= :linear (-> new-interaction :animation :easing))) + (t/is (= :right (-> new-interaction :animation :direction))))) + + (t/testing "Set animation type push with previous data" + (let [interaction (assoc i1 :animation {:animation-type :slide + :duration 1000 + :easing :ease-out + :way :out + :direction :left + :offset-effect true}) + new-interaction + (cti/set-animation-type interaction :push)] + (t/is (= :push (-> new-interaction :animation :animation-type))) + (t/is (= 1000 (-> new-interaction :animation :duration))) + (t/is (= :ease-out (-> new-interaction :animation :easing))) + (t/is (= :left (-> new-interaction :animation :direction))))))) + + +(t/deftest allowed-animation + (let [i1 (cti/set-action-type cti/default-interaction :open-overlay) + i2 (cti/set-action-type cti/default-interaction :close-overlay) + i3 (cti/set-action-type cti/default-interaction :toggle-overlay)] + + (t/testing "Cannot use animation push for an overlay action" + (let [bad-interaction-1 (assoc i1 :animation {:animation-type :push + :duration 1000 + :easing :ease-out + :direction :left}) + bad-interaction-2 (assoc i2 :animation {:animation-type :push + :duration 1000 + :easing :ease-out + :direction :left}) + bad-interaction-3 (assoc i3 :animation {:animation-type :push + :duration 1000 + :easing :ease-out + :direction :left})] + (t/is (not (cti/allowed-animation? (:action-type bad-interaction-1) + (-> bad-interaction-1 :animation :animation-type)))) + (t/is (not (cti/allowed-animation? (:action-type bad-interaction-2) + (-> bad-interaction-1 :animation :animation-type)))) + (t/is (not (cti/allowed-animation? (:action-type bad-interaction-3) + (-> bad-interaction-1 :animation :animation-type)))))) + + (t/testing "Remove animation if moving to an forbidden state" + (let [interaction (cti/set-animation-type cti/default-interaction :push) + new-interaction (cti/set-action-type interaction :open-overlay)] + (t/is (nil? (:animation new-interaction))))))) + + +(t/deftest option-duration + (let [i1 cti/default-interaction + i2 (cti/set-animation-type cti/default-interaction :dissolve)] + + (t/testing "Has duration?" + (t/is (not (cti/has-duration? i1))) + (t/is (cti/has-duration? i2))) + + (t/testing "Set duration" + (let [new-interaction (cti/set-duration i2 1000)] + (t/is (= 1000 (-> new-interaction :animation :duration))))))) + + +(t/deftest option-easing + (let [i1 cti/default-interaction + i2 (cti/set-animation-type cti/default-interaction :dissolve)] + + (t/testing "Has easing?" + (t/is (not (cti/has-easing? i1))) + (t/is (cti/has-easing? i2))) + + (t/testing "Set easing" + (let [new-interaction (cti/set-easing i2 :ease-in)] + (t/is (= :ease-in (-> new-interaction :animation :easing))))))) + + +(t/deftest option-way + (let [i1 cti/default-interaction + i2 (cti/set-animation-type cti/default-interaction :slide) + i3 (cti/set-action-type i2 :open-overlay)] + + (t/testing "Has way?" + (t/is (not (cti/has-way? i1))) + (t/is (cti/has-way? i2)) + (t/is (not (cti/has-way? i3))) + (t/is (some? (-> i3 :animation :way)))) ; <- it exists but is ignored + + (t/testing "Set way" + (let [new-interaction (cti/set-way i2 :out)] + (t/is (= :out (-> new-interaction :animation :way))))))) + + +(t/deftest option-direction + (let [i1 cti/default-interaction + i2 (cti/set-animation-type cti/default-interaction :push) + i3 (cti/set-animation-type cti/default-interaction :dissolve)] + + (t/testing "Has direction?" + (t/is (not (cti/has-direction? i1))) + (t/is (cti/has-direction? i2))) + + (t/testing "Set direction" + (let [new-interaction (cti/set-direction i2 :left)] + (t/is (= :left (-> new-interaction :animation :direction))))) + + (t/testing "Invert direction" + (let [a-none (:animation i3) + a-right (:animation i2) + a-left (assoc a-right :direction :left) + a-up (assoc a-right :direction :up) + a-down (assoc a-right :direction :down) + + a-nil' (cti/invert-direction nil) + a-none' (cti/invert-direction a-none) + a-right' (cti/invert-direction a-right) + a-left' (cti/invert-direction a-left) + a-up' (cti/invert-direction a-up) + a-down' (cti/invert-direction a-down)] + + (t/is (nil? a-nil')) + (t/is (nil? (:direction a-none'))) + (t/is (= :left (:direction a-right'))) + (t/is (= :right (:direction a-left'))) + (t/is (= :down (:direction a-up'))) + (t/is (= :up (:direction a-down'))))))) + + +(t/deftest option-offset-effect + (let [i1 cti/default-interaction + i2 (cti/set-animation-type cti/default-interaction :slide) + i3 (cti/set-action-type i2 :open-overlay)] + + (t/testing "Has offset-effect" + (t/is (not (cti/has-offset-effect? i1))) + (t/is (cti/has-offset-effect? i2)) + (t/is (not (cti/has-offset-effect? i3))) + (t/is (some? (-> i3 :animation :offset-effect)))) ; <- it exists but is ignored + + (t/testing "Set offset-effect" + (let [new-interaction (cti/set-offset-effect i2 true)] + (t/is (= true (-> new-interaction :animation :offset-effect))))))) + + +(t/deftest modify-interactions + (let [i1 (cti/set-action-type cti/default-interaction :open-overlay) + i2 (cti/set-action-type cti/default-interaction :close-overlay) + i3 (cti/set-action-type cti/default-interaction :prev-screen) + interactions [i1 i2]] + + (t/testing "Add interaction to nil" + (let [new-interactions (cti/add-interaction nil i3)] + (t/is (= (count new-interactions) 1)) + (t/is (= (:action-type (last new-interactions)) :prev-screen)))) + + (t/testing "Add interaction to normal" + (let [new-interactions (cti/add-interaction interactions i3)] + (t/is (= (count new-interactions) 3)) + (t/is (= (:action-type (last new-interactions)) :prev-screen)))) + + (t/testing "Remove interaction" + (let [new-interactions (cti/remove-interaction interactions 0)] + (t/is (= (count new-interactions) 1)) + (t/is (= (:action-type (last new-interactions)) :close-overlay)))) + + (t/testing "Update interaction" + (let [new-interactions (cti/update-interaction interactions 1 #(cti/set-action-type % :open-url))] + (t/is (= (count new-interactions) 2)) + (t/is (= (:action-type (last new-interactions)) :open-url)))))) + + +(t/deftest remap-interactions + (let [frame1 (cpi/make-minimal-shape :frame) + frame2 (cpi/make-minimal-shape :frame) + frame3 (cpi/make-minimal-shape :frame) + frame4 (cpi/make-minimal-shape :frame) + frame5 (cpi/make-minimal-shape :frame) + frame6 (cpi/make-minimal-shape :frame) + + objects {(:id frame3) frame3 + (:id frame4) frame4 + (:id frame5) frame5} + + ids-map {(:id frame1) (:id frame4) + (:id frame2) (:id frame5)} + + i1 (cti/set-destination cti/default-interaction (:id frame1)) + i2 (cti/set-destination cti/default-interaction (:id frame2)) + i3 (cti/set-destination cti/default-interaction (:id frame3)) + i4 (cti/set-destination cti/default-interaction nil) + i5 (cti/set-destination cti/default-interaction (:id frame6)) + + interactions [i1 i2 i3 i4 i5]] + + (t/testing "Remap interactions" + (let [new-interactions (cti/remap-interactions interactions ids-map objects)] + (t/is (= (count new-interactions) 4)) + (t/is (= (:id frame4) (:destination (get new-interactions 0)))) + (t/is (= (:id frame5) (:destination (get new-interactions 1)))) + (t/is (= (:id frame3) (:destination (get new-interactions 2)))) + (t/is (nil? (:destination (get new-interactions 3)))))))) + diff --git a/common/yarn.lock b/common/yarn.lock index 425c4fc04e..f59d710bf4 100644 --- a/common/yarn.lock +++ b/common/yarn.lock @@ -2,16 +2,556 @@ # yarn lockfile v1 +asn1.js@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + +assert@^1.1.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +base64-js@^1.0.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.0.0, bn.js@^5.1.1: + version "5.2.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" + integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== + +brorand@^1.0.1, brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" + integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== + dependencies: + bn.js "^5.0.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" + integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== + dependencies: + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.3" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + 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== +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= + +buffer@^4.3.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +console-browserify@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +create-ecdh@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" + integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== + dependencies: + bn.js "^4.1.0" + elliptic "^6.5.3" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +des.js@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" + integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +elliptic@^6.5.3: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +events@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= + +ieee754@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + luxon@^1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.27.0.tgz#ae10c69113d85dab8f15f5e8390d0cbeddf4f00f" integrity sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA== +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= + +pako@~1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +parse-asn1@^5.0.0, parse-asn1@^5.1.5: + version "5.1.6" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" + integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== + dependencies: + asn1.js "^5.2.0" + browserify-aes "^1.0.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +pbkdf2@^3.0.3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readline-sync@^1.4.7: + version "1.4.10" + resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" + integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shadow-cljs-jar@1.3.2: + version "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.16.12: + version "2.16.12" + resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.16.12.tgz#8757b3079dadfff15ca09192f81eb69b5d25266d" + integrity sha512-6JqOhN5X3n0IkxA/gSUcZ1lImwcW1LmpgzlaBDOC/u/pIysdNm0tiOxpOTEnExl9nKZBS/EYS7bXIIInywPJUA== + 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 "^7.4.6" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== + dependencies: + source-map "^0.5.6" + source-map-support@^0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" @@ -20,12 +560,111 @@ source-map-support@^0.5.19: buffer-from "^1.0.0" source-map "^0.6.0" +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + source-map@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +timers-browserify@^2.0.4: + version "2.0.12" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" + integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== + dependencies: + setimmediate "^1.0.4" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +vm-browserify@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + ws@^7.4.6: version "7.4.6" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 3fc07920a4..f45c3cc52e 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -3,10 +3,10 @@ LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive -ENV NODE_VERSION=v14.17.6 \ - CLOJURE_VERSION=1.10.3.967 \ - CLJKONDO_VERSION=2021.10.19 \ - BABASHKA_VERSION=0.6.1 \ +ENV NODE_VERSION=v16.13.1 \ + CLOJURE_VERSION=1.10.3.1058 \ + CLJKONDO_VERSION=2021.12.19 \ + BABASHKA_VERSION=0.7.3 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 @@ -27,6 +27,7 @@ RUN set -ex; \ git \ rlwrap \ unzip \ + rsync \ fakeroot \ netcat \ ; \ @@ -47,6 +48,7 @@ RUN set -ex; \ imagemagick \ ghostscript \ netpbm \ + poppler-utils \ potrace \ webp \ nginx \ @@ -96,6 +98,7 @@ RUN set -ex; \ libappindicator1 \ libnss3 \ libgbm1 \ + xvfb \ ; \ rm -rf /var/lib/apt/lists/*; @@ -107,16 +110,14 @@ RUN set -x; \ 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; \ - cd /usr/lib/jvm/openjdk16; \ + curl -LfsSo /tmp/openjdk.tar.gz https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.1%2B12/OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz; \ + mkdir -p /usr/lib/jvm/openjdk17; \ + cd /usr/lib/jvm/openjdk17; \ tar -xf /tmp/openjdk.tar.gz --strip-components=1; \ rm -rf /tmp/openjdk.tar.gz; -ENV PATH="/usr/lib/jvm/openjdk16/bin:/usr/local/nodejs/bin:$PATH" \ - JAVA_HOME=/usr/lib/jvm/openjdk16 +ENV PATH="/usr/lib/jvm/openjdk17/bin:/usr/local/nodejs/bin:$PATH" JAVA_HOME=/usr/lib/jvm/openjdk17 RUN set -ex; \ curl -LfsSo /tmp/clojure.sh https://download.clojure.org/install/linux-install-$CLOJURE_VERSION.sh; \ diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index e073eb2ce4..ee6b1d7df8 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -24,6 +24,7 @@ services: depends_on: - postgres - redis + # - keycloak volumes: - "user_data:/home/penpot/" @@ -40,7 +41,7 @@ services: environment: - EXTERNAL_UID=${CURRENT_USER_ID} - PENPOT_SECRET_KEY=super-secret-devenv-key - # STMP setup + # SMTP setup - PENPOT_SMTP_ENABLED=true - PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com - PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com @@ -90,7 +91,7 @@ services: environment: - EXTERNAL_UID=${CURRENT_USER_ID} - PENPOT_SECRET_KEY=super-secret-devenv-key - # STMP setup + # SMTP setup - PENPOT_SMTP_ENABLED=true - PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com - PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com @@ -115,6 +116,22 @@ services: - PENPOT_LDAP_ATTRS_FULLNAME=cn - PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto + # keycloak: + # image: "quay.io/keycloak/keycloak:15.0.2" + # environment: + # - DB_VENDOR=POSTGRES + # - DB_ADDR=postgres + # - DB_DATABASE=keycloak + # - DB_USER=keycloak + # - DB_SCHEMA=public + # - DB_PASSWORD=keycloak + # - KEYCLOAK_USER=admin + # - KEYCLOAK_PASSWORD=admin + # expose: + # - '8080' + # ports: + # - "8080:8080" + postgres: image: postgres:13 command: postgres -c config_file=/etc/postgresql.conf diff --git a/docker/devenv/files/bashrc b/docker/devenv/files/bashrc index c7c421504f..3ae8341c91 100644 --- a/docker/devenv/files/bashrc +++ b/docker/devenv/files/bashrc @@ -1,6 +1,6 @@ #!/usr/bin/env bash -export PATH=/usr/lib/jvm/openjdk16/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin +export PATH=/usr/lib/jvm/openjdk17/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin alias l='ls --color -GFlh' alias rm='rm -r' diff --git a/docker/devenv/files/entrypoint.sh b/docker/devenv/files/entrypoint.sh index b47fa50a88..d05b118c63 100755 --- a/docker/devenv/files/entrypoint.sh +++ b/docker/devenv/files/entrypoint.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -export PATH=/usr/lib/jvm/openjdk16/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin +export PATH=/usr/lib/jvm/openjdk17/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin set -e usermod -u ${EXTERNAL_UID:-1000} penpot diff --git a/docker/devenv/files/lein b/docker/devenv/files/lein index 01909f981c..5eb4f2aa77 100755 --- a/docker/devenv/files/lein +++ b/docker/devenv/files/lein @@ -24,7 +24,7 @@ else fi function command_not_found { - >&2 echo "Leiningen coundn't find $1 in your \$PATH ($PATH), which is required." + >&2 echo "Leiningen couldn't find $1 in your \$PATH ($PATH), which is required." exit 1 } @@ -224,7 +224,7 @@ fi if [ ! -x "$JAVA_CMD" ] && ! type -f java >/dev/null then - >&2 echo "Leiningen coundn't find 'java' executable, which is required." + >&2 echo "Leiningen couldn't find 'java' executable, which is required." >&2 echo "Please either set JAVA_CMD or put java (>=1.6) in your \$PATH ($PATH)." exit 1 fi diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 4aa3c67b1d..147c51eeab 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -46,7 +46,7 @@ http { listen 3449 default_server; server_name _; - client_max_body_size 20M; + client_max_body_size 50M; charset utf-8; proxy_http_version 1.1; diff --git a/docker/gitpod/Dockerfile b/docker/gitpod/Dockerfile index 00d9069764..1ed6876348 100644 --- a/docker/gitpod/Dockerfile +++ b/docker/gitpod/Dockerfile @@ -12,6 +12,7 @@ RUN set -ex; \ brew install ghostscript; \ brew install mailhog; \ brew install openldap; \ + brew install poppler-utils; \ sudo mkdir -p /var/log/nginx; \ sudo chown gitpod:gitpod /var/log/nginx diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 9a4f6e215a..84cd010bb8 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -23,6 +23,7 @@ RUN set -ex; \ imagemagick \ ghostscript \ netpbm \ + poppler-utils \ potrace \ gconf-service \ libasound2 \ diff --git a/docker/images/files/nginx.conf b/docker/images/files/nginx.conf index 1f034c2a33..ee9d131e3d 100644 --- a/docker/images/files/nginx.conf +++ b/docker/images/files/nginx.conf @@ -47,7 +47,7 @@ http { listen 80 default_server; server_name _; - client_max_body_size 6M; + client_max_body_size 50M; charset utf-8; proxy_http_version 1.1; diff --git a/experiments/js/imagetracer.js b/experiments/js/imagetracer.js index 6c62432e24..5c6b07d9ad 100644 --- a/experiments/js/imagetracer.js +++ b/experiments/js/imagetracer.js @@ -144,7 +144,7 @@ function ImageTracer(){ // 3. Batch pathscan var bps = _this.batchpathscan( ls, options.pathomit ); - // 4. Batch interpollation + // 4. Batch interpolation var bis = _this.batchinternodes( bps, options ); // 5. Batch tracing and creating tracedata object @@ -240,7 +240,7 @@ function ImageTracer(){ //////////////////////////////////////////////////////////// // 1. Color quantization - // Using a form of k-means clustering repeatead options.colorquantcycles times. http://en.wikipedia.org/wiki/Color_quantization + // Using a form of k-means clustering repeated options.colorquantcycles times. http://en.wikipedia.org/wiki/Color_quantization this.colorquantization = function( imgd, options ){ var arr = [], idx=0, cd,cdl,ci, paletteacc = [], pixelnum = imgd.width * imgd.height, i, j, k, cnt, palette; @@ -300,7 +300,7 @@ function ImageTracer(){ }// End of palette loop }// End of Average colors from the second iteration - // Reseting palette accumulator for averaging + // Resetting palette accumulator for averaging for( i=0; i < palette.length; i++ ){ paletteacc[i] = { r:0, g:0, b:0, a:0, n:0 }; } // loop through all pixels @@ -322,7 +322,7 @@ function ImageTracer(){ }// End of palette loop - // add to palettacc + // add to paletteacc paletteacc[ci].r += imgd.data[idx ]; paletteacc[ci].g += imgd.data[idx+1]; paletteacc[ci].b += imgd.data[idx+2]; @@ -620,7 +620,7 @@ function ImageTracer(){ return bpaths; }, - // 4. interpollating between path points for nodes with 8 directions ( East, SouthEast, S, SW, W, NW, N, NE ) + // 4. interpolating between path points for nodes with 8 directions ( East, SouthEast, S, SW, W, NW, N, NE ) this.internodes = function( paths, options ){ var ins = [], palen=0, nextidx=0, nextidx2=0, previdx=0, previdx2=0, pacnt, pcnt; @@ -718,7 +718,7 @@ function ImageTracer(){ return val; },// End of getdirection() - // 4. Batch interpollation + // 4. Batch interpolation this.batchinternodes = function( bpaths, options ){ var binternodes = []; for (var k in bpaths) { diff --git a/exporter/deps.edn b/exporter/deps.edn index ee277baf72..64cd9f7b33 100644 --- a/exporter/deps.edn +++ b/exporter/deps.edn @@ -2,14 +2,14 @@ :deps {penpot/common {:local/root "../common"} binaryage/devtools {:mvn/version "RELEASE"} - metosin/reitit-core {:mvn/version "0.5.13"} - com.lambdaisland/glogi {:mvn/version "1.1.144"} + metosin/reitit-core {:mvn/version "0.5.15"} funcool/beicon {:mvn/version "2021.07.05-1"} } :aliases {:outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"} - org.slf4j/slf4j-nop {:mvn/version "RELEASE"}} + ;; org.slf4j/slf4j-nop {:mvn/version "RELEASE"} + } :main-opts ["-m" "antq.core"]} :dev @@ -20,4 +20,3 @@ {:main-opts ["-m" "shadow.cljs.devtools.cli"]} }} - diff --git a/exporter/scripts/build b/exporter/scripts/build index 1df9a17070..a4c9df9dc8 100755 --- a/exporter/scripts/build +++ b/exporter/scripts/build @@ -2,17 +2,21 @@ set -ex +CURRENT_VERSION=$1; + yarn install rm -rf target export NODE_ENV=production; # Build the application -clojure -M:dev:shadow-cljs release main +clojure -M:dev:shadow-cljs release main; # Remove source -rm -rf target/app +rm -rf target/app; # Copy package*.json files -cp yarn.lock target/ -cp package.json target/ +cp yarn.lock target/; +cp package.json target/; + +sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/app.js; diff --git a/exporter/shadow-cljs.edn b/exporter/shadow-cljs.edn index da622bf434..526d65bed4 100644 --- a/exporter/shadow-cljs.edn +++ b/exporter/shadow-cljs.edn @@ -20,7 +20,7 @@ :compiler-options {:fn-invoke-direct true :source-map true - :optimizations :simple + :optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :simple] :pseudo-names true :pretty-print true :anon-fn-naming-policy :off diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 71fb4f965d..b16ad95cf3 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -6,14 +6,17 @@ (ns app.browser (:require - ["puppeteer-core" :as pp] ["generic-pool" :as gp] + ["puppeteer-core" :as pp] [app.common.data :as d] + [app.common.logging :as l] [app.common.uuid :as uuid] [app.config :as cf] - [lambdaisland.glogi :as log] + [app.util.object :as obj] [promesa.core :as p])) +(l/set-level! :trace) + ;; --- BROWSER API (def default-timeout 30000) @@ -32,7 +35,8 @@ [page {:keys [timeout cookie user-agent viewport]}] (let [timeout (or timeout default-timeout) user-agent (or user-agent default-user-agent) - viewport (d/merge default-viewport viewport)] + viewport (merge default-viewport viewport)] + (p/do! (.setViewport ^js page #js {:width (:width viewport) :height (:height viewport) @@ -60,29 +64,27 @@ (defn screenshot ([frame] (screenshot frame nil)) ([frame {:keys [full-page? omit-background? type] - :or {full-page? false - type "png" + :or {type "png" + full-page? false omit-background? false}}] - (.screenshot ^js frame #js {:fullPage full-page? - :type (name type) - :omitBackground omit-background?}))) + (let [options (-> (obj/new) + (obj/set! "type" (name type)) + (obj/set! "omitBackground" omit-background?) + (cond-> full-page? (-> (obj/set! "fullPage" true) + (obj/set! "clip" nil))))] + (.screenshot ^js frame options)))) (defn pdf ([page] (pdf page nil)) - ([page {:keys [viewport omit-background? prefer-css-page-size? save-path] - :or {viewport {} - omit-background? true - prefer-css-page-size? true - save-path nil}}] - (let [viewport (d/merge default-viewport viewport)] + ([page {:keys [viewport save-path]}] + (p/let [viewport (d/merge default-viewport viewport)] + (.emulateMediaType ^js page "screen") (.pdf ^js page #js {:path save-path :width (:width viewport) :height (:height viewport) :scale (:scale viewport) - :omitBackground omit-background? - :printBackground (not omit-background?) - :preferCSSPageSize prefer-css-page-size?})))) - + :printBackground true + :preferCSSPageSize true})))) (defn eval! [frame f] (.evaluate ^js frame f)) @@ -101,24 +103,31 @@ (defonce pool (atom nil)) (defonce pool-browser-id (atom 1)) +(def default-chrome-args + #js ["--no-sandbox" + "--font-render-hinting=none" + "--disable-setuid-sandbox" + "--disable-accelerated-2d-canvas" + "--disable-gpu"]) + (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" "--font-render-hinting=none"]}) + (-> (pp/launch #js {:executablePath path :args default-chrome-args}) (p/then (fn [browser] (let [id (deref pool-browser-id)] - (log/info :origin "factory" :action "create" :browser-id id) + (l/info :origin "factory" :action "create" :browser-id id) (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) + (l/info :origin "factory" :action "destroy" :browser-id id) (.close ^js obj))) (validate [obj] (let [id (unchecked-get obj "__id")] - (log/info :origin "factory" :action "validate" :browser-id id :obj obj) + (l/info :origin "factory" :action "validate" :browser-id id :obj obj) (p/resolved (.isConnected ^js obj))))] #js {:create create @@ -127,7 +136,7 @@ (defn init [] - (log/info :msg "initializing browser pool") + (l/info :msg "initializing browser pool") (let [opts #js {:max (cf/get :browser-pool-max 3) :min (cf/get :browser-pool-min 0) :testOnBorrow true @@ -142,34 +151,74 @@ (defn stop [] (when-let [pool (deref pool)] - (log/info :msg "finalizing browser pool") + (l/info :msg "finalizing browser pool") (-> (.drain ^js pool) (p/then (fn [] (.clear ^js pool)))))) (defn exec! [callback] - (letfn [(on-release [pool browser ctx result error] - (-> (p/do! (.close ^js ctx)) - (p/handle - (fn [_ _] - (.release ^js pool browser))) - (p/handle - (fn [_ _] - (let [id (unchecked-get browser "__id")] - (log/info :origin "exec" :action "release" :browser-id id)) - (if result - (p/resolved result) - (p/rejected error)))))) + (letfn [(release-browser [pool browser] + (let [id (unchecked-get browser "__id")] + (-> (p/do! (.release ^js pool browser)) + (p/handle (fn [res err] + (l/trace :action "exec:release-browser" :browser-id id) + (when err (js/console.log err)) + (if err + (p/rejected err) + (p/resolved res))))))) + + (destroy-browser [pool browser] + (let [id (unchecked-get browser "__id")] + (-> (p/do! (.destroy ^js pool browser)) + (p/handle (fn [res err] + (l/trace :action "exec:destroy-browser" :browser-id id) + (when err (js/console.log err)) + (if err + (p/rejected err) + (p/resolved res))))))) + + (handle-error [pool browser obj err] + (let [id (unchecked-get browser "__id")] + (if err + (do + (l/trace :action "exec:handle-error" :browser-id id) + (-> (p/do! (destroy-browser pool browser)) + (p/handle #(p/rejected err)))) + (p/resolved obj)))) + + (on-result [pool browser context result] + (let [id (unchecked-get browser "__id")] + (l/trace :action "exec:on-result" :browser-id id) + (-> (p/do! (.close ^js context)) + (p/handle (fn [_ err] + (if err + (destroy-browser pool browser) + (release-browser pool browser)))) + (p/handle #(p/resolved result))))) + + (on-page [pool browser context page] + (let [id (unchecked-get browser "__id")] + (l/trace :action "exec:on-page" :browser-id id) + (-> (p/do! (callback page)) + (p/handle (partial handle-error pool browser)) + (p/then (partial on-result pool browser context))))) (on-context [pool browser ctx] - (-> (p/do! (.newPage ^js ctx)) - (p/then callback) - (p/handle #(on-release pool browser ctx %1 %2)))) + (let [id (unchecked-get browser "__id")] + (l/trace :action "exec:on-context" :browser-id id) + (-> (p/do! (.newPage ^js ctx)) + (p/handle (partial handle-error pool browser)) + (p/then (partial on-page pool browser ctx))))) - (on-acquire [pool browser] - (-> (.createIncognitoBrowserContext ^js browser) - (p/then #(on-context pool browser %))))] + (on-acquire [pool browser err] + (let [id (unchecked-get browser "__id")] + (l/trace :action "exec:on-acquire" :browser-id id) + (if err + (js/console.log err) + (-> (p/do! (.createIncognitoBrowserContext ^js browser)) + (p/handle (partial handle-error pool browser)) + (p/then (partial on-context pool browser))))))] (when-let [pool (deref pool)] (-> (p/do! (.acquire ^js pool)) - (p/then (partial on-acquire pool)))))) + (p/handle (partial on-acquire pool)))))) diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index 307019171f..f1abb63526 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -71,9 +71,7 @@ (atom (prepare-config))) (def version - (atom (v/parse (or (some-> (ex/ignoring (fs/readFileSync "version.txt")) - (str/trim)) - "%version%")))) + (atom (v/parse "%version%"))) (defn get "A configuration getter." diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs index 29eb8865d8..1af7491f10 100644 --- a/exporter/src/app/core.cljs +++ b/exporter/src/app/core.cljs @@ -6,23 +6,22 @@ (ns app.core (:require - [lambdaisland.glogi :as log] - [lambdaisland.glogi.console :as glogi-console] - [promesa.core :as p] + [app.browser :as bwr] + [app.common.logging :as l] + [app.config] [app.http :as http] [app.sentry :as sentry] - [app.config] - [app.browser :as bwr])) + [promesa.core :as p])) -(glogi-console/install!) (enable-console-print!) +(l/initialize!) (sentry/init!) (defonce state (atom nil)) (defn start [& args] - (log/info :msg "initializing") + (l/info :msg "initializing") (p/do! (bwr/init) (http/init))) @@ -34,7 +33,7 @@ ;; an empty line for visual feedback of restart (js/console.log "") - (log/info :msg "stoping") + (l/info :msg "stoping") (p/do! (bwr/stop) (http/stop) diff --git a/exporter/src/app/http.cljs b/exporter/src/app/http.cljs index a36e030d9b..17e10a0bf4 100644 --- a/exporter/src/app/http.cljs +++ b/exporter/src/app/http.cljs @@ -6,17 +6,19 @@ (ns app.http (:require + [app.common.logging :as l] [app.config :as cf] [app.http.export :refer [export-handler]] [app.http.export-frames :refer [export-frames-handler]] [app.http.impl :as impl] - [app.util.transit :as t] [app.sentry :as sentry] + [app.util.transit :as t] [cuerdas.core :as str] - [lambdaisland.glogi :as log] [promesa.core :as p] [reitit.core :as r])) +(l/set-level! :info) + (def routes [["/export-frames" {:handler export-frames-handler}] ["/export" {:handler export-handler}]]) @@ -49,7 +51,7 @@ :else (do - (log/error :msg "Unexpected error" :error error) + (l/error :msg "Unexpected error" :error error) (js/console.error error) {:status 500 :headers {"x-error" (t/encode data)} @@ -62,10 +64,10 @@ server (impl/server handler on-error) port (cf/get :http-server-port 6061)] (.listen server port) - (log/info :msg "welcome to penpot" + (l/info :msg "welcome to penpot" :module "exporter" :version (:full @cf/version)) - (log/info :msg "starting http server" :port port) + (l/info :msg "starting http server" :port port) (reset! instance server))) (defn stop @@ -73,6 +75,6 @@ (if-let [server @instance] (p/create (fn [resolve] (.close server (fn [] - (log/info :msg "shutdown http server") + (l/info :msg "shutdown http server") (resolve))))) (p/resolved nil))) diff --git a/exporter/src/app/http/export.cljs b/exporter/src/app/http/export.cljs index e8b56b6665..493f53908b 100644 --- a/exporter/src/app/http/export.cljs +++ b/exporter/src/app/http/export.cljs @@ -9,12 +9,11 @@ [app.common.exceptions :as exc :include-macros true] [app.common.spec :as us] [app.renderer.bitmap :as rb] - [app.renderer.svg :as rs] [app.renderer.pdf :as rp] + [app.renderer.svg :as rs] [app.zipfile :as zip] [cljs.spec.alpha :as s] [cuerdas.core :as str] - [lambdaisland.glogi :as log] [promesa.core :as p])) (s/def ::name ::us/string) @@ -81,9 +80,11 @@ (p/then (fn [results] (reduce #(zip/add! %1 (:filename %2) (:content %2)) (zip/create) results))) (p/then (fn [fzip] + (.generateAsync ^js fzip #js {:type "uint8array"}))) + (p/then (fn [data] {:status 200 :headers {"content-type" "application/zip"} - :body (.generateNodeStream ^js fzip)}))))) + :body data}))))) (defn- perform-export [params] diff --git a/exporter/src/app/http/export_frames.cljs b/exporter/src/app/http/export_frames.cljs index 86339e6e1b..073caf0f30 100644 --- a/exporter/src/app/http/export_frames.cljs +++ b/exporter/src/app/http/export_frames.cljs @@ -35,13 +35,13 @@ :object-id frame-id :scale 1 :save-path spath})] - (cons spath spaths))) + (conj spaths spath))) (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)) + (-> (sh/run-cmd! (str "pdfunite " paths-str " " output-path)) (p/then (constantly output-path))))) (defn- clean-tmp-data @@ -54,16 +54,20 @@ [{: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)}}))) + (if (seq frame-ids) + (p/let [tdpath (sh/create-tmpdir! "pdfexport-") + data (-> (reduce (fn [promise frame-id] + (p/then promise (partial export-frame tdpath file-id page-id token frame-id))) + (p/future []) + (reverse 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)}}) + {:status 204 + :body "" + :headers {"content-type" "text/plain"}}))) diff --git a/exporter/src/app/http/impl.cljs b/exporter/src/app/http/impl.cljs index 67d54f6d89..b98e7d29fd 100644 --- a/exporter/src/app/http/impl.cljs +++ b/exporter/src/app/http/impl.cljs @@ -12,7 +12,6 @@ ["raw-body" :as raw-body] [app.util.transit :as t] [cuerdas.core :as str] - [lambdaisland.glogi :as log] [lambdaisland.uri :as u] [promesa.core :as p] [reitit.core :as r])) diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index c900a08623..6d04283c39 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -10,13 +10,13 @@ [app.browser :as bw] [app.common.data :as d] [app.common.exceptions :as ex :include-macros true] + [app.common.logging :as l] [app.common.pages :as cp] [app.common.spec :as us] [app.config :as cf] [cljs.spec.alpha :as s] [cuerdas.core :as str] [lambdaisland.uri :as u] - [lambdaisland.glogi :as log] [promesa.core :as p])) (defn create-cookie @@ -39,7 +39,7 @@ (screenshot page (str uri) cookie))) (screenshot [page uri cookie] - (log/info :uri uri) + (l/info :uri uri) (let [viewport {:width 1920 :height 1080 :scale scale} diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 7f88bc2384..b421befa02 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -9,11 +9,11 @@ (:require [app.browser :as bw] [app.common.exceptions :as ex :include-macros true] + [app.common.logging :as l] [app.common.spec :as us] [app.config :as cf] [cljs.spec.alpha :as s] [lambdaisland.uri :as u] - [lambdaisland.glogi :as log] [promesa.core :as p])) (defn create-cookie @@ -31,20 +31,26 @@ (let [path (str "/render-object/" file-id "/" page-id "/" object-id) uri (-> (u/uri (cf/get :public-uri)) (assoc :path "/") + (assoc :query "essential=t") (assoc :fragment path)) + cookie (create-cookie uri token)] (pdf-from page (str uri) cookie))) (pdf-from [page uri cookie] - (log/info :uri uri) - (let [options {:cookie cookie}] - (p/do! - (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)))))] + (l/info :uri uri) + (p/let [options {:cookie cookie}] + (bw/configure-page! page options) + (bw/navigate! page uri) + (bw/wait-for page "#screenshot") + ;; taking png screenshot before pdf, helps to make the + ;; pdf rendering works as expected. + (p/let [dom (bw/select page "#screenshot")] + (bw/screenshot dom {:full-page? true})) + + (if save-path + (bw/pdf page {:save-path save-path}) + (bw/pdf page))))] (bw/exec! handle))) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 274cdfc6bc..894d77c4d0 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -11,19 +11,19 @@ [app.browser :as bw] [app.common.data :as d] [app.common.exceptions :as ex :include-macros true] + [app.common.logging :as l] [app.common.pages :as cp] [app.common.spec :as us] [app.config :as cf] + [app.renderer.bitmap :refer [create-cookie]] [app.util.shell :as sh] [cljs.spec.alpha :as s] [clojure.walk :as walk] [cuerdas.core :as str] - [lambdaisland.glogi :as log] [lambdaisland.uri :as u] - [app.renderer.bitmap :refer [create-cookie]] [promesa.core :as p])) -(log/set-level "app.renderer.svg" :trace) +(l/set-level! :trace) (defn- xml->clj [data] @@ -116,22 +116,22 @@ (defn- render-object [{:keys [page-id file-id object-id token scale suffix type]}] (letfn [(convert-to-ppm [pngpath] - (log/trace :fn :convert-to-ppm) + (l/trace :fn :convert-to-ppm) (let [basepath (path/dirname pngpath) ppmpath (path/join basepath "origin.ppm")] (-> (sh/run-cmd! (str "convert " pngpath " " ppmpath)) (p/then (constantly ppmpath))))) (trace-color-mask [pbmpath] - (log/trace :fn :trace-color-mask :pbmpath pbmpath) + (l/trace :fn :trace-color-mask :pbmpath pbmpath) (let [basepath (path/dirname pbmpath) basename (path/basename pbmpath ".pbm") svgpath (path/join basepath (str basename ".svg"))] (-> (sh/run-cmd! (str "potrace --flat -b svg " pbmpath " -o " svgpath)) (p/then (constantly svgpath))))) - + (generate-color-layer [ppmpath color] - (log/trace :fn :generate-color-layer :ppmpath ppmpath :color color) + (l/trace :fn :generate-color-layer :ppmpath ppmpath :color color) (let [basepath (path/dirname ppmpath) pbmpath (path/join basepath (str "mask-" (subs color 1) ".pbm"))] (-> (sh/run-cmd! (str/format "ppmcolormask \"%s\" %s" color ppmpath)) @@ -193,7 +193,7 @@ (mapv (partial data->gradient-def id)))) (join-color-layers [{:keys [id x y width height mapping] :as node} layers] - (log/trace :fn :join-color-layers :mapping mapping) + (l/trace :fn :join-color-layers :mapping mapping) (loop [result (-> (:svgdata (first layers)) (assoc "elements" [])) layers (seq layers)] @@ -206,7 +206,7 @@ ;; Now we have the result containing the svgdata of a ;; SVG with all text layers. Now we need to transform - ;; this SVG to G (Group) and remove unnecesary metada + ;; this SVG to G (Group) and remove unnecessary metadata ;; objects. (let [vbox (-> (get-in result ["attributes" "viewBox"]) (parse-viewbox)) @@ -241,12 +241,12 @@ (assoc "elements" elements)))))) (convert-to-svg [ppmpath {:keys [colors] :as node}] - (log/trace :fn :convert-to-svg :ppmpath ppmpath :colors colors) + (l/trace :fn :convert-to-svg :ppmpath ppmpath :colors colors) (-> (p/all (map (partial generate-color-layer ppmpath) colors)) (p/then (partial join-color-layers node)))) (trace-node [{:keys [data] :as node}] - (log/trace :fn :trace-node) + (l/trace :fn :trace-node) (p/let [tdpath (sh/create-tmpdir! "svgexport-") pngpath (path/join tdpath "origin.png") _ (sh/write-file! pngpath data) @@ -270,7 +270,7 @@ :mapping (js/JSON.parse mapping)})) (extract-single-node [[shot node]] - (log/trace :fn :extract-single-node) + (l/trace :fn :extract-single-node) (p/let [attrs (bw/eval! node extract-element-attrs)] {:id (unchecked-get attrs "id") @@ -302,7 +302,7 @@ (p/then clean-temp-data))) (process-text-nodes [page] - (log/trace :fn :process-text-nodes) + (l/trace :fn :process-text-nodes) (-> (bw/select-all page "#screenshot foreignObject") (p/then (fn [nodes] (p/all (map (partial process-text-node page) nodes)))))) @@ -345,7 +345,7 @@ cookie (create-cookie uri token) rctx {:cookie cookie :uri (str uri)}] - (log/info :uri (:uri rctx)) + (l/info :uri (:uri rctx)) (bw/exec! (partial handle rctx))))) (s/def ::name ::us/string) diff --git a/exporter/src/app/util/object.cljs b/exporter/src/app/util/object.cljs new file mode 100644 index 0000000000..35dcaeccce --- /dev/null +++ b/exporter/src/app/util/object.cljs @@ -0,0 +1,86 @@ +;; 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.util.object + "A collection of helpers for work with javascript objects." + (:refer-clojure :exclude [set! get get-in merge clone contains?]) + (:require + [cuerdas.core :as str])) + +(defn new [] #js {}) + +(defn get + ([obj k] + (when-not (nil? obj) + (unchecked-get obj k))) + ([obj k default] + (let [result (get obj k)] + (if (undefined? result) default result)))) + +(defn contains? + [obj k] + (some? (unchecked-get obj k))) + +(defn get-keys + [obj] + (js/Object.keys ^js obj)) + +(defn get-in + ([obj keys] + (get-in obj keys nil)) + + ([obj keys default] + (loop [key (first keys) + keys (rest keys) + res obj] + (if (or (nil? key) (nil? res)) + (or res default) + (recur (first keys) + (rest keys) + (unchecked-get res key)))))) + +(defn clone + [a] + (js/Object.assign #js {} a)) + +(defn merge! + ([a b] + (js/Object.assign a b)) + ([a b & more] + (reduce merge! (merge! a b) more))) + +(defn merge + ([a b] + (js/Object.assign #js {} a b)) + ([a b & more] + (reduce merge! (merge a b) more))) + +(defn set! + [obj key value] + (unchecked-set obj key value) + obj) + +(defn update! + [obj key f & args] + (let [found (get obj key ::not-found)] + (if-not (identical? ::not-found found) + (do (unchecked-set obj key (apply f found args)) + obj) + obj))) + +(defn- props-key-fn + [key] + (if (or (= key :class) (= key :class-name)) + "className" + (str/camel (name key)))) + +(defn clj->props + [props] + (clj->js props :keyword-fn props-key-fn)) + +(defn ^boolean in? + [obj prop] + (js* "~{} in ~{}" prop obj)) diff --git a/exporter/src/app/util/shell.cljs b/exporter/src/app/util/shell.cljs index f269fe1319..8c5cca905b 100644 --- a/exporter/src/app/util/shell.cljs +++ b/exporter/src/app/util/shell.cljs @@ -11,10 +11,10 @@ ["fs" :as fs] ["os" :as os] ["path" :as path] - [lambdaisland.glogi :as log] + [app.common.logging :as l] [promesa.core :as p])) -(log/set-level "app.util.shell" :trace) +(l/set-level! :trace) (defn create-tmpdir! [prefix] @@ -47,10 +47,10 @@ [cmd] (p/create (fn [resolve reject] - (log/trace :fn :run-cmd :cmd cmd) + (l/trace :fn :run-cmd :cmd cmd) (chp/exec cmd #js {:encoding "buffer"} (fn [error stdout stderr] - ;; (log/trace :fn :run-cmd :stdout stdout) + ;; (l/trace :fn :run-cmd :stdout stdout) (if error (reject error) (resolve stdout))))))) diff --git a/frontend/cypress.json b/frontend/cypress.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/frontend/cypress.json @@ -0,0 +1 @@ +{} diff --git a/frontend/cypress/fixtures/example.json b/frontend/cypress/fixtures/example.json new file mode 100644 index 0000000000..02e4254378 --- /dev/null +++ b/frontend/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/frontend/cypress/integration-examples/1-getting-started/todo.spec.js b/frontend/cypress/integration-examples/1-getting-started/todo.spec.js new file mode 100644 index 0000000000..4768ff923e --- /dev/null +++ b/frontend/cypress/integration-examples/1-getting-started/todo.spec.js @@ -0,0 +1,143 @@ +/// + +// Welcome to Cypress! +// +// This spec file contains a variety of sample tests +// for a todo list app that are designed to demonstrate +// the power of writing tests in Cypress. +// +// To learn more about how Cypress works and +// what makes it such an awesome testing tool, +// please read our getting started guide: +// https://on.cypress.io/introduction-to-cypress + +describe('example to-do app', () => { + beforeEach(() => { + // Cypress starts out with a blank slate for each test + // so we must tell it to visit our website with the `cy.visit()` command. + // Since we want to visit the same URL at the start of all our tests, + // we include it in our beforeEach function so that it runs before each test + cy.visit('https://example.cypress.io/todo') + }) + + it('displays two todo items by default', () => { + // We use the `cy.get()` command to get all elements that match the selector. + // Then, we use `should` to assert that there are two matched items, + // which are the two default items. + cy.get('.todo-list li').should('have.length', 2) + + // We can go even further and check that the default todos each contain + // the correct text. We use the `first` and `last` functions + // to get just the first and last matched elements individually, + // and then perform an assertion with `should`. + cy.get('.todo-list li').first().should('have.text', 'Pay electric bill') + cy.get('.todo-list li').last().should('have.text', 'Walk the dog') + }) + + it('can add new todo items', () => { + // We'll store our item text in a variable so we can reuse it + const newItem = 'Feed the cat' + + // Let's get the input element and use the `type` command to + // input our new list item. After typing the content of our item, + // we need to type the enter key as well in order to submit the input. + // This input has a data-test attribute so we'll use that to select the + // element in accordance with best practices: + // https://on.cypress.io/selecting-elements + cy.get('[data-test=new-todo]').type(`${newItem}{enter}`) + + // Now that we've typed our new item, let's check that it actually was added to the list. + // Since it's the newest item, it should exist as the last element in the list. + // In addition, with the two default items, we should have a total of 3 elements in the list. + // Since assertions yield the element that was asserted on, + // we can chain both of these assertions together into a single statement. + cy.get('.todo-list li') + .should('have.length', 3) + .last() + .should('have.text', newItem) + }) + + it('can check off an item as completed', () => { + // In addition to using the `get` command to get an element by selector, + // we can also use the `contains` command to get an element by its contents. + // However, this will yield the