diff --git a/CHANGES.md b/CHANGES.md index 41e1ae1f5..65a48dc53 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,87 @@ # CHANGELOG +## 1.18.0 + +### :sparkles: New features +- Adds more accessibility improvements in dashboard [Taiga #4577](https://tree.taiga.io/project/penpot/us/4577) +- Adds paddings and gaps prediction on layout creation [Taiga #4838](https://tree.taiga.io/project/penpot/task/4838) +- Add visual feedback when proportionally scaling text elements with **K** [Taiga #3415](https://tree.taiga.io/project/penpot/us/3415) +- Add visualization and mouse control to paddings, margins and gaps in frames with layout [Taiga #4839](https://tree.taiga.io/project/penpot/task/4839) +- Allow for absolute positioned elements inside layout [Taiga #4834](https://tree.taiga.io/project/penpot/us/4834) +- Add z-index option for flex layout items [Taiga #2980](https://tree.taiga.io/project/penpot/us/2980) +- Scale content proportionally affects strokes, shadows, blurs and corners [Taiga #1951](https://tree.taiga.io/project/penpot/us/1951) +- Use tabulators to navigate layers [Taiga #5010](https://tree.taiga.io/project/penpot/issue/5010) + +### :bug: Bugs fixed + +- Fix problem with rules position on changing pages [Taiga #4847](https://tree.taiga.io/project/penpot/issue/4847) +- Fix error streen when uploading wrong SVG [#2995](https://github.com/penpot/penpot/issues/2995) +- Fix selecting children from hidden parent layers [Taiga #4934](https://tree.taiga.io/project/penpot/issue/4934) +- Fix problem when undoing multiple selected colors [Taiga #4920](https://tree.taiga.io/project/penpot/issue/4920) +- Allow selection of empty board by partial rect [Taiga #4806](https://tree.taiga.io/project/penpot/issue/4806) +- Improve behavior for undo on text edition [Taiga #4693](https://tree.taiga.io/project/penpot/issue/4693) +- Improve deeps selection of nested arboards [Taiga #4913](https://tree.taiga.io/project/penpot/issue/4913) +- Fix problem on selection numeric inputs on Firefox [#2991](https://github.com/penpot/penpot/issues/2991) +- Changed the text dominant-baseline to use ideographic [Taiga #4791](https://tree.taiga.io/project/penpot/issue/4791) +- Viewer wrong translations [Github #3035](https://github.com/penpot/penpot/issues/3035) +- Fix problem with text editor in Safari +- Fix unlink library color when blur color picker input [#3026](https://github.com/penpot/penpot/issues/3026) +- Fix snap pixel when moving path points on high zoom [#2930](https://github.com/penpot/penpot/issues/2930) +- Fix shortcuts for zoom now take into account the mouse position [#2924](https://github.com/penpot/penpot/issues/2924) +- Fix close colorpicker on Firefox when mouse-up is outside the picker [#2911](https://github.com/penpot/penpot/issues/2911) +- Fix problems with touch devices and Wacom tablets [#2216](https://github.com/penpot/penpot/issues/2216) +- Fix problem with board titles misplaced [Taiga #4738](https://tree.taiga.io/project/penpot/issue/4738) +- Fix problem with alt getting stuck when alt+tab [Taiga #5013](https://tree.taiga.io/project/penpot/issue/5013) +- Fix problem with z positioning of elements [Taiga #5014](https://tree.taiga.io/project/penpot/issue/5014) +- Fix problem in Firefox with scroll jumping when changin pages [#3052](https://github.com/penpot/penpot/issues/3052) +- Fix nested frame interaction created flow in wrong frame [Taiga #5043](https://tree.taiga.io/project/penpot/issue/5043) +- Font-Kerning does not work on Artboard Export to PNG/JPG/PDF [#3029](https://github.com/penpot/penpot/issues/3029) +- Fix manipulate duplicated project (delete, duplicate, rename, pin/unpin...) [Taiga #5027](https://tree.taiga.io/project/penpot/issue/5027) +- Fix deleted files appear in search results [Taiga #5002](https://tree.taiga.io/project/penpot/issue/5002) +- Fix problem with selected colors and texts [Taiga #5051](https://tree.taiga.io/project/penpot/issue/5051) +- Fix problem when assigning color from palette or assets [Taiga #5050](https://tree.taiga.io/project/penpot/issue/5050) +- Fix shortcuts for alignment [Taiga #5030](https://tree.taiga.io/project/penpot/issue/5030) +- Fix path options not showing when editing rects or ellipses [Taiga #5053](https://tree.taiga.io/project/penpot/issue/5053) +- Fix tooltips for some alignment options are truncated on design tab [Taiga #5040](https://tree.taiga.io/project/penpot/issue/5040) +- Fix horizontal margins drag don't always start from place [Taiga #5020](https://tree.taiga.io/project/penpot/issue/5020) +- Fix multiplayer username sometimes is not displayed correctly [Taiga #4400](https://tree.taiga.io/project/penpot/issue/4400) +- Show warning when trying to invite a user that is already in members [Taiga #4147](https://tree.taiga.io/project/penpot/issue/4147) +- Fix problem with text out of borders when changing from auto-width to fixed [Taiga #4308](https://tree.taiga.io/project/penpot/issue/4308) +- Fix header not showing when exiting fullscreen mode in viewer [Taiga #4244](https://tree.taiga.io/project/penpot/issue/4244) +- Fix visual problem in select options [Taiga #5028](https://tree.taiga.io/project/penpot/issue/5028) +- Forbid empty names for assets [Taiga #5056](https://tree.taiga.io/project/penpot/issue/5056) +- Select children after ungroup action [Taiga #4917](https://tree.taiga.io/project/penpot/issue/4917) +- Fix problem with guides not showing when moving over nested frames [Taiga #4905](https://tree.taiga.io/project/penpot/issue/4905) +- Fix change email and password for users signed in via social login [Taiga #4273](https://tree.taiga.io/project/penpot/issue/4273) +- Fix drag and drop files from browser or file explorer under circumstances [Taiga #5054](https://tree.taiga.io/project/penpot/issue/5054) +- Fix problem when copy/pasting shapes [Taiga #4931](https://tree.taiga.io/project/penpot/issue/4931) +- Fix problem with color picker not able to change hue [Taiga #5065](https://tree.taiga.io/project/penpot/issue/5065) +- Fix problem with outer stroke in texts [Taiga #5078](https://tree.taiga.io/project/penpot/issue/5078) +- Fix problem with text carring over next line when changing to fixed [Taiga #5067](https://tree.taiga.io/project/penpot/issue/5067) +- Fix don't show invite user hero to users with editor role [Taiga #5086](https://tree.taiga.io/project/penpot/issue/5086) +- Fix enter emails on onboarding new user creating team [Taiga #5089](https://tree.taiga.io/project/penpot/issue/5089) +- Fix invalid files amount after moving on dashboard [Taiga #5080](https://tree.taiga.io/project/penpot/issue/5080) +- Fix dashboard left sidebar, the [x] overlaps the field [Taiga #5064](https://tree.taiga.io/project/penpot/issue/5064) +- Fix expanded typography on assets sidebar is moving [Taiga #5063](https://tree.taiga.io/project/penpot/issue/5063) +- Fix spelling mistake in confirmation after importing only 1 file [Taiga #5095](https://tree.taiga.io/project/penpot/issue/5095) +- Fix problem with selection colors and texts [Taiga #5079](https://tree.taiga.io/project/penpot/issue/5079) +- Remove "show in view mode" flag when moving frame to frame [Taiga #5091](https://tree.taiga.io/project/penpot/issue/5091) +- Fix problem creating files in project page [Taiga #5060](https://tree.taiga.io/project/penpot/issue/5060) +- Disable empty names on rename files [Taiga #5088](https://tree.taiga.io/project/penpot/issue/5088) +- Fix problem with SVG and flex layout [Taiga #](https://tree.taiga.io/project/penpot/issue/5099) +- Fix unpublish and delete shared library warning messages [Taiga #5090](https://tree.taiga.io/project/penpot/issue/5090) +- Fix last update project timer update after creating new file [Taiga #5096](https://tree.taiga.io/project/penpot/issue/5096) +- Fix dashboard scrolling using 'Page Up' and 'Page Down' [Taiga #5081](https://tree.taiga.io/project/penpot/issue/5081) +- Fix view mode header buttons overlapping in small resolutions [Taiga #5058](https://tree.taiga.io/project/penpot/issue/5058) +- Fix precision for wrap in flex [Taiga #5072](https://tree.taiga.io/project/penpot/issue/5072) +- Fix relative position overlay positioning [Taiga #5092](https://tree.taiga.io/project/penpot/issue/5092) +- Fix hide grid keyboard shortcut [Github #3071](https://github.com/penpot/penpot/pull/3071) +- Fix problem with opacity in imported SVG's [Taiga #4923](https://tree.taiga.io/project/penpot/issue/4923) + +### :heart: Community contributions by (Thank you!) +- To @ondrejkonec: for contributing to the code with: +- Refactor CSS variables [Github #2948](https://github.com/penpot/penpot/pull/2948) + ## 1.17.3 ### :bug: Bugs fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5093f9021..ae27a0135 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,14 +101,14 @@ Each commit should have: Examples of good commit messages: -- :bug: Fix unexpected error on launching modal -- :bug: Set proper error message on generic error -- :sparkles: Enable new modal for profile -- :zap: Improve performance of dashboard navigation -- :wrench: Update default backend configuration -- :books: Add more documentation for authentication process -- :ambulance: Fix critical bug on user registration process -- :tada: Add new approach for user registration +- `:bug: Fix unexpected error on launching modal` +- `:bug: Set proper error message on generic error` +- `:sparkles: Enable new modal for profile` +- `:zap: Improve performance of dashboard navigation` +- `:wrench: Update default backend configuration` +- `:books: Add more documentation for authentication process` +- `:ambulance: Fix critical bug on user registration process` +- `:tada: Add new approach for user registration` ## Code of conduct ## diff --git a/README.md b/README.md index 2a535c03c..b19d279a3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@


- PENPOT + PENPOT

License: MPL-2.0 @@ -50,7 +50,7 @@ Being web based, Penpot is not dependent on operating systems or local installat Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work.

- Open Source + Open Source

@@ -74,7 +74,7 @@ Here’s a step-by-step guide on [getting started with Docker.](https://help.pen If you prefer not to install Penpot in a local environment, [login or register on our Penpot cloud app](https://design.penpot.app). Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** on your own.

- Getting started + Getting started

## Community ## @@ -93,7 +93,7 @@ You will find the following categories: - [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)

- Community + Communnity

## Contributing ## @@ -111,7 +111,7 @@ Every sort of contribution will be very helpful to enhance Penpot. How you’ll To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing-guide](https://help.penpot.app/contributing-guide/).

- Contributing + Contributing

## Resources ## diff --git a/backend/build.clj b/backend/build.clj index 9a7f3bba2..b36251825 100644 --- a/backend/build.clj +++ b/backend/build.clj @@ -16,16 +16,11 @@ {: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.*"] + :exclude [#".*Log4j2Plugins\.dat$"] :basis basis})) (defn compile [_] diff --git a/backend/deps.edn b/backend/deps.edn index 688f4ecec..eef16de18 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -3,9 +3,6 @@ org.clojure/clojure {:mvn/version "1.11.1"} org.clojure/core.async {:mvn/version "1.6.673"} - ;; Logging - org.zeromq/jeromq {:mvn/version "0.5.3"} - com.github.luben/zstd-jni {:mvn/version "1.5.2-5"} org.clojure/data.fressian {:mvn/version "1.0.0"} @@ -29,7 +26,7 @@ com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"} metosin/reitit-core {:mvn/version "0.5.18"} - org.postgresql/postgresql {:mvn/version "42.5.1"} + org.postgresql/postgresql {:mvn/version "42.5.2"} com.zaxxer/HikariCP {:mvn/version "5.0.1"} io.whitfin/siphash {:mvn/version "2.0.0"} @@ -55,7 +52,7 @@ ;; Pretty Print specs pretty-spec/pretty-spec {:mvn/version "0.1.4"} - software.amazon.awssdk/s3 {:mvn/version "2.19.8"} + software.amazon.awssdk/s3 {:mvn/version "2.19.29"} } :paths ["src" "resources" "target/classes"] @@ -70,10 +67,9 @@ mockery/mockery {:mvn/version "RELEASE"}} :extra-paths ["test" "dev"]} - :build {:extra-deps - {io.github.clojure/tools.build {:git/tag "v0.9.0" :git/sha "8c93e0c"}} + {io.github.clojure/tools.build {:git/tag "v0.9.3" :git/sha "e537cd1"}} :ns-default build} :test diff --git a/backend/resources/app/emails/change-email/en.html b/backend/resources/app/email/change-email/en.html similarity index 100% rename from backend/resources/app/emails/change-email/en.html rename to backend/resources/app/email/change-email/en.html diff --git a/backend/resources/app/emails/change-email/en.subj b/backend/resources/app/email/change-email/en.subj similarity index 100% rename from backend/resources/app/emails/change-email/en.subj rename to backend/resources/app/email/change-email/en.subj diff --git a/backend/resources/app/emails/change-email/en.txt b/backend/resources/app/email/change-email/en.txt similarity index 100% rename from backend/resources/app/emails/change-email/en.txt rename to backend/resources/app/email/change-email/en.txt diff --git a/backend/resources/app/emails/feedback/en.html b/backend/resources/app/email/feedback/en.html similarity index 100% rename from backend/resources/app/emails/feedback/en.html rename to backend/resources/app/email/feedback/en.html diff --git a/backend/resources/app/emails/feedback/en.subj b/backend/resources/app/email/feedback/en.subj similarity index 100% rename from backend/resources/app/emails/feedback/en.subj rename to backend/resources/app/email/feedback/en.subj diff --git a/backend/resources/app/emails/feedback/en.txt b/backend/resources/app/email/feedback/en.txt similarity index 100% rename from backend/resources/app/emails/feedback/en.txt rename to backend/resources/app/email/feedback/en.txt diff --git a/backend/resources/app/emails/invite-to-team/en.html b/backend/resources/app/email/invite-to-team/en.html similarity index 100% rename from backend/resources/app/emails/invite-to-team/en.html rename to backend/resources/app/email/invite-to-team/en.html diff --git a/backend/resources/app/emails/invite-to-team/en.subj b/backend/resources/app/email/invite-to-team/en.subj similarity index 100% rename from backend/resources/app/emails/invite-to-team/en.subj rename to backend/resources/app/email/invite-to-team/en.subj diff --git a/backend/resources/app/emails/invite-to-team/en.txt b/backend/resources/app/email/invite-to-team/en.txt similarity index 100% rename from backend/resources/app/emails/invite-to-team/en.txt rename to backend/resources/app/email/invite-to-team/en.txt diff --git a/backend/resources/app/emails/password-recovery/en.html b/backend/resources/app/email/password-recovery/en.html similarity index 100% rename from backend/resources/app/emails/password-recovery/en.html rename to backend/resources/app/email/password-recovery/en.html diff --git a/backend/resources/app/emails/password-recovery/en.subj b/backend/resources/app/email/password-recovery/en.subj similarity index 100% rename from backend/resources/app/emails/password-recovery/en.subj rename to backend/resources/app/email/password-recovery/en.subj diff --git a/backend/resources/app/emails/password-recovery/en.txt b/backend/resources/app/email/password-recovery/en.txt similarity index 100% rename from backend/resources/app/emails/password-recovery/en.txt rename to backend/resources/app/email/password-recovery/en.txt diff --git a/backend/resources/app/emails/register/en.html b/backend/resources/app/email/register/en.html similarity index 100% rename from backend/resources/app/emails/register/en.html rename to backend/resources/app/email/register/en.html diff --git a/backend/resources/app/emails/register/en.subj b/backend/resources/app/email/register/en.subj similarity index 100% rename from backend/resources/app/emails/register/en.subj rename to backend/resources/app/email/register/en.subj diff --git a/backend/resources/app/emails/register/en.txt b/backend/resources/app/email/register/en.txt similarity index 100% rename from backend/resources/app/emails/register/en.txt rename to backend/resources/app/email/register/en.txt diff --git a/backend/resources/app/emails-mjml/change-email/en.mjml b/backend/resources/app/emails-mjml/change-email/en.mjml deleted file mode 100644 index f69c418dc..000000000 --- a/backend/resources/app/emails-mjml/change-email/en.mjml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - Hello {{name}}! - We received a request to change your current email to {{ pending-email }}. - Click to the link below to confirm the change: - - Confirm email change - - - If you received this email by mistake, please consider changing your password - for security reasons. - - Enjoy! - The Penpot team. - - - - - - - Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. - - - - - - - - - - - - - - - - - - - - Penpot © 2020 | Made with <3 and Open Source - - - - - - diff --git a/backend/resources/app/emails-mjml/invite-to-team/en.mjml b/backend/resources/app/emails-mjml/invite-to-team/en.mjml deleted file mode 100644 index 886c7d1d6..000000000 --- a/backend/resources/app/emails-mjml/invite-to-team/en.mjml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - Hello! - - {{invited-by}} has invited you to join the team “{{ team }}”. - - - Accept invite - - Enjoy! - The Penpot team. - - - - - - - Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. - - - - - - - - - - - - - - - - - - - - Penpot © 2020 | Made with <3 and Open Source - - - - - - diff --git a/backend/resources/app/emails-mjml/password-recovery/en.mjml b/backend/resources/app/emails-mjml/password-recovery/en.mjml deleted file mode 100644 index 89bc817b5..000000000 --- a/backend/resources/app/emails-mjml/password-recovery/en.mjml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - Hello {{name}}! - - We have received a request to reset your password. Click the link - below to choose a new one: - - - Reset password - - - If you received this email by mistake, you can safely ignore - it. Your password won't be changed. - - Enjoy! - The Penpot team. - - - - - - - Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. - - - - - - - - - - - - - - - - - - - - Penpot © 2020 | Made with <3 and Open Source - - - - - - diff --git a/backend/resources/app/emails-mjml/register/en.mjml b/backend/resources/app/emails-mjml/register/en.mjml deleted file mode 100644 index 38b774e13..000000000 --- a/backend/resources/app/emails-mjml/register/en.mjml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - Hello {{name}}! - - Thanks for signing up for your Penpot account! Please verify your - email using the link below and get started building mockups and - prototypes today! - - - Verify email - - Enjoy! - The Penpot team. - - - - - - - Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. - - - - - - - - - - - - - - - - - - - - Penpot © 2020 | Made with <3 and Open Source - - - - - - diff --git a/backend/resources/app/templates/error-report.v2.tmpl b/backend/resources/app/templates/error-report.v2.tmpl new file mode 100644 index 000000000..93b43036e --- /dev/null +++ b/backend/resources/app/templates/error-report.v2.tmpl @@ -0,0 +1,112 @@ + {% extends "app/templates/base.tmpl" %} + +{% block title %} +penpot - error report v2 {{id}} +{% endblock %} + +{% block content %} + +
+
+
+
MESSAGE:
+ +
+

{{hint}}

+
+
+ +
+
LOG PROPS:
+
+
{{props}}
+
+
+ +
+
CONTEXT:
+ +
+
{{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/app/templates/styles.css b/backend/resources/app/templates/styles.css index d57fd0460..499f75cb3 100644 --- a/backend/resources/app/templates/styles.css +++ b/backend/resources/app/templates/styles.css @@ -23,6 +23,10 @@ input[type=text], input[type=submit] { padding: 3px; } +pre { + white-space: pre-wrap; +} + main { margin: 20px; } diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml index 426597008..4625a47bf 100644 --- a/backend/resources/log4j2-devenv.xml +++ b/backend/resources/log4j2-devenv.xml @@ -14,11 +14,6 @@ - - - tcp://localhost:45556 - - @@ -37,17 +32,12 @@ - - - - - diff --git a/backend/resources/log4j2.xml b/backend/resources/log4j2.xml index fba649ab7..685eac0fb 100644 --- a/backend/resources/log4j2.xml +++ b/backend/resources/log4j2.xml @@ -12,6 +12,7 @@ + diff --git a/backend/resources/rlimit.edn b/backend/resources/rlimit.edn index c7df92bdf..acb131bf1 100644 --- a/backend/resources/rlimit.edn +++ b/backend/resources/rlimit.edn @@ -3,8 +3,8 @@ {:default [[:default :window "200000/h"]] - #{:query/teams} + #{:command/get-teams} [[:burst :bucket "5/1/5s"]] - #{:query/profile} - [[:burst :bucket "100/60/1m"]]} + #{:command/get-profile} + [[:burst :bucket "60/60/1m"]]} diff --git a/backend/scripts/repl b/backend/scripts/repl index 9be6c1490..d253345ee 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -45,11 +45,12 @@ export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot export OPTIONS=" -A:jmx-remote -A:dev \ -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ - -J-Dlog4j2.configurationFile=log4j2-devenv.xml \ - -J-XX:+UseG1GC \ - -J-XX:-OmitStackTraceInFastThrow \ - -J-Xms50m -J-Xmx1024m \ -J-Djdk.attach.allowAttachSelf \ + -J-Dlog4j2.configurationFile=log4j2-devenv.xml \ + -J-Xms50m \ + -J-Xmx1024m \ + -J-XX:+UseZGC \ + -J-XX:-OmitStackTraceInFastThrow \ -J-XX:+UnlockDiagnosticVMOptions \ -J-XX:+DebugNonSafepoints"; diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index 603149d28..fa857d5d4 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -21,7 +21,7 @@ [app.http.session :as session] [app.loggers.audit :as audit] [app.main :as-alias main] - [app.rpc.queries.profile :as profile] + [app.rpc.commands.profile :as profile] [app.tokens :as tokens] [app.util.json :as json] [app.util.time :as dt] @@ -375,7 +375,7 @@ ::fullname ::props])) -(defn retrieve-info +(defn get-info [{:keys [provider] :as cfg} {:keys [params] :as request}] (letfn [(validate-oidc [info] ;; If the provider is OIDC, we can proceed to check @@ -422,14 +422,12 @@ (p/then' validate-oidc) (p/then' (partial post-process state)))))) -(defn- retrieve-profile +(defn- get-profile [{:keys [::db/pool ::wrk/executor] :as cfg} info] (px/with-dispatch executor (with-open [conn (db/open pool)] (some->> (:email info) - (profile/retrieve-profile-data-by-email conn) - (profile/populate-additional-data conn) - (profile/decode-profile-row))))) + (profile/get-profile-by-email conn))))) (defn- redirect-response [uri] @@ -443,9 +441,9 @@ (redirect-response uri))) (defn- generate-redirect - [{:keys [::session/session] :as cfg} request info profile] + [cfg request info profile] (if profile - (let [sxf (session/create-fn session (:id profile)) + (let [sxf (session/create-fn cfg (:id profile)) token (or (:invitation-token info) (tokens/generate (::main/props cfg) {:iss :auth @@ -496,8 +494,8 @@ (defn- callback-handler [cfg request] (letfn [(process-request [] - (p/let [info (retrieve-info cfg request) - profile (retrieve-profile cfg info)] + (p/let [info (get-info cfg request) + profile (get-profile cfg info)] (generate-redirect cfg request info profile))) (handle-error [cause] @@ -549,23 +547,24 @@ (s/def ::providers (s/map-of ::us/keyword (s/nilable ::provider))) +(s/def ::routes vector?) + (defmethod ig/pre-init-spec ::routes [_] - (s/keys :req [::http/client + (s/keys :req [::session/manager + ::http/client ::wrk/executor ::main/props ::db/pool - ::providers - ::session/session])) + ::providers])) (defmethod ig/init-key ::routes - [_ {:keys [::wrk/executor ::session/session] :as cfg}] + [_ {:keys [::wrk/executor] :as cfg}] (let [cfg (update cfg :provider d/without-nils)] - ["" {:middleware [[(:middleware session)] + ["" {:middleware [[session/authz cfg] [hmw/with-dispatch executor] [hmw/with-config cfg] - [provider-lookup] - ]} + [provider-lookup]]} ["/auth/oauth" ["/:provider" {:handler auth-handler @@ -573,4 +572,3 @@ ["/:provider/callback" {:handler callback-handler :allowed-methods #{:get}}]]])) - diff --git a/backend/src/app/cli/manage.clj b/backend/src/app/cli/manage.clj index fedf040bc..a1799434a 100644 --- a/backend/src/app/cli/manage.clj +++ b/backend/src/app/cli/manage.clj @@ -10,9 +10,8 @@ [app.common.logging :as l] [app.db :as db] [app.main :as main] - [app.rpc.commands.auth :as cmd.auth] - [app.rpc.mutations.profile :as profile] - [app.rpc.queries.profile :refer [retrieve-profile-data-by-email]] + [app.rpc.commands.auth :as auth] + [app.rpc.commands.profile :as profile] [clojure.string :as str] [clojure.tools.cli :refer [parse-opts]] [integrant.core :as ig]) @@ -55,16 +54,17 @@ :type :password}))] (try (db/with-atomic [conn (:app.db/pool system)] - (->> (cmd.auth/create-profile conn - {:fullname fullname - :email email - :password password - :is-active true - :is-demo false}) - (cmd.auth/create-profile-relations conn))) + (->> (auth/create-profile! conn + {:fullname fullname + :email email + :password password + :is-active true + :is-demo false}) + (auth/create-profile-rels! conn))) (when (pos? (:verbosity options)) (println "User created successfully.")) + (System/exit 0) (catch Exception _e @@ -79,7 +79,7 @@ (db/with-atomic [conn (:app.db/pool system)] (let [email (or (:email options) (read-from-console {:label "Email:"})) - profile (retrieve-profile-data-by-email conn email)] + profile (profile/get-profile-by-email conn email)] (when-not profile (when (pos? (:verbosity options)) (println "Profile does not exists.")) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index ff0a187ad..b66d60796 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -51,7 +51,6 @@ :database-password "penpot" :default-blob-version 5 - :loggers-zmq-uri "tcp://localhost:45556" :rpc-rlimit-config (fs/path "resources/rlimit.edn") :rpc-climit-config (fs/path "resources/climit.edn") @@ -126,6 +125,7 @@ (s/def ::database-max-pool-size ::us/integer) (s/def ::quotes-teams-per-profile ::us/integer) +(s/def ::quotes-access-tokens-per-profile ::us/integer) (s/def ::quotes-projects-per-team ::us/integer) (s/def ::quotes-invitations-per-team ::us/integer) (s/def ::quotes-profiles-per-team ::us/integer) @@ -174,8 +174,6 @@ (s/def ::ldap-ssl ::us/boolean) (s/def ::ldap-starttls ::us/boolean) (s/def ::ldap-user-query ::us/string) -(s/def ::loggers-loki-uri ::us/string) -(s/def ::loggers-zmq-uri ::us/string) (s/def ::media-directory ::us/string) (s/def ::media-uri ::us/string) (s/def ::profile-bounce-max-age ::dt/duration) @@ -271,8 +269,6 @@ ::ldap-starttls ::ldap-user-query ::local-assets-uri - ::loggers-loki-uri - ::loggers-zmq-uri ::media-max-file-size ::profile-bounce-max-age ::profile-bounce-threshold @@ -281,6 +277,7 @@ ::public-uri ::quotes-teams-per-profile + ::quotes-access-tokens-per-profile ::quotes-projects-per-team ::quotes-invitations-per-team ::quotes-profiles-per-team @@ -355,7 +352,7 @@ (merge defaults) (us/conform ::config)) (catch Throwable e - (when (ex/ex-info? e) + (when (ex/error? e) (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;") (println "Error on validating configuration:") (println (some-> e ex-data ex/explain)) diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index f23e1fbec..8bda9e1d0 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -17,7 +17,6 @@ [app.db.sql :as sql] [app.metrics :as mtx] [app.util.json :as json] - [app.util.migrations :as mg] [app.util.time :as dt] [clojure.java.io :as io] [clojure.spec.alpha :as s] @@ -32,7 +31,6 @@ io.whitfin.siphash.SipHasherContainer java.io.InputStream java.io.OutputStream - java.lang.AutoCloseable java.sql.Connection java.sql.Savepoint org.postgresql.PGConnection @@ -50,12 +48,9 @@ ;; Initialization ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare apply-migrations!) - (s/def ::connection-timeout ::us/integer) (s/def ::max-size ::us/integer) (s/def ::min-size ::us/integer) -(s/def ::migrations map?) (s/def ::name keyword?) (s/def ::password ::us/string) (s/def ::uri ::us/not-empty-string) @@ -64,26 +59,26 @@ (s/def ::read-only? ::us/boolean) (s/def ::pool-options - (s/keys :opt-un [::uri ::name - ::min-size - ::max-size - ::connection-timeout - ::validation-timeout - ::migrations - ::username - ::password - ::mtx/metrics - ::read-only?])) + (s/keys :opt [::uri + ::name + ::min-size + ::max-size + ::connection-timeout + ::validation-timeout + ::username + ::password + ::mtx/metrics + ::read-only?])) (def defaults - {:name :main - :min-size 0 - :max-size 60 - :connection-timeout 10000 - :validation-timeout 10000 - :idle-timeout 120000 ; 2min - :max-lifetime 1800000 ; 30m - :read-only? false}) + {::name :main + ::min-size 0 + ::max-size 60 + ::connection-timeout 10000 + ::validation-timeout 10000 + ::idle-timeout 120000 ; 2min + ::max-lifetime 1800000 ; 30m + ::read-only? false}) (defmethod ig/prep-key ::pool [_ cfg] @@ -93,39 +88,23 @@ (defmethod ig/pre-init-spec ::pool [_] ::pool-options) (defmethod ig/init-key ::pool - [_ {:keys [migrations read-only? uri] :as cfg}] - (if uri - (let [pool (create-pool cfg)] - (l/info :hint "initialize connection pool" - :name (d/name (:name cfg)) - :uri uri - :read-only read-only? - :with-credentials (and (contains? cfg :username) - (contains? cfg :password)) - :min-size (:min-size cfg) - :max-size (:max-size cfg)) - (when-not read-only? - (some->> (seq migrations) (apply-migrations! pool))) - pool) - - (do - (l/warn :hint "unable to initialize pool, missing url" - :name (d/name (:name cfg)) - :read-only read-only?) - nil))) + [_ {:keys [::uri ::read-only?] :as cfg}] + (when uri + (l/info :hint "initialize connection pool" + :name (d/name (::name cfg)) + :uri uri + :read-only read-only? + :with-credentials (and (contains? cfg ::username) + (contains? cfg ::password)) + :min-size (::min-size cfg) + :max-size (::max-size cfg)) + (create-pool cfg))) (defmethod ig/halt-key! ::pool [_ pool] (when pool (.close ^HikariDataSource pool))) -(defn- apply-migrations! - [pool migrations] - (with-open [conn ^AutoCloseable (open pool)] - (mg/setup! conn) - (doseq [[name steps] migrations] - (mg/migrate! conn {:name (d/name name) :steps steps})))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; API & Impl ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -135,19 +114,19 @@ "SET idle_in_transaction_session_timeout = 300000;")) (defn- create-datasource-config - [{:keys [metrics uri] :as cfg}] + [{:keys [::mtx/metrics ::uri] :as cfg}] (let [config (HikariConfig.)] (doto config (.setJdbcUrl (str "jdbc:" uri)) - (.setPoolName (d/name (:name cfg))) + (.setPoolName (d/name (::name cfg))) (.setAutoCommit true) - (.setReadOnly (:read-only? cfg)) - (.setConnectionTimeout (:connection-timeout cfg)) - (.setValidationTimeout (:validation-timeout cfg)) - (.setIdleTimeout (:idle-timeout cfg)) - (.setMaxLifetime (:max-lifetime cfg)) - (.setMinimumIdle (:min-size cfg)) - (.setMaximumPoolSize (:max-size cfg)) + (.setReadOnly (::read-only? cfg)) + (.setConnectionTimeout (::connection-timeout cfg)) + (.setValidationTimeout (::validation-timeout cfg)) + (.setIdleTimeout (::idle-timeout cfg)) + (.setMaxLifetime (::max-lifetime cfg)) + (.setMinimumIdle (::min-size cfg)) + (.setMaximumPoolSize (::max-size cfg)) (.setConnectionInitSql initsql) (.setInitializationFailTimeout -1)) @@ -157,8 +136,8 @@ (PrometheusMetricsTrackerFactory.) (.setMetricsTrackerFactory config))) - (some->> ^String (:username cfg) (.setUsername config)) - (some->> ^String (:password cfg) (.setPassword config)) + (some->> ^String (::username cfg) (.setUsername config)) + (some->> ^String (::password cfg) (.setPassword config)) config)) @@ -166,11 +145,9 @@ [v] (instance? javax.sql.DataSource v)) -(s/def ::pool pool?) (s/def ::conn some?) - -;; DEPRECATED: to be removed in 1.18 -(s/def ::conn-or-pool some?) +(s/def ::nilable-pool (s/nilable ::pool)) +(s/def ::pool pool?) (s/def ::pool-or-conn some?) (defn closed? @@ -178,8 +155,18 @@ (.isClosed ^HikariDataSource pool)) (defn read-only? - [pool] - (.isReadOnly ^HikariDataSource pool)) + [pool-or-conn] + (cond + (instance? HikariDataSource pool-or-conn) + (.isReadOnly ^HikariDataSource pool-or-conn) + + (instance? Connection pool-or-conn) + (.isReadOnly ^Connection pool-or-conn) + + :else + (ex/raise :type :internal + :code :invalid-connection + :hint "invalid connection provided"))) (defn create-pool [cfg] @@ -237,44 +224,46 @@ [pool] (jdbc/get-connection pool)) +(def ^:private default-opts + {:builder-fn sql/as-kebab-maps}) + (defn exec! ([ds sv] - (exec! ds sv {})) + (jdbc/execute! ds sv default-opts)) ([ds sv opts] - (jdbc/execute! ds sv (assoc opts :builder-fn sql/as-kebab-maps)))) + (jdbc/execute! ds sv (merge default-opts opts)))) (defn exec-one! - ([ds sv] (exec-one! ds sv {})) + ([ds sv] + (jdbc/execute-one! ds sv default-opts)) ([ds sv opts] - (jdbc/execute-one! ds sv (assoc opts :builder-fn sql/as-kebab-maps)))) + (jdbc/execute-one! ds sv + (-> (merge default-opts opts) + (assoc :return-keys (::return-keys? opts false)))))) (defn insert! - ([ds table params] (insert! ds table params nil)) - ([ds table params opts] - (exec-one! ds - (sql/insert table params opts) - (merge {:return-keys true} opts)))) + [ds table params & {:as opts}] + (exec-one! ds + (sql/insert table params opts) + (merge {::return-keys? true} opts))) (defn insert-multi! - ([ds table cols rows] (insert-multi! ds table cols rows nil)) - ([ds table cols rows opts] - (exec! ds - (sql/insert-multi table cols rows opts) - (merge {:return-keys true} opts)))) + [ds table cols rows & {:as opts}] + (exec! ds + (sql/insert-multi table cols rows opts) + (merge {::return-keys? true} opts))) (defn update! - ([ds table params where] (update! ds table params where nil)) - ([ds table params where opts] - (exec-one! ds - (sql/update table params where opts) - (merge {:return-keys true} opts)))) + [ds table params where & {:as opts}] + (exec-one! ds + (sql/update table params where opts) + (merge {::return-keys? true} opts))) (defn delete! - ([ds table params] (delete! ds table params nil)) - ([ds table params opts] - (exec-one! ds - (sql/delete table params opts) - (assoc opts :return-keys true)))) + [ds table params & {:as opts}] + (exec-one! ds + (sql/delete table params opts) + (merge {::return-keys? true} opts))) (defn is-row-deleted? [{:keys [deleted-at]}] @@ -283,56 +272,34 @@ (inst-ms (dt/now))))) (defn get* - "Internal function for retrieve a single row from database that - matches a simple filters." - ([ds table params] - (get* ds table params nil)) - ([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}] - (let [rows (exec! ds (sql/select table params opts)) - rows (cond->> rows - check-deleted? - (remove is-row-deleted?))] - (first rows)))) + "Retrieve a single row from database that matches a simple filters. Do + not raises exceptions." + [ds table params & {:as opts}] + (let [rows (exec! ds (sql/select table params opts)) + rows (cond->> rows + (::remove-deleted? opts true) + (remove is-row-deleted?))] + (first rows))) (defn get - ([ds table params] - (get ds table params nil)) - ([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}] - (let [row (get* ds table params opts)] - (when (and (not row) check-deleted?) - (ex/raise :type :not-found - :code :object-not-found - :table table - :hint "database object not found")) - row))) - -(defn get-by-params - "DEPRECATED" - ([ds table params] - (get-by-params ds table params nil)) - ([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}] - (let [row (get* ds table params (assoc opts :check-deleted? check-not-found))] - (when (and (not row) check-not-found) - (ex/raise :type :not-found - :code :object-not-found - :table table - :hint "database object not found")) - row))) + "Retrieve a single row from database that matches a simple + filters. Raises :not-found exception if no object is found." + [ds table params & {:as opts}] + (let [row (get* ds table params opts)] + (when (and (not row) (::check-deleted? opts true)) + (ex/raise :type :not-found + :code :object-not-found + :table table + :hint "database object not found")) + row)) (defn get-by-id - ([ds table id] - (get ds table {:id id} nil)) - ([ds table id opts] - (let [opts (cond-> opts - (contains? opts :check-not-found) - (assoc :check-deleted? (:check-not-found opts)))] - (get ds table {:id id} opts)))) + [ds table id & {:as opts}] + (get ds table {:id id} opts)) (defn query - ([ds table params] - (query ds table params nil)) - ([ds table params opts] - (exec! ds (sql/select table params opts)))) + [ds table params & {:as opts}] + (exec! ds (sql/select table params opts))) (defn pgobject? ([v] @@ -475,6 +442,11 @@ (.setType "jsonb") (.setValue (json/encode-str data))))) +(defn get-update-count + [result] + (:next.jdbc/update-count result)) + + ;; --- Locks (def ^:private siphash-state diff --git a/backend/src/app/db/sql.clj b/backend/src/app/db/sql.clj index 998d594c8..854b2211c 100644 --- a/backend/src/app/db/sql.clj +++ b/backend/src/app/db/sql.clj @@ -7,6 +7,7 @@ (ns app.db.sql (:refer-clojure :exclude [update]) (:require + [app.db :as-alias db] [clojure.string :as str] [next.jdbc.optional :as jdbc-opt] [next.jdbc.sql.builder :as sql])) @@ -43,8 +44,10 @@ ([table where-params opts] (let [opts (merge default-opts opts) opts (cond-> opts - (:for-update opts) (assoc :suffix "FOR UPDATE") - (:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))] + (::db/for-update? opts) (assoc :suffix "FOR UPDATE") + (::db/for-share? opts) (assoc :suffix "FOR KEY SHARE") + (:for-update opts) (assoc :suffix "FOR UPDATE") + (:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))] (sql/for-query table where-params opts)))) (defn update diff --git a/backend/src/app/emails.clj b/backend/src/app/email.clj similarity index 92% rename from backend/src/app/emails.clj rename to backend/src/app/email.clj index 8a69f11d6..40958cb81 100644 --- a/backend/src/app/emails.clj +++ b/backend/src/app/email.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.emails +(ns app.email "Main api for send emails." (:require [app.common.exceptions :as ex] @@ -14,7 +14,7 @@ [app.config :as cf] [app.db :as db] [app.db.sql :as sql] - [app.emails.invite-to-team :as-alias emails.invite-to-team] + [app.email.invite-to-team :as-alias email.invite-to-team] [app.metrics :as mtx] [app.util.template :as tmpl] [app.worker :as wrk] @@ -71,7 +71,7 @@ (.addFrom ^MimeMessage mmsg from))))) (defn- assign-reply-to - [mmsg {:keys [default-reply-to] :as cfg} {:keys [reply-to] :as params}] + [mmsg {:keys [::default-reply-to] :as cfg} {:keys [reply-to] :as params}] (let [reply-to (or reply-to default-reply-to)] (when reply-to (let [reply-to (parse-address reply-to)] @@ -127,9 +127,8 @@ mmsg)) (defn- opts->props - [{:keys [username tls host port timeout default-from] - :or {timeout 30000} - :as opts}] + [{:keys [::username ::tls ::host ::port ::timeout ::default-from] + :or {timeout 30000}}] (reduce-kv (fn [^Properties props k v] (if (nil? v) @@ -150,8 +149,8 @@ "mail.smtp.connectiontimeout" timeout})) (defn- create-smtp-session - [opts] - (let [props (opts->props opts)] + [cfg] + (let [props (opts->props cfg)] (Session/getInstance props))) (defn- create-smtp-message @@ -171,7 +170,7 @@ ;; TEMPLATE EMAIL IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def ^:private email-path "app/emails/%(id)s/%(lang)s.%(type)s") +(def ^:private email-path "app/email/%(id)s/%(lang)s.%(type)s") (defn- render-email-template-part [type id context] @@ -283,14 +282,14 @@ (s/def ::default-from ::cf/smtp-default-from) (s/def ::smtp-config - (s/keys :opt-un [::username - ::password - ::tls - ::ssl - ::host - ::port - ::default-from - ::default-reply-to])) + (s/keys :opt [::username + ::password + ::tls + ::ssl + ::host + ::port + ::default-from + ::default-reply-to])) (declare send-to-logger!) @@ -306,8 +305,8 @@ (let [session (create-smtp-session cfg)] (with-open [transport (.getTransport session (if (:ssl cfg) "smtps" "smtp"))] (.connect ^Transport transport - ^String (:username cfg) - ^String (:password cfg)) + ^String (::username cfg) + ^String (::password cfg)) (let [^MimeMessage message (create-smtp-message cfg session params)] (.sendMessage ^Transport transport @@ -319,10 +318,10 @@ (send-to-logger! cfg params)))) (defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::sendmail ::mtx/metrics])) + (s/keys :req [::sendmail ::mtx/metrics])) (defmethod ig/init-key ::handler - [_ {:keys [sendmail]}] + [_ {:keys [::sendmail]}] (fn [{:keys [props] :as task}] (sendmail props))) @@ -380,14 +379,14 @@ "Password change confirmation email" (template-factory ::change-email)) -(s/def ::emails.invite-to-team/invited-by ::us/string) -(s/def ::emails.invite-to-team/team ::us/string) -(s/def ::emails.invite-to-team/token ::us/string) +(s/def ::email.invite-to-team/invited-by ::us/string) +(s/def ::email.invite-to-team/team ::us/string) +(s/def ::email.invite-to-team/token ::us/string) (s/def ::invite-to-team - (s/keys :req-un [::emails.invite-to-team/invited-by - ::emails.invite-to-team/token - ::emails.invite-to-team/team])) + (s/keys :req-un [::email.invite-to-team/invited-by + ::email.invite-to-team/token + ::email.invite-to-team/team])) (def invite-to-team "Teams member invitation email." diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 6cb6b398c..cf212609c 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -6,13 +6,22 @@ (ns app.http (:require + [app.auth.oidc :as-alias oidc] [app.common.data :as d] [app.common.logging :as l] [app.common.transit :as t] + [app.db :as-alias db] + [app.http.access-token :as actoken] + [app.http.assets :as-alias assets] + [app.http.awsns :as-alias awsns] + [app.http.debug :as-alias debug] [app.http.errors :as errors] [app.http.middleware :as mw] [app.http.session :as session] + [app.http.websocket :as-alias ws] [app.metrics :as mtx] + [app.rpc :as-alias rpc] + [app.rpc.doc :as-alias rpc.doc] [app.worker :as wrk] [clojure.spec.alpha :as s] [integrant.core :as ig] @@ -37,47 +46,53 @@ (s/def ::max-body-size integer?) (s/def ::max-multipart-body-size integer?) (s/def ::io-threads integer?) -(s/def ::worker-threads integer?) (defmethod ig/prep-key ::server [_ cfg] - (merge {:name "http" - :port 6060 - :host "0.0.0.0" - :max-body-size (* 1024 1024 30) ; 30 MiB - :max-multipart-body-size (* 1024 1024 120)} ; 120 MiB + (merge {::port 6060 + ::host "0.0.0.0" + ::max-body-size (* 1024 1024 30) ; 30 MiB + ::max-multipart-body-size (* 1024 1024 120)} ; 120 MiB (d/without-nils cfg))) (defmethod ig/pre-init-spec ::server [_] - (s/and - (s/keys :req-un [::port ::host ::name ::max-body-size ::max-multipart-body-size] - :opt-un [::router ::handler ::io-threads ::worker-threads ::wrk/executor]) - (fn [cfg] - (or (contains? cfg :router) - (contains? cfg :handler))))) + (s/keys :req [::port ::host] + :opt [::max-body-size + ::max-multipart-body-size + ::router + ::handler + ::io-threads + ::wrk/executor])) (defmethod ig/init-key ::server - [_ {:keys [handler router port name host] :as cfg}] - (l/info :hint "starting http server" :port port :host host :name name) + [_ {:keys [::handler ::router ::host ::port] :as cfg}] + (l/info :hint "starting http server" :port port :host host) (let [options {:http/port port :http/host host - :http/max-body-size (:max-body-size cfg) - :http/max-multipart-body-size (:max-multipart-body-size cfg) - :xnio/io-threads (:io-threads cfg) - :xnio/worker-threads (:worker-threads cfg) - :xnio/dispatch (:executor cfg) + :http/max-body-size (::max-body-size cfg) + :http/max-multipart-body-size (::max-multipart-body-size cfg) + :xnio/io-threads (::io-threads cfg) + :xnio/dispatch (::wrk/executor cfg) :ring/async true} - handler (if (some? router) + handler (cond + (some? router) (wrap-router router) - handler) - server (yt/server handler (d/without-nils options))] - (assoc cfg :server (yt/start! server)))) + (some? handler) + handler + + :else + (throw (UnsupportedOperationException. "handler or router are required"))) + + options (d/without-nils options) + server (yt/server handler options)] + + (assoc cfg ::server (yt/start! server)))) (defmethod ig/halt-key! ::server - [_ {:keys [server name port] :as cfg}] - (l/info :msg "stopping http server" :name name :port port) + [_ {:keys [::server ::port] :as cfg}] + (l/info :msg "stopping http server" :port port) (yt/stop! server)) (defn- not-found-handler @@ -113,64 +128,41 @@ ;; HTTP ROUTER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::assets map?) -(s/def ::awsns-handler fn?) -(s/def ::debug-routes (s/nilable vector?)) -(s/def ::doc-routes (s/nilable vector?)) -(s/def ::feedback fn?) -(s/def ::oauth map?) -(s/def ::oidc-routes (s/nilable vector?)) -(s/def ::rpc-routes (s/nilable vector?)) -(s/def ::session ::session/session) -(s/def ::storage map?) -(s/def ::ws fn?) - (defmethod ig/pre-init-spec ::router [_] - (s/keys :req-un [::mtx/metrics - ::ws - ::storage - ::assets - ::session - ::feedback - ::awsns-handler - ::debug-routes - ::oidc-routes - ::rpc-routes - ::doc-routes])) + (s/keys :req [::session/manager + ::actoken/manager + ::ws/routes + ::rpc/routes + ::rpc.doc/routes + ::oidc/routes + ::assets/routes + ::debug/routes + ::db/pool + ::mtx/routes + ::awsns/routes])) (defmethod ig/init-key ::router - [_ {:keys [ws session metrics assets feedback] :as cfg}] + [_ cfg] (rr/router [["" {:middleware [[mw/server-timing] [mw/format-response] [mw/params] [mw/parse-request] - [session/middleware-1 session] + [session/soft-auth cfg] + [actoken/soft-auth cfg] [mw/errors errors/handle] [mw/restrict-methods]]} - ["/metrics" {:handler (::mtx/handler metrics) - :allowed-methods #{:get}}] - - ["/assets" {:middleware [[session/middleware-2 session]]} - ["/by-id/:id" {:handler (:objects-handler assets)}] - ["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}] - ["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]] - - (:debug-routes cfg) + (::mtx/routes cfg) + (::assets/routes cfg) + (::debug/routes cfg) ["/webhooks" - ["/sns" {:handler (:awsns-handler cfg) - :allowed-methods #{:post}}]] + (::awsns/routes cfg)] - ["/ws/notifications" {:middleware [[session/middleware-2 session]] - :handler ws - :allowed-methods #{:get}}] + (::ws/routes cfg) - ["/api" {:middleware [[mw/cors] - [session/middleware-2 session]]} - ["/feedback" {:handler feedback - :allowed-methods #{:post}}] - (:doc-routes cfg) - (:oidc-routes cfg) - (:rpc-routes cfg)]]])) + ["/api" {:middleware [[mw/cors]]} + (::oidc/routes cfg) + (::rpc.doc/routes cfg) + (::rpc/routes cfg)]]])) diff --git a/backend/src/app/http/access_token.clj b/backend/src/app/http/access_token.clj new file mode 100644 index 000000000..76cf07eef --- /dev/null +++ b/backend/src/app/http/access_token.clj @@ -0,0 +1,96 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.http.access-token + (:require + [app.common.logging :as l] + [app.common.spec :as us] + [app.config :as cf] + [app.db :as db] + [app.main :as-alias main] + [app.tokens :as tokens] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [promesa.core :as p] + [promesa.exec :as px] + [yetti.request :as yrq])) + + +(s/def ::manager + (s/keys :req [::db/pool ::wrk/executor ::main/props])) + +(defmethod ig/pre-init-spec ::manager [_] ::manager) +(defmethod ig/init-key ::manager [_ cfg] cfg) +(defmethod ig/halt-key! ::manager [_ _]) + +(def header-re #"^Token\s+(.*)") + +(defn- get-token + [request] + (some->> (yrq/get-header request "authorization") + (re-matches header-re) + (second))) + +(defn- decode-token + [props token] + (when token + (tokens/verify props {:token token :iss "access-token"}))) + +(defn- get-token-perms + [pool token-id] + (when-not (db/read-only? pool) + (when-let [token (db/get* pool :access-token {:id token-id} {:columns [:perms]})] + (some-> (:perms token) + (db/decode-pgarray #{}))))) + +(defn- wrap-soft-auth + [handler {:keys [::manager]}] + (us/assert! ::manager manager) + + (let [{:keys [::wrk/executor ::main/props]} manager] + (fn [request respond raise] + (let [token (get-token request)] + (->> (px/submit! executor (partial decode-token props token)) + (p/fnly (fn [claims cause] + (when cause + (l/trace :hint "exception on decoding malformed token" :cause cause)) + (let [request (cond-> request + (map? claims) + (assoc ::id (:tid claims)))] + (handler request respond raise))))))))) + +(defn- wrap-authz + [handler {:keys [::manager]}] + (us/assert! ::manager manager) + (let [{:keys [::wrk/executor ::db/pool]} manager] + (fn [request respond raise] + (if-let [token-id (::id request)] + (->> (px/submit! executor (partial get-token-perms pool token-id)) + (p/fnly (fn [perms cause] + (cond + (some? cause) + (raise cause) + + (nil? perms) + (handler request respond raise) + + :else + (let [request (assoc request ::perms perms)] + (handler request respond raise)))))) + (handler request respond raise))))) + +(def soft-auth + {:name ::soft-auth + :compile (fn [& _] + (when (contains? cf/flags :access-tokens) + wrap-soft-auth))}) + +(def authz + {:name ::authz + :compile (fn [& _] + (when (contains? cf/flags :access-tokens) + wrap-authz))}) diff --git a/backend/src/app/http/assets.clj b/backend/src/app/http/assets.clj index 0fd6d1510..56584e37f 100644 --- a/backend/src/app/http/assets.clj +++ b/backend/src/app/http/assets.clj @@ -7,18 +7,17 @@ (ns app.http.assets "Assets related handlers." (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uri :as u] [app.db :as db] - [app.metrics :as mtx] [app.storage :as sto] [app.util.time :as dt] [app.worker :as wrk] [clojure.spec.alpha :as s] [integrant.core :as ig] [promesa.core :as p] - [promesa.exec :as px] [yetti.response :as yrs])) (def ^:private cache-max-age @@ -27,105 +26,100 @@ (def ^:private signature-max-age (dt/duration {:hours 24 :minutes 15})) -(defn coerce-id - [id] - (let [res (parse-uuid id)] - (when-not (uuid? res) - (ex/raise :type :not-found - :hint "object not found")) - res)) +(defn get-id + [{:keys [path-params]}] + (if-let [id (some-> path-params :id d/parse-uuid)] + (p/resolved id) + (p/rejected (ex/error :type :not-found + :hunt "object not found")))) (defn- get-file-media-object - [{:keys [pool executor] :as storage} id] - (px/with-dispatch executor - (let [id (coerce-id id) - mobj (db/exec-one! pool ["select * from file_media_object where id=?" id])] - (when-not mobj - (ex/raise :type :not-found - :hint "object does not found")) - mobj))) + [pool id] + (db/get pool :file-media-object {:id id})) + +(defn- serve-object-from-s3 + [{:keys [::sto/storage] :as cfg} obj] + (let [mdata (meta obj)] + (->> (sto/get-object-url storage obj {:max-age signature-max-age}) + (p/fmap (fn [{:keys [host port] :as url}] + (let [headers {"location" (str url) + "x-host" (cond-> host port (str ":" port)) + "x-mtype" (:content-type mdata) + "cache-control" (str "max-age=" (inst-ms cache-max-age))}] + (yrs/response + :status 307 + :headers headers))))))) + +(defn- serve-object-from-fs + [{:keys [::path]} obj] + (let [purl (u/join (u/uri path) + (sto/object->relative-path obj)) + mdata (meta obj) + headers {"x-accel-redirect" (:path purl) + "content-type" (:content-type mdata) + "cache-control" (str "max-age=" (inst-ms cache-max-age))}] + (p/resolved + (yrs/response :status 204 :headers headers)))) (defn- serve-object "Helper function that returns the appropriate response depending on the storage object backend type." - [{:keys [storage] :as cfg} obj] - (let [mdata (meta obj) - backend (sto/resolve-backend storage (:backend obj))] - (case (:type backend) - :s3 - (p/let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})] - (yrs/response :status 307 - :headers {"location" (str url) - "x-host" (cond-> host port (str ":" port)) - "x-mtype" (:content-type mdata) - "cache-control" (str "max-age=" (inst-ms cache-max-age))})) - - :fs - (p/let [purl (u/uri (:assets-path cfg)) - purl (u/join purl (sto/object->relative-path obj))] - (yrs/response :status 204 - :headers {"x-accel-redirect" (:path purl) - "content-type" (:content-type mdata) - "cache-control" (str "max-age=" (inst-ms cache-max-age))}))))) + [{:keys [::sto/storage] :as cfg} {:keys [backend] :as obj}] + (let [backend (sto/resolve-backend storage backend)] + (case (::sto/type backend) + :s3 (serve-object-from-s3 cfg obj) + :fs (serve-object-from-fs cfg obj)))) (defn objects-handler "Handler that servers storage objects by id." - [{:keys [storage executor] :as cfg} request respond raise] - (-> (px/with-dispatch executor - (p/let [id (get-in request [:path-params :id]) - id (coerce-id id) - obj (sto/get-object storage id)] - (if obj - (serve-object cfg obj) - (yrs/response 404)))) - - (p/bind p/wrap) - (p/then' respond) - (p/catch raise))) + [{:keys [::sto/storage ::wrk/executor] :as cfg} request respond raise] + (->> (get-id request) + (p/mcat executor (fn [id] (sto/get-object storage id))) + (p/mcat executor (fn [obj] + (if (some? obj) + (serve-object cfg obj) + (p/resolved (yrs/response 404))))) + (p/fnly executor (fn [result cause] + (if cause (raise cause) (respond result)))))) (defn- generic-handler "A generic handler helper/common code for file-media based handlers." - [{:keys [storage] :as cfg} request kf] - (p/let [id (get-in request [:path-params :id]) - mobj (get-file-media-object storage id) - obj (sto/get-object storage (kf mobj))] - (if obj - (serve-object cfg obj) - (yrs/response 404)))) + [{:keys [::sto/storage ::wrk/executor] :as cfg} request kf] + (let [pool (::db/pool storage)] + (->> (get-id request) + (p/fmap executor (fn [id] (get-file-media-object pool id))) + (p/mcat executor (fn [mobj] (sto/get-object storage (kf mobj)))) + (p/mcat executor (fn [sobj] + (if sobj + (serve-object cfg sobj) + (p/resolved (yrs/response 404)))))))) (defn file-objects-handler "Handler that serves storage objects by file media id." [cfg request respond raise] - (-> (generic-handler cfg request :media-id) - (p/then respond) - (p/catch raise))) + (->> (generic-handler cfg request :media-id) + (p/fnly (fn [result cause] + (if cause (raise cause) (respond result)))))) (defn file-thumbnails-handler "Handler that serves storage objects by thumbnail-id and quick fallback to file-media-id if no thumbnail is available." [cfg request respond raise] - (-> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %))) - (p/then respond) - (p/catch raise))) + (->> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %))) + (p/fnly (fn [result cause] + (if cause (raise cause) (respond result)))))) ;; --- Initialization -(s/def ::storage some?) -(s/def ::assets-path ::us/string) -(s/def ::cache-max-age ::dt/duration) -(s/def ::signature-max-age ::dt/duration) +(s/def ::path ::us/string) +(s/def ::routes vector?) -(defmethod ig/pre-init-spec ::handlers [_] - (s/keys :req-un [::storage - ::wrk/executor - ::mtx/metrics - ::assets-path - ::cache-max-age - ::signature-max-age])) +(defmethod ig/pre-init-spec ::routes [_] + (s/keys :req [::sto/storage ::wrk/executor ::path])) -(defmethod ig/init-key ::handlers +(defmethod ig/init-key ::routes [_ cfg] - {:objects-handler (partial objects-handler cfg) - :file-objects-handler (partial file-objects-handler cfg) - :file-thumbnails-handler (partial file-thumbnails-handler cfg)}) - + ["/assets" + ["/by-id/:id" {:handler (partial objects-handler cfg)}] + ["/by-file-media-id/:id" {:handler (partial file-objects-handler cfg)}] + ["/by-file-media-id/:id/thumbnail" {:handler (partial file-thumbnails-handler cfg)}]]) diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj index bf5f32aeb..7ae00779c 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -28,18 +28,20 @@ (declare parse-notification) (declare process-report) -(defmethod ig/pre-init-spec ::handler [_] +(defmethod ig/pre-init-spec ::routes [_] (s/keys :req [::http/client ::main/props ::db/pool ::wrk/executor])) -(defmethod ig/init-key ::handler +(defmethod ig/init-key ::routes [_ {:keys [::wrk/executor] :as cfg}] - (fn [request respond _] - (let [data (-> request yrq/body slurp)] - (px/run! executor #(handle-request cfg data))) - (respond (yrs/response 200)))) + (letfn [(handler [request respond _] + (let [data (-> request yrq/body slurp)] + (px/run! executor #(handle-request cfg data))) + (respond (yrs/response 200)))] + ["/sns" {:handler handler + :allowed-methods #{:post}}])) (defn handle-request [cfg data] @@ -105,8 +107,7 @@ [cfg headers] (let [tdata (get headers "x-penpot-data")] (when-not (str/empty? tdata) - (let [sprops (::main/props cfg) - result (tokens/verify sprops {:token tdata :iss :profile-identity})] + (let [result (tokens/verify (::main/props cfg) {:token tdata :iss :profile-identity})] (:profile-id result))))) (defn- parse-notification diff --git a/backend/src/app/http/client.clj b/backend/src/app/http/client.clj index 542de4d18..f7bb86093 100644 --- a/backend/src/app/http/client.clj +++ b/backend/src/app/http/client.clj @@ -43,9 +43,9 @@ (defn req! "A convencience toplevel function for gradual migration to a new API convention." - ([{:keys [::client] :as holder} request] - (us/assert! ::client-holder holder) + ([{:keys [::client]} request] + (us/assert! ::client client) (send! client request {})) - ([{:keys [::client] :as holder} request options] - (us/assert! ::client-holder holder) + ([{:keys [::client]} request options] + (us/assert! ::client client) (send! client request options))) diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 4233909b4..5abfea32d 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -16,8 +16,8 @@ [app.http.middleware :as mw] [app.http.session :as session] [app.rpc.commands.binfile :as binf] - [app.rpc.commands.files.create :refer [create-file]] - [app.rpc.queries.profile :as profile] + [app.rpc.commands.files-create :refer [create-file]] + [app.rpc.commands.profile :as profile] [app.util.blob :as blob] [app.util.template :as tmpl] [app.util.time :as dt] @@ -39,9 +39,9 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn authorized? - [pool {:keys [profile-id]}] + [pool {:keys [::session/profile-id]}] (or (= "devenv" (cf/get :host)) - (let [profile (ex/ignoring (profile/retrieve-profile-data pool profile-id)) + (let [profile (ex/ignoring (profile/get-profile pool profile-id)) admins (or (cf/get :admins) #{})] (contains? admins (:email profile))))) @@ -61,7 +61,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn index-handler - [{:keys [pool]} request] + [{:keys [::db/pool]} request] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) @@ -81,7 +81,7 @@ "select revn, changes, data from file_change where file_id=? and revn = ?") (defn- retrieve-file-data - [{:keys [pool]} {:keys [params profile-id] :as request}] + [{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) @@ -107,8 +107,9 @@ (prepare-download-response data filename) (contains? params :clone) - (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id) - data (some-> data blob/decode)] + (let [profile (profile/get-profile pool profile-id) + project-id (:default-project-id profile) + data (blob/decode data)] (create-file pool {:id (uuid/next) :name (str "Cloned file: " filename) :project-id project-id @@ -117,7 +118,7 @@ (yrs/response 201 "OK CREATED")) :else - (prepare-response (some-> data blob/decode)))))) + (prepare-response (blob/decode data)))))) (defn- is-file-exists? [pool id] @@ -125,8 +126,9 @@ (-> (db/exec-one! pool [sql id]) :exists))) (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) + [{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}] + (let [profile (profile/get-profile pool profile-id) + project-id (:default-project-id profile) data (some-> params :file :path io/read-as-bytes blob/decode)] (if (and data project-id) @@ -162,7 +164,7 @@ :code :method-not-found))) (defn file-changes-handler - [{:keys [pool]} {:keys [params] :as request}] + [{:keys [::db/pool]} {:keys [params] :as request}] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) @@ -202,46 +204,48 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn error-handler - [{:keys [pool]} request] - (letfn [(parse-id [request] - (let [id (get-in request [:path-params :id]) - id (parse-uuid id)] - (when (uuid? id) - id))) - - (retrieve-report [id] + [{:keys [::db/pool]} request] + (letfn [(get-report [{:keys [path-params]}] (ex/ignoring - (some-> (db/get-by-id pool :server-error-report id) :content db/decode-transit-pgobject))) + (let [report-id (some-> path-params :id parse-uuid)] + (some-> (db/get-by-id pool :server-error-report report-id) + (update :content db/decode-transit-pgobject))))) - (render-template [report] - (let [context (dissoc report + (render-template-v1 [{:keys [content]}] + (let [context (dissoc content :trace :cause :params :data :spec-problems :message :spec-explain :spec-value :error :explain :hint) params {:context (pp/pprint-str context :width 200) - :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)}] + :hint (:hint content) + :spec-explain (:spec-explain content) + :spec-problems (:spec-problems content) + :spec-value (:spec-value content) + :data (:data content) + :trace (or (:trace content) + (some-> content :error :trace)) + :params (:params content)}] (-> (io/resource "app/templates/error-report.tmpl") - (tmpl/render params))))] + (tmpl/render params)))) + + (render-template-v2 [{report :content}] + (-> (io/resource "app/templates/error-report.v2.tmpl") + (tmpl/render report))) + + ] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) - (let [result (some-> (parse-id request) - (retrieve-report) - (render-template))] - (if result + (if-let [report (get-report request)] + (let [result (if (= 1 (:version report)) + (render-template-v1 report) + (render-template-v2 report))] (yrs/response :status 200 :body result :headers {"content-type" "text/html; charset=utf-8" - "x-robots-tag" "noindex"}) - (yrs/response 404 "not found"))))) + "x-robots-tag" "noindex"})) + (yrs/response 404 "not found")))) (def sql:error-reports "SELECT id, created_at, @@ -251,7 +255,7 @@ LIMIT 100") (defn error-list-handler - [{:keys [pool]} request] + [{:keys [::db/pool]} request] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) @@ -268,7 +272,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn export-handler - [{:keys [pool] :as cfg} {:keys [params profile-id] :as request}] + [{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}] (let [file-ids (->> (:file-ids params) (remove empty?) @@ -287,7 +291,8 @@ (assoc ::binf/include-libraries? libs?) (binf/export-to-tmpfile!))] (if clone? - (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)] + (let [profile (profile/get-profile pool profile-id) + project-id (:default-project-id profile)] (binf/import! (assoc cfg ::binf/input path @@ -309,15 +314,16 @@ (defn import-handler - [{:keys [pool] :as cfg} {:keys [params profile-id] :as request}] + [{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}] (when-not (contains? params :file) (ex/raise :type :validation :code :missing-upload-file :hint "missing upload file")) - (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id) + (let [profile (profile/get-profile pool profile-id) + project-id (:default-project-id profile) overwrite? (contains? params :overwrite) - migrate? (contains? params :migrate) + migrate? (contains? params :migrate) ignore-index-errors? (contains? params :ignore-index-errors)] (when-not project-id @@ -345,15 +351,14 @@ (defn health-handler "Mainly a task that performs a health check." - [{:keys [pool]} _] - (db/with-atomic [conn pool] - (try - (db/exec-one! conn ["select count(*) as count from server_prop;"]) - (yrs/response 200 "OK") - (catch Throwable cause - (l/warn :hint "unable to execute query on health handler" - :cause cause) - (yrs/response 503 "KO"))))) + [{:keys [::db/pool]} _] + (try + (db/exec-one! pool ["select count(*) as count from server_prop;"]) + (yrs/response 200 "OK") + (catch Throwable cause + (l/warn :hint "unable to execute query on health handler" + :cause cause) + (yrs/response 503 "KO")))) (defn changelog-handler [_ _] @@ -381,16 +386,17 @@ (raise (ex/error :type :authentication :code :only-admins-allowed))))))}) - (defmethod ig/pre-init-spec ::routes [_] - (s/keys :req-un [::db/pool ::wrk/executor ::session/session])) + (s/keys :req [::db/pool + ::wrk/executor + ::session/manager])) (defmethod ig/init-key ::routes - [_ {:keys [session pool executor] :as cfg}] + [_ {:keys [::db/pool ::wrk/executor] :as cfg}] [["/readyz" {:middleware [[mw/with-dispatch executor] [mw/with-config cfg]] :handler health-handler}] - ["/dbg" {:middleware [[session/middleware-2 session] + ["/dbg" {:middleware [[session/authz cfg] [with-authorization pool] [mw/with-dispatch executor] [mw/with-config cfg]]} diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 6e7a2b748..dbdedcbaa 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -7,37 +7,36 @@ (ns app.http.errors "A errors handling for the http server." (:require - [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] [app.http :as-alias http] + [app.http.access-token :as-alias actoken] + [app.http.session :as-alias session] [clojure.spec.alpha :as s] [cuerdas.core :as str] [yetti.request :as yrq] [yetti.response :as yrs])) -(def ^:dynamic *context* {}) - (defn- parse-client-ip [request] (or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first) (yrq/get-header request "x-real-ip") (yrq/remote-addr request))) -(defn get-context +(defn request->context + "Extracts error report relevant context data from request." [request] - (let [claims (:session-token-claims request)] - (merge - *context* - {:path (:path request) - :method (:method request) - :params (:params request) - :ip-addr (parse-client-ip request)} - (d/without-nils - {:user-agent (yrq/get-header request "user-agent") - :frontend-version (or (yrq/get-header request "x-frontend-version") - "unknown") - :profile-id (:uid claims)})))) + (let [claims (-> {} + (into (::session/token-claims request)) + (into (::actoken/token-claims request)))] + {:path (:path request) + :method (:method request) + :params (:params request) + :ip-addr (parse-client-ip request) + :user-agent (yrq/get-header request "user-agent") + :profile-id (:uid claims) + :version (or (yrq/get-header request "x-frontend-version") + "unknown")})) (defmulti handle-exception (fn [err & _rest] @@ -49,6 +48,10 @@ [err _] (yrs/response 401 (ex-data err))) +(defmethod handle-exception :authorization + [err _] + (yrs/response 403 (ex-data err))) + (defmethod handle-exception :restriction [err _] (yrs/response 400 (ex-data err))) @@ -79,15 +82,14 @@ [error request] (let [edata (ex-data error) explain (ex/explain edata)] - (l/error :hint (ex-message error) - :cause error - ::l/context (get-context request)) - (yrs/response :status 500 - :body {:type :server-error - :code :assertion - :data (-> edata - (dissoc ::s/problems ::s/value ::s/spec) - (cond-> explain (assoc :explain explain)))}))) + (binding [l/*context* (request->context request)] + (l/error :hint "Assertion error" :message (ex-message error) :cause error) + (yrs/response :status 500 + :body {:type :server-error + :code :assertion + :data (-> edata + (dissoc ::s/problems ::s/value ::s/spec) + (cond-> explain (assoc :explain explain)))})))) (defmethod handle-exception :not-found [err _] @@ -101,10 +103,8 @@ (yrs/response 429) :else - (do - (l/error :hint (ex-message error) - :cause error - ::l/context (get-context request)) + (binding [l/*context* (request->context request)] + (l/error :hint "Internal error" :message (ex-message error) :cause error) (yrs/response 500 {:type :server-error :code :unhandled :hint (ex-message error) @@ -113,25 +113,24 @@ (defmethod handle-exception org.postgresql.util.PSQLException [error request] (let [state (.getSQLState ^java.sql.SQLException error)] - (l/error :hint (ex-message error) - :cause error - ::l/context (get-context request)) - (cond - (= state "57014") - (yrs/response 504 {:type :server-error - :code :statement-timeout - :hint (ex-message error)}) + (binding [l/*context* (request->context request)] + (l/error :hint "PSQL error" :message (ex-message error) :cause error) + (cond + (= state "57014") + (yrs/response 504 {:type :server-error + :code :statement-timeout + :hint (ex-message error)}) - (= state "25P03") - (yrs/response 504 {:type :server-error - :code :idle-in-transaction-timeout - :hint (ex-message error)}) + (= state "25P03") + (yrs/response 504 {:type :server-error + :code :idle-in-transaction-timeout + :hint (ex-message error)}) - :else - (yrs/response 500 {:type :server-error - :code :unexpected - :hint (ex-message error) - :state state})))) + :else + (yrs/response 500 {:type :server-error + :code :unexpected + :hint (ex-message error) + :state state}))))) (defmethod handle-exception :default [error request] @@ -139,10 +138,8 @@ (cond ;; This means that exception is not a controlled exception. (nil? edata) - (do - (l/error :hint (ex-message error) - :cause error - ::l/context (get-context request)) + (binding [l/*context* (request->context request)] + (l/error :hint "Unexpected error" :message (ex-message error) :cause error) (yrs/response 500 {:type :server-error :code :unexpected :hint (ex-message error)})) @@ -157,10 +154,8 @@ (handle-exception (:handling edata) request) :else - (do - (l/error :hint (ex-message error) - :cause error - ::l/context (get-context request)) + (binding [l/*context* (request->context request)] + (l/error :hint "Unhandled error" :message (ex-message error) :cause error) (yrs/response 500 {:type :server-error :code :unhandled :hint (ex-message error) @@ -168,16 +163,7 @@ (defn handle [cause request] - (cond - (or (instance? java.util.concurrent.CompletionException cause) - (instance? java.util.concurrent.ExecutionException cause)) - (handle-exception (.getCause ^Throwable cause) request) - - (ex/wrapped? cause) - (let [context (meta cause) - cause (deref cause)] - (binding [*context* context] - (handle-exception cause request))) - - :else + (if (or (instance? java.util.concurrent.CompletionException cause) + (instance? java.util.concurrent.ExecutionException cause)) + (handle-exception (ex-cause cause) request) (handle-exception cause request))) diff --git a/backend/src/app/http/feedback.clj b/backend/src/app/http/feedback.clj deleted file mode 100644 index beaffc753..000000000 --- a/backend/src/app/http/feedback.clj +++ /dev/null @@ -1,80 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.http.feedback - "A general purpose feedback module." - (:require - [app.common.data :as d] - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.config :as cf] - [app.db :as db] - [app.emails :as eml] - [app.rpc.queries.profile :as profile] - [app.worker :as wrk] - [clojure.spec.alpha :as s] - [integrant.core :as ig] - [promesa.core :as p] - [promesa.exec :as px] - [yetti.request :as yrq] - [yetti.response :as yrs])) - -(declare ^:private send-feedback) -(declare ^:private handler) - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::db/pool ::wrk/executor])) - -(defmethod ig/init-key ::handler - [_ {:keys [executor] :as cfg}] - (let [enabled? (contains? cf/flags :user-feedback)] - (if enabled? - (fn [request respond raise] - (-> (px/submit! executor #(handler cfg request)) - (p/then' respond) - (p/catch raise))) - (fn [_ _ raise] - (raise (ex/error :type :validation - :code :feedback-disabled - :hint "feedback module is disabled")))))) - -(defn- handler - [{:keys [pool] :as cfg} {:keys [profile-id] :as request}] - (let [ftoken (cf/get :feedback-token ::no-token) - token (yrq/get-header request "x-feedback-token") - params (d/merge (:params request) - (:body-params request))] - (cond - (uuid? profile-id) - (let [profile (profile/retrieve-profile-data pool profile-id) - params (assoc params :from (:email profile))] - (send-feedback pool profile params)) - - (= token ftoken) - (send-feedback cfg nil params)) - - (yrs/response 204))) - -(s/def ::content ::us/string) -(s/def ::from ::us/email) -(s/def ::subject ::us/string) -(s/def ::feedback - (s/keys :req-un [::from ::subject ::content])) - -(defn- send-feedback - [pool profile params] - (let [params (us/conform ::feedback params) - destination (cf/get :feedback-destination)] - (eml/send! {::eml/conn pool - ::eml/factory eml/feedback - :from destination - :to destination - :profile profile - :reply-to (:from params) - :email (:from params) - :subject (:subject params) - :content (:content params)}) - nil)) diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index 40aee10d2..0d16ffe9d 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -80,8 +80,8 @@ (fn [request respond raise] (let [request (ex/try! (process-request request))] (if (ex/exception? request) - (if (instance? RuntimeException request) - (handle-error raise (or (ex/cause request) request)) + (if (ex/runtime-exception? request) + (handle-error raise (or (ex-cause request) request)) (handle-error raise request)) (handler request respond raise)))))) diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index 5d136ac44..4d951f800 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -8,15 +8,19 @@ (:refer-clojure :exclude [read]) (:require [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.logging :as l] + [app.common.spec :as us] [app.config :as cf] [app.db :as db] [app.db.sql :as sql] + [app.http.session.tasks :as-alias tasks] [app.main :as-alias main] [app.tokens :as tokens] [app.util.time :as dt] [app.worker :as wrk] [clojure.spec.alpha :as s] + [cuerdas.core :as str] [integrant.core :as ig] [promesa.core :as p] [promesa.exec :as px] @@ -45,55 +49,55 @@ (defprotocol ISessionManager (read [_ key]) - (decode [_ key]) (write! [_ key data]) (update! [_ data]) (delete! [_ key])) -(s/def ::session #(satisfies? ISessionManager %)) +(s/def ::manager #(satisfies? ISessionManager %)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; STORAGE IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(s/def ::session-params + (s/keys :req-un [::user-agent + ::profile-id + ::created-at])) + (defn- prepare-session-params - [props data] - (let [profile-id (:profile-id data) - user-agent (:user-agent data) - created-at (or (:created-at data) (dt/now)) - token (tokens/generate props {:iss "authentication" - :iat created-at - :uid profile-id})] - {:user-agent user-agent - :profile-id profile-id - :created-at created-at - :updated-at created-at - :id token})) + [key params] + (us/assert! ::us/not-empty-string key) + (us/assert! ::session-params params) + + {:user-agent (:user-agent params) + :profile-id (:profile-id params) + :created-at (:created-at params) + :updated-at (:created-at params) + :id key}) (defn- database-manager [{:keys [::db/pool ::wrk/executor ::main/props]}] + ^{::wrk/executor executor + ::db/pool pool + ::main/props props} (reify ISessionManager (read [_ token] (px/with-dispatch executor (db/exec-one! pool (sql/select :http-session {:id token})))) - (decode [_ token] + (write! [_ key params] (px/with-dispatch executor - (tokens/verify props {:token token :iss "authentication"}))) - - (write! [_ _ data] - (px/with-dispatch executor - (let [params (prepare-session-params props data)] + (let [params (prepare-session-params key params)] (db/insert! pool :http-session params) params))) - (update! [_ data] + (update! [_ params] (let [updated-at (dt/now)] (px/with-dispatch executor (db/update! pool :http-session {:updated-at updated-at} - {:id (:id data)}) - (assoc data :updated-at updated-at)))) + {:id (:id params)}) + (assoc params :updated-at updated-at)))) (delete! [_ token] (px/with-dispatch executor @@ -101,27 +105,26 @@ nil)))) (defn inmemory-manager - [{:keys [::wrk/executor ::main/props]}] + [{:keys [::db/pool ::wrk/executor ::main/props]}] (let [cache (atom {})] + ^{::main/props props + ::wrk/executor executor + ::db/pool pool} (reify ISessionManager (read [_ token] (p/do (get @cache token))) - (decode [_ token] - (px/with-dispatch executor - (tokens/verify props {:token token :iss "authentication"}))) - - (write! [_ _ data] + (write! [_ key params] (p/do - (let [{:keys [token] :as params} (prepare-session-params props data)] - (swap! cache assoc token params) + (let [params (prepare-session-params key params)] + (swap! cache assoc key params) params))) - (update! [_ data] + (update! [_ params] (p/do (let [updated-at (dt/now)] - (swap! cache update (:id data) assoc :updated-at updated-at) - (assoc data :updated-at updated-at)))) + (swap! cache update (:id params) assoc :updated-at updated-at) + (assoc params :updated-at updated-at)))) (delete! [_ token] (p/do @@ -144,25 +147,34 @@ ;; MANAGER IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare assign-auth-token-cookie) -(declare assign-authenticated-cookie) -(declare clear-auth-token-cookie) -(declare clear-authenticated-cookie) +(declare ^:private assign-auth-token-cookie) +(declare ^:private assign-authenticated-cookie) +(declare ^:private clear-auth-token-cookie) +(declare ^:private clear-authenticated-cookie) +(declare ^:private gen-token) (defn create-fn - [manager profile-id] - (fn [request response] - (let [uagent (yrq/get-header request "user-agent") - params {:profile-id profile-id - :user-agent uagent}] - (-> (write! manager nil params) - (p/then (fn [session] - (l/trace :hint "create" :profile-id profile-id) - (-> response - (assign-auth-token-cookie session) - (assign-authenticated-cookie session)))))))) + [{:keys [::manager]} profile-id] + (us/assert! ::manager manager) + (us/assert! ::us/uuid profile-id) + + (let [props (-> manager meta ::main/props)] + (fn [request response] + (let [uagent (yrq/get-header request "user-agent") + params {:profile-id profile-id + :user-agent uagent + :created-at (dt/now)} + token (gen-token props params)] + + (->> (write! manager token params) + (p/fmap (fn [session] + (l/trace :hint "create" :profile-id (str profile-id)) + (-> response + (assign-auth-token-cookie session) + (assign-authenticated-cookie session))))))))) (defn delete-fn - [manager] + [{:keys [::manager]}] + (us/assert! ::manager manager) (letfn [(delete [{:keys [profile-id] :as request}] (let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name) cookie (yrq/get-cookie request cname)] @@ -177,68 +189,93 @@ (clear-auth-token-cookie) (clear-authenticated-cookie)))))) -(def middleware-1 - (letfn [(decode-cookie [manager cookie] - (if-let [value (:value cookie)] - (decode manager value) - (p/resolved nil))) +(defn- gen-token + [props {:keys [profile-id created-at]}] + (tokens/generate props {:iss "authentication" + :iat created-at + :uid profile-id})) +(defn- decode-token + [props token] + (when token + (tokens/verify props {:token token :iss "authentication"}))) - (wrap-handler [manager handler request respond raise] - (let [cookie (some->> (cf/get :auth-token-cookie-name default-auth-token-cookie-name) - (yrq/get-cookie request))] - (->> (decode-cookie manager cookie) - (p/fnly (fn [claims _] - (cond-> request - (some? claims) (assoc :session-token-claims claims) - :always (handler respond raise)))))))] - {:name :session-1 - :compile (fn [& _] - (fn [handler manager] - (partial wrap-handler manager handler)))})) +(defn- get-token + [request] + (let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name) + cookie (some-> (yrq/get-cookie request cname) :value)] + (when-not (str/empty? cookie) + cookie))) -(def middleware-2 - (letfn [(wrap-handler [manager handler request respond raise] - (-> (retrieve-session manager request) - (p/finally (fn [session cause] - (cond - (some? cause) - (raise cause) +(defn- get-session + [manager token] + (some->> token (read manager))) - (nil? session) - (handler request respond raise) +(defn- renew-session? + [{:keys [updated-at] :as session}] + (and (dt/instant? updated-at) + (let [elapsed (dt/diff updated-at (dt/now))] + (neg? (compare default-renewal-max-age elapsed))))) - :else - (let [request (-> request - (assoc :profile-id (:profile-id session)) - (assoc :session-id (:id session))) - respond (cond-> respond - (renew-session? session) - (wrap-respond manager session))] - (handler request respond raise))))))) +(defn- wrap-reneval + [respond manager session] + (fn [response] + (p/let [session (update! manager session)] + (-> response + (assign-auth-token-cookie session) + (assign-authenticated-cookie session) + (respond))))) - (retrieve-session [manager request] - (let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name) - cookie (yrq/get-cookie request cname)] - (some->> (:value cookie) (read manager)))) +(defn- wrap-soft-auth + [handler {:keys [::manager]}] + (us/assert! ::manager manager) - (renew-session? [{:keys [updated-at] :as session}] - (and (dt/instant? updated-at) - (let [elapsed (dt/diff updated-at (dt/now))] - (neg? (compare default-renewal-max-age elapsed))))) + (let [{:keys [::wrk/executor ::main/props]} (meta manager)] + (fn [request respond raise] + (let [token (ex/try! (get-token request))] + (if (ex/exception? token) + (raise token) + (->> (px/submit! executor (partial decode-token props token)) + (p/fnly (fn [claims cause] + (when cause + (l/trace :hint "exception on decoding malformed token" :cause cause)) + (let [request (cond-> request + (map? claims) + (-> (assoc ::token-claims claims) + (assoc ::token token)))] + (handler request respond raise)))))))))) - ;; Wrap respond with session renewal code - (wrap-respond [respond manager session] - (fn [response] - (p/let [session (update! manager session)] - (-> response - (assign-auth-token-cookie session) - (assign-authenticated-cookie session) - (respond)))))] +(defn- wrap-authz + [handler {:keys [::manager]}] + (us/assert! ::manager manager) + (fn [request respond raise] + (if-let [token (::token request)] + (->> (get-session manager token) + (p/fnly (fn [session cause] + (cond + (some? cause) + (raise cause) - {:name :session-2 - :compile (fn [& _] - (fn [handler manager] - (partial wrap-handler manager handler)))})) + (nil? session) + (handler request respond raise) + + :else + (let [request (-> request + (assoc ::profile-id (:profile-id session)) + (assoc ::id (:id session))) + respond (cond-> respond + (renew-session? session) + (wrap-reneval manager session))] + (handler request respond raise)))))) + + (handler request respond raise)))) + +(def soft-auth + {:name ::soft-auth + :compile (constantly wrap-soft-auth)}) + +(def authz + {:name ::authz + :compile (constantly wrap-authz)}) ;; --- IMPL @@ -264,13 +301,16 @@ (defn- assign-authenticated-cookie [response {updated-at :updated-at}] (let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age) + domain (cf/get :authenticated-cookie-domain) + cname (cf/get :authenticated-cookie-name "authenticated") + created-at (or updated-at (dt/now)) renewal (dt/plus created-at default-renewal-max-age) expires (dt/plus created-at max-age) + comment (str "Renewal at: " (dt/format-instant renewal :rfc1123)) secure? (contains? cf/flags :secure-session-cookies) - domain (cf/get :authenticated-cookie-domain) - name (cf/get :authenticated-cookie-name "authenticated") + cookie {:domain domain :expires expires :path "/" @@ -280,41 +320,46 @@ :secure secure?}] (cond-> response (string? domain) - (update :cookies assoc name cookie)))) + (update :cookies assoc cname cookie)))) (defn- clear-auth-token-cookie [response] (let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)] - (update response :cookies assoc cname {:path "/" :value "" :max-age -1}))) + (update response :cookies assoc cname {:path "/" :value "" :max-age 0}))) (defn- clear-authenticated-cookie [response] - (let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name) + (let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name) domain (cf/get :authenticated-cookie-domain)] (cond-> response (string? domain) - (update :cookies assoc cname {:domain domain :path "/" :value "" :max-age -1})))) + (update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0})))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TASK: SESSION GC ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare sql:delete-expired) +(s/def ::tasks/max-age ::dt/duration) -(s/def ::max-age ::dt/duration) +(defmethod ig/pre-init-spec ::tasks/gc [_] + (s/keys :req [::db/pool] + :opt [::tasks/max-age])) -(defmethod ig/pre-init-spec ::gc-task [_] - (s/keys :req-un [::db/pool] - :opt-un [::max-age])) - -(defmethod ig/prep-key ::gc-task +(defmethod ig/prep-key ::tasks/gc [_ cfg] - (merge {:max-age default-cookie-max-age} - (d/without-nils cfg))) + (let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)] + (merge {::tasks/max-age max-age} (d/without-nils cfg)))) -(defmethod ig/init-key ::gc-task - [_ {:keys [pool max-age] :as cfg}] +(def ^:private + sql:delete-expired + "delete from http_session + where updated_at < now() - ?::interval + or (updated_at is null and + created_at < now() - ?::interval)") + +(defmethod ig/init-key ::tasks/gc + [_ {:keys [::db/pool ::tasks/max-age] :as cfg}] (l/debug :hint "initializing session gc task" :max-age max-age) (fn [_] (db/with-atomic [conn pool] @@ -326,9 +371,3 @@ :deleted result) result)))) -(def ^:private - sql:delete-expired - "delete from http_session - where updated_at < now() - ?::interval - or (updated_at is null and - created_at < now() - ?::interval)") diff --git a/backend/src/app/http/websocket.clj b/backend/src/app/http/websocket.clj index 7e16d74f6..1db238d2e 100644 --- a/backend/src/app/http/websocket.clj +++ b/backend/src/app/http/websocket.clj @@ -12,6 +12,7 @@ [app.common.pprint :as pp] [app.common.spec :as us] [app.db :as db] + [app.http.session :as session] [app.metrics :as mtx] [app.msgbus :as mbus] [app.util.time :as dt] @@ -34,7 +35,7 @@ (def state (atom {})) (defn- on-connect - [{:keys [metrics]} wsp] + [{:keys [::mtx/metrics]} wsp] (let [created-at (dt/now)] (swap! state assoc (::ws/id @wsp) wsp) (mtx/run! metrics @@ -48,7 +49,7 @@ :val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0))))) (defn- on-rcv-message - [{:keys [metrics]} _ message] + [{:keys [::mtx/metrics]} _ message] (mtx/run! metrics :id :websocket-messages-total :labels recv-labels @@ -56,7 +57,7 @@ message) (defn- on-snd-message - [{:keys [metrics]} _ message] + [{:keys [::mtx/metrics]} _ message] (mtx/run! metrics :id :websocket-messages-total :labels send-labels @@ -95,7 +96,6 @@ :user-agent (::ws/user-agent @wsp) :ip-addr (::ws/remote-addr @wsp) :last-activity-at (::ws/last-activity-at @wsp) - :http-session-id (::ws/http-session-id @wsp) :subscribed-file (-> wsp deref ::file-subscription :file-id) :subscribed-team (-> wsp deref ::team-subscription :team-id)})) @@ -120,7 +120,7 @@ (defmethod handle-message :connect [cfg wsp _] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) conn-id (::ws/id @wsp) profile-id (::profile-id @wsp) session-id (::session-id @wsp) @@ -139,7 +139,7 @@ (defmethod handle-message :disconnect [cfg wsp _] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) conn-id (::ws/id @wsp) profile-id (::profile-id @wsp) session-id (::session-id @wsp) @@ -173,7 +173,7 @@ (defmethod handle-message :subscribe-team [cfg wsp {:keys [team-id] :as params}] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) conn-id (::ws/id @wsp) session-id (::session-id @wsp) output-ch (::ws/output-ch @wsp) @@ -204,8 +204,8 @@ (a/! output-ch message) (recur)))) @@ -258,7 +259,7 @@ (defmethod handle-message :unsubscribe-file [cfg wsp {:keys [file-id] :as params}] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) conn-id (::ws/id @wsp) session-id (::session-id @wsp) profile-id (::profile-id @wsp) @@ -288,7 +289,7 @@ (defmethod handle-message :pointer-update [cfg wsp {:keys [file-id] :as message}] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) profile-id (::profile-id @wsp) session-id (::session-id @wsp) subs (::file-subscription @wsp) @@ -313,39 +314,47 @@ ;; HTTP HANDLER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::msgbus ::mbus/msgbus) (s/def ::session-id ::us/uuid) - (s/def ::handler-params (s/keys :req-un [::session-id])) -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::msgbus ::db/pool ::mtx/metrics])) +(defn- http-handler + [cfg {:keys [params ::session/profile-id] :as request} respond raise] + (let [{:keys [session-id]} (us/conform ::handler-params params)] + (cond + (not profile-id) + (raise (ex/error :type :authentication + :hint "Authentication required.")) -(defmethod ig/init-key ::handler + (not (yws/upgrade-request? request)) + (raise (ex/error :type :validation + :code :websocket-request-expected + :hint "this endpoint only accepts websocket connections")) + + :else + (do + (l/trace :hint "websocket request" :profile-id profile-id :session-id session-id) + + (->> (ws/handler + ::ws/on-rcv-message (partial on-rcv-message cfg) + ::ws/on-snd-message (partial on-snd-message cfg) + ::ws/on-connect (partial on-connect cfg) + ::ws/handler (partial handle-message cfg) + ::profile-id profile-id + ::session-id session-id) + (yws/upgrade request) + (respond)))))) + +(defmethod ig/pre-init-spec ::routes [_] + (s/keys :req [::mbus/msgbus + ::mtx/metrics + ::db/pool + ::session/manager])) + +(s/def ::routes vector?) + +(defmethod ig/init-key ::routes [_ cfg] - (fn [{:keys [profile-id params] :as req} respond raise] - (let [{:keys [session-id]} (us/conform ::handler-params params)] - (cond - (not profile-id) - (raise (ex/error :type :authentication - :hint "Authentication required.")) - - (not (yws/upgrade-request? req)) - (raise (ex/error :type :validation - :code :websocket-request-expected - :hint "this endpoint only accepts websocket connections")) - - :else - (do - (l/trace :hint "websocket request" :profile-id profile-id :session-id session-id) - - (->> (ws/handler - ::ws/on-rcv-message (partial on-rcv-message cfg) - ::ws/on-snd-message (partial on-snd-message cfg) - ::ws/on-connect (partial on-connect cfg) - ::ws/handler (partial handle-message cfg) - ::profile-id profile-id - ::session-id session-id) - (yws/upgrade req) - (respond))))))) + ["/ws/notifications" {:middleware [[session/authz cfg]] + :handler (partial http-handler cfg) + :allowed-methods #{:get}}]) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 1038e8249..4ded05800 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -149,7 +149,7 @@ ;; this case we just retry the operation. (rtry/with-retry {::rtry/when rtry/conflict-exception? ::rtry/max-retries 6 - ::rtry/label "persist-audit-log-event"} + ::rtry/label "persist-audit-log"} (let [now (dt/now)] (db/insert! conn-or-pool :audit-log (-> params diff --git a/backend/src/app/loggers/database.clj b/backend/src/app/loggers/database.clj index f9a853f72..110327273 100644 --- a/backend/src/app/loggers/database.clj +++ b/backend/src/app/loggers/database.clj @@ -7,16 +7,17 @@ (ns app.loggers.database "A specific logger impl that persists errors on the database." (:require + [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.logging :as l] - [app.common.uuid :as uuid] + [app.common.pprint :as pp] + [app.common.spec :as us] [app.config :as cf] [app.db :as db] - [app.loggers.zmq :as lzmq] - [clojure.core.async :as a] [clojure.spec.alpha :as s] - [cuerdas.core :as str] [integrant.core :as ig] - [promesa.exec :as px])) + [promesa.exec :as px] + [promesa.exec.csp :as sp])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Error Listener @@ -27,73 +28,79 @@ (defonce enabled (atom true)) (defn- persist-on-database! - [{:keys [::db/pool] :as cfg} {:keys [id] :as event}] + [pool id report] (when-not (db/read-only? pool) - (db/insert! pool :server-error-report {:id id :content (db/tjson event)}))) + (db/insert! pool :server-error-report + {:id id + :version 2 + :content (db/tjson report)}))) -(defn- parse-event-data - [event] - (reduce-kv - (fn [acc k v] - (cond - (= k :id) (assoc acc k (uuid/uuid v)) - (= k :profile-id) (assoc acc k (uuid/uuid v)) - (str/blank? v) acc - :else (assoc acc k v))) - {} - event)) +(defn record->report + [{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}] + (us/assert! ::l/record record) -(defn parse-event - [event] - (-> (parse-event-data event) - (assoc :hint (or (:hint event) (:message event))) - (assoc :tenant (cf/get :tenant)) - (assoc :host (cf/get :host)) - (assoc :public-uri (cf/get :public-uri)) - (assoc :version (:full cf/version)) - (update :id #(or % (uuid/next))))) + (merge + {:context (-> context + (assoc :tenant (cf/get :tenant)) + (assoc :host (cf/get :host)) + (assoc :public-uri (cf/get :public-uri)) + (assoc :version (:full cf/version)) + (assoc :logger-name logger) + (assoc :logger-level level) + (dissoc :params) + (pp/pprint-str :width 200)) + :params (some-> (:params context) + (pp/pprint-str :width 200)) + :props (pp/pprint-str props :width 200) + :hint (or (ex-message cause) @message) + :trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)} + + (when-let [data (ex-data cause)] + {:spec-value (some-> (::s/value data) (pp/pprint-str :width 200)) + :spec-explain (ex/explain data) + :data (-> data + (dissoc ::s/problems ::s/value ::s/spec :hint) + (pp/pprint-str :width 200))}))) (defn- handle-event - [cfg event] + [{:keys [::db/pool]} {:keys [::l/id] :as record}] (try - (let [event (parse-event event) - uri (cf/get :public-uri)] + (let [uri (cf/get :public-uri) + report (-> record record->report d/without-nils)] + (l/debug :hint "registering error on database" :id id + :uri (str uri "/dbg/error/" id)) - (l/debug :hint "registering error on database" :id (:id event) - :uri (str uri "/dbg/error/" (:id event))) - - (persist-on-database! cfg event)) + (persist-on-database! pool id report)) (catch Throwable cause (l/warn :hint "unexpected exception on database error logger" :cause cause)))) -(defn- error-event? - [event] - (= "error" (:logger/level event))) +(defn error-record? + [{:keys [::l/level ::l/cause]}] + (and (= :error level) + (ex/exception? cause))) (defmethod ig/pre-init-spec ::reporter [_] - (s/keys :req [::db/pool ::lzmq/receiver])) + (s/keys :req [::db/pool])) (defmethod ig/init-key ::reporter - [_ {:keys [::lzmq/receiver] :as cfg}] - (px/thread - {:name "penpot/database-reporter"} - (l/info :hint "initializing database error persistence") - - (let [input (a/chan (a/sliding-buffer 5) - (filter error-event?))] + [_ cfg] + (let [input (sp/chan (sp/sliding-buffer 32) (filter error-record?))] + (add-watch l/log-record ::reporter #(sp/put! input %4)) + (px/thread + {:name "penpot/database-reporter" :virtual true} + (l/info :hint "initializing database error persistence") (try - (lzmq/sub! receiver input) (loop [] - (when-let [msg (a/ thread px/interrupt!)) - -(defn- prepare-payload - [event] - (let [labels {:host (cf/get :host) - :tenant (cf/get :tenant) - :version (:full cf/version) - :logger (:logger/name event) - :level (:logger/level event)}] - {:streams - [{:stream labels - :values [[(str (* (inst-ms (:created-at event)) 1000000)) - (str (:message event) - (when-let [error (:trace event)] - (str "\n" error)))]]}]})) - -(defn- make-request - [{:keys [::uri] :as cfg} payload] - (http/req! cfg - {:uri uri - :timeout 3000 - :method :post - :headers {"content-type" "application/json"} - :body (json/encode payload)} - {:sync? true})) - -(defn- handle-event - [cfg event] - (try - (let [payload (prepare-payload event) - response (make-request cfg payload)] - (when-not (= 204 (:status response)) - (l/error :hint "error on sending log to loki (unexpected response)" - :response (pr-str response)))) - (catch Throwable cause - (l/error :hint "error on sending log to loki (unexpected exception)" - :cause cause)))) diff --git a/backend/src/app/loggers/mattermost.clj b/backend/src/app/loggers/mattermost.clj index de89ba207..51a627ff1 100644 --- a/backend/src/app/loggers/mattermost.clj +++ b/backend/src/app/loggers/mattermost.clj @@ -7,24 +7,35 @@ (ns app.loggers.mattermost "A mattermost integration for error reporting." (:require + [app.common.exceptions :as ex] [app.common.logging :as l] + [app.common.spec :as us] [app.config :as cf] [app.http.client :as http] [app.loggers.database :as ldb] - [app.loggers.zmq :as lzmq] [app.util.json :as json] - [clojure.core.async :as a] [clojure.spec.alpha :as s] [integrant.core :as ig] - [promesa.exec :as px])) + [promesa.exec :as px] + [promesa.exec.csp :as sp])) -(defonce enabled (atom true)) +(defonce enabled (atom false)) (defn- send-mattermost-notification! - [cfg {:keys [host id public-uri] :as event}] - (let [text (str "Exception on (host: " host ", url: " public-uri "/dbg/error/" id ")\n" - (when-let [pid (:profile-id event)] - (str "- profile-id: #uuid-" pid "\n"))) + [cfg {:keys [id public-uri] :as report}] + (let [text (str "Exception: " public-uri "/dbg/error/" id " " + (when-let [pid (:profile-id report)] + (str "(pid: #uuid-" pid ")")) + "\n" + "```\n" + "- host: `" (:host report) "`\n" + "- tenant: `" (:tenant report) "`\n" + "- version: `" (:version report) "`\n" + "\n" + "Trace:\n" + (:trace report) + "```") + resp (http/req! cfg {:uri (cf/get :error-report-webhook) :method :post @@ -36,32 +47,41 @@ (l/warn :hint "error on sending data" :response (pr-str resp))))) +(defn record->report + [{:keys [::l/context ::l/id ::l/cause] :as record}] + (us/assert! ::l/record record) + {:id id + :tenant (cf/get :tenant) + :host (cf/get :host) + :public-uri (cf/get :public-uri) + :version (:full cf/version) + :profile-id (:profile-id context) + :trace (ex/format-throwable cause :detail? false :header? false)}) + (defn handle-event - [cfg event] + [cfg record] (when @enabled (try - (let [event (ldb/parse-event event)] - (send-mattermost-notification! cfg event)) + (let [report (record->report record)] + (send-mattermost-notification! cfg report)) (catch Throwable cause - (l/warn :hint "unhandled error" - :cause cause))))) + (l/warn :hint "unhandled error" :cause cause))))) (defmethod ig/pre-init-spec ::reporter [_] - (s/keys :req [::http/client - ::lzmq/receiver])) + (s/keys :req [::http/client])) (defmethod ig/init-key ::reporter [_ cfg] (when-let [uri (cf/get :error-report-webhook)] (px/thread - {:name "penpot/mattermost-reporter"} - (l/info :msg "initializing error reporter" :uri uri) - (let [input (a/chan (a/sliding-buffer 128) - (filter #(= (:logger/level %) "error")))] + {:name "penpot/mattermost-reporter" + :virtual true} + (l/info :hint "initializing error reporter" :uri uri) + (let [input (sp/chan (sp/sliding-buffer 128) (filter ldb/error-record?))] + (add-watch l/log-record ::reporter #(sp/put! input %4)) (try - (lzmq/sub! (::lzmq/receiver cfg) input) (loop [] - (when-let [msg (a/= (:error-count res) max-errors) (db/update! pool :webhook {:is-active false} {:id (:id whook)}))) diff --git a/backend/src/app/loggers/zmq.clj b/backend/src/app/loggers/zmq.clj deleted file mode 100644 index 77b7de549..000000000 --- a/backend/src/app/loggers/zmq.clj +++ /dev/null @@ -1,130 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.loggers.zmq - "A generic ZMQ listener." - (:require - [app.common.exceptions :as ex] - [app.common.logging :as l] - [app.config :as cf] - [app.loggers.zmq.receiver :as-alias receiver] - [app.util.json :as json] - [app.util.time :as dt] - [clojure.core.async :as a] - [clojure.spec.alpha :as s] - [cuerdas.core :as str] - [integrant.core :as ig] - [promesa.exec :as px]) - (:import - org.zeromq.SocketType - org.zeromq.ZMQ$Socket - org.zeromq.ZContext)) - -(declare prepare) -(declare start-rcv-loop) - -(defmethod ig/init-key ::receiver - [_ cfg] - (let [uri (cf/get :loggers-zmq-uri) - buffer (a/chan 1) - output (a/chan 1 (comp (filter map?) - (keep prepare))) - mult (a/mult output) - thread (when uri - (px/thread - {:name "penpot/zmq-receiver" - :daemon false} - (l/info :hint "receiver started") - (try - (start-rcv-loop buffer uri) - (catch InterruptedException _ - (l/debug :hint "receiver interrupted")) - (catch java.lang.IllegalStateException cause - (if (= "errno 4" (ex-message cause)) - (l/debug :hint "receiver interrupted") - (l/error :hint "unhandled error" :cause cause))) - (catch Throwable cause - (l/error :hint "unhandled error" :cause cause)) - (finally - (l/info :hint "receiver terminated")))))] - - (a/pipe buffer output) - (-> cfg - (assoc ::receiver/mult mult) - (assoc ::receiver/thread thread) - (assoc ::receiver/output output) - (assoc ::receiver/buffer buffer)))) - -(s/def ::receiver/mult some?) -(s/def ::receiver/thread #(instance? Thread %)) -(s/def ::receiver/output some?) -(s/def ::receiver/buffer some?) -(s/def ::receiver - (s/keys :req [::receiver/mult - ::receiver/thread - ::receiver/output - ::receiver/buffer])) - -(defn sub! - [{:keys [::receiver/mult]} ch] - (a/tap mult ch)) - -(defmethod ig/halt-key! ::receiver - [_ {:keys [::receiver/buffer ::receiver/thread]}] - (some-> thread px/interrupt!) - (some-> buffer a/close!)) - -(def ^:private json-mapper - (json/mapper - {:encode-key-fn str/camel - :decode-key-fn (comp keyword str/kebab)})) - -(defn- start-rcv-loop - [output endpoint] - (let [zctx (ZContext. 1) - socket (.. zctx (createSocket SocketType/SUB))] - (try - (.. socket (connect ^String endpoint)) - (.. socket (subscribe "")) - (.. socket (setReceiveTimeOut 5000)) - (loop [] - (let [msg (.recv ^ZMQ$Socket socket) - msg (ex/ignoring (json/decode msg json-mapper)) - msg (if (nil? msg) :empty msg)] - (when (a/>!! output msg) - (recur)))) - - (finally - (.close ^java.lang.AutoCloseable socket) - (.destroy ^ZContext zctx))))) - -(s/def ::logger-name string?) -(s/def ::level string?) -(s/def ::thread string?) -(s/def ::time-millis integer?) -(s/def ::message string?) -(s/def ::context-map map?) -(s/def ::thrown map?) - -(s/def ::log4j-event - (s/keys :req-un [::logger-name ::level ::thread ::time-millis ::message] - :opt-un [::context-map ::thrown])) - -(defn- prepare - [event] - (if (s/valid? ::log4j-event event) - (merge {:message (:message event) - :created-at (dt/instant (:time-millis event)) - :logger/name (:logger-name event) - :logger/level (str/lower (:level event))} - - (when-let [trace (-> event :thrown :extended-stack-trace)] - {:trace trace}) - - (:context-map event)) - (do - (l/warn :hint "invalid event" :event event) - nil))) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index f9a9c5416..9e6da0164 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -12,16 +12,28 @@ [app.common.logging :as l] [app.config :as cf] [app.db :as-alias db] + [app.email :as-alias email] + [app.http :as-alias http] + [app.http.access-token :as-alias actoken] + [app.http.assets :as-alias http.assets] + [app.http.awsns :as http.awsns] [app.http.client :as-alias http.client] - [app.http.session :as-alias http.session] + [app.http.debug :as-alias http.debug] + [app.http.session :as-alias session] + [app.http.session.tasks :as-alias session.tasks] + [app.http.websocket :as http.ws] [app.loggers.audit.tasks :as-alias audit.tasks] [app.loggers.webhooks :as-alias webhooks] - [app.loggers.zmq :as-alias lzmq] [app.metrics :as-alias mtx] [app.metrics.definition :as-alias mdef] + [app.msgbus :as-alias mbus] [app.redis :as-alias rds] + [app.rpc :as-alias rpc] + [app.rpc.doc :as-alias rpc.doc] [app.srepl :as-alias srepl] [app.storage :as-alias sto] + [app.storage.fs :as-alias sto.fs] + [app.storage.s3 :as-alias sto.s3] [app.util.time :as dt] [app.worker :as-alias wrk] [cuerdas.core :as str] @@ -152,15 +164,13 @@ (def system-config {::db/pool - {:uri (cf/get :database-uri) - :username (cf/get :database-username) - :password (cf/get :database-password) - :read-only (cf/get :database-readonly false) - :metrics (ig/ref ::mtx/metrics) - :migrations (ig/ref :app.migrations/all) - :name :main - :min-size (cf/get :database-min-pool-size 0) - :max-size (cf/get :database-max-pool-size 60)} + {::db/uri (cf/get :database-uri) + ::db/username (cf/get :database-username) + ::db/password (cf/get :database-password) + ::db/read-only? (cf/get :database-readonly false) + ::db/min-size (cf/get :database-min-pool-size 0) + ::db/max-size (cf/get :database-max-pool-size 60) + ::mtx/metrics (ig/ref ::mtx/metrics)} ;; Default thread pool for IO operations ::wrk/executor @@ -175,19 +185,19 @@ ::wrk/executor (ig/ref ::wrk/executor)} :app.migrations/migrations - {} + {::db/pool (ig/ref ::db/pool)} ::mtx/metrics {:default default-metrics} - :app.migrations/all - {:main (ig/ref :app.migrations/migrations)} + ::mtx/routes + {::mtx/metrics (ig/ref ::mtx/metrics)} ::rds/redis {::rds/uri (cf/get :redis-uri) ::mtx/metrics (ig/ref ::mtx/metrics)} - :app.msgbus/msgbus + ::mbus/msgbus {:backend (cf/get :msgbus-backend :redis) :executor (ig/ref ::wrk/executor) :redis (ig/ref ::rds/redis)} @@ -197,40 +207,43 @@ ::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)} ::sto/gc-deleted-task - {:pool (ig/ref ::db/pool) - :storage (ig/ref ::sto/storage) - :executor (ig/ref ::wrk/executor)} + {::db/pool (ig/ref ::db/pool) + ::sto/storage (ig/ref ::sto/storage)} ::sto/gc-touched-task - {:pool (ig/ref ::db/pool)} + {::db/pool (ig/ref ::db/pool)} ::http.client/client {::wrk/executor (ig/ref ::wrk/executor)} - :app.http.session/manager + ::session/manager {::db/pool (ig/ref ::db/pool) ::wrk/executor (ig/ref ::wrk/executor) ::props (ig/ref :app.setup/props)} - :app.http.session/gc-task - {:pool (ig/ref ::db/pool) - :max-age (cf/get :auth-token-cookie-max-age)} + ::actoken/manager + {::db/pool (ig/ref ::db/pool) + ::wrk/executor (ig/ref ::wrk/executor) + ::props (ig/ref :app.setup/props)} - :app.http.awsns/handler + ::session.tasks/gc + {::db/pool (ig/ref ::db/pool)} + + ::http.awsns/routes {::props (ig/ref :app.setup/props) ::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client) ::wrk/executor (ig/ref ::wrk/executor)} - :app.http/server - {:port (cf/get :http-server-port) - :host (cf/get :http-server-host) - :router (ig/ref :app.http/router) - :metrics (ig/ref ::mtx/metrics) - :executor (ig/ref ::wrk/executor) - :io-threads (cf/get :http-server-io-threads) - :max-body-size (cf/get :http-server-max-body-size) - :max-multipart-body-size (cf/get :http-server-max-multipart-body-size)} + ::http/server + {::http/port (cf/get :http-server-port) + ::http/host (cf/get :http-server-host) + ::http/router (ig/ref ::http/router) + ::http/metrics (ig/ref ::mtx/metrics) + ::http/executor (ig/ref ::wrk/executor) + ::http/io-threads (cf/get :http-server-io-threads) + ::http/max-body-size (cf/get :http-server-max-body-size) + ::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)} ::ldap/provider {:host (cf/get :ldap-host) @@ -259,85 +272,74 @@ {::http.client/client (ig/ref ::http.client/client)} ::oidc/routes - {::http.client/client (ig/ref ::http.client/client) - ::db/pool (ig/ref ::db/pool) - ::props (ig/ref :app.setup/props) - ::wrk/executor (ig/ref ::wrk/executor) - ::oidc/providers {:google (ig/ref ::oidc.providers/google) - :github (ig/ref ::oidc.providers/github) - :gitlab (ig/ref ::oidc.providers/gitlab) - :oidc (ig/ref ::oidc.providers/generic)} - ::http.session/session (ig/ref :app.http.session/manager)} + {::http.client/client (ig/ref ::http.client/client) + ::db/pool (ig/ref ::db/pool) + ::props (ig/ref :app.setup/props) + ::wrk/executor (ig/ref ::wrk/executor) + ::oidc/providers {:google (ig/ref ::oidc.providers/google) + :github (ig/ref ::oidc.providers/github) + :gitlab (ig/ref ::oidc.providers/gitlab) + :oidc (ig/ref ::oidc.providers/generic)} + ::session/manager (ig/ref ::session/manager)} - ;; TODO: revisit the dependencies of this service, looks they are too much unused of them :app.http/router - {:assets (ig/ref :app.http.assets/handlers) - :feedback (ig/ref :app.http.feedback/handler) - :session (ig/ref :app.http.session/manager) - :awsns-handler (ig/ref :app.http.awsns/handler) - :debug-routes (ig/ref :app.http.debug/routes) - :oidc-routes (ig/ref ::oidc/routes) - :ws (ig/ref :app.http.websocket/handler) - :metrics (ig/ref ::mtx/metrics) - :public-uri (cf/get :public-uri) - :storage (ig/ref ::sto/storage) - :rpc-routes (ig/ref :app.rpc/routes) - :doc-routes (ig/ref :app.rpc.doc/routes) - :executor (ig/ref ::wrk/executor)} + {::session/manager (ig/ref ::session/manager) + ::actoken/manager (ig/ref ::actoken/manager) + ::wrk/executor (ig/ref ::wrk/executor) + ::db/pool (ig/ref ::db/pool) + ::rpc/routes (ig/ref ::rpc/routes) + ::rpc.doc/routes (ig/ref ::rpc.doc/routes) + ::props (ig/ref :app.setup/props) + ::mtx/routes (ig/ref ::mtx/routes) + ::oidc/routes (ig/ref ::oidc/routes) + ::http.debug/routes (ig/ref ::http.debug/routes) + ::http.assets/routes (ig/ref ::http.assets/routes) + ::http.ws/routes (ig/ref ::http.ws/routes) + ::http.awsns/routes (ig/ref ::http.awsns/routes)} :app.http.debug/routes - {:pool (ig/ref ::db/pool) - :executor (ig/ref ::wrk/executor) - :storage (ig/ref ::sto/storage) - :session (ig/ref :app.http.session/manager) + {::db/pool (ig/ref ::db/pool) + ::wrk/executor (ig/ref ::wrk/executor) + ::session/manager (ig/ref ::session/manager)} - ::db/pool (ig/ref ::db/pool) - ::wrk/executor (ig/ref ::wrk/executor) - ::sto/storage (ig/ref ::sto/storage)} + :app.http.websocket/routes + {::db/pool (ig/ref ::db/pool) + ::mtx/metrics (ig/ref ::mtx/metrics) + ::mbus/msgbus (ig/ref :app.msgbus/msgbus) + ::session/manager (ig/ref ::session/manager)} - :app.http.websocket/handler - {:pool (ig/ref ::db/pool) - :metrics (ig/ref ::mtx/metrics) - :msgbus (ig/ref :app.msgbus/msgbus)} - - :app.http.assets/handlers - {:metrics (ig/ref ::mtx/metrics) - :assets-path (cf/get :assets-path) - :storage (ig/ref ::sto/storage) - :executor (ig/ref ::wrk/executor) - :cache-max-age (dt/duration {:hours 24}) - :signature-max-age (dt/duration {:hours 24 :minutes 5})} - - :app.http.feedback/handler - {:pool (ig/ref ::db/pool) - :executor (ig/ref ::wrk/executor)} + :app.http.assets/routes + {::http.assets/path (cf/get :assets-path) + ::http.assets/cache-max-age (dt/duration {:hours 24}) + ::http.assets/cache-max-agesignature-max-age (dt/duration {:hours 24 :minutes 5}) + ::sto/storage (ig/ref ::sto/storage) + ::wrk/executor (ig/ref ::wrk/executor)} :app.rpc/climit - {:metrics (ig/ref ::mtx/metrics) - :executor (ig/ref ::wrk/executor)} + {::mtx/metrics (ig/ref ::mtx/metrics) + ::wrk/executor (ig/ref ::wrk/executor)} :app.rpc/rlimit - {:executor (ig/ref ::wrk/executor) - :scheduled-executor (ig/ref ::wrk/scheduled-executor)} + {::wrk/executor (ig/ref ::wrk/executor) + ::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)} :app.rpc/methods {::http.client/client (ig/ref ::http.client/client) ::db/pool (ig/ref ::db/pool) ::wrk/executor (ig/ref ::wrk/executor) - ::props (ig/ref :app.setup/props) + ::session/manager (ig/ref ::session/manager) ::ldap/provider (ig/ref ::ldap/provider) + ::sto/storage (ig/ref ::sto/storage) + ::mtx/metrics (ig/ref ::mtx/metrics) + ::mbus/msgbus (ig/ref ::mbus/msgbus) + ::rds/redis (ig/ref ::rds/redis) + + ::rpc/climit (ig/ref ::rpc/climit) + ::rpc/rlimit (ig/ref ::rpc/rlimit) + + ::props (ig/ref :app.setup/props) + :pool (ig/ref ::db/pool) - :session (ig/ref :app.http.session/manager) - :sprops (ig/ref :app.setup/props) - :metrics (ig/ref ::mtx/metrics) - :storage (ig/ref ::sto/storage) - :msgbus (ig/ref :app.msgbus/msgbus) - :public-uri (cf/get :public-uri) - :redis (ig/ref ::rds/redis) - :http-client (ig/ref ::http.client/client) - :climit (ig/ref :app.rpc/climit) - :rlimit (ig/ref :app.rpc/rlimit) - :executor (ig/ref ::wrk/executor) :templates (ig/ref :app.setup/builtin-templates) } @@ -345,12 +347,17 @@ {:methods (ig/ref :app.rpc/methods)} :app.rpc/routes - {:methods (ig/ref :app.rpc/methods)} + {::rpc/methods (ig/ref :app.rpc/methods) + ::db/pool (ig/ref ::db/pool) + ::wrk/executor (ig/ref ::wrk/executor) + ::session/manager (ig/ref ::session/manager) + ::actoken/manager (ig/ref ::actoken/manager) + ::props (ig/ref :app.setup/props)} ::wrk/registry - {:metrics (ig/ref ::mtx/metrics) - :tasks - {:sendmail (ig/ref :app.emails/handler) + {::mtx/metrics (ig/ref ::mtx/metrics) + ::wrk/tasks + {:sendmail (ig/ref ::email/handler) :objects-gc (ig/ref :app.tasks.objects-gc/handler) :file-gc (ig/ref :app.tasks.file-gc/handler) :file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler) @@ -358,7 +365,7 @@ :storage-gc-touched (ig/ref ::sto/gc-touched-task) :tasks-gc (ig/ref :app.tasks.tasks-gc/handler) :telemetry (ig/ref :app.tasks.telemetry/handler) - :session-gc (ig/ref :app.http.session/gc-task) + :session-gc (ig/ref ::session.tasks/gc) :audit-log-archive (ig/ref ::audit.tasks/archive) :audit-log-gc (ig/ref ::audit.tasks/gc) @@ -367,34 +374,32 @@ :run-webhook (ig/ref ::webhooks/run-webhook-handler)}} + ::email/sendmail + {::email/host (cf/get :smtp-host) + ::email/port (cf/get :smtp-port) + ::email/ssl (cf/get :smtp-ssl) + ::email/tls (cf/get :smtp-tls) + ::email/username (cf/get :smtp-username) + ::email/password (cf/get :smtp-password) + ::email/default-reply-to (cf/get :smtp-default-reply-to) + ::email/default-from (cf/get :smtp-default-from)} - :app.emails/sendmail - {:host (cf/get :smtp-host) - :port (cf/get :smtp-port) - :ssl (cf/get :smtp-ssl) - :tls (cf/get :smtp-tls) - :username (cf/get :smtp-username) - :password (cf/get :smtp-password) - :default-reply-to (cf/get :smtp-default-reply-to) - :default-from (cf/get :smtp-default-from)} - - :app.emails/handler - {:sendmail (ig/ref :app.emails/sendmail) - :metrics (ig/ref ::mtx/metrics)} + ::email/handler + {::email/sendmail (ig/ref ::email/sendmail) + ::mtx/metrics (ig/ref ::mtx/metrics)} :app.tasks.tasks-gc/handler - {:pool (ig/ref ::db/pool) - :max-age cf/deletion-delay} + {::db/pool (ig/ref ::db/pool)} :app.tasks.objects-gc/handler {::db/pool (ig/ref ::db/pool) ::sto/storage (ig/ref ::sto/storage)} :app.tasks.file-gc/handler - {:pool (ig/ref ::db/pool)} + {::db/pool (ig/ref ::db/pool)} :app.tasks.file-xlog-gc/handler - {:pool (ig/ref ::db/pool)} + {::db/pool (ig/ref ::db/pool)} :app.tasks.telemetry/handler {::db/pool (ig/ref ::db/pool) @@ -402,22 +407,23 @@ ::props (ig/ref :app.setup/props)} [::srepl/urepl ::srepl/server] - {:port (cf/get :urepl-port 6062) - :host (cf/get :urepl-host "localhost")} + {::srepl/port (cf/get :urepl-port 6062) + ::srepl/host (cf/get :urepl-host "localhost")} [::srepl/prepl ::srepl/server] - {:port (cf/get :prepl-port 6063) - :host (cf/get :prepl-host "localhost")} + {::srepl/port (cf/get :prepl-port 6063) + ::srepl/host (cf/get :prepl-host "localhost")} :app.setup/builtin-templates {::http.client/client (ig/ref ::http.client/client)} :app.setup/props - {:pool (ig/ref ::db/pool) - :key (cf/get :secret-key)} + {::db/pool (ig/ref ::db/pool) + ::key (cf/get :secret-key) - ::lzmq/receiver - {} + ;; NOTE: this dependency is only necessary for proper initialization ordering, props + ;; module requires the migrations to run before initialize. + ::migrations (ig/ref :app.migrations/migrations)} ::audit.tasks/archive {::props (ig/ref :app.setup/props) @@ -435,38 +441,27 @@ {::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client)} - :app.loggers.loki/reporter - {::lzmq/receiver (ig/ref ::lzmq/receiver) - ::http.client/client (ig/ref ::http.client/client)} - :app.loggers.mattermost/reporter - {::lzmq/receiver (ig/ref ::lzmq/receiver) - ::http.client/client (ig/ref ::http.client/client)} + {::http.client/client (ig/ref ::http.client/client)} :app.loggers.database/reporter - {::lzmq/receiver (ig/ref :app.loggers.zmq/receiver) - ::db/pool (ig/ref ::db/pool)} + {::db/pool (ig/ref ::db/pool)} ::sto/storage - {:pool (ig/ref ::db/pool) - :executor (ig/ref ::wrk/executor) - - :backends + {::db/pool (ig/ref ::db/pool) + ::wrk/executor (ig/ref ::wrk/executor) + ::sto/backends {:assets-s3 (ig/ref [::assets :app.storage.s3/backend]) - :assets-fs (ig/ref [::assets :app.storage.fs/backend]) - - ;; keep this for backward compatibility - :s3 (ig/ref [::assets :app.storage.s3/backend]) - :fs (ig/ref [::assets :app.storage.fs/backend])}} + :assets-fs (ig/ref [::assets :app.storage.fs/backend])}} [::assets :app.storage.s3/backend] - {:region (cf/get :storage-assets-s3-region) - :endpoint (cf/get :storage-assets-s3-endpoint) - :bucket (cf/get :storage-assets-s3-bucket) - :executor (ig/ref ::wrk/executor)} + {::sto.s3/region (cf/get :storage-assets-s3-region) + ::sto.s3/endpoint (cf/get :storage-assets-s3-endpoint) + ::sto.s3/bucket (cf/get :storage-assets-s3-bucket) + ::wrk/executor (ig/ref ::wrk/executor)} [::assets :app.storage.fs/backend] - {:directory (cf/get :storage-assets-fs-directory)} + {::sto.fs/directory (cf/get :storage-assets-fs-directory)} }) diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 689f50b3a..72dbb83d3 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -12,6 +12,8 @@ [app.common.media :as cm] [app.common.spec :as us] [app.config :as cf] + [app.db :as-alias db] + [app.storage :as-alias sto] [app.storage.tmp :as tmp] [app.util.svg :as svg] [buddy.core.bytes :as bb] @@ -297,8 +299,7 @@ "Given storage map, returns a storage configured with the appropriate backend for assets and optional connection attached." ([storage] - (assoc storage :backend (cf/get :assets-storage-backend :assets-fs))) - ([storage conn] - (-> storage - (assoc :conn conn) - (assoc :backend (cf/get :assets-storage-backend :assets-fs))))) + (assoc storage ::sto/backend (cf/get :assets-storage-backend :assets-fs))) + ([storage pool-or-conn] + (-> (configure-assets-storage storage) + (assoc ::db/pool-or-conn pool-or-conn)))) diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj index c11d3d112..e23e47133 100644 --- a/backend/src/app/metrics.clj +++ b/backend/src/app/metrics.clj @@ -87,6 +87,7 @@ ::definitions definitions ::registry registry})) + (defn- handler [registry _ respond _] (let [samples (.metricFamilySamples ^CollectorRegistry registry) @@ -95,6 +96,18 @@ (respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004} :body (.toString writer)}))) + + +(s/def ::routes vector?) +(defmethod ig/pre-init-spec ::routes [_] + (s/keys :req [::metrics])) + +(defmethod ig/init-key ::routes + [_ {:keys [::metrics]}] + (let [registry (::registry metrics)] + ["/metrics" {:handler (partial handler registry) + :allowed-methods #{:get}}])) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 14c83afeb..b8b41d0e9 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -6,8 +6,12 @@ (ns app.migrations (:require + [app.common.data.macros :as dm] + [app.common.logging :as l] + [app.db :as db] [app.migrations.clj.migration-0023 :as mg0023] [app.util.migrations :as mg] + [clojure.spec.alpha :as s] [integrant.core :as ig])) (def migrations @@ -302,7 +306,29 @@ {:name "0098-add-quotes-table" :fn (mg/resource "app/migrations/sql/0098-add-quotes-table.sql")} + {:name "0099-add-access-token-table" + :fn (mg/resource "app/migrations/sql/0099-add-access-token-table.sql")} + + {:name "0100-mod-profile-indexes" + :fn (mg/resource "app/migrations/sql/0100-mod-profile-indexes.sql")} + + {:name "0101-mod-server-error-report-table" + :fn (mg/resource "app/migrations/sql/0101-mod-server-error-report-table.sql")} + ]) +(defn apply-migrations! + [pool name migrations] + (dm/with-open [conn (db/open pool)] + (mg/setup! conn) + (mg/migrate! conn {:name name :steps migrations}))) -(defmethod ig/init-key ::migrations [_ _] migrations) +(defmethod ig/pre-init-spec ::migrations + [_] + (s/keys :req [::db/pool])) + +(defmethod ig/init-key ::migrations + [module {:keys [::db/pool]}] + (when-not (db/read-only? pool) + (l/info :hint "running migrations" :module module) + (some->> (seq migrations) (apply-migrations! pool "main")))) diff --git a/backend/src/app/migrations/sql/0099-add-access-token-table.sql b/backend/src/app/migrations/sql/0099-add-access-token-table.sql new file mode 100644 index 000000000..b46951cdc --- /dev/null +++ b/backend/src/app/migrations/sql/0099-add-access-token-table.sql @@ -0,0 +1,19 @@ +DROP TABLE IF EXISTS access_token; +CREATE TABLE access_token ( + id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY, + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE, + + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + name text NOT NULL, + token text NOT NULL, + perms text[] NULL +); + +ALTER TABLE access_token + ALTER COLUMN name SET STORAGE external, + ALTER COLUMN token SET STORAGE external, + ALTER COLUMN perms SET STORAGE external; + +CREATE INDEX access_token__profile_id__idx ON access_token(profile_id); diff --git a/backend/src/app/migrations/sql/0100-mod-profile-indexes.sql b/backend/src/app/migrations/sql/0100-mod-profile-indexes.sql new file mode 100644 index 000000000..2aa84b3e1 --- /dev/null +++ b/backend/src/app/migrations/sql/0100-mod-profile-indexes.sql @@ -0,0 +1,31 @@ +ALTER TABLE profile + ADD COLUMN default_project_id uuid NULL REFERENCES project(id) ON DELETE SET NULL DEFERRABLE, + ADD COLUMN default_team_id uuid NULL REFERENCES team(id) ON DELETE SET NULL DEFERRABLE; + +CREATE INDEX profile__default_project__idx ON profile(default_project_id); +CREATE INDEX profile__default_team__idx ON profile(default_team_id); + +with profiles as ( + select p.id, + tpr.team_id as default_team_id, + ppr.project_id as default_project_id + from profile as p + join team_profile_rel as tpr + on (tpr.profile_id = p.id and + tpr.is_owner is true) + join project_profile_rel as ppr + on (ppr.profile_id = p.id and + ppr.is_owner is true) + join project as pj + on (pj.id = ppr.project_id) + join team as tm + on (tm.id = tpr.team_id) + where pj.is_default is true + and tm.is_default is true + and pj.team_id = tm.id +) +update profile + set default_team_id = p.default_team_id, + default_project_id = p.default_project_id + from profiles as p + where profile.id = p.id; diff --git a/backend/src/app/migrations/sql/0101-mod-server-error-report-table.sql b/backend/src/app/migrations/sql/0101-mod-server-error-report-table.sql new file mode 100644 index 000000000..9de0c7679 --- /dev/null +++ b/backend/src/app/migrations/sql/0101-mod-server-error-report-table.sql @@ -0,0 +1,2 @@ +ALTER TABLE server_error_report + ADD COLUMN version integer DEFAULT 1; diff --git a/backend/src/app/msgbus.clj b/backend/src/app/msgbus.clj index 8dd5b3345..f0e4e28b4 100644 --- a/backend/src/app/msgbus.clj +++ b/backend/src/app/msgbus.clj @@ -79,7 +79,7 @@ (us/verify! ::msgbus msgbus) - (set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/async false)) + (set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true)) (set-error-mode! state :continue) (start-io-loop! msgbus) @@ -133,7 +133,7 @@ [nsubs cfg topic chan] (let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))] (when (= 1 (count nsubs)) - (l/trace :hint "open subscription" :topic topic ::l/async false) + (l/trace :hint "open subscription" :topic topic ::l/sync? true) (redis-sub cfg topic)) nsubs)) @@ -144,7 +144,7 @@ [nsubs cfg topic chan] (let [nsubs (disj nsubs chan)] (when (empty? nsubs) - (l/trace :hint "close subscription" :topic topic ::l/async false) + (l/trace :hint "close subscription" :topic topic ::l/sync? true) (redis-unsub cfg topic)) nsubs)) diff --git a/backend/src/app/redis.clj b/backend/src/app/redis.clj index 0ada4b0b9..b00d51c7c 100644 --- a/backend/src/app/redis.clj +++ b/backend/src/app/redis.clj @@ -193,6 +193,7 @@ (defn get-or-connect [{:keys [::cache] :as state} key options] + (us/assert! ::redis state) (-> state (assoc ::connection (or (get @cache key) @@ -205,7 +206,6 @@ (defn add-listener! [{:keys [::connection] :as conn} listener] - (us/assert! ::connection-holder conn) (us/assert! ::pubsub-connection connection) (us/assert! ::pubsub-listener listener) (.addListener ^StatefulRedisPubSubConnection @connection @@ -213,10 +213,9 @@ conn) (defn publish! - [{:keys [::connection] :as conn} topic message] + [{:keys [::connection]} topic message] (us/assert! ::us/string topic) (us/assert! ::us/bytes message) - (us/assert! ::connection-holder conn) (us/assert! ::default-connection connection) (let [pcomm (.async ^StatefulRedisConnection @connection)] @@ -224,8 +223,7 @@ (defn subscribe! "Blocking operation, intended to be used on a thread/agent thread." - [{:keys [::connection] :as conn} & topics] - (us/assert! ::connection-holder conn) + [{:keys [::connection]} & topics] (us/assert! ::pubsub-connection connection) (try (let [topics (into-array String (map str topics)) @@ -236,8 +234,7 @@ (defn unsubscribe! "Blocking operation, intended to be used on a thread/agent thread." - [{:keys [::connection] :as conn} & topics] - (us/assert! ::connection-holder conn) + [{:keys [::connection]} & topics] (us/assert! ::pubsub-connection connection) (try (let [topics (into-array String (map str topics)) @@ -247,8 +244,8 @@ (throw (InterruptedException. (ex-message cause)))))) (defn rpush! - [{:keys [::connection] :as conn} key payload] - (us/assert! ::connection-holder conn) + [{:keys [::connection]} key payload] + (us/assert! ::default-connection connection) (us/assert! (or (and (vector? payload) (every? bytes? payload)) (bytes? payload))) @@ -270,8 +267,8 @@ (throw (InterruptedException. (ex-message cause)))))) (defn blpop! - [{:keys [::connection] :as conn} timeout & keys] - (us/assert! ::connection-holder conn) + [{:keys [::connection]} timeout & keys] + (us/assert! ::default-connection connection) (try (let [keys (into-array Object (map str keys)) cmd (.sync ^StatefulRedisConnection @connection) @@ -286,8 +283,7 @@ (throw (InterruptedException. (ex-message cause)))))) (defn open? - [{:keys [::connection] :as conn}] - (us/assert! ::connection-holder conn) + [{:keys [::connection]}] (us/assert! ::pubsub-connection connection) (.isOpen ^StatefulConnection @connection)) @@ -335,7 +331,7 @@ (defn eval! [{:keys [::mtx/metrics ::connection] :as state} script] (us/assert! ::redis state) - (us/assert! ::connection-holder state) + (us/assert! ::default-connection connection) (us/assert! ::rscript/script script) (let [cmd (.async ^StatefulRedisConnection @connection) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index c2c50dc1c..617f0ea70 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -7,6 +7,7 @@ (ns app.rpc (:require [app.auth.ldap :as-alias ldap] + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] @@ -14,10 +15,12 @@ [app.config :as cf] [app.db :as db] [app.http :as-alias http] + [app.http.access-token :as actoken] [app.http.client :as-alias http.client] - [app.http.session :as-alias http.session] + [app.http.session :as session] [app.loggers.audit :as audit] [app.loggers.webhooks :as-alias webhooks] + [app.main :as-alias main] [app.metrics :as mtx] [app.msgbus :as-alias mbus] [app.rpc.climit :as climit] @@ -71,71 +74,78 @@ (defn- rpc-query-handler "Ring handler that dispatches query requests and convert between internal async flow into ring async flow." - [methods {:keys [profile-id session-id path-params params] :as request} respond raise] - (let [type (keyword (:type path-params)) - data (-> params - (assoc ::request-at (dt/now)) - (assoc ::http/request request)) - data (if profile-id - (-> data - (assoc :profile-id profile-id) - (assoc ::profile-id profile-id) - (assoc ::session-id session-id)) - (dissoc data :profile-id ::profile-id)) - method (get methods type default-handler)] + [methods {:keys [params path-params] :as request} respond raise] + (let [type (keyword (:type path-params)) + profile-id (or (::session/profile-id request) + (::actoken/profile-id request)) - (-> (method data) - (p/then (partial handle-response request)) - (p/then respond) - (p/catch (fn [cause] - (let [context {:profile-id profile-id}] - (raise (ex/wrap-with-context cause context)))))))) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::http/request request)) + data (if profile-id + (-> data + (assoc :profile-id profile-id) + (assoc ::profile-id profile-id)) + (dissoc data :profile-id ::profile-id)) + method (get methods type default-handler)] + + (->> (method data) + (p/mcat (partial handle-response request)) + (p/fnly (fn [response cause] + (if cause + (raise cause) + (respond response))))))) (defn- rpc-mutation-handler "Ring handler that dispatches mutation requests and convert between internal async flow into ring async flow." - [methods {:keys [profile-id session-id path-params params] :as request} respond raise] - (let [type (keyword (:type path-params)) - data (-> params - (assoc ::request-at (dt/now)) - (assoc ::http/request request)) - data (if profile-id - (-> data - (assoc :profile-id profile-id) - (assoc ::profile-id profile-id) - (assoc ::session-id session-id)) - (dissoc data :profile-id ::profile-id)) - method (get methods type default-handler)] - (-> (method data) - (p/then (partial handle-response request)) - (p/then respond) - (p/catch (fn [cause] - (let [context {:profile-id profile-id}] - (raise (ex/wrap-with-context cause context)))))))) + [methods {:keys [params path-params] :as request} respond raise] + (let [type (keyword (:type path-params)) + profile-id (or (::session/profile-id request) + (::actoken/profile-id request)) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::http/request request)) + data (if profile-id + (-> data + (assoc :profile-id profile-id) + (assoc ::profile-id profile-id)) + (dissoc data :profile-id)) + method (get methods type default-handler)] + + (->> (method data) + (p/mcat (partial handle-response request)) + (p/fnly (fn [response cause] + (if cause + (raise cause) + (respond response))))))) (defn- rpc-command-handler "Ring handler that dispatches cmd requests and convert between internal async flow into ring async flow." - [methods {:keys [profile-id session-id path-params params] :as request} respond raise] - (let [cmd (keyword (:type path-params)) - etag (yrq/get-header request "if-none-match") + [methods {:keys [params path-params] :as request} respond raise] + (let [type (keyword (:type path-params)) + etag (yrq/get-header request "if-none-match") + profile-id (or (::session/profile-id request) + (::actoken/profile-id request)) - data (-> params - (assoc ::request-at (dt/now)) - (assoc ::http/request request) - (assoc ::cond/key etag) - (cond-> (uuid? profile-id) - (-> (assoc ::profile-id profile-id) - (assoc ::session-id session-id)))) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::session/id (::session/id request)) + (assoc ::http/request request) + (assoc ::cond/key etag) + (cond-> (uuid? profile-id) + (assoc ::profile-id profile-id))) + + method (get methods type default-handler)] - method (get methods cmd default-handler)] (binding [cond/*enabled* true] - (-> (method data) - (p/then (partial handle-response request)) - (p/then respond) - (p/catch (fn [cause] - (let [context {:profile-id profile-id}] - (raise (ex/wrap-with-context cause context))))))))) + (->> (method data) + (p/mcat (partial handle-response request)) + (p/fnly (fn [response cause] + (if cause + (raise cause) + (respond response)))))))) (defn- wrap-metrics "Wrap service method with metrics measurement." @@ -143,18 +153,46 @@ (let [labels (into-array String [(::sv/name mdata)])] (fn [cfg params] (let [tp (dt/tpoint)] - (p/finally - (f cfg params) - (fn [_ _] - (mtx/run! metrics - :id metrics-id - :val (inst-ms (tp)) - :labels labels))))))) + (->> (f cfg params) + (p/fnly (fn [_ _] + (mtx/run! metrics + :id metrics-id + :val (inst-ms (tp)) + :labels labels)))))))) + + +(defn- wrap-authentication + [_ f mdata] + (fn [cfg params] + (let [profile-id (::profile-id params)] + (if (and (::auth mdata true) (not (uuid? profile-id))) + (p/rejected + (ex/error :type :authentication + :code :authentication-required + :hint "authentication required for this endpoint")) + (f cfg params))))) + +(defn- wrap-access-token + "Wraps service method with access token validation." + [_ f {:keys [::sv/name] :as mdata}] + (if (contains? cf/flags :access-tokens) + (fn [cfg params] + (let [request (::http/request params)] + (if (contains? request ::actoken/id) + (let [perms (::actoken/perms request #{})] + (if (contains? perms name) + (f cfg params) + (p/rejected + (ex/error :type :authorization + :code :operation-not-allowed + :allowed perms)))) + (f cfg params)))) + f)) (defn- wrap-dispatch "Wraps service method into async flow, with the ability to dispatching it to a preconfigured executor service." - [{:keys [executor] :as cfg} f mdata] + [{:keys [::wrk/executor] :as cfg} f mdata] (with-meta (fn [cfg params] (->> (px/submit! executor (px/wrap-bindings #(f cfg params))) @@ -223,37 +261,34 @@ f)) f)) +(defn- wrap-spec-conform + [_ f mdata] + (let [spec (or (::sv/spec mdata) (s/spec any?))] + (fn [cfg params] + (let [params (ex/try! (us/conform spec params))] + (if (ex/exception? params) + (p/rejected params) + (f cfg params)))))) + +(defn- wrap-all + [cfg f mdata] + (as-> f $ + (wrap-dispatch cfg $ mdata) + (wrap-metrics cfg $ mdata) + (cond/wrap cfg $ mdata) + (retry/wrap-retry cfg $ mdata) + (climit/wrap cfg $ mdata) + (rlimit/wrap cfg $ mdata) + (wrap-audit cfg $ mdata) + (wrap-spec-conform cfg $ mdata) + (wrap-authentication cfg $ mdata) + (wrap-access-token cfg $ mdata))) + (defn- wrap [cfg f mdata] - (let [f (as-> f $ - (wrap-dispatch cfg $ mdata) - (cond/wrap cfg $ mdata) - (retry/wrap-retry cfg $ mdata) - (wrap-metrics cfg $ mdata) - (climit/wrap cfg $ mdata) - (rlimit/wrap cfg $ mdata) - (wrap-audit cfg $ mdata)) - - spec (or (::sv/spec mdata) (s/spec any?)) - auth? (::auth mdata true)] - - - (l/debug :hint "register method" :name (::sv/name mdata)) - (with-meta - (fn [params] - ;; Raise authentication error when rpc method requires auth but - ;; no profile-id is found in the request. - (let [profile-id (if (= "command" (::type cfg)) - (::profile-id params) - (:profile-id params))] - (p/do! - (if (and auth? (not (uuid? profile-id))) - (ex/raise :type :authentication - :code :authentication-required - :hint "authentication required for this endpoint") - (let [params (us/conform spec params)] - (f cfg params)))))) - mdata))) + (l/debug :hint "register method" :name (::sv/name mdata)) + (let [f (wrap-all cfg f mdata)] + (with-meta #(f cfg %) mdata))) (defn- process-method [cfg vfn] @@ -264,79 +299,76 @@ (defn- resolve-query-methods [cfg] (let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)] - (->> (sv/scan-ns 'app.rpc.queries.projects - 'app.rpc.queries.files - 'app.rpc.queries.teams - 'app.rpc.queries.profile - 'app.rpc.queries.viewer - 'app.rpc.queries.fonts) + (->> (sv/scan-ns + 'app.rpc.queries.projects + 'app.rpc.queries.profile + 'app.rpc.queries.viewer + 'app.rpc.queries.fonts) (map (partial process-method cfg)) (into {})))) (defn- resolve-mutation-methods [cfg] (let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)] - (->> (sv/scan-ns 'app.rpc.mutations.media - 'app.rpc.mutations.profile - 'app.rpc.mutations.files - 'app.rpc.mutations.projects - 'app.rpc.mutations.teams - 'app.rpc.mutations.fonts - 'app.rpc.mutations.share-link) + (->> (sv/scan-ns + 'app.rpc.mutations.media + 'app.rpc.mutations.profile + 'app.rpc.mutations.projects + 'app.rpc.mutations.fonts + 'app.rpc.mutations.share-link) (map (partial process-method cfg)) (into {})))) (defn- resolve-command-methods [cfg] (let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)] - (->> (sv/scan-ns 'app.rpc.commands.binfile - 'app.rpc.commands.comments - 'app.rpc.commands.management - 'app.rpc.commands.verify-token - 'app.rpc.commands.search - 'app.rpc.commands.media - 'app.rpc.commands.teams - 'app.rpc.commands.auth - 'app.rpc.commands.ldap - 'app.rpc.commands.demo - 'app.rpc.commands.webhooks - 'app.rpc.commands.audit - 'app.rpc.commands.files - 'app.rpc.commands.files.update - 'app.rpc.commands.files.create - 'app.rpc.commands.files.temp) + (->> (sv/scan-ns + 'app.rpc.commands.access-token + 'app.rpc.commands.audit + 'app.rpc.commands.auth + 'app.rpc.commands.feedback + 'app.rpc.commands.fonts + 'app.rpc.commands.binfile + 'app.rpc.commands.comments + 'app.rpc.commands.demo + 'app.rpc.commands.files + 'app.rpc.commands.files-create + 'app.rpc.commands.files-share + 'app.rpc.commands.files-temp + 'app.rpc.commands.files-update + 'app.rpc.commands.ldap + 'app.rpc.commands.management + 'app.rpc.commands.media + 'app.rpc.commands.profile + 'app.rpc.commands.projects + 'app.rpc.commands.search + 'app.rpc.commands.teams + 'app.rpc.commands.verify-token + 'app.rpc.commands.viewer + 'app.rpc.commands.webhooks) (map (partial process-method cfg)) (into {})))) -(s/def ::ldap (s/nilable map?)) -(s/def ::msgbus ::mbus/msgbus) -(s/def ::climit (s/nilable ::climit/climit)) -(s/def ::rlimit (s/nilable ::rlimit/rlimit)) - -(s/def ::public-uri ::us/not-empty-string) -(s/def ::sprops map?) - (defmethod ig/pre-init-spec ::methods [_] - (s/keys :req [::http.client/client + (s/keys :req [::session/manager + ::http.client/client ::db/pool + ::mbus/msgbus ::ldap/provider + ::sto/storage + ::mtx/metrics + ::main/props ::wrk/executor] - :req-un [::sto/storage - ::http.session/session - ::sprops - ::public-uri - ::msgbus - ::rlimit - ::climit - ::wrk/executor - ::mtx/metrics - ::db/pool])) + :opt [::climit + ::rlimit] + :req-un [::db/pool])) (defmethod ig/init-key ::methods [_ cfg] - {:mutations (resolve-mutation-methods cfg) - :queries (resolve-query-methods cfg) - :commands (resolve-command-methods cfg)}) + (let [cfg (d/without-nils cfg)] + {:mutations (resolve-mutation-methods cfg) + :queries (resolve-query-methods cfg) + :commands (resolve-command-methods cfg)})) (s/def ::mutations (s/map-of keyword? fn?)) @@ -352,12 +384,20 @@ ::queries ::commands])) +(s/def ::routes vector?) + (defmethod ig/pre-init-spec ::routes [_] - (s/keys :req-un [::methods])) + (s/keys :req [::methods + ::db/pool + ::main/props + ::wrk/executor + ::session/manager + ::actoken/manager])) (defmethod ig/init-key ::routes - [_ {:keys [methods] :as cfg}] - [["/rpc" + [_ {:keys [::methods] :as cfg}] + [["/rpc" {:middleware [[session/authz cfg] + [actoken/authz cfg]]} ["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}] ["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}] ["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods)) diff --git a/backend/src/app/rpc/climit.clj b/backend/src/app/rpc/climit.clj index 8ccf07300..4985f6f25 100644 --- a/backend/src/app/rpc/climit.clj +++ b/backend/src/app/rpc/climit.clj @@ -32,7 +32,7 @@ (defn- capacity-exception? [o] - (and (ex/ex-info? o) + (and (ex/error? o) (let [data (ex-data o)] (and (= :bulkhead-error (:type data)) (= :capacity-limit-reached (:code data)))))) @@ -46,7 +46,7 @@ (p/rejected (ex/error :type :internal :code :concurrency-limit-reached - :queue (-> limiter meta :bkey name) + :queue (-> limiter meta ::bkey name) :cause cause)) (some? cause) @@ -56,7 +56,7 @@ (p/resolved result)))))) (defn- create-limiter - [{:keys [executor metrics concurrency queue-size bkey skey]}] + [{:keys [::wrk/executor ::mtx/metrics ::bkey ::skey concurrency queue-size]}] (let [labels (into-array String [(name bkey)]) on-queue (fn [instance] (l/trace :hint "enqueued" @@ -100,10 +100,10 @@ :on-run on-run}] (-> (pxb/create options) - (vary-meta assoc :bkey bkey :skey skey)))) + (vary-meta assoc ::bkey bkey ::skey skey)))) (defn- create-cache - [{:keys [executor] :as params} config] + [{:keys [::wrk/executor] :as params} config] (let [listener (reify RemovalListener (onRemoval [_ key _val cause] (l/trace :hint "cache: remove" :key key :reason (str cause)))) @@ -113,8 +113,8 @@ (let [[bkey skey] key] (when-let [config (get config bkey)] (-> (merge params config) - (assoc :bkey bkey) - (assoc :skey skey) + (assoc ::bkey bkey) + (assoc ::skey skey) (create-limiter))))))] (.. (Caffeine/newBuilder) @@ -134,14 +134,16 @@ (defmethod ig/prep-key ::rpc/climit [_ cfg] - (merge {:path (cf/get :rpc-climit-config)} + (merge {::path (cf/get :rpc-climit-config)} (d/without-nils cfg))) +(s/def ::path ::fs/path) + (defmethod ig/pre-init-spec ::rpc/climit [_] - (s/keys :req-un [::wrk/executor ::mtx/metrics ::fs/path])) + (s/keys :req [::wrk/executor ::mtx/metrics ::path])) (defmethod ig/init-key ::rpc/climit - [_ {:keys [path] :as params}] + [_ {:keys [::path] :as params}] (when (contains? cf/flags :rpc-climit) (if-let [config (some->> path slurp edn/read-string)] (do @@ -163,7 +165,8 @@ (l/warn :hint "unable to load configuration" :config (str path))))) -(s/def ::climit #(satisfies? IConcurrencyManager %)) +(s/def ::rpc/climit + (s/nilable #(satisfies? IConcurrencyManager %))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PUBLIC API @@ -176,7 +179,7 @@ (p/wrap (do ~@body)))) (defn wrap - [{:keys [climit]} f {:keys [::queue ::key-fn] :as mdata}] + [{:keys [::rpc/climit]} f {:keys [::queue ::key-fn] :as mdata}] (if (and (some? climit) (some? queue)) (if-let [config (get @climit queue)] @@ -192,7 +195,6 @@ (let [key [queue (key-fn params)] lim (get climit key)] (invoke! lim (partial f cfg params)))) - (let [lim (get climit queue)] (fn [cfg params] (invoke! lim (partial f cfg params)))))) diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj new file mode 100644 index 000000000..9abf99c49 --- /dev/null +++ b/backend/src/app/rpc/commands/access_token.clj @@ -0,0 +1,87 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.rpc.commands.access-token + (:require + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.db :as db] + [app.main :as-alias main] + [app.rpc :as-alias rpc] + [app.rpc.doc :as-alias doc] + [app.rpc.quotes :as quotes] + [app.tokens :as tokens] + [app.util.services :as sv] + [app.util.time :as dt] + [clojure.spec.alpha :as s])) + +(defn- decode-row + [{:keys [perms] :as row}] + (cond-> row + (db/pgarray? perms "text") + (assoc :perms (db/decode-pgarray perms #{})))) + +(defn- create-access-token + [{:keys [::conn ::main/props]} profile-id name perms] + (let [created-at (dt/now) + token-id (uuid/next) + token (tokens/generate props {:iss "access-token" + :tid token-id + :iat created-at})] + (db/insert! conn :access-token + {:id token-id + :name name + :token token + :profile-id profile-id + :created-at created-at + :updated-at created-at + :perms (db/create-array conn "text" perms)}))) + +(defn repl-create-access-token + [{:keys [::db/pool] :as system} profile-id name perms] + (db/with-atomic [conn pool] + (let [props (:app.setup/props system)] + (create-access-token {::conn conn ::main/props props} + profile-id + name + perms)))) + +(s/def ::name ::us/not-empty-string) +(s/def ::perms ::us/set-of-strings) + +(s/def ::create-access-token + (s/keys :req [::rpc/profile-id] + :req-un [::name ::perms])) + +(sv/defmethod ::create-access-token + {::doc/added "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name perms]}] + (db/with-atomic [conn pool] + (let [cfg (assoc cfg ::conn conn)] + (quotes/check-quote! conn + {::quotes/id ::quotes/access-tokens-per-profile + ::quotes/profile-id profile-id}) + (-> (create-access-token cfg profile-id name perms) + (decode-row))))) + +(s/def ::delete-access-token + (s/keys :req [::rpc/profile-id] + :req-un [::us/id])) + +(sv/defmethod ::delete-access-token + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id id]}] + (db/delete! pool :access-token {:id id :profile-id profile-id}) + nil) + +(s/def ::get-access-tokens + (s/keys :req [::rpc/profile-id])) + +(sv/defmethod ::get-access-tokens + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id]}] + (->> (db/query pool :access-token {:profile-id profile-id}) + (mapv decode-row))) diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index b12e12b22..9e5e4c76a 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -42,7 +42,7 @@ :profile-id :ip-addr :props :context]) (defn- handle-events - [{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request] :as params}] + [{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request]}] (let [ip-addr (audit/parse-client-ip request) xform (comp (map #(assoc % :profile-id profile-id)) diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index bcd4f1044..aaeb4835b 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -13,16 +13,16 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] - [app.emails :as eml] + [app.email :as eml] [app.http.session :as session] [app.loggers.audit :as audit] [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.climit :as climit] + [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.profile :as profile] [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] @@ -52,24 +52,10 @@ (str/split #"@" 2))] (contains? domains candidate)))) -(def ^:private sql:profile-existence - "select exists (select * from profile - where email = ? - and deleted_at is null) as val") - -(defn check-profile-existence! - [conn {:keys [email] :as params}] - (let [email (str/lower email) - result (db/exec-one! conn [sql:profile-existence email])] - (when (:val result) - (ex/raise :type :validation - :code :email-already-exists)) - params)) - ;; ---- COMMAND: login with password (defn login-with-password - [{:keys [::db/pool session] :as cfg} {:keys [email password] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [email password] :as params}] (when-not (or (contains? cf/flags :login) (contains? cf/flags :login-with-password)) @@ -105,11 +91,10 @@ profile)] (db/with-atomic [conn pool] - (let [profile (->> (profile/retrieve-profile-data-by-email conn email) + (let [profile (->> (profile/get-profile-by-email conn email) (validate-profile) - (profile/strip-private-attrs) - (profile/populate-additional-data conn) - (profile/decode-profile-row)) + (profile/decode-row) + (profile/strip-private-attrs)) invitation (when-let [token (:invitation-token params)] (tokens/verify (::main/props cfg) {:token token :iss :team-invitation})) @@ -122,14 +107,13 @@ (assoc profile :is-admin (let [admins (cf/get :admins)] (contains? admins (:email profile)))))] (-> response - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/props (audit/profile->props profile) ::audit/profile-id (:id profile)})))))) -(s/def ::scope ::us/string) (s/def ::login-with-password (s/keys :req-un [::email ::password] - :opt-un [::invitation-token ::scope])) + :opt-un [::invitation-token])) (sv/defmethod ::login-with-password "Performs authentication using penpot password." @@ -148,8 +132,8 @@ "Clears the authentication cookie and logout the current session." {::rpc/auth false ::doc/added "1.15"} - [{:keys [session] :as cfg} _] - (rph/with-transform {} (session/delete-fn session))) + [cfg _] + (rph/with-transform {} (session/delete-fn cfg))) ;; ---- COMMAND: Recover Profile @@ -226,7 +210,7 @@ (validate-register-attempt! cfg params) - (let [profile (when-let [profile (profile/retrieve-profile-data-by-email pool (:email params))] + (let [profile (when-let [profile (profile/get-profile-by-email pool (:email params))] (cond (:is-blocked profile) (ex/raise :type :restriction @@ -267,10 +251,11 @@ ;; ---- COMMAND: Register Profile -(defn create-profile +(defn create-profile! "Create the profile entry on the database with limited set of input attrs (all the other attrs are filled with default values)." - [conn params] + [conn {:keys [email] :as params}] + (us/assert! ::us/email email) (let [id (or (:id params) (uuid/next)) props (-> (audit/extract-utm-params params) (merge (:props params)) @@ -291,7 +276,7 @@ is-demo (:is-demo params false) is-muted (:is-muted params false) is-active (:is-active params false) - email (str/lower (:email params)) + email (str/lower email) params {:id id :fullname (:fullname params) @@ -306,7 +291,7 @@ :is-demo is-demo}] (try (-> (db/insert! conn :profile params) - (profile/decode-profile-row)) + (profile/decode-row)) (catch org.postgresql.util.PSQLException e (let [state (.getSQLState e)] (if (not= state "23505") @@ -316,15 +301,17 @@ :hint "email already exists" :cause e))))))) -(defn create-profile-relations - [conn profile] - (let [team (teams/create-team conn {:profile-id (:id profile) +(defn create-profile-rels! + [conn {:keys [id] :as profile}] + (let [team (teams/create-team conn {:profile-id id :name "Default" :is-default true})] - (-> profile - (profile/strip-private-attrs) - (assoc :default-team-id (:id team)) - (assoc :default-project-id (:default-project-id team))))) + (-> (db/update! conn :profile + {:default-team-id (:id team) + :default-project-id (:default-project-id team)} + {:id id}) + (profile/decode-row)))) + (defn send-email-verification! [conn props profile] @@ -348,22 +335,18 @@ :extra-data ptoken}))) (defn register-profile - [{:keys [::db/conn session] :as cfg} {:keys [token] :as params}] + [{:keys [::db/conn] :as cfg} {:keys [token] :as params}] (let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register}) params (merge params claims) is-active (or (:is-active params) - (not (contains? cf/flags :email-verification)) - - ;; DEPRECATED: v1.15 - (contains? cf/flags :insecure-register)) + (not (contains? cf/flags :email-verification))) profile (if-let [profile-id (:profile-id claims)] - (profile/retrieve-profile conn profile-id) - (->> (assoc params :is-active is-active) - (create-profile conn) - (create-profile-relations conn) - (profile/decode-profile-row))) + (profile/get-profile conn profile-id) + (->> (create-profile! conn (assoc params :is-active is-active)) + (create-profile-rels! conn))) + invitation (when-let [token (:invitation-token params)] (tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))] @@ -389,7 +372,7 @@ token (tokens/generate (::main/props cfg) claims) resp {:invitation-token token}] (-> resp - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)}))) @@ -398,7 +381,7 @@ ;; we need to mark this session as logged. (not= "penpot" (:auth-backend profile)) (-> (profile/strip-private-attrs profile) - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)})) @@ -406,7 +389,7 @@ ;; to sign in the user directly, without email verification. (true? is-active) (-> (profile/strip-private-attrs profile) - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)})) @@ -448,7 +431,7 @@ :exp (dt/in-future {:days 30})})] (eml/send! {::eml/conn conn ::eml/factory eml/password-recovery - :public-uri (:public-uri cfg) + :public-uri (cf/get :public-uri) :to (:email profile) :token (:token profile) :name (:fullname profile) @@ -456,7 +439,7 @@ nil))] (db/with-atomic [conn pool] - (when-let [profile (profile/retrieve-profile-data-by-email conn email)] + (when-let [profile (profile/get-profile-by-email conn email)] (when-not (eml/allow-send-emails? conn profile) (ex/raise :type :validation :code :profile-is-muted diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 98587428c..3fefa9109 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -21,9 +21,9 @@ [app.media :as media] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as projects] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.projects :as projects] [app.storage :as sto] [app.storage.tmp :as tmp] [app.tasks.file-gc] @@ -109,20 +109,20 @@ (defn write-byte! [^DataOutputStream output data] - (l/trace :fn "write-byte!" :data data :position @*position* ::l/async false) + (l/trace :fn "write-byte!" :data data :position @*position* ::l/sync? true) (.writeByte output (byte data)) (swap! *position* inc)) (defn read-byte! [^DataInputStream input] (let [v (.readByte input)] - (l/trace :fn "read-byte!" :val v :position @*position* ::l/async false) + (l/trace :fn "read-byte!" :val v :position @*position* ::l/sync? true) (swap! *position* inc) v)) (defn write-long! [^DataOutputStream output data] - (l/trace :fn "write-long!" :data data :position @*position* ::l/async false) + (l/trace :fn "write-long!" :data data :position @*position* ::l/sync? true) (.writeLong output (long data)) (swap! *position* + 8)) @@ -130,14 +130,14 @@ (defn read-long! [^DataInputStream input] (let [v (.readLong input)] - (l/trace :fn "read-long!" :val v :position @*position* ::l/async false) + (l/trace :fn "read-long!" :val v :position @*position* ::l/sync? true) (swap! *position* + 8) v)) (defn write-bytes! [^DataOutputStream output ^bytes data] (let [size (alength data)] - (l/trace :fn "write-bytes!" :size size :position @*position* ::l/async false) + (l/trace :fn "write-bytes!" :size size :position @*position* ::l/sync? true) (.write output data 0 size) (swap! *position* + size))) @@ -145,7 +145,7 @@ [^InputStream input ^bytes buff] (let [size (alength buff) readed (.readNBytes input buff 0 size)] - (l/trace :fn "read-bytes!" :expected (alength buff) :readed readed :position @*position* ::l/async false) + (l/trace :fn "read-bytes!" :expected (alength buff) :readed readed :position @*position* ::l/sync? true) (swap! *position* + readed) readed)) @@ -153,7 +153,7 @@ (defn write-uuid! [^DataOutputStream output id] - (l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/async false) + (l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/sync? true) (doto output (write-byte! (get-mark :uuid)) @@ -162,7 +162,7 @@ (defn read-uuid! [^DataInputStream input] - (l/trace :fn "read-uuid!" :position @*position* ::l/async false) + (l/trace :fn "read-uuid!" :position @*position* ::l/sync? true) (let [m (read-byte! input)] (assert-mark m :uuid) (let [a (read-long! input) @@ -171,7 +171,7 @@ (defn write-obj! [^DataOutputStream output data] - (l/trace :fn "write-obj!" :position @*position* ::l/async false) + (l/trace :fn "write-obj!" :position @*position* ::l/sync? true) (let [^bytes data (fres/encode data)] (doto output (write-byte! (get-mark :obj)) @@ -180,7 +180,7 @@ (defn read-obj! [^DataInputStream input] - (l/trace :fn "read-obj!" :position @*position* ::l/async false) + (l/trace :fn "read-obj!" :position @*position* ::l/sync? true) (let [m (read-byte! input)] (assert-mark m :obj) (let [size (read-long! input)] @@ -191,14 +191,14 @@ (defn write-label! [^DataOutputStream output label] - (l/trace :fn "write-label!" :label label :position @*position* ::l/async false) + (l/trace :fn "write-label!" :label label :position @*position* ::l/sync? true) (doto output (write-byte! (get-mark :label)) (write-obj! label))) (defn read-label! [^DataInputStream input] - (l/trace :fn "read-label!" :position @*position* ::l/async false) + (l/trace :fn "read-label!" :position @*position* ::l/sync? true) (let [m (read-byte! input)] (assert-mark m :label) (read-obj! input))) @@ -208,7 +208,7 @@ (l/trace :fn "write-header!" :version version :position @*position* - ::l/async false) + ::l/sync? true) (let [vers (-> version name (subs 1) parse-long) output (io/data-output-stream output)] (doto output @@ -218,7 +218,7 @@ (defn read-header! [^InputStream input] - (l/trace :fn "read-header!" :position @*position* ::l/async false) + (l/trace :fn "read-header!" :position @*position* ::l/sync? true) (let [input (io/data-input-stream input) mark (read-byte! input) mnum (read-long! input) @@ -235,13 +235,13 @@ (defn copy-stream! [^OutputStream output ^InputStream input ^long size] (let [written (io/copy! input output :size size)] - (l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/async false) + (l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/sync? true) (swap! *position* + written) written)) (defn write-stream! [^DataOutputStream output stream size] - (l/trace :fn "write-stream!" :position @*position* ::l/async false :size size) + (l/trace :fn "write-stream!" :position @*position* ::l/sync? true :size size) (doto output (write-byte! (get-mark :stream)) (write-long! size)) @@ -250,7 +250,7 @@ (defn read-stream! [^DataInputStream input] - (l/trace :fn "read-stream!" :position @*position* ::l/async false) + (l/trace :fn "read-stream!" :position @*position* ::l/sync? true) (let [m (read-byte! input) s (read-long! input) p (tmp/tempfile :prefix "penpot.binfile.")] @@ -264,7 +264,7 @@ (if (> s temp-file-threshold) (with-open [^OutputStream output (io/output-stream p)] (let [readed (io/copy! input output :offset 0 :size s)] - (l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/async false) + (l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/sync? true) (swap! *position* + readed) [s p])) [s (io/read-as-bytes input :size s)]))) @@ -438,9 +438,8 @@ (s/def ::embed-assets? (s/nilable ::us/boolean)) (s/def ::write-export-options - (s/keys :req-un [::db/pool ::sto/storage] - :req [::output ::file-ids] - :opt [::include-libraries? ::embed-assets?])) + (s/keys :req [::db/pool ::sto/storage ::output ::file-ids] + :opt [::include-libraries? ::embed-assets?])) (defn write-export! "Do the exportation of a specified file in custom penpot binary @@ -453,6 +452,7 @@ `::embed-assets?`: instead of including the libraries, embed in the same file library all assets used from external libraries." [{:keys [::include-libraries? ::embed-assets?] :as options}] + (us/assert! ::write-export-options options) (us/verify! :expr (not (and include-libraries? embed-assets?)) @@ -466,7 +466,7 @@ (with-open [output (io/data-output-stream output)] (binding [*state* (volatile! {})] (run! (fn [section] - (l/debug :hint "write section" :section section ::l/async false) + (l/debug :hint "write section" :section section ::l/sync? true) (write-label! output section) (let [options (-> options (assoc ::output output) @@ -477,7 +477,7 @@ [:v1/metadata :v1/files :v1/rels :v1/sobjects]))))) (defmethod write-section :v1/metadata - [{:keys [pool ::output ::file-ids ::include-libraries?]}] + [{:keys [::db/pool ::output ::file-ids ::include-libraries?]}] (let [libs (when include-libraries? (retrieve-libraries pool file-ids)) files (into file-ids libs)] @@ -485,7 +485,7 @@ (vswap! *state* assoc :files files))) (defmethod write-section :v1/files - [{:keys [pool ::output ::embed-assets?]}] + [{:keys [::db/pool ::output ::embed-assets?]}] ;; Initialize SIDS with empty vector (vswap! *state* assoc :sids []) @@ -500,7 +500,7 @@ (l/debug :hint "write penpot file" :id file-id :media (count media) - ::l/async false) + ::l/sync? true) (doto output (write-obj! file) @@ -509,26 +509,26 @@ (vswap! *state* update :sids into storage-object-id-xf media)))) (defmethod write-section :v1/rels - [{:keys [pool ::output ::include-libraries?]}] + [{:keys [::db/pool ::output ::include-libraries?]}] (let [rels (when include-libraries? (retrieve-library-relations pool (-> *state* deref :files)))] - (l/debug :hint "found rels" :total (count rels) ::l/async false) + (l/debug :hint "found rels" :total (count rels) ::l/sync? true) (write-obj! output rels))) (defmethod write-section :v1/sobjects - [{:keys [storage ::output]}] + [{:keys [::sto/storage ::output]}] (let [sids (-> *state* deref :sids) storage (media/configure-assets-storage storage)] (l/debug :hint "found sobjects" :items (count sids) - ::l/async false) + ::l/sync? true) ;; Write all collected storage objects (write-obj! output sids) (doseq [id sids] (let [{:keys [size] :as obj} @(sto/get-object storage id)] - (l/debug :hint "write sobject" :id id ::l/async false) + (l/debug :hint "write sobject" :id id ::l/sync? true) (doto output (write-uuid! id) (write-obj! (meta obj))) @@ -557,9 +557,8 @@ (s/def ::ignore-index-errors? (s/nilable ::us/boolean)) (s/def ::read-import-options - (s/keys :req-un [::db/pool ::sto/storage] - :req [::project-id ::input] - :opt [::overwrite? ::migrate? ::ignore-index-errors?])) + (s/keys :req [::db/pool ::sto/storage ::project-id ::input] + :opt [::overwrite? ::migrate? ::ignore-index-errors?])) (defn read-import! "Do the importation of the specified resource in penpot custom binary @@ -582,14 +581,14 @@ (read-import (assoc options ::version version ::timestamp timestamp)))) (defmethod read-import :v1 - [{:keys [pool ::input] :as options}] + [{:keys [::db/pool ::input] :as options}] (with-open [input (zstd-input-stream input)] (with-open [input (io/data-input-stream input)] (db/with-atomic [conn pool] (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED;"]) (binding [*state* (volatile! {:media [] :index {}})] (run! (fn [section] - (l/debug :hint "reading section" :section section ::l/async false) + (l/debug :hint "reading section" :section section ::l/sync? true) (assert-read-label! input section) (let [options (-> options (assoc ::section section) @@ -607,7 +606,7 @@ (defmethod read-section :v1/metadata [{:keys [::input]}] (let [{:keys [version files]} (read-obj! input)] - (l/debug :hint "metadata readed" :version (:full version) :files files ::l/async false) + (l/debug :hint "metadata readed" :version (:full version) :files files ::l/sync? true) (vswap! *state* update :index update-index files) (vswap! *state* assoc :version version :files files))) @@ -635,14 +634,14 @@ :hint "the penpot file seems corrupt, found unexpected uuid (file-id)")) ;; Update index using with media - (l/debug :hint "update index with media" ::l/async false) + (l/debug :hint "update index with media" ::l/sync? true) (vswap! *state* update :index update-index (map :id media')) ;; Store file media for later insertion - (l/debug :hint "update media references" ::l/async false) + (l/debug :hint "update media references" ::l/sync? true) (vswap! *state* update :media into (map #(update % :id lookup-index)) media') - (l/debug :hint "processing file" :file-id file-id ::features features ::l/async false) + (l/debug :hint "processing file" :file-id file-id ::features features ::l/sync? true) (binding [ffeat/*current* features ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity) @@ -668,7 +667,7 @@ :created-at timestamp :modified-at timestamp}] - (l/debug :hint "create file" :id file-id' ::l/async false) + (l/debug :hint "create file" :id file-id' ::l/sync? true) (if overwrite? (create-or-update-file conn params) @@ -691,11 +690,11 @@ (l/debug :hint "create file library link" :file-id (:file-id rel) :lib-id (:library-file-id rel) - ::l/async false) + ::l/sync? true) (db/insert! conn :file-library-rel rel))))) (defmethod read-section :v1/sobjects - [{:keys [storage conn ::input ::overwrite?]}] + [{:keys [::sto/storage conn ::input ::overwrite?]}] (let [storage (media/configure-assets-storage storage) ids (read-obj! input)] @@ -708,7 +707,7 @@ :code :inconsistent-penpot-file :hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)")) - (l/debug :hint "readed storage object" :id id ::l/async false) + (l/debug :hint "readed storage object" :id id ::l/sync? true) (let [[size resource] (read-stream! input) hash (sto/calculate-hash resource) @@ -722,18 +721,18 @@ sobject @(sto/put-object! storage params)] - (l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/async false) + (l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true) (vswap! *state* update :index assoc id (:id sobject))))) (doseq [item (:media @*state*)] (l/debug :hint "inserting file media object" :id (:id item) :file-id (:file-id item) - ::l/async false) + ::l/sync? true) (let [file-id (lookup-index (:file-id item))] (if (= file-id (:file-id item)) - (l/warn :hint "ignoring file media object" :file-id (:file-id item) ::l/async false) + (l/warn :hint "ignoring file media object" :file-id (:file-id item) ::l/sync? true) (db/insert! conn :file-media-object (-> item (assoc :file-id file-id) @@ -744,7 +743,7 @@ (defn- lookup-index [id] (let [val (get-in @*state* [:index id])] - (l/trace :fn "lookup-index" :id id :val val ::l/async false) + (l/trace :fn "lookup-index" :id id :val val ::l/sync? true) (when (and (not (::ignore-index-errors? *options*)) (not val)) (ex/raise :type :validation :code :incomplete-index @@ -757,7 +756,7 @@ index index] (if-let [id (first items)] (let [new-id (if (::overwrite? *options*) id (uuid/next))] - (l/trace :fn "update-index" :id id :new-id new-id ::l/async false) + (l/trace :fn "update-index" :id id :new-id new-id ::l/sync? true) (recur (rest items) (assoc index id new-id))) index))) @@ -805,7 +804,7 @@ (try (process-map-form form) (catch Throwable cause - (l/warn :hint "failed form" :form (pr-str form) ::l/async false) + (l/warn :hint "failed form" :form (pr-str form) ::l/sync? true) (throw cause))) form)) data))) @@ -893,13 +892,14 @@ (s/def ::embed-assets? ::us/boolean) (s/def ::export-binfile - (s/keys :req [::rpc/profile-id] :req-un [::file-id ::include-libraries? ::embed-assets?])) + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::include-libraries? ::embed-assets?])) (sv/defmethod ::export-binfile "Export a penpot file in a binary format." {::doc/added "1.15" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}] (files/check-read-permissions! pool profile-id file-id) (let [body (reify yrs/StreamableResponseBody (-write-body-to-stream [_ _ output-stream] @@ -914,13 +914,14 @@ (s/def ::file ::media/upload) (s/def ::import-binfile - (s/keys :req [::rpc/profile-id] :req-un [::project-id ::file])) + (s/keys :req [::rpc/profile-id] + :req-un [::project-id ::file])) (sv/defmethod ::import-binfile "Import a penpot file in a binary format." {::doc/added "1.15" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}] (db/with-atomic [conn pool] (projects/check-read-permissions! conn profile-id project-id) (let [ids (import! (assoc cfg diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index 73e40642f..44366b894 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -54,8 +54,8 @@ :hint "file not found")))) (defn- get-comment-thread - [conn thread-id & {:keys [for-update?]}] - (-> (db/get-by-id conn :comment-thread thread-id {:for-update for-update?}) + [conn thread-id & {:as opts}] + (-> (db/get-by-id conn :comment-thread thread-id opts) (decode-row))) (defn- get-comment @@ -100,7 +100,7 @@ (sv/defmethod ::get-comment-threads {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] (with-open [conn (db/open pool)] (files/check-comment-permissions! conn profile-id file-id share-id) (get-comment-threads conn profile-id file-id))) @@ -143,7 +143,7 @@ (sv/defmethod ::get-unread-comment-threads {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) (get-unread-comment-threads conn profile-id team-id))) @@ -190,7 +190,7 @@ (sv/defmethod ::get-comment-thread {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}] (with-open [conn (db/open pool)] (files/check-comment-permissions! conn profile-id file-id share-id) (let [sql (str "with threads as (" sql:comment-threads ")" @@ -210,7 +210,7 @@ (sv/defmethod ::get-comments {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}] (with-open [conn (db/open pool)] (let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)] (files/check-comment-permissions! conn profile-id file-id share-id) @@ -262,7 +262,7 @@ participants on comment threads of the file." {::doc/added "1.15" ::doc/changes ["1.15" "Imported from queries and renamed."]} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}] (with-open [conn (db/open pool)] (files/check-comment-permissions! conn profile-id file-id share-id) (get-file-comments-users conn file-id profile-id))) @@ -372,9 +372,9 @@ (sv/defmethod ::update-comment-thread-status {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (upsert-comment-thread-status! conn profile-id id)))) @@ -389,9 +389,9 @@ (sv/defmethod ::update-comment-thread {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (db/update! conn :comment-thread {:is-resolved is-resolved} @@ -412,9 +412,9 @@ (sv/defmethod ::create-comment {::doc/added "1.15" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id :for-update? true) + (let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true) {:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)] (files/check-comment-permissions! conn profile-id (:id file) share-id) @@ -465,10 +465,10 @@ (sv/defmethod ::update-comment {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}] (db/with-atomic [conn pool] - (let [{:keys [thread-id] :as comment} (get-comment conn id :for-update? true) - {:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id :for-update? true)] + (let [{:keys [thread-id] :as comment} (get-comment conn id ::db/for-update? true) + {:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) @@ -498,9 +498,9 @@ (sv/defmethod ::delete-comment-thread {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (when-not (= owner-id profile-id) (ex/raise :type :validation @@ -518,9 +518,9 @@ (sv/defmethod ::delete-comment {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [owner-id thread-id] :as comment} (get-comment conn id :for-update? true) + (let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::db/for-update? true) {:keys [file-id] :as thread} (get-comment-thread conn thread-id)] (files/check-comment-permissions! conn profile-id file-id share-id) (when-not (= owner-id profile-id) @@ -538,9 +538,9 @@ (sv/defmethod ::update-comment-thread-position {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (db/update! conn :comment-thread {:modified-at (::rpc/request-at params) @@ -558,9 +558,9 @@ (sv/defmethod ::update-comment-thread-frame {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (db/update! conn :comment-thread {:modified-at (::rpc/request-at params) diff --git a/backend/src/app/rpc/commands/demo.clj b/backend/src/app/rpc/commands/demo.clj index bcc3d1d6e..32897de92 100644 --- a/backend/src/app/rpc/commands/demo.clj +++ b/backend/src/app/rpc/commands/demo.clj @@ -8,12 +8,11 @@ "A demo specific mutations." (:require [app.common.exceptions :as ex] - [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.loggers.audit :as audit] [app.rpc :as-alias rpc] - [app.rpc.commands.auth :as cmd.auth] + [app.rpc.commands.auth :as auth] [app.rpc.doc :as-alias doc] [app.util.services :as sv] [app.util.time :as dt] @@ -30,32 +29,31 @@ {::rpc/auth false ::doc/added "1.15" ::doc/changes ["1.15" "This method is migrated from mutations to commands."]} - [{:keys [pool] :as cfg} _] - (let [id (uuid/next) - sem (System/currentTimeMillis) + [{:keys [::db/pool] :as cfg} _] + + (when-not (contains? cf/flags :demo-users) + (ex/raise :type :validation + :code :demo-users-not-allowed + :hint "Demo users are disabled by config.")) + + (let [sem (System/currentTimeMillis) email (str "demo-" sem ".demo@example.com") fullname (str "Demo User " sem) + password (-> (bn/random-bytes 16) (bc/bytes->b64u) (bc/bytes->str)) - params {:id id - :email email + + params {:email email :fullname fullname :is-active true :deleted-at (dt/in-future cf/deletion-delay) :password password - :props {} - }] - - (when-not (contains? cf/flags :demo-users) - (ex/raise :type :validation - :code :demo-users-not-allowed - :hint "Demo users are disabled by config.")) + :props {}}] (db/with-atomic [conn pool] - (->> (cmd.auth/create-profile conn params) - (cmd.auth/create-profile-relations conn)) - - (with-meta {:email email - :password password} - {::audit/profile-id id})))) + (let [profile (->> (auth/create-profile! conn params) + (auth/create-profile-rels! conn))] + (with-meta {:email email + :password password} + {::audit/profile-id (:id profile)}))))) diff --git a/backend/src/app/rpc/commands/feedback.clj b/backend/src/app/rpc/commands/feedback.clj new file mode 100644 index 000000000..7d2ab1c88 --- /dev/null +++ b/backend/src/app/rpc/commands/feedback.clj @@ -0,0 +1,56 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.rpc.commands.feedback + "A general purpose feedback module." + (:require + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.config :as cf] + [app.db :as db] + [app.email :as eml] + [app.rpc :as-alias rpc] + [app.rpc.commands.profile :as profile] + [app.rpc.doc :as-alias doc] + [app.util.services :as sv] + [clojure.spec.alpha :as s])) + +(declare ^:private send-feedback!) + +(s/def ::content ::us/string) +(s/def ::from ::us/email) +(s/def ::subject ::us/string) + +(s/def ::send-user-feedback + (s/keys :req [::rpc/profile-id] + :req-un [::subject + ::content])) + +(sv/defmethod ::send-user-feedback + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}] + (when-not (contains? cf/flags :user-feedback) + (ex/raise :type :restriction + :code :feedback-disabled + :hint "feedback not enabled")) + + (let [profile (profile/get-profile pool profile-id)] + (send-feedback! pool profile params) + nil)) + +(defn- send-feedback! + [pool profile params] + (let [dest (cf/get :feedback-destination)] + (eml/send! {::eml/conn pool + ::eml/factory eml/feedback + :from dest + :to dest + :profile profile + :reply-to (:email profile) + :email (:email profile) + :subject (:subject params) + :content (:content params)}) + nil)) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 7083a852e..ddc299f1f 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -22,13 +22,12 @@ [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] [app.rpc.commands.files.thumbnails :as-alias thumbs] + [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] [app.rpc.cond :as-alias cond] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.permissions :as perms] - [app.rpc.queries.projects :as projects] - [app.rpc.queries.share-link :refer [retrieve-share-link]] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.services :as sv] @@ -128,7 +127,9 @@ ([conn profile-id file-id share-id] (let [perms (get-permissions conn profile-id file-id) - ldata (retrieve-share-link conn file-id share-id)] + ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id}) + (dissoc :flags) + (update :pages db/decode-pgarray #{}))] ;; NOTE: in a future when share-link becomes more powerful and ;; will allow us specify which parts of the app is available, we @@ -196,7 +197,7 @@ (let [row (db/get conn :file-data-fragment {:id id :file-id file-id} {:columns [:content] - :check-deleted? false})] + ::db/check-deleted? false})] (blob/decode (:content row)))) (defn persist-pointers! @@ -258,7 +259,7 @@ (handle-file-features client-features)))) (defn get-minimal-file - [{:keys [pool] :as cfg} id] + [{:keys [::db/pool] :as cfg} id] (db/get pool :file {:id id} {:columns [:id :modified-at :revn]})) (defn get-file-etag @@ -275,7 +276,7 @@ {::doc/added "1.17" ::cond/get-object #(get-minimal-file %1 (:id %2)) ::cond/key-fn get-file-etag} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features]}] (with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id id)] (check-read-permissions! perms) @@ -303,7 +304,7 @@ "Retrieve a file by its ID. Only authenticated users." {::doc/added "1.17" ::rpc/:auth false} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }] (with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id file-id share-id)] (check-read-permissions! perms) @@ -339,7 +340,7 @@ ::cond/get-object #(get-minimal-file %1 (:file-id %2)) ::cond/reuse-key? true ::cond/key-fn get-file-etag} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (get-object-thumbnails conn file-id))) @@ -370,7 +371,7 @@ (sv/defmethod ::get-project-files "Get all files for the specified project." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}] (with-open [conn (db/open pool)] (projects/check-read-permissions! conn profile-id project-id) (get-project-files conn project-id))) @@ -389,7 +390,7 @@ (sv/defmethod ::has-file-libraries "Checks if the file has libraries. Returns a boolean" {::doc/added "1.15.1"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] (with-open [conn (db/open pool)] (check-read-permissions! pool profile-id file-id) (get-has-file-libraries conn file-id))) @@ -456,7 +457,7 @@ Mainly used for rendering purposes." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (get-page conn params))) @@ -509,7 +510,7 @@ (sv/defmethod ::get-team-shared-files "Get all file (libraries) for the specified team." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) (get-team-shared-files conn team-id))) @@ -563,7 +564,7 @@ (sv/defmethod ::get-file-libraries "Get libraries used by the specified file." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features]}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (get-file-libraries conn file-id features))) @@ -589,7 +590,7 @@ (sv/defmethod ::get-library-file-references "Returns all the file references that use specified file (library) id." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (get-library-file-references conn file-id))) @@ -626,7 +627,7 @@ (sv/defmethod ::get-team-recent-files {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) (get-team-recent-files conn team-id))) @@ -660,7 +661,7 @@ (sv/defmethod ::get-file-thumbnail {::doc/added "1.17"} - [{:keys [pool]} {:keys [::rpc/profile-id file-id revn]}] + [{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (-> (get-file-thumbnail conn file-id revn) @@ -756,7 +757,7 @@ "Retrieves the data for generate the thumbnail of the file. Used mainly for render thumbnails on dashboard." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) ;; NOTE: we force here the "storage/pointer-map" feature, because @@ -788,7 +789,7 @@ (sv/defmethod ::rename-file {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) (let [file (rename-file conn params)] @@ -819,7 +820,7 @@ (let [ldata (-> library decode-row pmg/migrate-file :data)] (->> (db/query conn :file-library-rel {:library-file-id id}) (map :file-id) - (keep #(db/get-by-id conn :file % {:check-deleted? false})) + (keep #(db/get-by-id conn :file % ::db/check-deleted? false)) (map decode-row) (map pmg/migrate-file) (run! (fn [{:keys [id data revn] :as file}] @@ -837,7 +838,7 @@ (sv/defmethod ::set-file-shared {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-shared] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-shared] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) (when-not is-shared @@ -866,7 +867,7 @@ (sv/defmethod ::delete-file {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) (absorb-library conn params) @@ -896,7 +897,7 @@ (sv/defmethod ::link-file-to-library {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}] (when (= file-id library-id) (ex/raise :type :validation :code :invalid-library @@ -921,7 +922,7 @@ (sv/defmethod ::unlink-file-from-library {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (unlink-file-from-library conn params))) @@ -945,7 +946,7 @@ (sv/defmethod ::update-file-library-sync-status "Update the synchronization statos of a file->library link" {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (update-sync conn params))) @@ -967,7 +968,7 @@ (sv/defmethod ::ignore-file-library-sync-status "Ignore updates in linked files" {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (-> (ignore-sync conn params) @@ -998,7 +999,7 @@ (sv/defmethod ::upsert-file-object-thumbnail {::doc/added "1.17" ::audit/skip true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (upsert-file-object-thumbnail! conn params) @@ -1006,13 +1007,13 @@ ;; --- MUTATION COMMAND: upsert-file-thumbnail -(def sql:upsert-file-thumbnail +(def ^:private sql:upsert-file-thumbnail "insert into file_thumbnail (file_id, revn, data, props) values (?, ?, ?, ?::jsonb) on conflict(file_id, revn) do update set data = ?, props=?, updated_at=now();") -(defn upsert-file-thumbnail +(defn- upsert-file-thumbnail! [conn {:keys [file-id revn data props]}] (let [props (db/tjson (or props {}))] (db/exec-one! conn [sql:upsert-file-thumbnail @@ -1029,8 +1030,9 @@ grid thumbnails." {::doc/added "1.17" ::audit/skip true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) - (upsert-file-thumbnail conn params) + (when-not (db/read-only? conn) + (upsert-file-thumbnail! conn params)) nil)) diff --git a/backend/src/app/rpc/commands/files/create.clj b/backend/src/app/rpc/commands/files_create.clj similarity index 91% rename from backend/src/app/rpc/commands/files/create.clj rename to backend/src/app/rpc/commands/files_create.clj index 2d4a7a808..f2cceed47 100644 --- a/backend/src/app/rpc/commands/files/create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.rpc.commands.files.create +(ns app.rpc.commands.files-create (:require [app.common.data :as d] [app.common.files.features :as ffeat] @@ -15,14 +15,15 @@ [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as projects] [app.rpc.doc :as-alias doc] [app.rpc.permissions :as perms] - [app.rpc.queries.projects :as proj] [app.rpc.quotes :as quotes] [app.util.blob :as blob] [app.util.objects-map :as omap] [app.util.pointer-map :as pmap] [app.util.services :as sv] + [app.util.time :as dt] [clojure.spec.alpha :as s])) (defn create-file-role! @@ -67,6 +68,10 @@ (->> (assoc params :file-id id :role :owner) (create-file-role! conn)) + (db/update! conn :project + {:modified-at (dt/now)} + {:id project-id}) + (files/decode-row file))) (s/def ::create-file @@ -80,9 +85,9 @@ (sv/defmethod ::create-file {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id project-id) + (projects/check-edition-permissions! conn profile-id project-id) (let [team-id (files/get-team-id conn project-id) params (assoc params :profile-id profile-id)] diff --git a/backend/src/app/rpc/commands/files_share.clj b/backend/src/app/rpc/commands/files_share.clj new file mode 100644 index 000000000..f517fbb1f --- /dev/null +++ b/backend/src/app/rpc/commands/files_share.clj @@ -0,0 +1,71 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.rpc.commands.files-share + "Share link related rpc mutation methods." + (:require + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.db :as db] + [app.rpc :as-alias rpc] + [app.rpc.commands.files :as files] + [app.rpc.doc :as-alias doc] + [app.util.services :as sv] + [clojure.spec.alpha :as s])) + +;; --- Helpers & Specs + +(s/def ::file-id ::us/uuid) +(s/def ::who-comment ::us/string) +(s/def ::who-inspect ::us/string) +(s/def ::pages (s/every ::us/uuid :kind set?)) + +;; --- MUTATION: Create Share Link + +(declare create-share-link) + +(s/def ::create-share-link + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::who-comment ::who-inspect ::pages])) + +(sv/defmethod ::create-share-link + "Creates a share-link object. + + Share links are resources that allows external users access to specific + pages of a file with specific permissions (who-comment and who-inspect)." + {::doc/added "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + (db/with-atomic [conn pool] + (files/check-edition-permissions! conn profile-id file-id) + (create-share-link conn (assoc params :profile-id profile-id)))) + +(defn create-share-link + [conn {:keys [profile-id file-id pages who-comment who-inspect]}] + (let [pages (db/create-array conn "uuid" pages) + slink (db/insert! conn :share-link + {:id (uuid/next) + :file-id file-id + :who-comment who-comment + :who-inspect who-inspect + :pages pages + :owner-id profile-id})] + + (update slink :pages db/decode-pgarray #{}))) + +;; --- MUTATION: Delete Share Link + +(s/def ::delete-share-link + (s/keys :req [::rpc/profile-id] + :req-un [::us/id])) + +(sv/defmethod ::delete-share-link + {::doc/added "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + (db/with-atomic [conn pool] + (let [slink (db/get-by-id conn :share-link id)] + (files/check-edition-permissions! conn profile-id (:file-id slink)) + (db/delete! conn :share-link {:id id}) + nil))) diff --git a/backend/src/app/rpc/commands/files/temp.clj b/backend/src/app/rpc/commands/files_temp.clj similarity index 77% rename from backend/src/app/rpc/commands/files/temp.clj rename to backend/src/app/rpc/commands/files_temp.clj index 0bc2c1c87..b0982c4a1 100644 --- a/backend/src/app/rpc/commands/files/temp.clj +++ b/backend/src/app/rpc/commands/files_temp.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.rpc.commands.files.temp +(ns app.rpc.commands.files-temp (:require [app.common.exceptions :as ex] [app.common.pages :as cp] @@ -13,10 +13,10 @@ [app.db :as db] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] - [app.rpc.commands.files.create :as files.create] - [app.rpc.commands.files.update :as files.update] + [app.rpc.commands.files-create :refer [create-file]] + [app.rpc.commands.files-update :as-alias files.update] + [app.rpc.commands.projects :as projects] [app.rpc.doc :as-alias doc] - [app.rpc.queries.projects :as proj] [app.util.blob :as blob] [app.util.services :as sv] [app.util.time :as dt] @@ -37,15 +37,15 @@ (sv/defmethod ::create-temp-file {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id project-id) - (files.create/create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1}))))) + (projects/check-edition-permissions! conn profile-id project-id) + (create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1}))))) ;; --- MUTATION COMMAND: update-temp-file (defn update-temp-file - [conn {:keys [::rpc/profile-id session-id id revn changes] :as params}] + [conn {:keys [profile-id session-id id revn changes] :as params}] (db/insert! conn :file-change {:id (uuid/next) :session-id session-id @@ -57,16 +57,17 @@ :changes (blob/encode changes)})) (s/def ::update-temp-file - (s/keys :req-un [::files.update/changes + (s/keys :req [::rpc/profile-id] + :req-un [::files.update/changes ::files.update/revn ::files.update/session-id ::files/id])) (sv/defmethod ::update-temp-file {::doc/added "1.17"} - [{:keys [pool] :as cfg} params] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] - (update-temp-file conn params) + (update-temp-file conn (assoc params :profile-id profile-id)) nil)) ;; --- MUTATION COMMAND: persist-temp-file @@ -101,7 +102,7 @@ (sv/defmethod ::persist-temp-file {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id id) (persist-temp-file conn params))) diff --git a/backend/src/app/rpc/commands/files/update.clj b/backend/src/app/rpc/commands/files_update.clj similarity index 97% rename from backend/src/app/rpc/commands/files/update.clj rename to backend/src/app/rpc/commands/files_update.clj index 79cf1d759..d48d609e6 100644 --- a/backend/src/app/rpc/commands/files/update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.rpc.commands.files.update +(ns app.rpc.commands.files-update (:require [app.common.exceptions :as ex] [app.common.files.features :as ffeat] @@ -132,7 +132,7 @@ ::webhooks/batch-timeout (dt/duration "2m") ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id) ::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id id) (db/xact-lock! conn id) @@ -145,7 +145,7 @@ (l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))) (defn update-file - [{:keys [conn metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}] + [{:keys [conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}] (let [file (get-file conn id) features (->> (concat (:features file) (:features params)) @@ -275,7 +275,7 @@ (defn- send-notifications! [{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}] (let [lchanges (filter library-change? changes) - msgbus (:msgbus cfg)] + msgbus (::mbus/msgbus cfg)] ;; Asynchronously publish message to the msgbus (mbus/pub! msgbus diff --git a/backend/src/app/rpc/commands/fonts.clj b/backend/src/app/rpc/commands/fonts.clj new file mode 100644 index 000000000..6df517e63 --- /dev/null +++ b/backend/src/app/rpc/commands/fonts.clj @@ -0,0 +1,236 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.rpc.commands.fonts + (:require + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.db :as db] + [app.loggers.audit :as-alias audit] + [app.loggers.webhooks :as-alias webhooks] + [app.media :as media] + [app.rpc :as-alias rpc] + [app.rpc.climit :as climit] + [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as projects] + [app.rpc.commands.teams :as teams] + [app.rpc.doc :as-alias doc] + [app.rpc.helpers :as rph] + [app.rpc.quotes :as quotes] + [app.storage :as sto] + [app.util.services :as sv] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [promesa.core :as p] + [promesa.exec :as px])) + +(def valid-weight #{100 200 300 400 500 600 700 800 900 950}) +(def valid-style #{"normal" "italic"}) + +(s/def ::data (s/map-of ::us/string any?)) +(s/def ::file-id ::us/uuid) +(s/def ::font-id ::us/uuid) +(s/def ::id ::us/uuid) +(s/def ::name ::us/not-empty-string) +(s/def ::project-id ::us/uuid) +(s/def ::style valid-style) +(s/def ::team-id ::us/uuid) +(s/def ::weight valid-weight) + +;; --- QUERY: Get font variants + +(s/def ::get-font-variants + (s/and + (s/keys :req [::rpc/profile-id] + :opt-un [::team-id + ::file-id + ::project-id]) + (fn [o] + (or (contains? o :team-id) + (contains? o :file-id) + (contains? o :project-id))))) + +(sv/defmethod ::get-font-variants + {::doc/added "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}] + (with-open [conn (db/open pool)] + (cond + (uuid? team-id) + (do + (teams/check-read-permissions! conn profile-id team-id) + (db/query conn :team-font-variant + {:team-id team-id + :deleted-at nil})) + + (uuid? project-id) + (let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})] + (projects/check-read-permissions! conn profile-id project-id) + (db/query conn :team-font-variant + {:team-id (:team-id project) + :deleted-at nil})) + + (uuid? file-id) + (let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]}) + project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})] + (files/check-read-permissions! conn profile-id file-id) + (db/query conn :team-font-variant + {:team-id (:team-id project) + :deleted-at nil}))))) + + +(declare create-font-variant) + +(s/def ::create-font-variant + (s/keys :req [::rpc/profile-id] + :req-un [::team-id + ::data + ::font-id + ::font-family + ::font-weight + ::font-style])) + +(sv/defmethod ::create-font-variant + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] + (teams/check-edition-permissions! pool profile-id team-id) + (quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team + ::quotes/profile-id profile-id + ::quotes/team-id team-id}) + (create-font-variant cfg (assoc params :profile-id profile-id)))) + +(defn create-font-variant + [{:keys [::sto/storage ::db/pool ::wrk/executor ::rpc/climit]} {:keys [data] :as params}] + (letfn [(generate-fonts [data] + (climit/with-dispatch (:process-font climit) + (media/run {:cmd :generate-fonts :input data}))) + + ;; Function responsible of calculating cryptographyc hash of + ;; the provided data. + (calculate-hash [data] + (px/with-dispatch executor + (sto/calculate-hash data))) + + (validate-data [data] + (when (and (not (contains? data "font/otf")) + (not (contains? data "font/ttf")) + (not (contains? data "font/woff")) + (not (contains? data "font/woff2"))) + (ex/raise :type :validation + :code :invalid-font-upload)) + data) + + (persist-font-object [data mtype] + (when-let [resource (get data mtype)] + (p/let [hash (calculate-hash resource) + content (-> (sto/content resource) + (sto/wrap-with-hash hash))] + (sto/put-object! storage {::sto/content content + ::sto/touched-at (dt/now) + ::sto/deduplicate? true + :content-type mtype + :bucket "team-font-variant"})))) + + (persist-fonts [data] + (p/let [otf (persist-font-object data "font/otf") + ttf (persist-font-object data "font/ttf") + woff1 (persist-font-object data "font/woff") + woff2 (persist-font-object data "font/woff2")] + + (d/without-nils + {:otf otf + :ttf ttf + :woff1 woff1 + :woff2 woff2}))) + + (insert-into-db [{:keys [woff1 woff2 otf ttf]}] + (db/insert! pool :team-font-variant + {:id (uuid/next) + :team-id (:team-id params) + :font-id (:font-id params) + :font-family (:font-family params) + :font-weight (:font-weight params) + :font-style (:font-style params) + :woff1-file-id (:id woff1) + :woff2-file-id (:id woff2) + :otf-file-id (:id otf) + :ttf-file-id (:id ttf)})) + ] + + (->> (generate-fonts data) + (p/fmap validate-data) + (p/mcat executor persist-fonts) + (p/fmap executor insert-into-db) + (p/fmap (fn [result] + (let [params (update params :data (comp vec keys))] + (rph/with-meta result {::audit/replace-props params}))))))) + +;; --- UPDATE FONT FAMILY + +(s/def ::update-font + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::id ::name])) + +(sv/defmethod ::update-font + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id id name]}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + (rph/with-meta + (db/update! conn :team-font-variant + {:font-family name} + {:font-id id + :team-id team-id}) + {::audit/replace-props {:id id + :name name + :team-id team-id + :profile-id profile-id}}))) + +;; --- DELETE FONT + +(s/def ::delete-font + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::id])) + +(sv/defmethod ::delete-font + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + (let [font (db/update! conn :team-font-variant + {:deleted-at (dt/now)} + {:font-id id :team-id team-id})] + (rph/with-meta (rph/wrap) + {::audit/props {:id id + :team-id team-id + :name (:font-family font) + :profile-id profile-id}})))) + +;; --- DELETE FONT VARIANT + +(s/def ::delete-font-variant + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::id])) + +(sv/defmethod ::delete-font-variant + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + (let [variant (db/update! conn :team-font-variant + {:deleted-at (dt/now)} + {:id id :team-id team-id})] + (rph/with-meta (rph/wrap) + {::audit/props {:font-family (:font-family variant) + :font-id (:font-id variant)}})))) + diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index 6283e1423..e434f537c 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -14,10 +14,10 @@ [app.loggers.audit :as-alias audit] [app.main :as-alias main] [app.rpc :as-alias rpc] - [app.rpc.commands.auth :as cmd.auth] + [app.rpc.commands.auth :as auth] + [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.profile :as profile] [app.tokens :as tokens] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -39,7 +39,7 @@ is properly configured and enabled with `login-with-ldap` flag." {::rpc/auth false ::doc/added "1.15"} - [{:keys [::main/props ::ldap/provider session] :as cfg} params] + [{:keys [::main/props ::ldap/provider] :as cfg} params] (when-not provider (ex/raise :type :restriction :code :ldap-not-initialized @@ -67,24 +67,23 @@ :member-email (:email profile)) token (tokens/generate props claims)] (-> {:invitation-token token} - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/props (:props profile) ::audit/profile-id (:id profile)}))) (-> profile - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/props (:props profile) ::audit/profile-id (:id profile)})))))) (defn- login-or-register - [{:keys [pool] :as cfg} info] + [{:keys [::db/pool] :as cfg} info] (db/with-atomic [conn pool] (or (some->> (:email info) - (profile/retrieve-profile-data-by-email conn) - (profile/populate-additional-data conn) - (profile/decode-profile-row)) + (profile/get-profile-by-email conn) + (profile/decode-row)) (->> (assoc info :is-active true :is-demo false) - (cmd.auth/create-profile conn) - (cmd.auth/create-profile-relations conn) + (auth/create-profile! conn) + (auth/create-profile-rels! conn) (profile/strip-private-attrs))))) diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 2337ff844..0c5813a29 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -17,9 +17,9 @@ [app.rpc :as-alias rpc] [app.rpc.commands.binfile :as binfile] [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as proj] [app.rpc.commands.teams :as teams :refer [create-project-role create-project]] [app.rpc.doc :as-alias doc] - [app.rpc.queries.projects :as proj] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.services :as sv] @@ -46,7 +46,7 @@ "Duplicate a single file in the same team." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (duplicate-file conn (assoc params :profile-id profile-id)))) @@ -221,7 +221,7 @@ "Duplicate an entire project with all the files" {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} params] + [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] (duplicate-project conn (assoc params :profile-id (::rpc/profile-id params))))) @@ -231,12 +231,13 @@ ;; Defer all constraints (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) - (let [project (db/get-by-id conn :project project-id) - + (let [project (-> (db/get-by-id conn :project project-id) + (assoc :is-pinned false)) + files (db/query conn :file - {:project-id (:id project) - :deleted-at nil} - {:columns [:id]}) + {:project-id (:id project) + :deleted-at nil} + {:columns [:id]}) project (cond-> project (string? name) @@ -329,7 +330,7 @@ "Move a set of files from one project to other." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (move-files conn (assoc params :profile-id profile-id)))) @@ -369,7 +370,7 @@ "Move projects between teams." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (move-project conn (assoc params :profile-id profile-id)))) @@ -386,7 +387,7 @@ "Clone into the specified project the template by its id." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (-> (assoc cfg :conn conn) (clone-template (assoc params :profile-id profile-id))))) diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index e22a7bb85..168a78538 100644 --- a/backend/src/app/rpc/commands/media.clj +++ b/backend/src/app/rpc/commands/media.clj @@ -23,6 +23,7 @@ [app.storage.tmp :as tmp] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as-alias wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.io :as io] @@ -66,8 +67,8 @@ (sv/defmethod ::upload-file-media-object {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}] - (let [cfg (update cfg :storage media/configure-assets-storage)] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) (media/validate-media-type! content) (validate-content-size! content) @@ -110,7 +111,7 @@ ;; inverse, soft referential integrity). (defn create-file-media-object - [{:keys [storage pool climit executor]} + [{:keys [::sto/storage ::db/pool climit ::wrk/executor]} {:keys [id file-id is-local name content]}] (letfn [;; Function responsible to retrieve the file information, as ;; it is synchronous operation it should be wrapped into @@ -186,8 +187,8 @@ (sv/defmethod ::create-file-media-object-from-url {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] - (let [cfg (update cfg :storage media/configure-assets-storage)] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) (create-file-media-object-from-url cfg params))) @@ -253,7 +254,7 @@ (sv/defmethod ::clone-file-media-object {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) (-> (assoc cfg :conn conn) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj new file mode 100644 index 000000000..f93aaad25 --- /dev/null +++ b/backend/src/app/rpc/commands/profile.clj @@ -0,0 +1,426 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.rpc.commands.profile + (:require + [app.auth :as auth] + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.email :as eml] + [app.http.session :as session] + [app.loggers.audit :as audit] + [app.main :as-alias main] + [app.media :as media] + [app.rpc :as-alias rpc] + [app.rpc.climit :as climit] + [app.rpc.doc :as-alias doc] + [app.rpc.helpers :as rph] + [app.storage :as sto] + [app.tokens :as tokens] + [app.util.services :as sv] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [promesa.core :as p] + [promesa.exec :as px])) + +(declare decode-row) +(declare get-profile) +(declare strip-private-attrs) +(declare filter-props) +(declare check-profile-existence!) + +;; --- QUERY: Get profile (own) + +(s/def ::get-profile + (s/keys :opt [::rpc/profile-id])) + +(sv/defmethod ::get-profile + {::rpc/auth false + ::doc/added "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id]}] + ;; 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. + (try + (-> (get-profile pool profile-id) + (strip-private-attrs) + (update :props filter-props)) + (catch Throwable _ + {:id uuid/zero :fullname "Anonymous User"}))) + +(defn get-profile + "Get profile by id. Throws not-found exception if no profile found." + [conn id & {:as attrs}] + (-> (db/get-by-id conn :profile id attrs) + (decode-row))) + + +;; --- MUTATION: Update Profile (own) + +(s/def ::email ::us/email) +(s/def ::fullname ::us/not-empty-string) +(s/def ::lang ::us/string) +(s/def ::theme ::us/string) + +(s/def ::update-profile + (s/keys :req [::rpc/profile-id] + :req-un [::fullname] + :opt-un [::lang ::theme])) + +(sv/defmethod ::update-profile + {::doc/added "1.0"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}] + (db/with-atomic [conn pool] + ;; NOTE: we need to retrieve the profile independently if we use + ;; it or not for explicit locking and avoid concurrent updates of + ;; the same row/object. + (let [profile (-> (db/get-by-id conn :profile profile-id ::db/for-update? true) + (decode-row)) + + ;; Update the profile map with direct params + profile (-> profile + (assoc :fullname fullname) + (assoc :lang lang) + (assoc :theme theme)) + ] + + (db/update! conn :profile + {:fullname fullname + :lang lang + :theme theme + :props (db/tjson (:props profile))} + {:id profile-id}) + + (-> profile + (strip-private-attrs) + (d/without-nils) + (rph/with-meta {::audit/props (audit/profile->props profile)}))))) + + +;; --- MUTATION: Update Password + +(declare validate-password!) +(declare update-profile-password!) +(declare invalidate-profile-session!) + +(s/def ::password ::us/not-empty-string) +(s/def ::old-password (s/nilable ::us/string)) + +(s/def ::update-profile-password + (s/keys :req [::rpc/profile-id] + :req-un [::password ::old-password])) + +(sv/defmethod ::update-profile-password + {::climit/queue :auth} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}] + (db/with-atomic [conn pool] + (let [profile (validate-password! conn (assoc params :profile-id profile-id)) + session-id (::session/id params)] + + (when (= (str/lower (:email profile)) + (str/lower (:password params))) + (ex/raise :type :validation + :code :email-as-password + :hint "you can't use your email as password")) + + (update-profile-password! conn (assoc profile :password password)) + (invalidate-profile-session! conn profile-id 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 ::db/for-update? true)] + (when (and (not= (:password profile) "!") + (not (:valid (auth/verify-password old-password (:password profile))))) + (ex/raise :type :validation + :code :old-password-not-match)) + profile)) + +(defn update-profile-password! + [conn {:keys [id password] :as profile}] + (when-not (db/read-only? conn) + (db/update! conn :profile + {:password (auth/derive-password password)} + {:id id}))) + +;; --- MUTATION: Update Photo + +(declare upload-photo) +(declare update-profile-photo) + +(s/def ::file ::media/upload) +(s/def ::update-profile-photo + (s/keys :req [::rpc/profile-id] + :req-un [::file])) + +(sv/defmethod ::update-profile-photo + [cfg {:keys [::rpc/profile-id file] :as params}] + ;; Validate incoming mime type + (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] + (update-profile-photo cfg (assoc params :profile-id profile-id)))) + +;; TODO: reimplement it without p/let + +(defn update-profile-photo + [{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id file] :as params}] + (letfn [(on-uploaded [photo] + (let [profile (db/get-by-id pool :profile profile-id ::db/for-update? true)] + + ;; Schedule deletion of old photo + (when-let [id (:photo-id profile)] + (sto/touch-object! storage id)) + + ;; Save new photo + (db/update! pool :profile + {:photo-id (:id photo)} + {:id profile-id}) + + (-> (rph/wrap) + (rph/with-meta {::audit/replace-props + {:file-name (:filename file) + :file-size (:size file) + :file-path (str (:path file)) + :file-mtype (:mtype file)}}))))] + (->> (upload-photo cfg params) + (p/fmap executor on-uploaded)))) + +(defn upload-photo + [{:keys [::sto/storage ::wrk/executor climit] :as cfg} {:keys [file]}] + (letfn [(get-info [content] + (climit/with-dispatch (:process-image climit) + (media/run {:cmd :info :input content}))) + + (generate-thumbnail [info] + (climit/with-dispatch (:process-image climit) + (media/run {:cmd :profile-thumbnail + :format :jpeg + :quality 85 + :width 256 + :height 256 + :input info}))) + + ;; Function responsible of calculating cryptographyc hash of + ;; the provided data. + (calculate-hash [data] + (px/with-dispatch executor + (sto/calculate-hash data)))] + + (p/let [info (get-info file) + thumb (generate-thumbnail info) + hash (calculate-hash (:data thumb)) + content (-> (sto/content (:data thumb) (:size thumb)) + (sto/wrap-with-hash hash))] + (sto/put-object! storage {::sto/content content + ::sto/deduplicate? true + :bucket "profile" + :content-type (:mtype thumb)})))) + + +;; --- MUTATION: Request Email Change + +(declare ^:private request-email-change!) +(declare ^:private change-email-immediately!) + +(s/def ::request-email-change + (s/keys :req [::rpc/profile-id] + :req-un [::email])) + +(sv/defmethod ::request-email-change + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id email] :as params}] + (db/with-atomic [conn pool] + (let [profile (db/get-by-id conn :profile profile-id) + cfg (assoc cfg ::conn conn) + params (assoc params + :profile profile + :email (str/lower email))] + (if (contains? cf/flags :smtp) + (request-email-change! cfg params) + (change-email-immediately! cfg params))))) + +(defn- change-email-immediately! + [{:keys [::conn]} {:keys [profile email] :as params}] + (when (not= email (:email profile)) + (check-profile-existence! conn params)) + + (db/update! conn :profile + {:email email} + {:id (:id profile)}) + + {:changed true}) + +(defn- request-email-change! + [{:keys [::conn] :as cfg} {:keys [profile email] :as params}] + (let [token (tokens/generate (::main/props cfg) + {:iss :change-email + :exp (dt/in-future "15m") + :profile-id (:id profile) + :email email}) + ptoken (tokens/generate (::main/props cfg) + {:iss :profile-identity + :profile-id (:id profile) + :exp (dt/in-future {:days 30})})] + + (when (not= email (:email profile)) + (check-profile-existence! conn params)) + + (when-not (eml/allow-send-emails? conn profile) + (ex/raise :type :validation + :code :profile-is-muted + :hint "looks like the profile has reported repeatedly as spam or has permanent bounces.")) + + (when (eml/has-bounce-reports? conn email) + (ex/raise :type :validation + :code :email-has-permanent-bounces + :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) + + (eml/send! {::eml/conn conn + ::eml/factory eml/change-email + :public-uri (cf/get :public-uri) + :to (:email profile) + :name (:fullname profile) + :pending-email email + :token token + :extra-data ptoken}) + nil)) + + +;; --- MUTATION: Update Profile Props + +(s/def ::props map?) +(s/def ::update-profile-props + (s/keys :req [::rpc/profile-id] + :req-un [::props])) + +(sv/defmethod ::update-profile-props + [{:keys [::db/pool]} {:keys [::rpc/profile-id props]}] + (db/with-atomic [conn pool] + (let [profile (get-profile conn profile-id ::db/for-update? true) + props (reduce-kv (fn [props k v] + ;; We don't accept namespaced keys + (if (simple-ident? k) + (if (nil? v) + (dissoc props k) + (assoc props k v)) + props)) + (:props profile) + props)] + + (db/update! conn :profile + {:props (db/tjson props)} + {:id profile-id}) + + (filter-props props)))) + + +;; --- MUTATION: Delete Profile + +(declare ^:private get-owned-teams-with-participants) + +(s/def ::delete-profile + (s/keys :req [::rpc/profile-id])) + +(sv/defmethod ::delete-profile + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + (db/with-atomic [conn pool] + (let [teams (get-owned-teams-with-participants conn profile-id) + deleted-at (dt/now)] + + ;; If we found owned teams with participants, we don't allow + ;; delete profile until the user properly transfer ownership or + ;; explicitly removes all participants from the team + (when (some pos? (map :participants teams)) + (ex/raise :type :validation + :code :owner-teams-with-people + :hint "The user need to transfer ownership of owned teams." + :context {:teams (mapv :id teams)})) + + (doseq [{:keys [id]} teams] + (db/update! conn :team + {:deleted-at deleted-at} + {:id id})) + + (db/update! conn :profile + {:deleted-at deleted-at} + {:id profile-id}) + + (rph/with-transform {} (session/delete-fn cfg))))) + + +;; --- HELPERS + +(def sql:owned-teams + "with owner_teams as ( + select tpr.team_id as id + from team_profile_rel as tpr + where tpr.is_owner is true + and tpr.profile_id = ? + ) + select tpr.team_id as id, + count(tpr.profile_id) - 1 as participants + from team_profile_rel as tpr + where tpr.team_id in (select id from owner_teams) + and tpr.profile_id != ? + group by 1") + +(defn- get-owned-teams-with-participants + [conn profile-id] + (db/exec! conn [sql:owned-teams profile-id profile-id])) + +(def ^:private sql:profile-existence + "select exists (select * from profile + where email = ? + and deleted_at is null) as val") + +(defn check-profile-existence! + [conn {:keys [email] :as params}] + (let [email (str/lower email) + result (db/exec-one! conn [sql:profile-existence email])] + (when (:val result) + (ex/raise :type :validation + :code :email-already-exists)) + params)) + +(def ^:private sql:profile-by-email + "select p.* from profile as p + where p.email = ? + and (p.deleted_at is null or + p.deleted_at > now())") + +(defn get-profile-by-email + "Returns a profile looked up by email or `nil` if not match found." + [conn email] + (->> (db/exec! conn [sql:profile-by-email (str/lower email)]) + (map decode-row) + (first))) + +(defn strip-private-attrs + "Only selects a publicly visible profile attrs." + [row] + (dissoc row :password :deleted-at)) + +(defn filter-props + "Removes all namespace qualified props from `props` attr." + [props] + (into {} (filter (fn [[k _]] (simple-ident? k))) props)) + +(defn decode-row + [{:keys [props] :as row}] + (cond-> row + (db/pgobject? props "jsonb") + (assoc :props (db/decode-transit-pgobject props)))) diff --git a/backend/src/app/rpc/commands/projects.clj b/backend/src/app/rpc/commands/projects.clj new file mode 100644 index 000000000..f9555479d --- /dev/null +++ b/backend/src/app/rpc/commands/projects.clj @@ -0,0 +1,268 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.rpc.commands.projects + (:require + [app.common.spec :as us] + [app.db :as db] + [app.loggers.audit :as-alias audit] + [app.loggers.webhooks :as webhooks] + [app.rpc :as-alias rpc] + [app.rpc.commands.teams :as teams] + [app.rpc.doc :as-alias doc] + [app.rpc.helpers :as rph] + [app.rpc.permissions :as perms] + [app.rpc.quotes :as quotes] + [app.util.services :as sv] + [app.util.time :as dt] + [clojure.spec.alpha :as s])) + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) + +;; --- Check Project Permissions + +(def ^:private sql:project-permissions + "select tpr.is_owner, + tpr.is_admin, + tpr.can_edit + from team_profile_rel as tpr + inner join project as p on (p.team_id = tpr.team_id) + where p.id = ? + and tpr.profile_id = ? + union all + select ppr.is_owner, + ppr.is_admin, + ppr.can_edit + from project_profile_rel as ppr + where ppr.project_id = ? + and ppr.profile_id = ?") + +(defn- get-permissions + [conn profile-id project-id] + (let [rows (db/exec! conn [sql:project-permissions + project-id profile-id + project-id profile-id]) + is-owner (boolean (some :is-owner rows)) + is-admin (boolean (some :is-admin rows)) + can-edit (boolean (some :can-edit rows))] + (when (seq rows) + {:is-owner is-owner + :is-admin (or is-owner is-admin) + :can-edit (or is-owner is-admin can-edit) + :can-read true}))) + +(def has-edit-permissions? + (perms/make-edition-predicate-fn get-permissions)) + +(def has-read-permissions? + (perms/make-read-predicate-fn get-permissions)) + +(def check-edition-permissions! + (perms/make-check-fn has-edit-permissions?)) + +(def check-read-permissions! + (perms/make-check-fn has-read-permissions?)) + +;; --- QUERY: Get projects + +(declare get-projects) + +(s/def ::team-id ::us/uuid) +(s/def ::get-projects + (s/keys :req [::rpc/profile-id] + :req-un [::team-id])) + +(sv/defmethod ::get-projects + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}] + (with-open [conn (db/open pool)] + (teams/check-read-permissions! conn profile-id team-id) + (get-projects conn profile-id team-id))) + +(def sql:projects + "select p.*, + coalesce(tpp.is_pinned, false) as is_pinned, + (select count(*) from file as f + where f.project_id = p.id + and deleted_at is null) as count + from project as p + inner join team as t on (t.id = p.team_id) + left join team_project_profile_rel as tpp + on (tpp.project_id = p.id and + tpp.team_id = p.team_id and + tpp.profile_id = ?) + where p.team_id = ? + and p.deleted_at is null + and t.deleted_at is null + order by p.modified_at desc") + +(defn get-projects + [conn profile-id team-id] + (db/exec! conn [sql:projects profile-id team-id])) + +;; --- QUERY: Get all projects + +(declare get-all-projects) + +(s/def ::get-all-projects + (s/keys :req [::rpc/profile-id])) + +(sv/defmethod ::get-all-projects + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id]}] + (with-open [conn (db/open pool)] + (get-all-projects conn profile-id))) + +(def sql:all-projects + "select p1.*, t.name as team_name, t.is_default as is_default_team + from project as p1 + inner join team as t on (t.id = p1.team_id) + where t.id in (select team_id + from team_profile_rel as tpr + where tpr.profile_id = ? + and (tpr.can_edit = true or + tpr.is_owner = true or + tpr.is_admin = true)) + and t.deleted_at is null + and p1.deleted_at is null + union + select p2.*, t.name as team_name, t.is_default as is_default_team + from project as p2 + inner join team as t on (t.id = p2.team_id) + where p2.id in (select project_id + from project_profile_rel as ppr + where ppr.profile_id = ? + and (ppr.can_edit = true or + ppr.is_owner = true or + ppr.is_admin = true)) + and t.deleted_at is null + and p2.deleted_at is null + order by team_name, name;") + +(defn get-all-projects + [conn profile-id] + (db/exec! conn [sql:all-projects profile-id profile-id])) + + +;; --- QUERY: Get project + +(s/def ::get-project + (s/keys :req [::rpc/profile-id] + :req-un [::id])) + +(sv/defmethod ::get-project + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id id]}] + (with-open [conn (db/open pool)] + (let [project (db/get-by-id conn :project id)] + (check-read-permissions! conn profile-id id) + project))) + + + +;; --- MUTATION: Create Project + +(s/def ::create-project + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::name] + :opt-un [::id])) + +(sv/defmethod ::create-project + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + (quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team + ::quotes/profile-id profile-id + ::quotes/team-id team-id}) + + (let [params (assoc params :profile-id profile-id) + project (teams/create-project conn params)] + (teams/create-project-role conn profile-id (:id project) :owner) + (db/insert! conn :team-project-profile-rel + {:project-id (:id project) + :profile-id profile-id + :team-id team-id + :is-pinned true}) + (assoc project :is-pinned true)))) + + +;; --- MUTATION: Toggle Project Pin + +(def ^:private + sql:update-project-pin + "insert into team_project_profile_rel (team_id, project_id, profile_id, is_pinned) + values (?, ?, ?, ?) + on conflict (team_id, project_id, profile_id) + do update set is_pinned=?") + +(s/def ::is-pinned ::us/boolean) +(s/def ::project-id ::us/uuid) +(s/def ::update-project-pin + (s/keys :req [::rpc/profile-id] + :req-un [::id ::team-id ::is-pinned])) + +(sv/defmethod ::update-project-pin + {::doc/added "1.18" + ::webhooks/batch-timeout (dt/duration "5s") + ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id) + ::webhooks/event? true} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}] + (db/with-atomic [conn pool] + (check-edition-permissions! conn profile-id id) + (db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned]) + nil)) + +;; --- MUTATION: Rename Project + +(declare rename-project) + +(s/def ::rename-project + (s/keys :req [::rpc/profile-id] + :req-un [::name ::id])) + +(sv/defmethod ::rename-project + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}] + (db/with-atomic [conn pool] + (check-edition-permissions! conn profile-id id) + (let [project (db/get-by-id conn :project id ::db/for-update? true)] + (db/update! conn :project + {:name name} + {:id id}) + (rph/with-meta (rph/wrap) + {::audit/props {:team-id (:team-id project) + :prev-name (:name project)}})))) + +;; --- MUTATION: Delete Project + +(s/def ::delete-project + (s/keys :req [::rpc/profile-id] + :req-un [::id])) + +;; TODO: right now, we just don't allow delete default projects, in a +;; future we need to ensure raise a correct exception signaling that +;; this is not allowed. + +(sv/defmethod ::delete-project + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + (db/with-atomic [conn pool] + (check-edition-permissions! conn profile-id id) + (let [project (db/update! conn :project + {:deleted-at (dt/now)} + {:id id :is-default false})] + (rph/with-meta (rph/wrap) + {::audit/props {:team-id (:team-id project) + :name (:name project) + :created-at (:created-at project) + :modified-at (:modified-at project)}})))) + + diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj index 5f1ddc559..e44a21cdb 100644 --- a/backend/src/app/rpc/commands/search.clj +++ b/backend/src/app/rpc/commands/search.clj @@ -45,6 +45,7 @@ from file as f inner join projects as pr on (f.project_id = pr.id) where f.name ilike ('%' || ? || '%') + and (f.deleted_at is null or f.deleted_at > now()) order by f.created_at asc") (defn search-files @@ -64,5 +65,5 @@ (sv/defmethod ::search-files {::doc/added "1.17"} - [{:keys [pool]} {:keys [::rpc/profile-id team-id search-term]}] + [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id search-term]}] (some->> search-term (search-files pool profile-id team-id))) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 8d37d5e86..a06d77220 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -13,21 +13,21 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] - [app.emails :as eml] + [app.email :as eml] [app.loggers.audit :as audit] [app.main :as-alias main] [app.media :as media] [app.rpc :as-alias rpc] - [app.rpc.climit :as climit] + [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.permissions :as perms] - [app.rpc.queries.profile :as profile] [app.rpc.quotes :as quotes] [app.storage :as sto] [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as-alias wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] [promesa.core :as p] @@ -62,12 +62,18 @@ :can-edit (or is-owner is-admin can-edit) :can-read true}))) +(def has-admin-permissions? + (perms/make-admin-predicate-fn get-permissions)) + (def has-edit-permissions? (perms/make-edition-predicate-fn get-permissions)) (def has-read-permissions? (perms/make-read-predicate-fn get-permissions)) +(def check-admin-permissions! + (perms/make-check-fn has-admin-permissions?)) + (def check-edition-permissions! (perms/make-check-fn has-edit-permissions?)) @@ -83,7 +89,7 @@ (sv/defmethod ::get-teams {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (with-open [conn (db/open pool)] (retrieve-teams conn profile-id))) @@ -114,8 +120,8 @@ (defn retrieve-teams [conn profile-id] - (let [defaults (profile/retrieve-additional-data conn profile-id)] - (->> (db/exec! conn [sql:teams (:default-team-id defaults) profile-id]) + (let [profile (profile/get-profile conn profile-id)] + (->> (db/exec! conn [sql:teams (:default-team-id profile) profile-id]) (mapv process-permissions)))) ;; --- Query: Team (by ID) @@ -128,20 +134,21 @@ (sv/defmethod ::get-team {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}] (with-open [conn (db/open pool)] (retrieve-team conn profile-id id))) (defn retrieve-team [conn profile-id team-id] - (let [defaults (profile/retrieve-additional-data conn profile-id) - sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?") - result (db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id])] + (let [profile (profile/get-profile conn profile-id) + sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?") + result (db/exec-one! conn [sql (:default-team-id profile) profile-id team-id])] + (when-not result (ex/raise :type :not-found :code :team-does-not-exist)) - (process-permissions result))) + (process-permissions result))) ;; --- Query: Team Members @@ -439,7 +446,7 @@ (sv/defmethod ::leave-team {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (leave-team conn (assoc params :profile-id profile-id)))) @@ -455,7 +462,7 @@ (sv/defmethod ::delete-team {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id id)] (when-not (:is-owner perms) @@ -551,7 +558,7 @@ (sv/defmethod ::delete-team-member {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] (when-not (or (:is-owner perms) @@ -570,7 +577,7 @@ ;; --- Mutation: Update Team Photo -(declare ^:private upload-photo) +(declare upload-photo) (declare ^:private update-team-photo) (s/def ::file ::media/upload) @@ -583,57 +590,28 @@ [cfg {:keys [::rpc/profile-id file] :as params}] ;; Validate incoming mime type (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) - (let [cfg (update cfg :storage media/configure-assets-storage)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (update-team-photo cfg (assoc params :profile-id profile-id)))) (defn update-team-photo - [{:keys [pool storage executor] :as cfg} {:keys [profile-id team-id] :as params}] + [{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id team-id] :as params}] (p/let [team (px/with-dispatch executor (retrieve-team pool profile-id team-id)) - photo (upload-photo cfg params)] + photo (profile/upload-photo cfg params)] - ;; Mark object as touched for make it ellegible for tentative - ;; garbage collection. - (when-let [id (:photo-id team)] - (sto/touch-object! storage id)) + (db/with-atomic [conn pool] + (check-admin-permissions! conn profile-id team-id) + ;; Mark object as touched for make it ellegible for tentative + ;; garbage collection. + (when-let [id (:photo-id team)] + (sto/touch-object! storage id)) - ;; Save new photo - (db/update! pool :team - {:photo-id (:id photo)} - {:id team-id}) + ;; Save new photo + (db/update! pool :team + {:photo-id (:id photo)} + {:id team-id}) - (assoc team :photo-id (:id photo)))) - -(defn upload-photo - [{:keys [storage executor climit] :as cfg} {:keys [file]}] - (letfn [(get-info [content] - (climit/with-dispatch (:process-image climit) - (media/run {:cmd :info :input content}))) - - (generate-thumbnail [info] - (climit/with-dispatch (:process-image climit) - (media/run {:cmd :profile-thumbnail - :format :jpeg - :quality 85 - :width 256 - :height 256 - :input info}))) - - ;; Function responsible of calculating cryptographyc hash of - ;; the provided data. - (calculate-hash [data] - (px/with-dispatch executor - (sto/calculate-hash data)))] - - (p/let [info (get-info file) - thumb (generate-thumbnail info) - hash (calculate-hash (:data thumb)) - content (-> (sto/content (:data thumb) (:size thumb)) - (sto/wrap-with-hash hash))] - (sto/put-object! storage {::sto/content content - ::sto/deduplicate? true - :bucket "profile" - :content-type (:mtype thumb)})))) + (assoc team :photo-id (:id photo))))) ;; --- Mutation: Create Team Invitation @@ -664,7 +642,7 @@ (defn- create-invitation [{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}] - (let [member (profile/retrieve-profile-data-by-email conn email)] + (let [member (profile/get-profile-by-email conn email)] (when (and member (not (eml/allow-send-emails? conn member))) (ex/raise :type :validation @@ -753,13 +731,18 @@ "A rpc call that allow to send a single or multiple invitations to join the team." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id) profile (db/get-by-id conn :profile profile-id) team (db/get-by-id conn :team team-id) - emails (cond-> (or emails #{}) (string? email) (conj email))] + ;; Members emails. We don't re-send inviation to already existing members + member? (into #{} + (map :email) + (db/exec! conn [sql:team-members team-id])) + + emails (cond-> (or emails #{}) (string? email) (conj email))] (run! (partial quotes/check-quote! conn) (list {::quotes/id ::quotes/invitations-per-team @@ -783,6 +766,7 @@ (let [cfg (assoc cfg ::db/conn conn) invitations (->> emails + (remove member?) (map (fn [email] {:email (str/lower email) :team team @@ -802,7 +786,7 @@ (sv/defmethod ::create-team-with-invitations {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}] (db/with-atomic [conn pool] (let [params (assoc params :profile-id profile-id) team (create-team conn params) @@ -855,7 +839,7 @@ {:team-id team-id :email-to (str/lower email)}) (update :role keyword)) - member (profile/retrieve-profile-data-by-email pool (:email-to invit)) + member (profile/get-profile-by-email pool (:email-to invit)) token (create-invitation-token cfg {:team-id (:team-id invit) :profile-id profile-id :valid-until (:valid-until invit) @@ -872,7 +856,7 @@ (sv/defmethod ::update-team-invitation-role {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] @@ -893,7 +877,7 @@ (sv/defmethod ::delete-team-invitation {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 6949b2f65..6f800515a 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -11,11 +11,12 @@ [app.db :as db] [app.http.session :as session] [app.loggers.audit :as audit] + [app.main :as-alias main] [app.rpc :as-alias rpc] + [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.profile :as profile] [app.rpc.quotes :as quotes] [app.tokens :as tokens] [app.tokens.spec.team-invitation :as-alias spec.team-invitation] @@ -34,15 +35,15 @@ (sv/defmethod ::verify-token {::rpc/auth false ::doc/added "1.15"} - [{:keys [pool sprops] :as cfg} {:keys [token] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [token] :as params}] (db/with-atomic [conn pool] - (let [claims (tokens/verify sprops {:token token}) + (let [claims (tokens/verify (::main/props cfg) {:token token}) cfg (assoc cfg :conn conn)] (process-token cfg params claims)))) (defmethod process-token :change-email [{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}] - (when (profile/retrieve-profile-data-by-email conn email) + (when (profile/get-profile-by-email conn email) (ex/raise :type :validation :code :email-already-exists)) @@ -56,8 +57,8 @@ ::audit/profile-id profile-id})) (defmethod process-token :verify-email - [{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}] - (let [profile (profile/retrieve-profile conn profile-id) + [{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}] + (let [profile (profile/get-profile conn profile-id) claims (assoc claims :profile profile)] (when-not (:is-active profile) @@ -71,14 +72,14 @@ {:id (:id profile)})) (-> claims - (rph/with-transform (session/create-fn session profile-id)) + (rph/with-transform (session/create-fn cfg profile-id)) (rph/with-meta {::audit/name "verify-profile-email" ::audit/props (audit/profile->props profile) ::audit/profile-id (:id profile)})))) (defmethod process-token :auth [{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}] - (let [profile (profile/retrieve-profile conn profile-id)] + (let [profile (profile/get-profile conn profile-id)] (assoc claims :profile profile))) ;; --- Team Invitation diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index c19e84824..f61df10c6 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -13,11 +13,10 @@ [app.rpc.commands.files :as files] [app.rpc.cond :as-alias cond] [app.rpc.doc :as-alias doc] - [app.rpc.queries.share-link :as slnk] [app.util.services :as sv] [clojure.spec.alpha :as s])) -;; --- Query: View Only Bundle +;; --- QUERY: View Only Bundle (defn- get-project [conn id] @@ -31,7 +30,15 @@ users (comments/get-file-comments-users conn file-id profile-id) links (->> (db/query conn :share-link {:file-id file-id}) - (mapv slnk/decode-share-link-row)) + (mapv (fn [row] + (-> row + (update :pages db/decode-pgarray #{}) + ;; NOTE: the flags are deprecated but are still present + ;; on the table on old rows. The flags are pgarray and + ;; for avoid decoding it (because they are no longer used + ;; on frontend) we just dissoc the column attribute from + ;; row. + (dissoc :flags))))) fonts (db/query conn :team-font-variant {:team-id (:team-id project) @@ -84,6 +91,6 @@ ::cond/key-fn files/get-file-etag ::cond/reuse-key? true ::doc/added "1.17"} - [{:keys [pool]} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}] (with-open [conn (db/open pool)] (get-view-only-bundle conn (assoc params :profile-id profile-id)))) diff --git a/backend/src/app/rpc/commands/webhooks.clj b/backend/src/app/rpc/commands/webhooks.clj index cc7900f03..0d072c92a 100644 --- a/backend/src/app/rpc/commands/webhooks.clj +++ b/backend/src/app/rpc/commands/webhooks.clj @@ -148,7 +148,7 @@ from webhook where team_id = ? order by uri") (sv/defmethod ::get-webhooks - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id team-id) (->> (db/exec! conn [sql:get-webhooks team-id]) diff --git a/backend/src/app/rpc/doc.clj b/backend/src/app/rpc/doc.clj index 112ecfad0..32889c9b7 100644 --- a/backend/src/app/rpc/doc.clj +++ b/backend/src/app/rpc/doc.clj @@ -70,6 +70,8 @@ (respond (yrs/response 404))))) +(s/def ::routes vector?) + (defmethod ig/pre-init-spec ::routes [_] (s/keys :req-un [::rpc/methods])) diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj deleted file mode 100644 index 635f92920..000000000 --- a/backend/src/app/rpc/mutations/files.clj +++ /dev/null @@ -1,239 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.rpc.mutations.files - (:require - [app.common.exceptions :as ex] - [app.common.logging :as l] - [app.common.spec :as us] - [app.db :as db] - [app.loggers.audit :as-alias audit] - [app.rpc.climit :as-alias climit] - [app.rpc.commands.files :as cmd.files] - [app.rpc.commands.files.create :as cmd.files.create] - [app.rpc.commands.files.temp :as cmd.files.temp] - [app.rpc.commands.files.update :as cmd.files.update] - [app.rpc.doc :as-alias doc] - [app.rpc.helpers :as rph] - [app.rpc.queries.projects :as proj] - [app.util.services :as sv] - [app.util.time :as dt] - [clojure.spec.alpha :as s])) - -;; --- Mutation: Create File - -(s/def ::create-file ::cmd.files.create/create-file) - -(sv/defmethod ::create-file - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id project-id features components-v2] :as params}] - (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id project-id) - (let [team-id (cmd.files/get-team-id conn project-id) - features (cond-> (or features #{}) - ;; BACKWARD COMPATIBILITY with the components-v2 param - components-v2 (conj "components/v2")) - params (assoc params :features features)] - (-> (cmd.files.create/create-file conn params) - (vary-meta assoc ::audit/props {:team-id team-id}))))) - - -;; --- Mutation: Rename File - -(s/def ::rename-file ::cmd.files/rename-file) - -(sv/defmethod ::rename-file - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id id) - (cmd.files/rename-file conn params))) - - -;; --- Mutation: Set File shared - -(s/def ::set-file-shared ::cmd.files/set-file-shared) - -(sv/defmethod ::set-file-shared - {::doc/added "1.2" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id is-shared] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id id) - (when-not is-shared - (cmd.files/absorb-library conn params) - (cmd.files/unlink-files conn params)) - (cmd.files/set-file-shared conn params))) - -;; --- Mutation: Delete File - -(s/def ::delete-file ::cmd.files/delete-file) - -(sv/defmethod ::delete-file - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id id) - (cmd.files/absorb-library conn params) - (cmd.files/mark-file-deleted conn params) - nil)) - -;; --- Mutation: Link file to library - -(s/def ::link-file-to-library ::cmd.files/link-file-to-library) - -(sv/defmethod ::link-file-to-library - {::doc/added "1.3" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}] - (when (= file-id library-id) - (ex/raise :type :validation - :code :invalid-library - :hint "A file cannot be linked to itself")) - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/check-edition-permissions! conn profile-id library-id) - (cmd.files/link-file-to-library conn params))) - -;; --- Mutation: Unlink file from library - -(s/def ::unlink-file-from-library ::cmd.files/unlink-file-from-library) - -(sv/defmethod ::unlink-file-from-library - {::doc/added "1.3" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/unlink-file-from-library conn params))) - - -;; --- Mutation: Update synchronization status of a link - -(s/def ::update-sync ::cmd.files/update-file-library-sync-status) - -(sv/defmethod ::update-sync - {::doc/added "1.10" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/update-sync conn params))) - - -;; --- Mutation: Ignore updates in linked files - -(declare ignore-sync) - -(s/def ::ignore-sync ::cmd.files/ignore-file-library-sync-status) - -(sv/defmethod ::ignore-sync - {::doc/added "1.10" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/ignore-sync conn params))) - - -;; --- MUTATION: update-file - -(s/def ::components-v2 ::us/boolean) -(s/def ::update-file - (s/and ::cmd.files.update/update-file - (s/keys :opt-un [::components-v2]))) - -(sv/defmethod ::update-file - {::climit/queue :update-file - ::climit/key-fn :id - ::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id features components-v2] :as params}] - (db/with-atomic [conn pool] - (db/xact-lock! conn id) - (cmd.files/check-edition-permissions! conn profile-id id) - - (let [;; BACKWARD COMPATIBILITY with the components-v2 parameter - features (cond-> (or features #{}) - components-v2 (conj "components/v2")) - tpoint (dt/tpoint) - params (assoc params :features features) - cfg (assoc cfg :conn conn)] - - (-> (cmd.files.update/update-file cfg params) - (rph/with-defer #(let [elapsed (tpoint)] - (l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))) - -;; --- Mutation: upsert object thumbnail - -(s/def ::upsert-file-object-thumbnail ::cmd.files/upsert-file-object-thumbnail) - -(sv/defmethod ::upsert-file-object-thumbnail - {::doc/added "1.13" - ::doc/deprecated "1.17" - ::audit/skip true} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/upsert-file-object-thumbnail! conn params) - nil)) - - -;; --- Mutation: upsert file thumbnail - -(s/def ::upsert-file-thumbnail ::cmd.files/upsert-file-thumbnail) - -(sv/defmethod ::upsert-file-thumbnail - "Creates or updates the file thumbnail. Mainly used for paint the - grid thumbnails." - {::doc/added "1.13" - ::doc/deprecated "1.17" - ::audit/skip true} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/upsert-file-thumbnail conn params) - nil)) - - -;; --- MUTATION COMMAND: create-temp-file - -(s/def ::create-temp-file ::cmd.files.temp/create-temp-file) - -(sv/defmethod ::create-temp-file - {::doc/added "1.7" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] - (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id project-id) - (cmd.files.create/create-file conn (assoc params :deleted-at (dt/in-future {:days 1}))))) - -;; --- MUTATION COMMAND: update-temp-file - -(s/def ::update-temp-file ::cmd.files.temp/update-temp-file) - -(sv/defmethod ::update-temp-file - {::doc/added "1.7" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.files.temp/update-temp-file conn params) - nil)) - -;; --- MUTATION COMMAND: persist-temp-file - -(s/def ::persist-temp-file ::cmd.files.temp/persist-temp-file) - -(sv/defmethod ::persist-temp-file - {::doc/added "1.7" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id id) - (cmd.files.temp/persist-temp-file conn params))) diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj index e354074f8..a53bd9fd1 100644 --- a/backend/src/app/rpc/mutations/fonts.clj +++ b/backend/src/app/rpc/mutations/fonts.clj @@ -6,15 +6,12 @@ (ns app.rpc.mutations.fonts (:require - [app.common.data :as d] - [app.common.exceptions :as ex] [app.common.spec :as us] - [app.common.uuid :as uuid] [app.db :as db] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] [app.media :as media] - [app.rpc.climit :as-alias climit] + [app.rpc.commands.fonts :as fonts] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] @@ -22,9 +19,7 @@ [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s] - [promesa.core :as p] - [promesa.exec :as px])) + [clojure.spec.alpha :as s])) (declare create-font-variant) @@ -44,82 +39,19 @@ (s/keys :req-un [::profile-id ::team-id ::data ::font-id ::font-family ::font-weight ::font-style])) +(declare create-font-variant) + (sv/defmethod ::create-font-variant {::doc/added "1.3" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}] - (let [cfg (update cfg :storage media/configure-assets-storage)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (teams/check-edition-permissions! pool profile-id team-id) (quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team ::quotes/profile-id profile-id ::quotes/team-id team-id}) - (create-font-variant cfg params))) - -(defn create-font-variant - [{:keys [storage pool executor climit] :as cfg} {:keys [data] :as params}] - (letfn [(generate-fonts [data] - (climit/with-dispatch (:process-font climit) - (media/run {:cmd :generate-fonts :input data}))) - - ;; Function responsible of calculating cryptographyc hash of - ;; the provided data. - (calculate-hash [data] - (px/with-dispatch executor - (sto/calculate-hash data))) - - (validate-data [data] - (when (and (not (contains? data "font/otf")) - (not (contains? data "font/ttf")) - (not (contains? data "font/woff")) - (not (contains? data "font/woff2"))) - (ex/raise :type :validation - :code :invalid-font-upload)) - data) - - (persist-font-object [data mtype] - (when-let [resource (get data mtype)] - (p/let [hash (calculate-hash resource) - content (-> (sto/content resource) - (sto/wrap-with-hash hash))] - (sto/put-object! storage {::sto/content content - ::sto/touched-at (dt/now) - ::sto/deduplicate? true - :content-type mtype - :bucket "team-font-variant"})))) - - (persist-fonts [data] - (p/let [otf (persist-font-object data "font/otf") - ttf (persist-font-object data "font/ttf") - woff1 (persist-font-object data "font/woff") - woff2 (persist-font-object data "font/woff2")] - - (d/without-nils - {:otf otf - :ttf ttf - :woff1 woff1 - :woff2 woff2}))) - - (insert-into-db [{:keys [woff1 woff2 otf ttf]}] - (db/insert! pool :team-font-variant - {:id (uuid/next) - :team-id (:team-id params) - :font-id (:font-id params) - :font-family (:font-family params) - :font-weight (:font-weight params) - :font-style (:font-style params) - :woff1-file-id (:id woff1) - :woff2-file-id (:id woff2) - :otf-file-id (:id otf) - :ttf-file-id (:id ttf)})) - ] - - (->> (generate-fonts data) - (p/fmap validate-data) - (p/mcat executor persist-fonts) - (p/fmap executor insert-into-db) - (p/fmap (fn [result] - (let [params (update params :data (comp vec keys))] - (rph/with-meta result {::audit/replace-props params}))))))) + (fonts/create-font-variant cfg params))) ;; --- UPDATE FONT FAMILY @@ -128,6 +60,7 @@ (sv/defmethod ::update-font {::doc/added "1.3" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [team-id profile-id id name] :as params}] (db/with-atomic [conn pool] @@ -149,6 +82,7 @@ (sv/defmethod ::delete-font {::doc/added "1.3" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}] (db/with-atomic [conn pool] @@ -169,6 +103,7 @@ (sv/defmethod ::delete-font-variant {::doc/added "1.3" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}] (db/with-atomic [conn pool] diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index f66739549..c547f22e4 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -11,6 +11,7 @@ [app.rpc.commands.files :as files] [app.rpc.commands.media :as cmd.media] [app.rpc.doc :as-alias doc] + [app.storage :as-alias sto] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -20,9 +21,9 @@ (sv/defmethod ::upload-file-media-object {::doc/added "1.2" - ::doc/deprecated "1.17"} + ::doc/deprecated "1.18"} [{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}] - (let [cfg (update cfg :storage media/configure-assets-storage)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) (media/validate-media-type! content) (cmd.media/validate-content-size! content) @@ -34,9 +35,9 @@ (sv/defmethod ::create-file-media-object-from-url {::doc/added "1.3" - ::doc/deprecated "1.17"} + ::doc/deprecated "1.18"} [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (let [cfg (update cfg :storage media/configure-assets-storage)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) (#'cmd.media/create-file-media-object-from-url cfg params))) @@ -46,7 +47,7 @@ (sv/defmethod ::clone-file-media-object {::doc/added "1.2" - ::doc/deprecated "1.17"} + ::doc/deprecated "1.18"} [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 8112bb661..89b59083a 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -6,31 +6,23 @@ (ns app.rpc.mutations.profile (:require - [app.auth :as auth] [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.config :as cf] [app.db :as db] - [app.emails :as eml] [app.http.session :as session] [app.loggers.audit :as audit] [app.media :as media] - [app.rpc :as-alias rpc] [app.rpc.climit :as-alias climit] - [app.rpc.commands.auth :as cmd.auth] - [app.rpc.commands.teams :as teams] + [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.profile :as profile] [app.storage :as sto] - [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s] - [cuerdas.core :as str] - [promesa.core :as p] - [promesa.exec :as px])) + [cuerdas.core :as str])) ;; --- Helpers & Specs @@ -40,7 +32,7 @@ (s/def ::path ::us/string) (s/def ::profile-id ::us/uuid) (s/def ::password ::us/not-empty-string) -(s/def ::old-password ::us/not-empty-string) +(s/def ::old-password (s/nilable ::us/string)) (s/def ::theme ::us/string) ;; --- MUTATION: Update Profile (own) @@ -50,14 +42,15 @@ :opt-un [::lang ::theme])) (sv/defmethod ::update-profile - {::doc/added "1.0"} - [{:keys [pool] :as cfg} {:keys [profile-id fullname lang theme] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id fullname lang theme] :as params}] (db/with-atomic [conn pool] ;; NOTE: we need to retrieve the profile independently if we use ;; it or not for explicit locking and avoid concurrent updates of ;; the same row/object. - (let [profile (-> (db/get-by-id conn :profile profile-id {:for-update true}) - (profile/decode-profile-row)) + (let [profile (-> (db/get-by-id conn :profile profile-id ::db/for-update? true) + (profile/decode-row)) ;; Update the profile map with direct params profile (-> profile @@ -74,161 +67,68 @@ {:id profile-id}) (-> profile - profile/strip-private-attrs - d/without-nils + (profile/strip-private-attrs) + (d/without-nils) (rph/with-meta {::audit/props (audit/profile->props profile)}))))) ;; --- MUTATION: Update Password -(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])) (sv/defmethod ::update-profile-password - {::climit/queue :auth} - [{:keys [pool] :as cfg} {:keys [password] :as params}] + {::climit/queue :auth + ::doc/added "1.0" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [password] :as params}] (db/with-atomic [conn pool] - (let [profile (validate-password! conn params) - session-id (::rpc/session-id params)] + (let [profile (#'profile/validate-password! conn params) + session-id (::session/id params)] (when (= (str/lower (:email profile)) (str/lower (:password params))) (ex/raise :type :validation :code :email-as-password :hint "you can't use your email as password")) - (update-profile-password! conn (assoc profile :password password)) - (invalidate-profile-session! conn (:id profile) session-id) + (profile/update-profile-password! conn (assoc profile :password password)) + (#'profile/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)] - (when-not (:valid (auth/verify-password old-password (:password profile))) - (ex/raise :type :validation - :code :old-password-not-match)) - profile)) - -(defn update-profile-password! - [conn {:keys [id password] :as profile}] - (db/update! conn :profile - {:password (auth/derive-password password)} - {:id id})) ;; --- MUTATION: Update Photo -(declare update-profile-photo) - (s/def ::file ::media/upload) (s/def ::update-profile-photo (s/keys :req-un [::profile-id ::file])) (sv/defmethod ::update-profile-photo + {::doc/added "1.0" + ::doc/deprecated "1.18"} [cfg {:keys [file] :as params}] ;; Validate incoming mime type (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) - (let [cfg (update cfg :storage media/configure-assets-storage)] - (update-profile-photo cfg params))) - -(defn update-profile-photo - [{:keys [pool storage executor] :as cfg} {:keys [profile-id file] :as params}] - (p/let [profile (px/with-dispatch executor - (db/get-by-id pool :profile profile-id)) - photo (teams/upload-photo cfg params)] - - ;; Schedule deletion of old photo - (when-let [id (:photo-id profile)] - (sto/touch-object! storage id)) - - ;; Save new photo - (db/update! pool :profile - {:photo-id (:id photo)} - {:id profile-id}) - - (-> (rph/wrap) - (rph/with-meta {::audit/replace-props - {:file-name (:filename file) - :file-size (:size file) - :file-path (str (:path file)) - :file-mtype (:mtype file)}})))) + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] + (profile/update-profile-photo cfg params))) ;; --- MUTATION: Request Email Change -(declare request-email-change) -(declare change-email-immediately) - (s/def ::request-email-change (s/keys :req-un [::email])) (sv/defmethod ::request-email-change - [{:keys [pool] :as cfg} {:keys [profile-id email] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id email] :as params}] (db/with-atomic [conn pool] (let [profile (db/get-by-id conn :profile profile-id) - cfg (assoc cfg :conn conn) + cfg (assoc cfg ::profile/conn conn) params (assoc params :profile profile :email (str/lower email))] + (if (contains? cf/flags :smtp) - (request-email-change cfg params) - (change-email-immediately cfg params))))) - -(defn- change-email-immediately - [{:keys [conn]} {:keys [profile email] :as params}] - (when (not= email (:email profile)) - (cmd.auth/check-profile-existence! conn params)) - (db/update! conn :profile - {:email email} - {:id (:id profile)}) - {:changed true}) - -(defn- request-email-change - [{:keys [conn sprops] :as cfg} {:keys [profile email] :as params}] - (let [token (tokens/generate sprops - {:iss :change-email - :exp (dt/in-future "15m") - :profile-id (:id profile) - :email email}) - ptoken (tokens/generate sprops - {:iss :profile-identity - :profile-id (:id profile) - :exp (dt/in-future {:days 30})})] - - (when (not= email (:email profile)) - (cmd.auth/check-profile-existence! conn params)) - - (when-not (eml/allow-send-emails? conn profile) - (ex/raise :type :validation - :code :profile-is-muted - :hint "looks like the profile has reported repeatedly as spam or has permanent bounces.")) - - (when (eml/has-bounce-reports? conn email) - (ex/raise :type :validation - :code :email-has-permanent-bounces - :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) - - (eml/send! {::eml/conn conn - ::eml/factory eml/change-email - :public-uri (:public-uri cfg) - :to (:email profile) - :name (:fullname profile) - :pending-email email - :token token - :extra-data ptoken}) - nil)) - - -(defn select-profile-for-update - [conn id] - (db/get-by-id conn :profile id {:for-update true})) - + (#'profile/request-email-change! cfg params) + (#'profile/change-email-immediately! cfg params))))) ;; --- MUTATION: Update Profile Props @@ -237,9 +137,11 @@ (s/keys :req-un [::profile-id ::props])) (sv/defmethod ::update-profile-props - [{:keys [pool] :as cfg} {:keys [profile-id props]}] + {::doc/added "1.0" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id props]}] (db/with-atomic [conn pool] - (let [profile (profile/retrieve-profile-data conn profile-id) + (let [profile (profile/get-profile conn profile-id ::db/for-update? true) props (reduce-kv (fn [props k v] ;; We don't accept namespaced keys (if (simple-ident? k) @@ -254,22 +156,20 @@ {:props (db/tjson props)} {:id profile-id}) - (profile/filter-profile-props props)))) + (profile/filter-props props)))) ;; --- MUTATION: Delete Profile -(declare get-owned-teams-with-participants) -(declare check-can-delete-profile!) -(declare mark-profile-as-deleted!) - (s/def ::delete-profile (s/keys :req-un [::profile-id])) (sv/defmethod ::delete-profile - [{:keys [pool session] :as cfg} {:keys [profile-id] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id] :as params}] (db/with-atomic [conn pool] - (let [teams (get-owned-teams-with-participants conn profile-id) + (let [teams (#'profile/get-owned-teams-with-participants conn profile-id) deleted-at (dt/now)] ;; If we found owned teams with participants, we don't allow @@ -290,22 +190,4 @@ {:deleted-at deleted-at} {:id profile-id}) - (rph/with-transform {} (session/delete-fn session))))) - -(def sql:owned-teams - "with owner_teams as ( - select tpr.team_id as id - from team_profile_rel as tpr - where tpr.is_owner is true - and tpr.profile_id = ? - ) - select tpr.team_id as id, - count(tpr.profile_id) - 1 as participants - from team_profile_rel as tpr - where tpr.team_id in (select id from owner_teams) - and tpr.profile_id != ? - group by 1") - -(defn- get-owned-teams-with-participants - [conn profile-id] - (db/exec! conn [sql:owned-teams profile-id profile-id])) + (rph/with-transform {} (session/delete-fn cfg))))) diff --git a/backend/src/app/rpc/mutations/projects.clj b/backend/src/app/rpc/mutations/projects.clj index 1a49d4fc1..a400b9b6d 100644 --- a/backend/src/app/rpc/mutations/projects.clj +++ b/backend/src/app/rpc/mutations/projects.clj @@ -10,10 +10,10 @@ [app.db :as db] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] + [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.projects :as proj] [app.rpc.quotes :as quotes] [app.util.services :as sv] [app.util.time :as dt] @@ -34,6 +34,7 @@ (sv/defmethod ::create-project {::doc/added "1.0" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] (db/with-atomic [conn pool] @@ -70,12 +71,13 @@ (sv/defmethod ::update-project-pin {::doc/added "1.0" + ::doc/deprecated "1.18" ::webhooks/batch-timeout (dt/duration "5s") ::webhooks/batch-key :id ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id profile-id team-id is-pinned] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id id) + (projects/check-edition-permissions! conn profile-id id) (db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned]) nil)) @@ -88,10 +90,11 @@ (sv/defmethod ::rename-project {::doc/added "1.0" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id profile-id name] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id id) + (projects/check-edition-permissions! conn profile-id id) (let [project (db/get-by-id conn :project id)] (db/update! conn :project {:name name} @@ -112,10 +115,11 @@ (sv/defmethod ::delete-project {::doc/added "1.0" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id id) + (projects/check-edition-permissions! conn profile-id id) (let [project (db/update! conn :project {:deleted-at (dt/now)} {:id id :is-default false})] diff --git a/backend/src/app/rpc/mutations/share_link.clj b/backend/src/app/rpc/mutations/share_link.clj index 9e8ab45d6..365aa77bd 100644 --- a/backend/src/app/rpc/mutations/share_link.clj +++ b/backend/src/app/rpc/mutations/share_link.clj @@ -11,6 +11,7 @@ [app.common.uuid :as uuid] [app.db :as db] [app.rpc.commands.files :as files] + [app.rpc.doc :as-alias doc] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -35,8 +36,9 @@ Share links are resources that allows external users access to specific pages of a file with specific permissions (who-comment and who-inspect)." - - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + {::doc/added "1.5" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) (create-share-link conn params))) @@ -51,18 +53,17 @@ :who-inspect who-inspect :pages pages :owner-id profile-id})] - (-> slink - (update :pages db/decode-pgarray #{})))) + (update slink :pages db/decode-pgarray #{}))) ;; --- Mutation: Delete Share Link -(declare delete-share-link) - (s/def ::delete-share-link (s/keys :req-un [::profile-id ::id])) (sv/defmethod ::delete-share-link - [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] + {::doc/added "1.5" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id id] :as params}] (db/with-atomic [conn pool] (let [slink (db/get-by-id conn :share-link id)] (files/check-edition-permissions! conn profile-id (:file-id slink)) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj deleted file mode 100644 index 650ac1884..000000000 --- a/backend/src/app/rpc/mutations/teams.clj +++ /dev/null @@ -1,240 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.rpc.mutations.teams - (:require - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.db :as db] - [app.emails :as eml] - [app.loggers.audit :as audit] - [app.media :as media] - [app.rpc.commands.teams :as cmd.teams] - [app.rpc.doc :as-alias doc] - [app.rpc.helpers :as rph] - [app.util.services :as sv] - [app.util.time :as dt] - [clojure.spec.alpha :as s] - [cuerdas.core :as str])) - -;; --- Helpers & Specs - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) -(s/def ::profile-id ::us/uuid) - -;; --- Mutation: Create Team - -(s/def ::create-team ::cmd.teams/create-team) - -(sv/defmethod ::create-team - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.teams/create-team conn params))) - -;; --- Mutation: Update Team - -(s/def ::update-team ::cmd.teams/update-team) - -(sv/defmethod ::update-team - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [id name profile-id] :as params}] - (db/with-atomic [conn pool] - (cmd.teams/check-edition-permissions! conn profile-id id) - (db/update! conn :team - {:name name} - {:id id}) - nil)) - -;; --- Mutation: Leave Team - -(s/def ::leave-team ::cmd.teams/leave-team) - -(sv/defmethod ::leave-team - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.teams/leave-team conn params))) - -;; --- Mutation: Delete Team - -(s/def ::delete-team ::cmd.teams/delete-team) - -(sv/defmethod ::delete-team - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [id profile-id] :as params}] - (db/with-atomic [conn pool] - (let [perms (cmd.teams/get-permissions conn profile-id id)] - (when-not (:is-owner perms) - (ex/raise :type :validation - :code :only-owner-can-delete-team)) - (db/update! conn :team - {:deleted-at (dt/now)} - {:id id :is-default false}) - nil))) - - -;; --- Mutation: Team Update Role - -(s/def ::update-team-member-role ::cmd.teams/update-team-member-role) - -(sv/defmethod ::update-team-member-role - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.teams/update-team-member-role conn params))) - -;; --- Mutation: Delete Team Member - -(s/def ::delete-team-member ::cmd.teams/delete-team-member) - -(sv/defmethod ::delete-team-member - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [team-id profile-id member-id] :as params}] - (db/with-atomic [conn pool] - (let [perms (cmd.teams/get-permissions conn profile-id team-id)] - (when-not (or (:is-owner perms) - (:is-admin perms)) - (ex/raise :type :validation - :code :insufficient-permissions)) - (when (= member-id profile-id) - (ex/raise :type :validation - :code :cant-remove-yourself)) - - (db/delete! conn :team-profile-rel {:profile-id member-id - :team-id team-id}) - - nil))) - -;; --- Mutation: Update Team Photo - -(s/def ::update-team-photo ::cmd.teams/update-team-photo) - -(sv/defmethod ::update-team-photo - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [cfg {:keys [file] :as params}] - ;; Validate incoming mime type - (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) - (let [cfg (update cfg :storage media/configure-assets-storage)] - (cmd.teams/update-team-photo cfg params))) - -;; --- Mutation: Invite Member - -(s/def ::invite-team-member ::cmd.teams/create-team-invitations) - -(sv/defmethod ::invite-team-member - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}] - (db/with-atomic [conn pool] - (let [perms (cmd.teams/get-permissions conn profile-id team-id) - profile (db/get-by-id conn :profile profile-id) - team (db/get-by-id conn :team team-id) - emails (cond-> (or emails #{}) (string? email) (conj email))] - - (when-not (:is-admin perms) - (ex/raise :type :validation - :code :insufficient-permissions)) - - ;; First check if the current profile is allowed to send emails. - (when-not (eml/allow-send-emails? conn profile) - (ex/raise :type :validation - :code :profile-is-muted - :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) - - (let [cfg (assoc cfg ::cmd.teams/conn conn) - invitations (->> emails - (map (fn [email] - {:email (str/lower email) - :team team - :profile profile - :role role})) - (map (partial #'cmd.teams/create-invitation cfg)))] - (with-meta (vec invitations) - {::audit/props {:invitations (count invitations)}}))))) - -;; --- Mutation: Create Team & Invite Members - -(s/def ::create-team-and-invite-members ::cmd.teams/create-team-with-invitations) - -(sv/defmethod ::create-team-and-invite-members - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [profile-id emails role] :as params}] - (db/with-atomic [conn pool] - (let [team (cmd.teams/create-team conn params) - profile (db/get-by-id conn :profile profile-id) - cfg (assoc cfg ::cmd.teams/conn conn)] - - ;; Create invitations for all provided emails. - (->> emails - (map (fn [email] - {:team team - :profile profile - :email (str/lower email) - :role role})) - (run! (partial #'cmd.teams/create-invitation cfg))) - - (-> team - (vary-meta assoc ::audit/props {:invitations (count emails)}) - (rph/with-defer - #(when-let [collector (::audit/collector cfg)] - (audit/submit! collector - {:type "mutation" - :name "invite-team-member" - :profile-id profile-id - :props {:emails emails - :role role - :profile-id profile-id - :invitations (count emails)}}))))))) - -;; --- Mutation: Update invitation role - -(s/def ::update-team-invitation-role - (s/keys :req-un [::profile-id ::team-id ::email ::role])) - -(sv/defmethod ::update-team-invitation-role - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email role] :as params}] - (db/with-atomic [conn pool] - (let [perms (cmd.teams/get-permissions conn profile-id team-id)] - - (when-not (:is-admin perms) - (ex/raise :type :validation - :code :insufficient-permissions)) - - (db/update! conn :team-invitation - {:role (name role) :updated-at (dt/now)} - {:team-id team-id :email-to (str/lower email)}) - nil))) - -;; --- Mutation: Delete invitation - -(s/def ::delete-team-invitation ::cmd.teams/delete-team-invitation) - -(sv/defmethod ::delete-team-invitation - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email] :as params}] - (db/with-atomic [conn pool] - (let [perms (cmd.teams/get-permissions conn profile-id team-id)] - - (when-not (:is-admin perms) - (ex/raise :type :validation - :code :insufficient-permissions)) - - (db/delete! conn :team-invitation - {:team-id team-id :email-to (str/lower email)}) - nil))) diff --git a/backend/src/app/rpc/permissions.clj b/backend/src/app/rpc/permissions.clj index 809e6640f..7cca62d0f 100644 --- a/backend/src/app/rpc/permissions.clj +++ b/backend/src/app/rpc/permissions.clj @@ -37,6 +37,14 @@ :is-admin false :can-edit false))) +(defn make-admin-predicate-fn + "A simple factory for admin permission predicate functions." + [qfn] + (us/assert fn? qfn) + (fn check + ([perms] (:is-admin perms)) + ([conn & args] (check (apply qfn conn args))))) + (defn make-edition-predicate-fn "A simple factory for edition permission predicate functions." [qfn] diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj deleted file mode 100644 index 3dc5d9f92..000000000 --- a/backend/src/app/rpc/queries/files.clj +++ /dev/null @@ -1,183 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.rpc.queries.files - (:require - [app.common.spec :as us] - [app.db :as db] - [app.rpc.commands.files :as files] - [app.rpc.commands.search :as search] - [app.rpc.commands.teams :as teams] - [app.rpc.doc :as-alias doc] - [app.rpc.helpers :as rph] - [app.rpc.queries.projects :as projects] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) - -;; --- Query: Project Files - -(s/def ::project-files ::files/get-project-files) - -(sv/defmethod ::project-files - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] - (with-open [conn (db/open pool)] - (projects/check-read-permissions! conn profile-id project-id) - (files/get-project-files conn project-id))) - -;; --- Query: File (By ID) - -(s/def ::components-v2 ::us/boolean) -(s/def ::file - (s/and ::files/get-file - (s/keys :opt-un [::components-v2]))) - -(defn get-file - [conn id features] - (let [file (files/get-file conn id features) - thumbs (files/get-object-thumbnails conn id)] - (assoc file :thumbnails thumbs))) - -(sv/defmethod ::file - "Retrieve a file by its ID. Only authenticated users." - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id id features components-v2] :as params}] - (with-open [conn (db/open pool)] - (let [perms (files/get-permissions pool profile-id id) - ;; BACKWARD COMPATIBILTY with the components-v2 parameter - features (cond-> (or features #{}) - components-v2 (conj "components/v2"))] - - (files/check-read-permissions! perms) - (-> (get-file conn id features) - (assoc :permissions perms))))) - -;; --- QUERY: page - -(s/def ::page - (s/and ::files/get-page - (s/keys :opt-un [::components-v2]))) - -(sv/defmethod ::page - "Retrieves the page data from file and returns it. If no page-id is - specified, the first page will be returned. If object-id is - specified, only that object and its children will be returned in the - page objects data structure. - - If you specify the object-id, the page-id parameter becomes - mandatory. - - Mainly used for rendering purposes." - {::doc/added "1.5" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as params}] - (with-open [conn (db/open pool)] - (files/check-read-permissions! conn profile-id file-id) - (let [;; BACKWARD COMPATIBILTY with the components-v2 parameter - features (cond-> (or features #{}) - components-v2 (conj "components/v2")) - params (assoc params :features features)] - - (files/get-page conn params)))) - -;; --- QUERY: file-data-for-thumbnail - -(s/def ::file-data-for-thumbnail - (s/and ::files/get-file-data-for-thumbnail - (s/keys :opt-un [::components-v2]))) - -(sv/defmethod ::file-data-for-thumbnail - "Retrieves the data for generate the thumbnail of the file. Used - mainly for render thumbnails on dashboard." - {::doc/added "1.11" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as props}] - (with-open [conn (db/open pool)] - (files/check-read-permissions! conn profile-id file-id) - (let [;; BACKWARD COMPATIBILTY with the components-v2 parameter - features (cond-> (or features #{}) - components-v2 (conj "components/v2")) - file (files/get-file conn file-id features)] - {:file-id file-id - :revn (:revn file) - :page (files/get-file-data-for-thumbnail conn file)}))) - -;; --- Query: Shared Library Files - -(s/def ::team-shared-files ::files/get-team-shared-files) - -(sv/defmethod ::team-shared-files - {::doc/added "1.3" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] - (with-open [conn (db/open pool)] - (teams/check-read-permissions! conn profile-id team-id) - (files/get-team-shared-files conn params))) - - -;; --- Query: File Libraries used by a File - -(s/def ::file-libraries ::files/get-file-libraries) - -(sv/defmethod ::file-libraries - {::doc/added "1.3" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id features] :as params}] - (with-open [conn (db/open pool)] - (files/check-read-permissions! conn profile-id file-id) - (files/get-file-libraries conn file-id features))) - - -;; --- Query: Files that use this File library - -(s/def ::library-using-files ::files/get-library-file-references) - -(sv/defmethod ::library-using-files - {::doc/added "1.13" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (with-open [conn (db/open pool)] - (files/check-read-permissions! conn profile-id file-id) - (files/get-library-file-references conn file-id))) - -;; --- QUERY: team-recent-files - -(s/def ::team-recent-files ::files/get-team-recent-files) - -(sv/defmethod ::team-recent-files - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] - (with-open [conn (db/open pool)] - (teams/check-read-permissions! conn profile-id team-id) - (files/get-team-recent-files conn team-id))) - - -;; --- QUERY: get file thumbnail - -(s/def ::file-thumbnail ::files/get-file-thumbnail) - -(sv/defmethod ::file-thumbnail - {::doc/added "1.13" - ::doc/deprecated "1.17"} - [{:keys [pool]} {:keys [profile-id file-id revn]}] - (with-open [conn (db/open pool)] - (files/check-read-permissions! conn profile-id file-id) - (-> (files/get-file-thumbnail conn file-id revn) - (rph/with-http-cache files/long-cache-duration)))) - - -;; --- QUERY: search files - -(s/def ::search-files ::search/search-files) - -(sv/defmethod ::search-files - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool]} {:keys [profile-id team-id search-term]}] - (some->> search-term (search/search-files pool profile-id team-id))) diff --git a/backend/src/app/rpc/queries/fonts.clj b/backend/src/app/rpc/queries/fonts.clj index 6c9623c54..b6c96c59a 100644 --- a/backend/src/app/rpc/queries/fonts.clj +++ b/backend/src/app/rpc/queries/fonts.clj @@ -9,9 +9,9 @@ [app.common.spec :as us] [app.db :as db] [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] - [app.rpc.queries.projects :as projects] [app.util.services :as sv] [clojure.spec.alpha :as s])) diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj index 1d5d605cc..86b7ee015 100644 --- a/backend/src/app/rpc/queries/profile.clj +++ b/backend/src/app/rpc/queries/profile.clj @@ -6,112 +6,27 @@ (ns app.rpc.queries.profile (:require - [app.common.exceptions :as ex] - [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] [app.rpc :as-alias rpc] + [app.rpc.commands.profile :as profile] + [app.rpc.doc :as-alias doc] [app.util.services :as sv] - [clojure.spec.alpha :as s] - [cuerdas.core :as str])) + [clojure.spec.alpha :as s])) -;; --- Helpers & Specs - -(declare strip-private-attrs) - -(s/def ::email ::us/email) -(s/def ::fullname ::us/string) -(s/def ::old-password ::us/string) -(s/def ::password ::us/string) -(s/def ::path ::us/string) -(s/def ::user ::us/uuid) -(s/def ::profile-id ::us/uuid) -(s/def ::theme ::us/string) - -;; --- Query: Profile (own) - -(declare retrieve-profile) -(declare retrieve-additional-data) - -(s/def ::profile - (s/keys :opt-un [::profile-id])) +(s/def ::profile ::profile/get-profile) (sv/defmethod ::profile - {::rpc/auth false} - [{:keys [pool] :as cfg} {:keys [profile-id] :as params}] + {::rpc/auth false + ::doc/added "1.0" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id]}] ;; 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. - (or (ex/try* - #(some->> profile-id (retrieve-profile pool)) - #(when (not= :not-found (:type (ex-data %))) (throw %))) - {:id uuid/zero - :fullname "Anonymous User"})) - -(def ^:private sql:default-profile-team - "select t.id, name - from team as t - inner join team_profile_rel as tp on (tp.team_id = t.id) - where tp.profile_id = ? - and tp.is_owner is true - and t.is_default is true") - -(def ^:private sql:default-profile-project - "select p.id, name - from project as p - inner join project_profile_rel as tp on (tp.project_id = p.id) - where tp.profile_id = ? - and tp.is_owner is true - and p.is_default is true - and p.team_id = ?") - -(defn retrieve-additional-data - [conn id] - (let [team (db/exec-one! conn [sql:default-profile-team id]) - project (db/exec-one! conn [sql:default-profile-project id (:id team)])] - {:default-team-id (:id team) - :default-project-id (:id project)})) - -(defn populate-additional-data - [conn profile] - (merge profile (retrieve-additional-data conn (:id profile)))) - -(defn filter-profile-props - [props] - (into {} (filter (fn [[k _]] (simple-ident? k))) props)) - -(defn decode-profile-row - [{:keys [props] :as row}] - (cond-> row - (db/pgobject? props "jsonb") - (assoc :props (db/decode-transit-pgobject props)))) - -(defn retrieve-profile-data - [conn id] - (-> (db/get-by-id conn :profile id) - (decode-profile-row))) - -(defn retrieve-profile - [conn id] - (let [profile (->> (retrieve-profile-data conn id) - (strip-private-attrs) - (populate-additional-data conn))] - (update profile :props filter-profile-props))) - -(def ^:private sql:profile-by-email - "select p.* from profile as p - where p.email = ? - and (p.deleted_at is null or - p.deleted_at > now())") - -(defn retrieve-profile-data-by-email - [conn email] - (ex/ignoring - (db/exec-one! conn [sql:profile-by-email (str/lower email)]))) - -;; --- Attrs Helpers - -(defn strip-private-attrs - "Only selects a publicly visible profile attrs." - [row] - (dissoc row :password :deleted-at)) + (try + (-> (profile/get-profile pool profile-id) + (profile/strip-private-attrs) + (update :props profile/filter-props)) + (catch Throwable _ + {:id uuid/zero :fullname "Anonymous User"}))) diff --git a/backend/src/app/rpc/queries/projects.clj b/backend/src/app/rpc/queries/projects.clj index 64c4a9b42..4329c13cb 100644 --- a/backend/src/app/rpc/queries/projects.clj +++ b/backend/src/app/rpc/queries/projects.clj @@ -8,135 +8,39 @@ (:require [app.common.spec :as us] [app.db :as db] + [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] - [app.rpc.permissions :as perms] + [app.rpc.doc :as-alias doc] [app.util.services :as sv] [clojure.spec.alpha :as s])) -;; --- Check Project Permissions - -(def ^:private sql:project-permissions - "select tpr.is_owner, - tpr.is_admin, - tpr.can_edit - from team_profile_rel as tpr - inner join project as p on (p.team_id = tpr.team_id) - where p.id = ? - and tpr.profile_id = ? - union all - select ppr.is_owner, - ppr.is_admin, - ppr.can_edit - from project_profile_rel as ppr - where ppr.project_id = ? - and ppr.profile_id = ?") - -(defn- get-permissions - [conn profile-id project-id] - (let [rows (db/exec! conn [sql:project-permissions - project-id profile-id - project-id profile-id]) - is-owner (boolean (some :is-owner rows)) - is-admin (boolean (some :is-admin rows)) - can-edit (boolean (some :can-edit rows))] - (when (seq rows) - {:is-owner is-owner - :is-admin (or is-owner is-admin) - :can-edit (or is-owner is-admin can-edit) - :can-read true}))) - -(def has-edit-permissions? - (perms/make-edition-predicate-fn get-permissions)) - -(def has-read-permissions? - (perms/make-read-predicate-fn get-permissions)) - -(def check-edition-permissions! - (perms/make-check-fn has-edit-permissions?)) - -(def check-read-permissions! - (perms/make-check-fn has-read-permissions?)) - ;; --- Query: Projects -(declare retrieve-projects) - (s/def ::team-id ::us/uuid) (s/def ::profile-id ::us/uuid) (s/def ::projects (s/keys :req-un [::profile-id ::team-id])) (sv/defmethod ::projects + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [pool]} {:keys [profile-id team-id]}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) - (retrieve-projects conn profile-id team-id))) - -(def sql:projects - "select p.*, - coalesce(tpp.is_pinned, false) as is_pinned, - (select count(*) from file as f - where f.project_id = p.id - and deleted_at is null) as count - from project as p - inner join team as t on (t.id = p.team_id) - left join team_project_profile_rel as tpp - on (tpp.project_id = p.id and - tpp.team_id = p.team_id and - tpp.profile_id = ?) - where p.team_id = ? - and p.deleted_at is null - and t.deleted_at is null - order by p.modified_at desc") - -(defn retrieve-projects - [conn profile-id team-id] - (db/exec! conn [sql:projects profile-id team-id])) - + (projects/get-projects conn profile-id team-id))) ;; --- Query: All projects -(declare retrieve-all-projects) - (s/def ::profile-id ::us/uuid) (s/def ::all-projects (s/keys :req-un [::profile-id])) (sv/defmethod ::all-projects + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [pool]} {:keys [profile-id]}] (with-open [conn (db/open pool)] - (retrieve-all-projects conn profile-id))) - -(def sql:all-projects - "select p1.*, t.name as team_name, t.is_default as is_default_team - from project as p1 - inner join team as t on (t.id = p1.team_id) - where t.id in (select team_id - from team_profile_rel as tpr - where tpr.profile_id = ? - and (tpr.can_edit = true or - tpr.is_owner = true or - tpr.is_admin = true)) - and t.deleted_at is null - and p1.deleted_at is null - union - select p2.*, t.name as team_name, t.is_default as is_default_team - from project as p2 - inner join team as t on (t.id = p2.team_id) - where p2.id in (select project_id - from project_profile_rel as ppr - where ppr.profile_id = ? - and (ppr.can_edit = true or - ppr.is_owner = true or - ppr.is_admin = true)) - and t.deleted_at is null - and p2.deleted_at is null - order by team_name, name;") - -(defn retrieve-all-projects - [conn profile-id] - (db/exec! conn [sql:all-projects profile-id profile-id])) - + (projects/get-all-projects conn profile-id))) ;; --- Query: Project @@ -145,9 +49,11 @@ (s/keys :req-un [::profile-id ::id])) (sv/defmethod ::project + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [pool]} {:keys [profile-id id]}] (with-open [conn (db/open pool)] (let [project (db/get-by-id conn :project id)] - (check-read-permissions! conn profile-id id) + (projects/check-read-permissions! conn profile-id id) project))) diff --git a/backend/src/app/rpc/queries/share_link.clj b/backend/src/app/rpc/queries/share_link.clj deleted file mode 100644 index 852d05cd1..000000000 --- a/backend/src/app/rpc/queries/share_link.clj +++ /dev/null @@ -1,23 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.rpc.queries.share-link - (:require - [app.db :as db])) - -(defn decode-share-link-row - [row] - (-> row - (dissoc :flags) - (update :pages db/decode-pgarray #{}))) - -(defn retrieve-share-link - [conn file-id share-id] - (some-> (db/get-by-params conn :share-link - {:id share-id :file-id file-id} - {:check-not-found false}) - (decode-share-link-row))) - diff --git a/backend/src/app/rpc/queries/teams.clj b/backend/src/app/rpc/queries/teams.clj deleted file mode 100644 index 10801413e..000000000 --- a/backend/src/app/rpc/queries/teams.clj +++ /dev/null @@ -1,87 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.rpc.queries.teams - (:require - [app.db :as db] - [app.rpc.commands.teams :as cmd.teams] - [app.rpc.doc :as-alias doc] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) - -;; --- Query: Teams - -(s/def ::teams ::cmd.teams/get-teams) - -(sv/defmethod ::teams - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id]}] - (with-open [conn (db/open pool)] - (cmd.teams/retrieve-teams conn profile-id))) - -;; --- Query: Team (by ID) - -(s/def ::team ::cmd.teams/get-team) - -(sv/defmethod ::team - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id id]}] - (with-open [conn (db/open pool)] - (cmd.teams/retrieve-team conn profile-id id))) - -;; --- Query: Team Members - -(s/def ::team-members ::cmd.teams/get-team-members) - -(sv/defmethod ::team-members - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] - (with-open [conn (db/open pool)] - (cmd.teams/check-read-permissions! conn profile-id team-id) - (cmd.teams/retrieve-team-members conn team-id))) - -;; --- Query: Team Users -(s/def ::team-users ::cmd.teams/get-team-users) - -(sv/defmethod ::team-users - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id file-id]}] - (with-open [conn (db/open pool)] - (if team-id - (do - (cmd.teams/check-read-permissions! conn profile-id team-id) - (cmd.teams/retrieve-users conn team-id)) - (let [{team-id :id} (cmd.teams/retrieve-team-for-file conn file-id)] - (cmd.teams/check-read-permissions! conn profile-id team-id) - (cmd.teams/retrieve-users conn team-id))))) - -;; --- Query: Team Stats - -(s/def ::team-stats ::cmd.teams/get-team-stats) - -(sv/defmethod ::team-stats - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] - (with-open [conn (db/open pool)] - (cmd.teams/check-read-permissions! conn profile-id team-id) - (cmd.teams/retrieve-team-stats conn team-id))) - -;; --- Query: Team invitations - -(s/def ::team-invitations ::cmd.teams/get-team-invitations) - -(sv/defmethod ::team-invitations - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] - (with-open [conn (db/open pool)] - (cmd.teams/check-read-permissions! conn profile-id team-id) - (cmd.teams/get-team-invitations conn team-id))) diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index 837dab94f..14a016096 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -22,7 +22,7 @@ (sv/defmethod ::view-only-bundle {::rpc/auth false ::doc/added "1.3" - ::doc/deprecated "1.17"} + ::doc/deprecated "1.18"} [{:keys [pool] :as cfg} {:keys [features components-v2] :as params}] (with-open [conn (db/open pool)] (let [;; BACKWARD COMPATIBILTY with the components-v2 parameter diff --git a/backend/src/app/rpc/quotes.clj b/backend/src/app/rpc/quotes.clj index 76cfc82f7..4cdc3800d 100644 --- a/backend/src/app/rpc/quotes.clj +++ b/backend/src/app/rpc/quotes.clj @@ -23,7 +23,7 @@ ;; PUBLIC API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::conn ::db/conn-or-pool) +(s/def ::conn ::db/pool-or-conn) (s/def ::file-id ::us/uuid) (s/def ::team-id ::us/uuid) (s/def ::project-id ::us/uuid) @@ -53,7 +53,7 @@ (defn check-quote! [conn quote] - (us/assert! ::db/conn-or-pool conn) + (us/assert! ::db/pool-or-conn conn) (us/assert! ::quote quote) (when (contains? cf/flags :quotes) (when @enabled @@ -160,6 +160,28 @@ (assoc ::count-sql [sql:get-teams-per-profile profile-id]) (generic-check!))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; QUOTE: ACCESS-TOKENS-PER-PROFILE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private sql:get-access-tokens-per-profile + "select count(*) as total + from access_token + where profile_id = ?") + +(s/def ::access-tokens-per-profile + (s/keys :req [::profile-id ::target])) + +(defmethod check-quote ::access-tokens-per-profile + [{:keys [::profile-id ::target] :as quote}] + (us/assert! ::access-tokens-per-profile quote) + (-> quote + (assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE)) + (assoc ::quote-sql [sql:get-quotes-1 target profile-id]) + (assoc ::count-sql [sql:get-access-tokens-per-profile profile-id]) + (generic-check!))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; QUOTE: PROJECTS-PER-TEAM ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -279,7 +301,6 @@ (assoc ::count-sql [sql:get-files-per-project project-id]) (generic-check!))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; QUOTE: COMMENT-THREADS-PER-FILE ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/rpc/rlimit.clj b/backend/src/app/rpc/rlimit.clj index 67be94684..028a59a78 100644 --- a/backend/src/app/rpc/rlimit.clj +++ b/backend/src/app/rpc/rlimit.clj @@ -52,7 +52,7 @@ [app.config :as cf] [app.http :as-alias http] [app.loggers.audit :refer [parse-client-ip]] - [app.redis :as redis] + [app.redis :as rds] [app.redis.script :as-alias rscript] [app.rpc :as-alias rpc] [app.rpc.rlimit.result :as-alias lresult] @@ -71,7 +71,7 @@ (dt/duration 400)) (def ^:private default-options - {:codec redis/string-codec + {:codec rds/string-codec :timeout default-timeout}) (def ^:private bucket-rate-limit-script @@ -141,23 +141,23 @@ (let [script (-> bucket-rate-limit-script (assoc ::rscript/keys [(str key "." service "." user-id)]) (assoc ::rscript/vals (conj params (dt/->seconds now))))] - (-> (redis/eval! redis script) - (p/then (fn [result] - (let [allowed? (boolean (nth result 0)) - remaining (nth result 1) - reset (* (/ (inst-ms interval) rate) - (- capacity remaining))] - (l/trace :hint "limit processed" - :service service - :limit (name (::name limit)) - :strategy (name (::strategy limit)) - :opts (::opts limit) + (->> (rds/eval! redis script) + (p/fmap (fn [result] + (let [allowed? (boolean (nth result 0)) + remaining (nth result 1) + reset (* (/ (inst-ms interval) rate) + (- capacity remaining))] + (l/trace :hint "limit processed" + :service service + :limit (name (::name limit)) + :strategy (name (::strategy limit)) + :opts (::opts limit) :allowed? allowed? :remaining remaining) - (-> limit - (assoc ::lresult/allowed? allowed?) - (assoc ::lresult/reset (dt/plus now reset)) - (assoc ::lresult/remaining remaining)))))))) + (-> limit + (assoc ::lresult/allowed? allowed?) + (assoc ::lresult/reset (dt/plus now reset)) + (assoc ::lresult/remaining remaining)))))))) (defmethod process-limit :window [redis user-id now {:keys [::nreq ::unit ::key ::service] :as limit}] @@ -166,21 +166,21 @@ script (-> window-rate-limit-script (assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))]) (assoc ::rscript/vals [nreq (dt/->seconds ttl)]))] - (-> (redis/eval! redis script) - (p/then (fn [result] - (let [allowed? (boolean (nth result 0)) - remaining (nth result 1)] - (l/trace :hint "limit processed" - :service service - :limit (name (::name limit)) - :strategy (name (::strategy limit)) - :opts (::opts limit) - :allowed? allowed? + (->> (rds/eval! redis script) + (p/fmap (fn [result] + (let [allowed? (boolean (nth result 0)) + remaining (nth result 1)] + (l/trace :hint "limit processed" + :service service + :limit (name (::name limit)) + :strategy (name (::strategy limit)) + :opts (::opts limit) + :allowed? allowed? :remaining remaining) - (-> limit - (assoc ::lresult/allowed? allowed?) - (assoc ::lresult/remaining remaining) - (assoc ::lresult/reset (dt/plus ts {unit 1}))))))))) + (-> limit + (assoc ::lresult/allowed? allowed?) + (assoc ::lresult/remaining remaining) + (assoc ::lresult/reset (dt/plus ts {unit 1}))))))))) (defn- process-limits! [redis user-id limits now] @@ -237,7 +237,10 @@ uuid/zero)) (defn wrap - [{:keys [rlimit redis] :as cfg} f mdata] + [{:keys [::rpc/rlimit ::rds/redis] :as cfg} f mdata] + (us/assert! ::rpc/rlimit rlimit) + (us/assert! ::rds/redis redis) + (if rlimit (let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name)) sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))] @@ -247,7 +250,7 @@ (try (let [uid (get-uid params) rsp (when-let [limits (get-limits rlimit skey sname)] - (let [redis (redis/get-or-connect redis ::rpc/rlimit default-options) + (let [redis (rds/get-or-connect redis ::rpc/rlimit default-options) rsp (->> (process-limits! redis uid limits (dt/now)) (p/merr (fn [cause] ;; If we have an error on processing the rate-limit we just skip @@ -305,8 +308,9 @@ (s/keys :req [::nreq ::unit])))) -(s/def ::rlimit - #(instance? clojure.lang.Agent %)) +(s/def ::rpc/rlimit + (s/nilable + #(instance? clojure.lang.Agent %))) (s/def ::config (s/map-of (s/or :kw keyword? :set set?) @@ -348,7 +352,7 @@ ::limits limits})))) (defn- refresh-config - [{:keys [state path executor scheduled-executor] :as params}] + [{:keys [::state ::path ::wrk/executor ::wrk/scheduled-executor] :as cfg}] (letfn [(update-config [{:keys [::updated-at] :as state}] (let [updated-at' (fs/last-modified-time path)] (merge state @@ -359,13 +363,13 @@ (let [state (read-config path)] (l/info :hint "config refreshed" :loaded-limits (count (::limits state)) - ::l/async false) + ::l/sync? true) state))))) (schedule-next [state] (px/schedule! scheduled-executor (inst-ms (::refresh state)) - (partial refresh-config params)) + (partial refresh-config cfg)) state)] (send-via executor state update-config) @@ -376,10 +380,10 @@ (when-not (instance? java.util.concurrent.RejectedExecutionException cause) (if-let [explain (-> cause ex-data ex/explain)] (l/warn ::l/raw (str "unable to refresh config, invalid format:\n" explain) - ::l/async false) + ::l/sync? true) (l/warn :hint "unexpected exception on loading config" :cause cause - ::l/async false)))) + ::l/sync? true)))) (defn- get-config-path [] @@ -387,10 +391,11 @@ (and (fs/exists? path) (fs/regular-file? path) path))) (defmethod ig/pre-init-spec :app.rpc/rlimit [_] - (s/keys :req-un [::wrk/executor ::wrk/scheduled-executor])) + (s/keys :req [::wrk/executor + ::wrk/scheduled-executor])) (defmethod ig/init-key ::rpc/rlimit - [_ {:keys [executor] :as params}] + [_ {:keys [::wrk/executor] :as cfg}] (when (contains? cf/flags :rpc-rlimit) (let [state (agent {})] (set-error-handler! state on-refresh-error) @@ -403,6 +408,6 @@ (send-via executor state (constantly {::refresh (dt/duration "5s")})) ;; Force a refresh - (refresh-config (assoc params :path path :state state))) + (refresh-config (assoc cfg ::path path ::state state))) state))) diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj index f8af36050..856853fc8 100644 --- a/backend/src/app/setup.clj +++ b/backend/src/app/setup.clj @@ -50,14 +50,16 @@ :cause cause)))) instance-id))) +(s/def ::main/key ::us/string) (s/def ::main/props (s/map-of ::us/keyword some?)) (defmethod ig/pre-init-spec ::props [_] - (s/keys :req-un [::db/pool])) + (s/keys :req [::db/pool] + :opt [::main/key])) (defmethod ig/init-key ::props - [_ {:keys [pool key] :as cfg}] + [_ {:keys [::db/pool ::main/key] :as cfg}] (db/with-atomic [conn pool] (db/xact-lock! conn 0) (when-not key diff --git a/backend/src/app/srepl.clj b/backend/src/app/srepl.clj index 31177cf7a..fcb802c02 100644 --- a/backend/src/app/srepl.clj +++ b/backend/src/app/srepl.clj @@ -50,15 +50,14 @@ (defmethod ig/pre-init-spec ::server [_] - (s/keys :req [::flag] - :req-un [::port ::host])) + (s/keys :req [::flag ::host ::port])) (defmethod ig/prep-key ::server [[type _] cfg] (assoc cfg ::flag (keyword (str (name type) "-server")))) (defmethod ig/init-key ::server - [[type _] {:keys [::flag port host] :as cfg}] + [[type _] {:keys [::flag ::port ::host] :as cfg}] (when (contains? cf/flags flag) (let [accept (case type ::prepl 'app.srepl/json-repl diff --git a/backend/src/app/srepl/ext.clj b/backend/src/app/srepl/ext.clj index a37804f11..40b44fafe 100644 --- a/backend/src/app/srepl/ext.clj +++ b/backend/src/app/srepl/ext.clj @@ -42,8 +42,8 @@ :is-active is-active :password password :props {}}] - (->> (cmd.auth/create-profile conn params) - (cmd.auth/create-profile-relations conn)))))) + (->> (cmd.auth/create-profile! conn params) + (cmd.auth/create-profile-rels! conn)))))) (defmethod run-json-cmd* :update-profile [{:keys [fullname email password is-active]}] diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index 38c943a08..7f9f7ed23 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -70,7 +70,7 @@ [system & {:keys [update-fn id save? migrate? inc-revn?] :or {save? false migrate? true inc-revn? true}}] (db/with-atomic [conn (:app.db/pool system)] - (let [file (-> (db/get-by-id conn :file id {:for-update true}) + (let [file (-> (db/get-by-id conn :file id {::db/for-update? true}) (update :features db/decode-pgarray #{}))] (binding [*conn* conn pmap/*tracked* (atom {}) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index ae09aa2ff..77413995b 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -12,8 +12,8 @@ [app.common.pprint :as p] [app.common.spec :as us] [app.db :as db] - [app.rpc.commands.auth :as cmd.auth] - [app.rpc.queries.profile :as profile] + [app.rpc.commands.auth :as auth] + [app.rpc.commands.profile :as profile] [app.srepl.fixes :as f] [app.srepl.helpers :as h] [app.util.blob :as blob] @@ -58,7 +58,7 @@ :expr (string? destination) :hint "destination should be provided") - (let [handler (:app.emails/sendmail system)] + (let [handler (:app.email/sendmail system)] (handler {:body "test email" :subject "test email" :to [destination]}))) @@ -71,9 +71,9 @@ (let [sprops (:app.setup/props system) pool (:app.db/pool system) - profile (profile/retrieve-profile-data-by-email pool email)] + profile (profile/get-profile-by-email pool email)] - (cmd.auth/send-email-verification! pool sprops profile) + (auth/send-email-verification! pool sprops profile) :email-sent)) (defn mark-profile-as-active! @@ -81,10 +81,9 @@ associated with the profile-id." [system email] (db/with-atomic [conn (:app.db/pool system)] - (when-let [profile (db/get-by-params conn :profile - {:email (str/lower email)} - {:columns [:id :email] - :check-not-found false})] + (when-let [profile (db/get* conn :profile + {:email (str/lower email)} + {:columns [:id :email]})] (when-not (:is-blocked profile) (db/update! conn :profile {:is-active true} {:id (:id profile)}) :activated)))) @@ -94,10 +93,9 @@ associated with the profile-id." [system email] (db/with-atomic [conn (:app.db/pool system)] - (when-let [profile (db/get-by-params conn :profile - {:email (str/lower email)} - {:columns [:id :email] - :check-not-found false})] + (when-let [profile (db/get* conn :profile + {:email (str/lower email)} + {:columns [:id :email]})] (when-not (:is-blocked profile) (db/update! conn :profile {:is-blocked true} {:id (:id profile)}) (db/delete! conn :http-session {:profile-id (:id profile)}) @@ -146,3 +144,23 @@ [system & {:as params}] (enable-objects-map-feature-on-file! system params) (enable-pointer-map-feature-on-file! system params)) + +(defn instrument-var + [var] + (alter-var-root var (fn [f] + (let [mf (meta f)] + (if (::original mf) + f + (with-meta + (fn [& params] + (tap> params) + (let [result (apply f params)] + (tap> result) + result)) + {::original f})))))) + +(defn uninstrument-var + [var] + (alter-var-root var (fn [f] + (or (::original (meta f)) f)))) + diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index 8de524c1a..dc013261b 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -29,8 +29,10 @@ ;; Storage Module State ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(s/def ::id #{:assets-fs :assets-s3}) (s/def ::s3 ::ss3/backend) (s/def ::fs ::sfs/backend) +(s/def ::type #{:fs :s3}) (s/def ::backends (s/map-of ::us/keyword @@ -39,34 +41,26 @@ :fs ::sfs/backend)))) (defmethod ig/pre-init-spec ::storage [_] - (s/keys :req-un [::db/pool ::wrk/executor ::backends])) - -(defmethod ig/prep-key ::storage - [_ {:keys [backends] :as cfg}] - (-> (d/without-nils cfg) - (assoc :backends (d/without-nils backends)))) + (s/keys :req [::db/pool ::wrk/executor ::backends])) (defmethod ig/init-key ::storage - [_ {:keys [backends] :as cfg}] + [_ {:keys [::backends ::db/pool] :as cfg}] (-> (d/without-nils cfg) - (assoc :backends (d/without-nils backends)))) + (assoc ::backends (d/without-nils backends)) + (assoc ::db/pool-or-conn pool))) +(s/def ::backend keyword?) (s/def ::storage - (s/keys :req-un [::backends ::db/pool])) + (s/keys :req [::backends ::db/pool ::db/pool-or-conn] + :opt [::backend])) + +(s/def ::storage-with-backend + (s/and ::storage #(contains? % ::backend))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Database Objects ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defrecord StorageObject [id size created-at expired-at touched-at backend]) - -(defn storage-object? - [v] - (instance? StorageObject v)) - -(s/def ::storage-object storage-object?) -(s/def ::storage-content impl/content?) - (defn get-metadata [params] (into {} @@ -74,19 +68,18 @@ params)) (defn- get-database-object-by-hash - [conn backend bucket hash] + [pool-or-conn backend bucket hash] (let [sql (str "select * from storage_object " " where (metadata->>'~:hash') = ? " " and (metadata->>'~:bucket') = ? " " and backend = ?" " and deleted_at is null" " limit 1")] - (some-> (db/exec-one! conn [sql hash bucket (name backend)]) + (some-> (db/exec-one! pool-or-conn [sql hash bucket (name backend)]) (update :metadata db/decode-transit-pgobject)))) (defn- create-database-object - [{:keys [conn backend executor]} {:keys [::content ::expired-at ::touched-at] :as params}] - (us/assert ::storage-content content) + [{:keys [::backend ::wrk/executor ::db/pool-or-conn]} {:keys [::content ::expired-at ::touched-at] :as params}] (px/with-dispatch executor (let [id (uuid/random) @@ -101,10 +94,10 @@ result (when (and (::deduplicate? params) (:hash mdata) (:bucket mdata)) - (get-database-object-by-hash conn backend (:bucket mdata) (:hash mdata))) + (get-database-object-by-hash pool-or-conn backend (:bucket mdata) (:hash mdata))) result (or result - (-> (db/insert! conn :storage-object + (-> (db/insert! pool-or-conn :storage-object {:id id :size (impl/get-size content) :backend (name backend) @@ -114,33 +107,33 @@ (update :metadata db/decode-transit-pgobject) (update :metadata assoc ::created? true)))] - (StorageObject. (:id result) - (:size result) - (:created-at result) - (:deleted-at result) - (:touched-at result) - backend - (:metadata result) - nil)))) + (impl/storage-object + (:id result) + (:size result) + (:created-at result) + (:deleted-at result) + (:touched-at result) + backend + (:metadata result))))) (def ^:private sql:retrieve-storage-object "select * from storage_object where id = ? and (deleted_at is null or deleted_at > now())") (defn row->storage-object [res] (let [mdata (or (some-> (:metadata res) (db/decode-transit-pgobject)) {})] - (StorageObject. (:id res) - (:size res) - (:created-at res) - (:deleted-at res) - (:touched-at res) - (keyword (:backend res)) - mdata - nil))) + (impl/storage-object + (:id res) + (:size res) + (:created-at res) + (:deleted-at res) + (:touched-at res) + (keyword (:backend res)) + mdata))) (defn- retrieve-database-object - [{:keys [conn] :as storage} id] - (when-let [res (db/exec-one! conn [sql:retrieve-storage-object id])] - (row->storage-object res))) + [conn id] + (some-> (db/exec-one! conn [sql:retrieve-storage-object id]) + (row->storage-object))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; API @@ -152,103 +145,99 @@ (defn file-url->path [url] - (fs/path (java.net.URI. (str url)))) + (when url + (fs/path (java.net.URI. (str url))))) (dm/export impl/content) (dm/export impl/wrap-with-hash) +(dm/export impl/object?) (defn get-object - [{:keys [conn pool] :as storage} id] - (us/assert ::storage storage) - (p/do - (-> (assoc storage :conn (or conn pool)) - (retrieve-database-object id)))) + [{:keys [::db/pool-or-conn ::wrk/executor] :as storage} id] + (us/assert! ::storage storage) + (px/with-dispatch executor + (retrieve-database-object pool-or-conn id))) (defn put-object! "Creates a new object with the provided content." - [{:keys [pool conn backend] :as storage} {:keys [::content] :as params}] - (us/assert ::storage storage) - (us/assert ::storage-content content) - (us/assert ::us/keyword backend) - (p/let [storage (assoc storage :conn (or conn pool)) - object (create-database-object storage params)] - - (when (::created? (meta object)) - ;; Store the data finally on the underlying storage subsystem. - (-> (impl/resolve-backend storage backend) - (impl/put-object object content))) - - object)) + [{:keys [::backend] :as storage} {:keys [::content] :as params}] + (us/assert! ::storage-with-backend storage) + (us/assert! ::impl/content content) + (->> (create-database-object storage params) + (p/mcat (fn [object] + (if (::created? (meta object)) + ;; Store the data finally on the underlying storage subsystem. + (-> (impl/resolve-backend storage backend) + (impl/put-object object content)) + (p/resolved object)))))) (defn touch-object! "Mark object as touched." - [{:keys [pool conn] :as storage} object-or-id] - (p/do - (let [id (if (storage-object? object-or-id) (:id object-or-id) object-or-id) - res (db/update! (or conn pool) :storage-object - {:touched-at (dt/now)} - {:id id} - {:return-keys false})] - (pos? (:next.jdbc/update-count res))))) + [{:keys [::db/pool-or-conn ::wrk/executor] :as storage} object-or-id] + (us/assert! ::storage storage) + (px/with-dispatch executor + (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id) + rs (db/update! pool-or-conn :storage-object + {:touched-at (dt/now)} + {:id id} + {::db/return-keys? false})] + (pos? (db/get-update-count rs))))) (defn get-object-data "Return an input stream instance of the object content." - [{:keys [pool conn] :as storage} object] - (us/assert ::storage storage) - (p/do - (when (or (nil? (:expired-at object)) - (dt/is-after? (:expired-at object) (dt/now))) - (-> (assoc storage :conn (or conn pool)) - (impl/resolve-backend (:backend object)) - (impl/get-object-data object))))) + [storage object] + (us/assert! ::storage storage) + (if (or (nil? (:expired-at object)) + (dt/is-after? (:expired-at object) (dt/now))) + (-> (impl/resolve-backend storage (:backend object)) + (impl/get-object-data object)) + (p/resolved nil))) (defn get-object-bytes "Returns a byte array of object content." - [{:keys [pool conn] :as storage} object] - (us/assert ::storage storage) - (p/do - (when (or (nil? (:expired-at object)) - (dt/is-after? (:expired-at object) (dt/now))) - (-> (assoc storage :conn (or conn pool)) - (impl/resolve-backend (:backend object)) - (impl/get-object-bytes object))))) + [storage object] + (us/assert! ::storage storage) + (if (or (nil? (:expired-at object)) + (dt/is-after? (:expired-at object) (dt/now))) + (-> (impl/resolve-backend storage (:backend object)) + (impl/get-object-bytes object)) + (p/resolved nil))) (defn get-object-url ([storage object] (get-object-url storage object nil)) - ([{:keys [conn pool] :as storage} object options] - (us/assert ::storage storage) - (p/do - (when (or (nil? (:expired-at object)) - (dt/is-after? (:expired-at object) (dt/now))) - (-> (assoc storage :conn (or conn pool)) - (impl/resolve-backend (:backend object)) - (impl/get-object-url object options)))))) + ([storage object options] + (us/assert! ::storage storage) + (if (or (nil? (:expired-at object)) + (dt/is-after? (:expired-at object) (dt/now))) + (-> (impl/resolve-backend storage (:backend object)) + (impl/get-object-url object options)) + (p/resolved nil)))) (defn get-object-path "Get the Path to the object. Only works with `:fs` type of storages." [storage object] - (p/do - (let [backend (impl/resolve-backend storage (:backend object))] - (when (not= :fs (:type backend)) - (ex/raise :type :internal - :code :operation-not-allowed - :hint "get-object-path only works with fs type backends")) - (when (or (nil? (:expired-at object)) - (dt/is-after? (:expired-at object) (dt/now))) - (p/-> (impl/get-object-url backend object nil) file-url->path))))) + (us/assert! ::storage storage) + (let [backend (impl/resolve-backend storage (:backend object))] + (if (not= :fs (::type backend)) + (p/resolved nil) + (if (or (nil? (:expired-at object)) + (dt/is-after? (:expired-at object) (dt/now))) + (->> (impl/get-object-url backend object nil) + (p/fmap file-url->path)) + (p/resolved nil))))) (defn del-object! - [{:keys [conn pool] :as storage} object-or-id] - (us/assert ::storage storage) - (p/do - (let [id (if (storage-object? object-or-id) (:id object-or-id) object-or-id) - res (db/update! (or conn pool) :storage-object + [{:keys [::db/pool-or-conn ::wrk/executor] :as storage} object-or-id] + (us/assert! ::storage storage) + (px/with-dispatch executor + (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id) + res (db/update! pool-or-conn :storage-object {:deleted-at (dt/now)} {:id id} - {:return-keys false})] - (pos? (:next.jdbc/update-count res))))) + {::db/return-keys? false})] + (pos? (db/get-update-count res))))) (dm/export impl/resolve-backend) (dm/export impl/calculate-hash) @@ -265,18 +254,15 @@ (declare sql:retrieve-deleted-objects-chunk) -(s/def ::min-age ::dt/duration) - (defmethod ig/pre-init-spec ::gc-deleted-task [_] - (s/keys :req-un [::storage ::db/pool ::min-age ::wrk/executor])) + (s/keys :req [::storage ::db/pool])) (defmethod ig/prep-key ::gc-deleted-task [_ cfg] - (merge {:min-age (dt/duration {:hours 2})} - (d/without-nils cfg))) + (assoc cfg ::min-age (dt/duration {:hours 2}))) (defmethod ig/init-key ::gc-deleted-task - [_ {:keys [pool storage] :as cfg}] + [_ {:keys [::db/pool ::storage ::min-age]}] (letfn [(retrieve-deleted-objects-chunk [conn min-age cursor] (let [min-age (db/interval min-age) rows (db/exec! conn [sql:retrieve-deleted-objects-chunk min-age cursor])] @@ -289,27 +275,26 @@ :vf second :kf first)) - (delete-in-bulk [conn backend-name ids] - (let [backend (impl/resolve-backend storage backend-name) - backend (assoc backend :conn conn)] + (delete-in-bulk [backend-id ids] + (let [backend (impl/resolve-backend storage backend-id)] (doseq [id ids] - (l/debug :hint "permanently delete storage object" :task "gc-deleted" :backend backend-name :id id)) + (l/debug :hint "gc-deleted: permanently delete storage object" :backend backend-id :id id)) @(impl/del-objects-in-bulk backend ids)))] (fn [params] - (let [min-age (or (:min-age params) (:min-age cfg))] + (let [min-age (or (:min-age params) min-age)] (db/with-atomic [conn pool] (loop [total 0 groups (retrieve-deleted-objects conn min-age)] - (if-let [[backend ids] (first groups)] + (if-let [[backend-id ids] (first groups)] (do - (delete-in-bulk conn backend ids) + (delete-in-bulk backend-id ids) (recur (+ total (count ids)) (rest groups))) (do - (l/info :hint "task finished" :min-age (dt/format-duration min-age) :task "gc-deleted" :total total) + (l/info :hint "gc-deleted: task finished" :min-age (dt/format-duration min-age) :total total) {:deleted total})))))))) (def sql:retrieve-deleted-objects-chunk @@ -349,10 +334,10 @@ (declare sql:retrieve-profile-nrefs) (defmethod ig/pre-init-spec ::gc-touched-task [_] - (s/keys :req-un [::db/pool])) + (s/keys :req [::db/pool])) (defmethod ig/init-key ::gc-touched-task - [_ {:keys [pool] :as cfg}] + [_ {:keys [::db/pool]}] (letfn [(get-team-font-variant-nrefs [conn id] (-> (db/exec-one! conn [sql:retrieve-team-font-variant-nrefs id id id id]) :nrefs)) @@ -409,13 +394,13 @@ (let [nrefs (get-fn conn id)] (if (pos? nrefs) (do - (l/debug :hint "processing storage object" - :task "gc-touched" :id id :status "freeze" + (l/debug :hint "gc-touched: processing storage object" + :id id :status "freeze" :bucket bucket :refs nrefs) (recur (conj to-freeze id) to-delete (rest ids))) (do - (l/debug :hint "processing storage object" - :task "gc-touched" :id id :status "delete" + (l/debug :hint "gc-touched: processing storage object" + :id id :status "delete" :bucket bucket :refs nrefs) (recur to-freeze (conj to-delete id) (rest ids))))) (do @@ -441,7 +426,7 @@ (+ to-delete d) (rest groups))) (do - (l/info :hint "task finished" :task "gc-touched" :to-freeze to-freeze :to-delete to-delete) + (l/info :hint "gc-touched: task finished" :to-freeze to-freeze :to-delete to-delete) {:freeze to-freeze :delete to-delete}))))))) (def sql:retrieve-touched-objects-chunk diff --git a/backend/src/app/storage/fs.clj b/backend/src/app/storage/fs.clj index c88d4b33e..f6240e2ad 100644 --- a/backend/src/app/storage/fs.clj +++ b/backend/src/app/storage/fs.clj @@ -9,7 +9,9 @@ [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uri :as u] + [app.storage :as-alias sto] [app.storage.impl :as impl] + [app.worker :as-alias wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.fs :as fs] @@ -28,42 +30,49 @@ (s/def ::directory ::us/string) (defmethod ig/pre-init-spec ::backend [_] - (s/keys :opt-un [::directory])) + (s/keys :opt [::directory])) (defmethod ig/init-key ::backend [_ cfg] ;; Return a valid backend data structure only if all optional ;; parameters are provided. - (when (string? (:directory cfg)) - (let [dir (fs/normalize (:directory cfg))] + (when (string? (::directory cfg)) + (let [dir (fs/normalize (::directory cfg))] (assoc cfg - :type :fs - :directory (str dir) - :uri (u/uri (str "file://" dir)))))) + ::sto/type :fs + ::directory (str dir) + ::uri (u/uri (str "file://" dir)))))) -(s/def ::type ::us/keyword) (s/def ::uri u/uri?) (s/def ::backend - (s/keys :req-un [::type ::directory ::uri])) + (s/keys :req [::directory + ::uri] + :opt [::sto/type + ::sto/id + ::wrk/executor])) ;; --- API IMPL (defmethod impl/put-object :fs - [{:keys [executor] :as backend} {:keys [id] :as object} content] + [{:keys [::wrk/executor] :as backend} {:keys [id] :as object} content] + (us/assert! ::backend backend) (px/with-dispatch executor - (let [base (fs/path (:directory backend)) + (let [base (fs/path (::directory backend)) path (fs/path (impl/id->path id)) full (fs/normalize (fs/join base path))] (when-not (fs/exists? (fs/parent full)) (fs/create-dir (fs/parent full))) (with-open [^InputStream src (io/input-stream content) ^OutputStream dst (io/output-stream full)] - (io/copy! src dst))))) + (io/copy! src dst)) + + object))) (defmethod impl/get-object-data :fs - [{:keys [executor] :as backend} {:keys [id] :as object}] + [{:keys [::wrk/executor] :as backend} {:keys [id] :as object}] + (us/assert! ::backend backend) (px/with-dispatch executor - (let [^Path base (fs/path (:directory backend)) + (let [^Path base (fs/path (::directory backend)) ^Path path (fs/path (impl/id->path id)) ^Path full (fs/normalize (fs/join base path))] (when-not (fs/exists? full) @@ -74,33 +83,37 @@ (defmethod impl/get-object-bytes :fs [backend object] - (p/let [input (impl/get-object-data backend object)] - (try - (io/read-as-bytes input) - (finally - (io/close! input))))) + (->> (impl/get-object-data backend object) + (p/fmap (fn [input] + (try + (io/read-as-bytes input) + (finally + (io/close! input))))))) (defmethod impl/get-object-url :fs - [{:keys [uri executor] :as backend} {:keys [id] :as object} _] - (px/with-dispatch executor - (update uri :path - (fn [existing] - (if (str/ends-with? existing "/") - (str existing (impl/id->path id)) - (str existing "/" (impl/id->path id))))))) + [{:keys [::uri] :as backend} {:keys [id] :as object} _] + (us/assert! ::backend backend) + (p/resolved + (update uri :path + (fn [existing] + (if (str/ends-with? existing "/") + (str existing (impl/id->path id)) + (str existing "/" (impl/id->path id))))))) (defmethod impl/del-object :fs - [{:keys [executor] :as backend} {:keys [id] :as object}] + [{:keys [::wrk/executor] :as backend} {:keys [id] :as object}] + (us/assert! ::backend backend) (px/with-dispatch executor - (let [base (fs/path (:directory backend)) + (let [base (fs/path (::directory backend)) path (fs/path (impl/id->path id)) path (fs/join base path)] (Files/deleteIfExists ^Path path)))) (defmethod impl/del-objects-in-bulk :fs - [{:keys [executor] :as backend} ids] + [{:keys [::wrk/executor] :as backend} ids] + (us/assert! ::backend backend) (px/with-dispatch executor - (let [base (fs/path (:directory backend))] + (let [base (fs/path (::directory backend))] (doseq [id ids] (let [path (fs/path (impl/id->path id)) path (fs/join base path)] diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj index a4b60335b..771ea95e7 100644 --- a/backend/src/app/storage/impl.clj +++ b/backend/src/app/storage/impl.clj @@ -9,9 +9,13 @@ (:require [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.db :as-alias db] + [app.storage :as-alias sto] + [app.worker :as-alias wrk] [buddy.core.codecs :as bc] [buddy.core.hash :as bh] [clojure.java.io :as jio] + [clojure.spec.alpha :as s] [datoteka.io :as io]) (:import java.nio.ByteBuffer @@ -21,7 +25,7 @@ ;; --- API Definition -(defmulti put-object (fn [cfg _ _] (:type cfg))) +(defmulti put-object (fn [cfg _ _] (::sto/type cfg))) (defmethod put-object :default [cfg _ _] @@ -29,7 +33,7 @@ :code :invalid-storage-backend :context cfg)) -(defmulti get-object-data (fn [cfg _] (:type cfg))) +(defmulti get-object-data (fn [cfg _] (::sto/type cfg))) (defmethod get-object-data :default [cfg _] @@ -37,7 +41,7 @@ :code :invalid-storage-backend :context cfg)) -(defmulti get-object-bytes (fn [cfg _] (:type cfg))) +(defmulti get-object-bytes (fn [cfg _] (::sto/type cfg))) (defmethod get-object-bytes :default [cfg _] @@ -45,7 +49,7 @@ :code :invalid-storage-backend :context cfg)) -(defmulti get-object-url (fn [cfg _ _] (:type cfg))) +(defmulti get-object-url (fn [cfg _ _] (::sto/type cfg))) (defmethod get-object-url :default [cfg _ _] @@ -54,7 +58,7 @@ :context cfg)) -(defmulti del-object (fn [cfg _] (:type cfg))) +(defmulti del-object (fn [cfg _] (::sto/type cfg))) (defmethod del-object :default [cfg _] @@ -62,7 +66,7 @@ :code :invalid-storage-backend :context cfg)) -(defmulti del-objects-in-bulk (fn [cfg _] (:type cfg))) +(defmulti del-objects-in-bulk (fn [cfg _] (::sto/type cfg))) (defmethod del-objects-in-bulk :default [cfg _] @@ -189,10 +193,6 @@ (make-output-stream [_ opts] (jio/make-output-stream content opts)))) -(defn content? - [v] - (satisfies? IContentObject v)) - (defn calculate-hash [resource] (let [result (with-open [input (io/input-stream resource)] @@ -201,13 +201,37 @@ (str "blake2b:" result))) (defn resolve-backend - [{:keys [conn pool executor] :as storage} backend-id] - (let [backend (get-in storage [:backends backend-id])] + [{:keys [::db/pool ::wrk/executor] :as storage} backend-id] + (let [backend (get-in storage [::sto/backends backend-id])] (when-not backend (ex/raise :type :internal :code :backend-not-configured :hint (dm/fmt "backend '%' not configured" backend-id))) - (assoc backend - :executor executor - :conn (or conn pool) - :id backend-id))) + (-> backend + (assoc ::sto/id backend-id) + (assoc ::wrk/executor executor) + (assoc ::db/pool pool)))) + +(defrecord StorageObject [id size created-at expired-at touched-at backend]) + +(ns-unmap *ns* '->StorageObject) +(ns-unmap *ns* 'map->StorageObject) + +(defn storage-object + ([id size created-at expired-at touched-at backend] + (StorageObject. id size created-at expired-at touched-at backend)) + ([id size created-at expired-at touched-at backend mdata] + (StorageObject. id size created-at expired-at touched-at backend mdata nil))) + +(defn object? + [v] + (instance? StorageObject v)) + +(defn content? + [v] + (satisfies? IContentObject v)) + +(s/def ::object object?) +(s/def ::content content?) + + diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index 6933b3d41..fc26cccb4 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -8,9 +8,12 @@ "S3 Storage backend implementation." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.spec :as us] [app.common.uri :as u] + [app.storage :as-alias sto] [app.storage.impl :as impl] [app.storage.tmp :as tmp] [app.util.time :as dt] @@ -64,6 +67,9 @@ (declare build-s3-client) (declare build-s3-presigner) +;; (set! *warn-on-reflection* true) +;; (set! *unchecked-math* :warn-on-boxed) + ;; --- BACKEND INIT (s/def ::region ::us/keyword) @@ -72,26 +78,26 @@ (s/def ::endpoint ::us/string) (defmethod ig/pre-init-spec ::backend [_] - (s/keys :opt-un [::region ::bucket ::prefix ::endpoint ::wrk/executor])) + (s/keys :opt [::region ::bucket ::prefix ::endpoint ::wrk/executor])) (defmethod ig/prep-key ::backend - [_ {:keys [prefix region] :as cfg}] + [_ {:keys [::prefix ::region] :as cfg}] (cond-> (d/without-nils cfg) - (some? prefix) (assoc :prefix prefix) - (nil? region) (assoc :region :eu-central-1))) + (some? prefix) (assoc ::prefix prefix) + (nil? region) (assoc ::region :eu-central-1))) (defmethod ig/init-key ::backend [_ cfg] ;; Return a valid backend data structure only if all optional ;; parameters are provided. - (when (and (contains? cfg :region) - (string? (:bucket cfg))) + (when (and (contains? cfg ::region) + (string? (::bucket cfg))) (let [client (build-s3-client cfg) presigner (build-s3-presigner cfg)] (assoc cfg - :client @client - :presigner presigner - :type :s3 + ::sto/type :s3 + ::client @client + ::presigner presigner ::close-fn #(.close ^java.lang.AutoCloseable client))))) (defmethod ig/halt-key! ::backend @@ -99,21 +105,27 @@ (when (fn? close-fn) (px/run! close-fn))) -(s/def ::type ::us/keyword) (s/def ::client #(instance? S3AsyncClient %)) (s/def ::presigner #(instance? S3Presigner %)) (s/def ::backend - (s/keys :req-un [::region ::bucket ::client ::type ::presigner] - :opt-un [::prefix])) + (s/keys :req [::region + ::bucket + ::client + ::presigner] + :opt [::prefix + ::sto/id + ::wrk/executor])) ;; --- API IMPL (defmethod impl/put-object :s3 [backend object content] + (us/assert! ::backend backend) (put-object backend object content)) (defmethod impl/get-object-data :s3 [backend object] + (us/assert! ::backend backend) (letfn [(no-such-key? [cause] (instance? software.amazon.awssdk.services.s3.model.NoSuchKeyException cause)) (handle-not-found [cause] @@ -127,18 +139,22 @@ (defmethod impl/get-object-bytes :s3 [backend object] + (us/assert! ::backend backend) (get-object-bytes backend object)) (defmethod impl/get-object-url :s3 [backend object options] + (us/assert! ::backend backend) (get-object-url backend object options)) (defmethod impl/del-object :s3 [backend object] + (us/assert! ::backend backend) (del-object backend object)) (defmethod impl/del-objects-in-bulk :s3 [backend ids] + (us/assert! ::backend backend) (del-object-in-bulk backend ids)) ;; --- HELPERS @@ -152,8 +168,8 @@ [region] (Region/of (name region))) -(defn build-s3-client - [{:keys [region endpoint executor]}] +(defn- build-s3-client + [{:keys [::region ::endpoint ::wrk/executor]}] (let [aconfig (-> (ClientAsyncConfiguration/builder) (.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor) (.build)) @@ -188,8 +204,8 @@ (.close ^NettyNioAsyncHttpClient hclient) (.close ^S3AsyncClient client))))) -(defn build-s3-presigner - [{:keys [region endpoint]}] +(defn- build-s3-presigner + [{:keys [::region ::endpoint]}] (let [config (-> (S3Configuration/builder) (cond-> (some? endpoint) (.pathStyleAccessEnabled true)) (.build))] @@ -200,65 +216,87 @@ (.serviceConfiguration ^S3Configuration config) (.build)))) +(defn- upload-thread + [id subscriber sem content] + (px/thread + {:name "penpot/s3/uploader" + :daemon true} + (l/trace :hint "start upload thread" + :object-id (str id) + :size (impl/get-size content) + ::l/sync? true) + (let [stream (io/input-stream content) + bsize (* 1024 64) + tpoint (dt/tpoint)] + (try + (loop [] + (.acquire ^Semaphore sem 1) + (let [buffer (byte-array bsize) + readed (.read ^InputStream stream buffer)] + (when (pos? readed) + (let [data (ByteBuffer/wrap ^bytes buffer 0 readed)] + (.onNext ^Subscriber subscriber ^ByteBuffer data) + (when (= readed bsize) + (recur)))))) + (.onComplete ^Subscriber subscriber) + (catch InterruptedException _ + (l/trace :hint "interrupted upload thread" + :object-:id (str id) + ::l/sync? true) + nil) + (catch Throwable cause + (.onError ^Subscriber subscriber cause)) + (finally + (l/trace :hint "end upload thread" + :object-id (str id) + :elapsed (dt/format-duration (tpoint)) + ::l/sync? true) + (.close ^InputStream stream)))))) + (defn- make-request-body - [content] - (let [is (io/input-stream content) - buff-size (* 1024 64) - sem (Semaphore. 0) + [id content] + (reify + AsyncRequestBody + (contentLength [_] + (Optional/of (long (impl/get-size content)))) - writer-fn (fn [^Subscriber s] - (try - (loop [] - (.acquire sem 1) - (let [buffer (byte-array buff-size) - readed (.read is buffer)] - (when (pos? readed) - (.onNext ^Subscriber s (ByteBuffer/wrap buffer 0 readed)) - (when (= readed buff-size) - (recur))))) - (.onComplete s) - (catch Throwable cause - (.onError s cause)) - (finally - (.close ^InputStream is))))] - - (reify - AsyncRequestBody - (contentLength [_] - (Optional/of (long (impl/get-size content)))) - - (^void subscribe [_ ^Subscriber s] - (let [thread (Thread. #(writer-fn s))] - (.setDaemon thread true) - (.setName thread "penpot/storage:s3") - (.start thread) - - (.onSubscribe s (reify Subscription - (cancel [_] - (.interrupt thread) - (.release sem 1)) - (request [_ n] - (.release sem (int n)))))))))) + (^void subscribe [_ ^Subscriber subscriber] + (let [sem (Semaphore. 0) + thr (upload-thread id subscriber sem content)] + (.onSubscribe subscriber + (reify Subscription + (cancel [_] + (px/interrupt! thr) + (.release sem 1)) + (request [_ n] + (.release sem (int n))))))))) -(defn put-object - [{:keys [client bucket prefix]} {:keys [id] :as object} content] - (p/let [path (str prefix (impl/id->path id)) - mdata (meta object) - mtype (:content-type mdata "application/octet-stream") - request (.. (PutObjectRequest/builder) - (bucket bucket) - (contentType mtype) - (key path) - (build))] +(defn- put-object + [{:keys [::client ::bucket ::prefix]} {:keys [id] :as object} content] + (let [path (dm/str prefix (impl/id->path id)) + mdata (meta object) + mtype (:content-type mdata "application/octet-stream") + rbody (make-request-body id content) + request (.. (PutObjectRequest/builder) + (bucket bucket) + (contentType mtype) + (key path) + (build))] + (->> (.putObject ^S3AsyncClient client + ^PutObjectRequest request + ^AsyncRequestBody rbody) + (p/fmap (constantly object))))) - (let [content (make-request-body content)] - (.putObject ^S3AsyncClient client - ^PutObjectRequest request - ^AsyncRequestBody content)))) +(defn- path->stream + [path] + (proxy [FilterInputStream] [(io/input-stream path)] + (close [] + (fs/delete path) + (proxy-super close)))) -(defn get-object-data - [{:keys [client bucket prefix]} {:keys [id size]}] +(defn- get-object-data + [{:keys [::client ::bucket ::prefix]} {:keys [id size]}] (let [gor (.. (GetObjectRequest/builder) (bucket bucket) (key (str prefix (impl/id->path id))) @@ -267,83 +305,83 @@ ;; If the file size is greater than 2MiB then stream the content ;; to the filesystem and then read with buffered inputstream; if ;; not, read the contento into memory using bytearrays. - (if (> size (* 1024 1024 2)) - (p/let [path (tmp/tempfile :prefix "penpot.storage.s3.") - rxf (AsyncResponseTransformer/toFile ^Path path) - _ (.getObject ^S3AsyncClient client - ^GetObjectRequest gor - ^AsyncResponseTransformer rxf)] - (proxy [FilterInputStream] [(io/input-stream path)] - (close [] - (fs/delete path) - (proxy-super close)))) + (if (> ^long size (* 1024 1024 2)) + (let [path (tmp/tempfile :prefix "penpot.storage.s3.") + rxf (AsyncResponseTransformer/toFile ^Path path)] + (->> (.getObject ^S3AsyncClient client + ^GetObjectRequest gor + ^AsyncResponseTransformer rxf) + (p/fmap (constantly path)) + (p/fmap path->stream))) - (p/let [rxf (AsyncResponseTransformer/toBytes) - obj (.getObject ^S3AsyncClient client - ^GetObjectRequest gor - ^AsyncResponseTransformer rxf)] - (.asInputStream ^ResponseBytes obj))))) + (let [rxf (AsyncResponseTransformer/toBytes)] + (->> (.getObject ^S3AsyncClient client + ^GetObjectRequest gor + ^AsyncResponseTransformer rxf) + (p/fmap #(.asInputStream ^ResponseBytes %))))))) -(defn get-object-bytes - [{:keys [client bucket prefix]} {:keys [id]}] - (p/let [gor (.. (GetObjectRequest/builder) - (bucket bucket) - (key (str prefix (impl/id->path id))) - (build)) - rxf (AsyncResponseTransformer/toBytes) - obj (.getObject ^S3AsyncClient client - ^GetObjectRequest gor - ^AsyncResponseTransformer rxf)] - (.asByteArray ^ResponseBytes obj))) +(defn- get-object-bytes + [{:keys [::client ::bucket ::prefix]} {:keys [id]}] + (let [gor (.. (GetObjectRequest/builder) + (bucket bucket) + (key (str prefix (impl/id->path id))) + (build)) + rxf (AsyncResponseTransformer/toBytes)] + (->> (.getObject ^S3AsyncClient client + ^GetObjectRequest gor + ^AsyncResponseTransformer rxf) + (p/fmap #(.asByteArray ^ResponseBytes %))))) (def default-max-age (dt/duration {:minutes 10})) -(defn get-object-url - [{:keys [presigner bucket prefix]} {:keys [id]} {:keys [max-age] :or {max-age default-max-age}}] +(defn- get-object-url + [{:keys [::presigner ::bucket ::prefix]} {:keys [id]} {:keys [max-age] :or {max-age default-max-age}}] (us/assert dt/duration? max-age) - (p/do - (let [gor (.. (GetObjectRequest/builder) - (bucket bucket) - (key (str prefix (impl/id->path id))) - (build)) - gopr (.. (GetObjectPresignRequest/builder) - (signatureDuration ^Duration max-age) - (getObjectRequest ^GetObjectRequest gor) - (build)) - pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)] - (u/uri (str (.url ^PresignedGetObjectRequest pgor)))))) + (let [gor (.. (GetObjectRequest/builder) + (bucket bucket) + (key (dm/str prefix (impl/id->path id))) + (build)) + gopr (.. (GetObjectPresignRequest/builder) + (signatureDuration ^Duration max-age) + (getObjectRequest ^GetObjectRequest gor) + (build)) + pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)] + (p/resolved + (u/uri (str (.url ^PresignedGetObjectRequest pgor)))))) -(defn del-object - [{:keys [bucket client prefix]} {:keys [id] :as obj}] - (p/let [dor (.. (DeleteObjectRequest/builder) - (bucket bucket) - (key (str prefix (impl/id->path id))) - (build))] - (.deleteObject ^S3AsyncClient client - ^DeleteObjectRequest dor))) +(defn- del-object + [{:keys [::bucket ::client ::prefix]} {:keys [id] :as obj}] + (let [dor (.. (DeleteObjectRequest/builder) + (bucket bucket) + (key (dm/str prefix (impl/id->path id))) + (build))] + (->> (.deleteObject ^S3AsyncClient client ^DeleteObjectRequest dor) + (p/fmap (constantly nil))))) -(defn del-object-in-bulk - [{:keys [bucket client prefix]} ids] - (p/let [oids (map (fn [id] - (.. (ObjectIdentifier/builder) - (key (str prefix (impl/id->path id))) - (build))) - ids) - delc (.. (Delete/builder) - (objects ^Collection oids) - (build)) - dor (.. (DeleteObjectsRequest/builder) - (bucket bucket) - (delete ^Delete delc) - (build)) - dres (.deleteObjects ^S3AsyncClient client - ^DeleteObjectsRequest dor)] - (when (.hasErrors ^DeleteObjectsResponse dres) - (let [errors (seq (.errors ^DeleteObjectsResponse dres))] - (ex/raise :type :internal - :code :error-on-s3-bulk-delete - :s3-errors (mapv (fn [^S3Error error] - {:key (.key error) - :msg (.message error)}) - errors)))))) +(defn- del-object-in-bulk + [{:keys [::bucket ::client ::prefix]} ids] + + (let [oids (map (fn [id] + (.. (ObjectIdentifier/builder) + (key (str prefix (impl/id->path id))) + (build))) + ids) + delc (.. (Delete/builder) + (objects ^Collection oids) + (build)) + dor (.. (DeleteObjectsRequest/builder) + (bucket bucket) + (delete ^Delete delc) + (build))] + + (->> (.deleteObjects ^S3AsyncClient client ^DeleteObjectsRequest dor) + (p/fmap (fn [dres] + (when (.hasErrors ^DeleteObjectsResponse dres) + (let [errors (seq (.errors ^DeleteObjectsResponse dres))] + (ex/raise :type :internal + :code :error-on-s3-bulk-delete + :s3-errors (mapv (fn [^S3Error error] + {:key (.key error) + :msg (.message error)}) + errors))))))))) diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index 81d14eeca..f673213b3 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -32,27 +32,24 @@ ;; HANDLER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::min-age ::dt/duration) - (defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::db/pool ::min-age])) + (s/keys :req [::db/pool])) (defmethod ig/prep-key ::handler [_ cfg] - (merge {:min-age cf/deletion-delay} - (d/without-nils cfg))) + (assoc cfg ::min-age cf/deletion-delay)) (defmethod ig/init-key ::handler - [_ {:keys [pool] :as cfg}] + [_ {:keys [::db/pool] :as cfg}] (fn [{:keys [file-id] :as params}] (db/with-atomic [conn pool] - (let [min-age (or (:min-age params) (:min-age cfg)) - cfg (assoc cfg :min-age min-age :conn conn :file-id file-id)] + (let [min-age (or (:min-age params) (::min-age cfg)) + cfg (assoc cfg ::min-age min-age ::conn conn ::file-id file-id)] (loop [total 0 files (retrieve-candidates cfg)] (if-let [file (first files)] (do - (process-file cfg file) + (process-file conn file) (recur (inc total) (rest files))) (do @@ -84,7 +81,7 @@ for update skip locked") (defn- retrieve-candidates - [{:keys [conn min-age file-id] :as cfg}] + [{:keys [::conn ::min-age ::file-id]}] (if (uuid? file-id) (do (l/warn :hint "explicit file id passed on params" :file-id file-id) @@ -256,7 +253,7 @@ (db/delete! conn :file-data-fragment {:id fragment-id :file-id file-id})))) (defn- process-file - [{:keys [conn] :as cfg} {:keys [id data revn modified-at features] :as file}] + [conn {:keys [id data revn modified-at features] :as file}] (l/debug :hint "processing file" :id id :modified-at modified-at) (binding [pmap/*load-fn* (partial files/load-pointer conn id)] diff --git a/backend/src/app/tasks/file_xlog_gc.clj b/backend/src/app/tasks/file_xlog_gc.clj index 561f0548b..5ee2e1bbc 100644 --- a/backend/src/app/tasks/file_xlog_gc.clj +++ b/backend/src/app/tasks/file_xlog_gc.clj @@ -8,42 +8,36 @@ "A maintenance task that performs a garbage collection of the file change (transaction) log." (:require - [app.common.data :as d] [app.common.logging :as l] [app.db :as db] [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig])) -(declare sql:delete-files-xlog) - -(s/def ::min-age ::dt/duration) +(def ^:private + sql:delete-files-xlog + "delete from file_change + where created_at < now() - ?::interval") (defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::db/pool] - :opt-un [::min-age])) + (s/keys :req [::db/pool])) (defmethod ig/prep-key ::handler [_ cfg] - (merge {:min-age (dt/duration {:hours 72})} - (d/without-nils cfg))) + (assoc cfg ::min-age (dt/duration {:hours 72}))) (defmethod ig/init-key ::handler - [_ {:keys [pool] :as cfg}] + [_ {:keys [::db/pool] :as cfg}] (fn [params] - (let [min-age (or (:min-age params) (:min-age cfg))] + (let [min-age (or (:min-age params) (::min-age cfg))] (db/with-atomic [conn pool] (let [interval (db/interval min-age) result (db/exec-one! conn [sql:delete-files-xlog interval]) - result (:next.jdbc/update-count result)] + result (db/get-update-count result)] + (l/info :hint "task finished" :min-age (dt/format-duration min-age) :total result) (when (:rollback? params) (db/rollback! conn)) result))))) - -(def ^:private - sql:delete-files-xlog - "delete from file_change - where created_at < now() - ?::interval") diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj index eacd52341..4169cd88f 100644 --- a/backend/src/app/tasks/objects_gc.clj +++ b/backend/src/app/tasks/objects_gc.clj @@ -25,16 +25,12 @@ (declare ^:private delete-files!) (declare ^:private delete-orphan-teams!) -(s/def ::min-age ::dt/duration) - (defmethod ig/pre-init-spec ::handler [_] - (s/keys :req [::db/pool ::sto/storage] - :opt [::min-age])) + (s/keys :req [::db/pool ::sto/storage])) (defmethod ig/prep-key ::handler [_ cfg] - (merge {::min-age cf/deletion-delay} - (d/without-nils cfg))) + (assoc cfg ::min-age cf/deletion-delay)) (defmethod ig/init-key ::handler [_ {:keys [::db/pool ::sto/storage] :as cfg}] @@ -133,7 +129,6 @@ :kf first :initk (dt/now))))) - (def ^:private sql:get-orphan-teams-chunk "select t.id, t.created_at from team as t @@ -154,14 +149,15 @@ [(some->> rows peek :created-at) rows]))] (reduce (fn [total {:keys [id]}] - (l/debug :hint "mark team for deletion" :id (str id)) + (let [result (db/update! conn :team + {:deleted-at (dt/now)} + {:id id :deleted-at nil} + {::db/return-keys? false}) + count (db/get-update-count result)] + (when (pos? count) + (l/debug :hint "mark team for deletion" :id (str id) )) - ;; And finally, permanently delete the team. - (db/update! conn :team - {:deleted-at (dt/now)} - {:id id}) - - (inc total)) + (+ total count))) 0 (d/iteration get-chunk :vf second diff --git a/backend/src/app/tasks/tasks_gc.clj b/backend/src/app/tasks/tasks_gc.clj index 81155f494..69dd11dfd 100644 --- a/backend/src/app/tasks/tasks_gc.clj +++ b/backend/src/app/tasks/tasks_gc.clj @@ -8,35 +8,33 @@ "A maintenance task that performs a cleanup of already executed tasks from the database table." (:require - [app.common.data :as d] [app.common.logging :as l] [app.config :as cf] [app.db :as db] - [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig])) -(declare sql:delete-completed-tasks) - -(s/def ::min-age ::dt/duration) +(def ^:private + sql:delete-completed-tasks + "delete from task_completed + where scheduled_at < now() - ?::interval") (defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::db/pool] - :opt-un [::min-age])) + (s/keys :req [::db/pool])) (defmethod ig/prep-key ::handler [_ cfg] - (merge {:min-age cf/deletion-delay} - (d/without-nils cfg))) + (assoc cfg ::min-age cf/deletion-delay)) (defmethod ig/init-key ::handler - [_ {:keys [pool] :as cfg}] + [_ {:keys [::db/pool ::min-age] :as cfg}] (fn [params] - (let [min-age (or (:min-age params) (:min-age cfg))] + (let [min-age (or (:min-age params) min-age)] (db/with-atomic [conn pool] (let [interval (db/interval min-age) result (db/exec-one! conn [sql:delete-completed-tasks interval]) - result (:next.jdbc/update-count result)] + result (db/get-update-count result)] + (l/debug :hint "task finished" :total result) (when (:rollback? params) @@ -44,7 +42,3 @@ result))))) -(def ^:private - sql:delete-completed-tasks - "delete from task_completed - where scheduled_at < now() - ?::interval") diff --git a/backend/src/app/util/migrations.clj b/backend/src/app/util/migrations.clj index 76db6660f..3037ac923 100644 --- a/backend/src/app/util/migrations.clj +++ b/backend/src/app/util/migrations.clj @@ -13,7 +13,7 @@ (s/def ::name string?) (s/def ::step (s/keys :req-un [::name ::fn])) -(s/def ::steps (s/every ::step :kind vector?)) +(s/def ::steps (s/every ::step)) (s/def ::migrations (s/keys :req-un [::name ::steps])) diff --git a/backend/src/app/util/retry.clj b/backend/src/app/util/retry.clj index 666a09f47..cd3ae6d91 100644 --- a/backend/src/app/util/retry.clj +++ b/backend/src/app/util/retry.clj @@ -29,6 +29,6 @@ (throw cause#))))] (if (= ::retry result#) (do - (l/warn :hint "retrying operation" :label ~label) + (l/warn :hint "retrying operation" :label ~label :retry tnum#) (recur (inc tnum#))) result#)))) diff --git a/backend/src/app/util/websocket.clj b/backend/src/app/util/websocket.clj index 07dc5a52a..5f8ec55c5 100644 --- a/backend/src/app/util/websocket.clj +++ b/backend/src/app/util/websocket.clj @@ -60,7 +60,7 @@ (assert (fn? on-snd-message) "'on-snd-message' should be a function") (assert (fn? on-connect) "'on-connect' should be a function") - (fn [{:keys [::yws/channel session-id] :as request}] + (fn [{:keys [::yws/channel] :as request}] (let [input-ch (a/chan input-buff-size) output-ch (a/chan output-buff-size) hbeat-ch (a/chan (a/sliding-buffer 6)) @@ -81,7 +81,6 @@ ::stop-ch stop-ch ::channel channel ::remote-addr ip-addr - ::http-session-id session-id ::user-agent uagent}) (atom)) @@ -243,7 +242,7 @@ (let [result (a/! output-ch {:type :error :error (ex-data result)}) (ex/exception? result) diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj index 900988a7a..8f4b9a50f 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -45,7 +45,7 @@ (defmethod ig/init-key ::executor [skey {:keys [::parallelism]}] - (let [prefix (if (vector? skey) (-> skey first name keyword) :default) + (let [prefix (if (vector? skey) (-> skey first name) "default") tname (str "penpot/" prefix "/%s") factory (px/forkjoin-thread-factory :name tname)] (px/forkjoin-executor @@ -90,10 +90,10 @@ (s/def ::registry (s/map-of ::us/string fn?)) (defmethod ig/pre-init-spec ::registry [_] - (s/keys :req-un [::mtx/metrics ::tasks])) + (s/keys :req [::mtx/metrics ::tasks])) (defmethod ig/init-key ::registry - [_ {:keys [metrics tasks]}] + [_ {:keys [::mtx/metrics ::tasks]}] (l/info :hint "registry initialized" :tasks (count tasks)) (reduce-kv (fn [registry k v] (let [tname (name k)] diff --git a/backend/test/backend_tests/bounce_handling_test.clj b/backend/test/backend_tests/bounce_handling_test.clj index e0c094969..efb03d136 100644 --- a/backend/test/backend_tests/bounce_handling_test.clj +++ b/backend/test/backend_tests/bounce_handling_test.clj @@ -6,12 +6,12 @@ (ns backend-tests.bounce-handling-test (:require - [backend-tests.helpers :as th] [app.db :as db] - [app.emails :as emails] + [app.email :as email] [app.http.awsns :as awsns] [app.tokens :as tokens] [app.util.time :as dt] + [backend-tests.helpers :as th] [clojure.pprint :refer [pprint]] [clojure.test :as t] [mockery.core :refer [with-mocks]])) @@ -261,11 +261,11 @@ (th/create-complaint-for pool {:type :bounce :id (:id profile)}) (th/create-complaint-for pool {:type :bounce :id (:id profile)}) - (t/is (true? (emails/allow-send-emails? pool profile))) + (t/is (true? (email/allow-send-emails? pool profile))) (t/is (= 4 (:call-count @mock))) (th/create-complaint-for pool {:type :bounce :id (:id profile)}) - (t/is (false? (emails/allow-send-emails? pool profile)))))) + (t/is (false? (email/allow-send-emails? pool profile)))))) (t/deftest test-allow-send-messages-predicate-with-complaints @@ -281,32 +281,32 @@ (th/create-complaint-for pool {:type :bounce :id (:id profile)}) (th/create-complaint-for pool {:type :complaint :id (:id profile)}) - (t/is (true? (emails/allow-send-emails? pool profile))) + (t/is (true? (email/allow-send-emails? pool profile))) (t/is (= 4 (:call-count @mock))) (th/create-complaint-for pool {:type :complaint :id (:id profile)}) - (t/is (false? (emails/allow-send-emails? pool profile)))))) + (t/is (false? (email/allow-send-emails? pool profile)))))) (t/deftest test-has-complaint-reports-predicate (let [profile (th/create-profile* 1) pool (:app.db/pool th/*system*)] - (t/is (false? (emails/has-complaint-reports? pool (:email profile)))) + (t/is (false? (email/has-complaint-reports? pool (:email profile)))) (th/create-global-complaint-for pool {:type :bounce :email (:email profile)}) - (t/is (false? (emails/has-complaint-reports? pool (:email profile)))) + (t/is (false? (email/has-complaint-reports? pool (:email profile)))) (th/create-global-complaint-for pool {:type :complaint :email (:email profile)}) - (t/is (true? (emails/has-complaint-reports? pool (:email profile)))))) + (t/is (true? (email/has-complaint-reports? pool (:email profile)))))) (t/deftest test-has-bounce-reports-predicate (let [profile (th/create-profile* 1) pool (:app.db/pool th/*system*)] - (t/is (false? (emails/has-bounce-reports? pool (:email profile)))) + (t/is (false? (email/has-bounce-reports? pool (:email profile)))) (th/create-global-complaint-for pool {:type :complaint :email (:email profile)}) - (t/is (false? (emails/has-bounce-reports? pool (:email profile)))) + (t/is (false? (email/has-bounce-reports? pool (:email profile)))) (th/create-global-complaint-for pool {:type :bounce :email (:email profile)}) - (t/is (true? (emails/has-bounce-reports? pool (:email profile)))))) + (t/is (true? (email/has-bounce-reports? pool (:email profile)))))) diff --git a/backend/test/backend_tests/email_sending_test.clj b/backend/test/backend_tests/email_sending_test.clj index 49802dd9e..8d572bc81 100644 --- a/backend/test/backend_tests/email_sending_test.clj +++ b/backend/test/backend_tests/email_sending_test.clj @@ -8,7 +8,7 @@ (:require [backend-tests.helpers :as th] [app.db :as db] - [app.emails :as emails] + [app.email :as emails] [clojure.test :as t] [promesa.core :as p])) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index a14dd4374..d81b30cb3 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -8,6 +8,7 @@ (:require [app.auth] [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.flags :as flags] [app.common.pages :as cp] [app.common.pprint :as pp] @@ -16,13 +17,15 @@ [app.config :as cf] [app.db :as db] [app.main :as main] + [app.media :as-alias mtx] [app.media] [app.migrations] + [app.msgbus :as-alias mbus] [app.rpc :as-alias rpc] [app.rpc.commands.auth :as cmd.auth] [app.rpc.commands.files :as files] - [app.rpc.commands.files.create :as files.create] - [app.rpc.commands.files.update :as files.update] + [app.rpc.commands.files-create :as files.create] + [app.rpc.commands.files-update :as files.update] [app.rpc.commands.teams :as teams] [app.rpc.helpers :as rph] [app.rpc.mutations.profile :as profile] @@ -64,52 +67,50 @@ (defn state-init [next] - (let [templates [{:id "test" - :name "test" - :file-uri "test" - :thumbnail-uri "test" - :path (-> "backend_tests/test_files/template.penpot" io/resource fs/path)}] - system (-> (merge main/system-config main/worker-config) - (assoc-in [:app.redis/redis :app.redis/uri] (:redis-uri config)) - (assoc-in [:app.db/pool :uri] (:database-uri config)) - (assoc-in [:app.db/pool :username] (:database-username config)) - (assoc-in [:app.db/pool :password] (:database-password config)) - (assoc-in [:app.rpc/methods :templates] templates) - (dissoc :app.srepl/server - :app.http/server - :app.http/router - :app.http.awsns/handler - :app.http.session/updater - :app.auth.oidc/google-provider - :app.auth.oidc/gitlab-provider - :app.auth.oidc/github-provider - :app.auth.oidc/generic-provider - :app.setup/builtin-templates - :app.auth.oidc/routes - :app.worker/executors-monitor - :app.http.oauth/handler - :app.notifications/handler - :app.loggers.sentry/reporter - :app.loggers.mattermost/reporter - :app.loggers.loki/reporter - :app.loggers.database/reporter - :app.loggers.zmq/receiver - :app.worker/cron - :app.worker/worker)) - _ (ig/load-namespaces system) - system (-> (ig/prep system) - (ig/init))] - (try - (binding [*system* system - *pool* (:app.db/pool system)] - (with-redefs [app.config/flags (flags/parse flags/default default-flags (:flags config)) - app.config/config config - app.loggers.audit/submit! (constantly nil) - app.auth/derive-password identity - app.auth/verify-password (fn [a b] {:valid (= a b)})] - (next))) - (finally - (ig/halt! system))))) + (with-redefs [app.config/flags (flags/parse flags/default default-flags) + app.config/config config + app.loggers.audit/submit! (constantly nil) + app.auth/derive-password identity + app.auth/verify-password (fn [a b] {:valid (= a b)})] + + (let [templates [{:id "test" + :name "test" + :file-uri "test" + :thumbnail-uri "test" + :path (-> "backend_tests/test_files/template.penpot" io/resource fs/path)}] + system (-> (merge main/system-config main/worker-config) + (assoc-in [:app.redis/redis :app.redis/uri] (:redis-uri config)) + (assoc-in [::db/pool ::db/uri] (:database-uri config)) + (assoc-in [::db/pool ::db/username] (:database-username config)) + (assoc-in [::db/pool ::db/password] (:database-password config)) + (assoc-in [:app.rpc/methods :templates] templates) + (dissoc :app.srepl/server + :app.http/server + :app.http/router + :app.auth.oidc/google-provider + :app.auth.oidc/gitlab-provider + :app.auth.oidc/github-provider + :app.auth.oidc/generic-provider + :app.setup/builtin-templates + :app.auth.oidc/routes + :app.worker/executors-monitor + :app.http.oauth/handler + :app.notifications/handler + :app.loggers.mattermost/reporter + :app.loggers.loki/reporter + :app.loggers.database/reporter + :app.loggers.zmq/receiver + :app.worker/cron + :app.worker/worker)) + _ (ig/load-namespaces system) + system (-> (ig/prep system) + (ig/init))] + (try + (binding [*system* system + *pool* (:app.db/pool system)] + (next)) + (finally + (ig/halt! system)))))) (defn database-reset [next] @@ -163,8 +164,8 @@ params)] (with-open [conn (db/open pool)] (->> params - (cmd.auth/create-profile conn) - (cmd.auth/create-profile-relations conn)))))) + (cmd.auth/create-profile! conn) + (cmd.auth/create-profile-rels! conn)))))) (defn create-project* ([i params] (create-project* *pool* i params)) @@ -274,12 +275,10 @@ ([pool {:keys [file-id changes session-id profile-id revn] :or {session-id (uuid/next) revn 0}}] (with-open [conn (db/open pool)] - (let [msgbus (:app.msgbus/msgbus *system*) - metrics (:app.metrics/metrics *system*) - features #{"components/v2"}] - (files.update/update-file {:conn conn - :msgbus msgbus - :metrics metrics} + (let [features #{"components/v2"} + cfg (-> (select-keys *system* [::mbus/msgbus ::mtx/metrics]) + (assoc :conn conn))] + (files.update/update-file cfg {:id file-id :revn revn :features features @@ -322,6 +321,11 @@ (defn command! [{:keys [::type] :as data}] (let [method-fn (get-in *system* [:app.rpc/methods :commands type])] + (when-not method-fn + (ex/raise :type :assertion + :code :rpc-method-not-found + :hint (str/ffmt "rpc method '%' not found" (name type)))) + ;; (app.common.pprint/pprint (:app.rpc/methods *system*)) (try-on! (method-fn (-> data (dissoc ::type) @@ -386,7 +390,7 @@ (defn ex-info? [v] - (instance? clojure.lang.ExceptionInfo v)) + (ex/error? v)) (defn ex-type [e] diff --git a/backend/test/backend_tests/rpc_access_tokens_test.clj b/backend/test/backend_tests/rpc_access_tokens_test.clj new file mode 100644 index 000000000..7868eb179 --- /dev/null +++ b/backend/test/backend_tests/rpc_access_tokens_test.clj @@ -0,0 +1,76 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns backend-tests.rpc-access-tokens-test + (:require + [app.common.uuid :as uuid] + [app.db :as db] + [app.http :as http] + [app.storage :as sto] + [app.rpc :as-alias rpc] + [backend-tests.helpers :as th] + [clojure.test :as t] + [mockery.core :refer [with-mocks]])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest access-tokens-crud + (let [prof (th/create-profile* 1 {:is-active true}) + team-id (:default-team-id prof) + proj-id (:default-project-id prof) + atoken (atom nil)] + + (t/testing "create access token" + (let [params {::th/type :create-access-token + ::rpc/profile-id (:id prof) + :name "token 1" + :perms ["get-profile"]} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (reset! atoken result) + (t/is (contains? result :id)) + (t/is (contains? result :created-at)) + (t/is (contains? result :updated-at)) + (t/is (contains? result :token)) + (t/is (contains? result :perms))))) + + (t/testing "get access token" + (let [params {::th/type :get-access-tokens + ::rpc/profile-id (:id prof)} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [[result :as results] (:result out)] + (t/is (= 1 (count results))) + (t/is (contains? result :id)) + (t/is (contains? result :created-at)) + (t/is (contains? result :updated-at)) + (t/is (contains? result :token)) + (t/is (contains? result :perms)) + (t/is (= @atoken result))))) + + (t/testing "delete access token" + (let [params {::th/type :delete-access-token + ::rpc/profile-id (:id prof) + :id (:id @atoken)} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))))) + + (t/testing "get access token after delete" + (let [params {::th/type :get-access-tokens + ::rpc/profile-id (:id prof)} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [results (:result out)] + (t/is (= 0 (count results)))))) + )) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 7e906dfbb..189f1e631 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -652,7 +652,9 @@ ;; check that the unknown frame thumbnail is deleted (let [res (th/db-exec! ["select * from file_object_thumbnail"])] (t/is (= 1 (count res))) - (t/is (= "new-data" (get-in res [0 :data]))))))) + (t/is (= "new-data" (get-in res [0 :data]))))) + + )) (t/deftest file-thumbnail-ops diff --git a/backend/test/backend_tests/rpc_font_test.clj b/backend/test/backend_tests/rpc_font_test.clj index bf27cba90..d1c3bdd60 100644 --- a/backend/test/backend_tests/rpc_font_test.clj +++ b/backend/test/backend_tests/rpc_font_test.clj @@ -9,6 +9,7 @@ [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] + [app.rpc :as-alias rpc] [app.storage :as sto] [backend-tests.helpers :as th] [clojure.test :as t] @@ -31,14 +32,14 @@ io/read-as-bytes) params {::th/type :create-font-variant - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :team-id team-id :font-id font-id :font-family "somefont" :font-weight 400 :font-style "normal" :data {"font/ttf" ttfdata}} - out (th/mutation! params)] + out (th/command! params)] (t/is (= 1 (:call-count @mock))) @@ -68,14 +69,14 @@ io/read-as-bytes) params {::th/type :create-font-variant - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :team-id team-id :font-id font-id :font-family "somefont" :font-weight 400 :font-style "normal" :data {"font/woff" data}} - out (th/mutation! params)] + out (th/command! params)] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -91,8 +92,3 @@ :font-family :font-weight :font-style)))) - - - - - diff --git a/backend/test/backend_tests/rpc_media_test.clj b/backend/test/backend_tests/rpc_media_test.clj index 138ac4d6a..ab2cd1de9 100644 --- a/backend/test/backend_tests/rpc_media_test.clj +++ b/backend/test/backend_tests/rpc_media_test.clj @@ -44,8 +44,8 @@ (let [storage (:app.storage/storage th/*system*) mobj1 @(sto/get-object storage media-id) mobj2 @(sto/get-object storage thumbnail-id)] - (t/is (sto/storage-object? mobj1)) - (t/is (sto/storage-object? mobj2)) + (t/is (sto/object? mobj1)) + (t/is (sto/object? mobj2)) (t/is (= 122785 (:size mobj1))) ;; This is because in ubuntu 21.04 generates different ;; thumbnail that in ubuntu 22.04. This hack should be removed @@ -85,8 +85,8 @@ (let [storage (:app.storage/storage th/*system*) mobj1 @(sto/get-object storage media-id) mobj2 @(sto/get-object storage thumbnail-id)] - (t/is (sto/storage-object? mobj1)) - (t/is (sto/storage-object? mobj2)) + (t/is (sto/object? mobj1)) + (t/is (sto/object? mobj2)) (t/is (= 312043 (:size mobj1))) (t/is (= 3887 (:size mobj2))))) )) @@ -164,8 +164,8 @@ (let [storage (:app.storage/storage th/*system*) mobj1 @(sto/get-object storage media-id) mobj2 @(sto/get-object storage thumbnail-id)] - (t/is (sto/storage-object? mobj1)) - (t/is (sto/storage-object? mobj2)) + (t/is (sto/object? mobj1)) + (t/is (sto/object? mobj2)) (t/is (= 122785 (:size mobj1))) ;; This is because in ubuntu 21.04 generates different ;; thumbnail that in ubuntu 22.04. This hack should be removed @@ -205,8 +205,8 @@ (let [storage (:app.storage/storage th/*system*) mobj1 @(sto/get-object storage media-id) mobj2 @(sto/get-object storage thumbnail-id)] - (t/is (sto/storage-object? mobj1)) - (t/is (sto/storage-object? mobj2)) + (t/is (sto/object? mobj1)) + (t/is (sto/object? mobj2)) (t/is (= 312043 (:size mobj1))) (t/is (= 3887 (:size mobj2))))) )) diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 56a67c029..afc13388c 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -6,14 +6,14 @@ (ns backend-tests.rpc-profile-test (:require - [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.rpc :as-alias rpc] [app.rpc.commands.auth :as cauth] - [app.rpc.mutations.profile :as profile] [app.tokens :as tokens] [app.util.time :as dt] + [backend-tests.helpers :as th] [clojure.java.io :as io] [clojure.test :as t] [cuerdas.core :as str] @@ -67,9 +67,9 @@ (t/deftest profile-query-and-manipulation (let [profile (th/create-profile* 1)] (t/testing "query profile" - (let [data {::th/type :profile - :profile-id (:id profile)} - out (th/query! data)] + (let [data {::th/type :get-profile + ::rpc/profile-id (:id profile)} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -82,20 +82,20 @@ (t/testing "update profile" (let [data (assoc profile ::th/type :update-profile - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :fullname "Full Name" :lang "en" :theme "dark") - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (map? (:result out))))) (t/testing "query profile after update" - (let [data {::th/type :profile - :profile-id (:id profile)} - out (th/query! data)] + (let [data {::th/type :get-profile + ::rpc/profile-id (:id profile)} + out (th/command! data)] #_(th/print-result! out) (t/is (nil? (:error out))) @@ -107,12 +107,12 @@ (t/testing "update photo" (let [data {::th/type :update-profile-photo - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :file {:filename "sample.jpg" :size 123123 :path (th/tempfile "backend_tests/test_files/sample.jpg") :mtype "image/jpeg"}} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))))) @@ -131,15 +131,15 @@ ;; Request profile to be deleted (let [params {::th/type :delete-profile - :profile-id (:id prof)} - out (th/mutation! params)] + ::rpc/profile-id (:id prof)} + out (th/command! params)] (t/is (nil? (:error out)))) ;; query files after profile soft deletion - (let [params {::th/type :project-files - :project-id (:default-project-id prof) - :profile-id (:id prof)} - out (th/query! params)] + (let [params {::th/type :get-project-files + ::rpc/profile-id (:id prof) + :project-id (:default-project-id prof)} + out (th/command! params)] ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (= 1 (count (:result out))))) @@ -150,13 +150,13 @@ (let [row (th/db-get :team {:id (:default-team-id prof)} - {:check-deleted? false})] + {::db/remove-deleted? false})] (t/is (dt/instant? (:deleted-at row)))) ;; query profile after delete - (let [params {::th/type :profile - :profile-id (:id prof)} - out (th/query! params)] + (let [params {::th/type :get-profile + ::rpc/profile-id (:id prof)} + out (th/command! params)] ;; (th/print-result! out) (let [result (:result out)] (t/is (= uuid/zero (:id result))))))) @@ -174,7 +174,7 @@ (let [data {::th/type :prepare-register-profile :email "user@example.com" :password "foobar"} - out (th/mutation! data) + out (th/command! data) token (get-in out [:result :token])] (t/is (string? token)) @@ -183,7 +183,7 @@ (let [data {::th/type :register-profile :fullname "foobar" :accept-terms-and-privacy true} - out (th/mutation! data)] + out (th/command! data)] (let [error (:error out)] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) @@ -195,7 +195,7 @@ :fullname "foobar" :accept-terms-and-privacy true :accept-newsletter-subscription true}] - (let [{:keys [result error]} (th/mutation! data)] + (let [{:keys [result error]} (th/command! data)] (t/is (nil? error)))) )) @@ -231,7 +231,7 @@ (t/deftest prepare-register-and-register-profile-2 (with-redefs [app.rpc.commands.auth/register-retry-threshold (dt/duration 500)] - (with-mocks [mock {:target 'app.emails/send! :return nil}] + (with-mocks [mock {:target 'app.email/send! :return nil}] (let [current-token (atom nil)] ;; PREPARE REGISTER @@ -409,15 +409,15 @@ (t/is (= :email-as-password (:code edata)))))) (t/deftest email-change-request - (with-mocks [mock {:target 'app.emails/send! :return nil}] + (with-mocks [mock {:target 'app.email/send! :return nil}] (let [profile (th/create-profile* 1) pool (:app.db/pool th/*system*) data {::th/type :request-email-change - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :email "user1@example.com"}] ;; without complaints - (let [out (th/mutation! data)] + (let [out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:result out))) (let [mock @mock] @@ -426,14 +426,14 @@ ;; with complaints (th/create-global-complaint-for pool {:type :complaint :email (:email data)}) - (let [out (th/mutation! data)] + (let [out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:result out))) (t/is (= 2 (:call-count @mock)))) ;; with bounces (th/create-global-complaint-for pool {:type :bounce :email (:email data)}) - (let [out (th/mutation! data) + (let [out (th/command! data) error (:error out)] ;; (th/print-result! out) (t/is (th/ex-info? error)) @@ -443,14 +443,14 @@ (t/deftest email-change-request-without-smtp - (with-mocks [mock {:target 'app.emails/send! :return nil}] + (with-mocks [mock {:target 'app.email/send! :return nil}] (with-redefs [app.config/flags #{}] (let [profile (th/create-profile* 1) pool (:app.db/pool th/*system*) data {::th/type :request-email-change - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :email "user1@example.com"} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (false? (:called? @mock))) @@ -459,7 +459,7 @@ (t/deftest request-profile-recovery - (with-mocks [mock {:target 'app.emails/send! :return nil}] + (with-mocks [mock {:target 'app.email/send! :return nil}] (let [profile1 (th/create-profile* 1) profile2 (th/create-profile* 2 {:is-active true}) pool (:app.db/pool th/*system*) @@ -467,7 +467,7 @@ ;; with invalid email (let [data (assoc data :email "foo@bar.com") - out (th/mutation! data)] + out (th/command! data)] (t/is (nil? (:result out))) (t/is (= 0 (:call-count @mock)))) @@ -512,10 +512,10 @@ (t/deftest update-profile-password (let [profile (th/create-profile* 1) data {::th/type :update-profile-password - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :old-password "123123" :password "foobarfoobar"} - out (th/mutation! data)] + out (th/command! data)] (t/is (nil? (:error out))) (t/is (nil? (:result out))) )) @@ -524,10 +524,10 @@ (t/deftest update-profile-password-bad-old-password (let [profile (th/create-profile* 1) data {::th/type :update-profile-password - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :old-password "badpassword" :password "foobarfoobar"} - {:keys [result error] :as out} (th/mutation! data)] + {:keys [result error] :as out} (th/command! data)] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) (t/is (th/ex-of-code? error :old-password-not-match)))) @@ -536,10 +536,10 @@ (t/deftest update-profile-password-email-as-password (let [profile (th/create-profile* 1) data {::th/type :update-profile-password - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :old-password "123123" :password "profile1.test@nodomain.com"} - {:keys [result error] :as out} (th/mutation! data)] + {:keys [result error] :as out} (th/command! data)] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) (t/is (th/ex-of-code? error :email-as-password)))) diff --git a/backend/test/backend_tests/rpc_project_test.clj b/backend/test/backend_tests/rpc_project_test.clj index 24c745a83..ade2cf953 100644 --- a/backend/test/backend_tests/rpc_project_test.clj +++ b/backend/test/backend_tests/rpc_project_test.clj @@ -9,6 +9,7 @@ [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.db :as db] + [app.rpc :as-alias rpc] [app.http :as http] [app.util.time :as dt] [clojure.test :as t])) @@ -214,10 +215,10 @@ (t/is (= 0 (:processed result)))) ;; query the list of files of a after soft deletion - (let [data {::th/type :project-files - :project-id (:id project) - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-project-files + ::rpc/profile-id (:id profile1) + :project-id (:id project)} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] @@ -228,10 +229,10 @@ (t/is (= 1 (:processed result)))) ;; query the list of files of a after hard deletion - (let [data {::th/type :project-files - :project-id (:id project) - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-project-files + ::rpc/profile-id (:id profile1) + :project-id (:id project)} + out (th/command! data)] ;; (th/print-result! out) (let [error (:error out) error-data (ex-data error)] diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index d581b19eb..3d304ab89 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -22,7 +22,7 @@ (t/use-fixtures :each th/database-reset) (t/deftest create-team-invitations - (with-mocks [mock {:target 'app.emails/send! :return nil}] + (with-mocks [mock {:target 'app.email/send! :return nil}] (let [profile1 (th/create-profile* 1 {:is-active true}) profile2 (th/create-profile* 2 {:is-active true}) profile3 (th/create-profile* 3 {:is-active true :is-muted true}) @@ -105,7 +105,7 @@ (t/deftest invitation-tokens - (with-mocks [mock {:target 'app.emails/send! :return nil}] + (with-mocks [mock {:target 'app.email/send! :return nil}] (let [profile1 (th/create-profile* 1 {:is-active true}) profile2 (th/create-profile* 2 {:is-active true}) @@ -247,7 +247,7 @@ ))) (t/deftest create-team-invitations-with-email-verification-disabled - (with-mocks [mock {:target 'app.emails/send! :return nil}] + (with-mocks [mock {:target 'app.email/send! :return nil}] (let [profile1 (th/create-profile* 1 {:is-active true}) profile2 (th/create-profile* 2 {:is-active true}) profile3 (th/create-profile* 3 {:is-active true :is-muted true}) diff --git a/backend/test/backend_tests/storage_test.clj b/backend/test/backend_tests/storage_test.clj index b47e98b09..032e85c2e 100644 --- a/backend/test/backend_tests/storage_test.clj +++ b/backend/test/backend_tests/storage_test.clj @@ -27,11 +27,11 @@ "Given storage map, returns a storage configured with the appropriate backend for assets." ([storage] - (assoc storage :backend :assets-fs)) + (assoc storage ::sto/backend :assets-fs)) ([storage conn] (-> storage - (assoc :conn conn) - (assoc :backend :assets-fs)))) + (assoc ::db/pool-or-conn conn) + (assoc ::sto/backend :assets-fs)))) (t/deftest put-and-retrieve-object (let [storage (-> (:app.storage/storage th/*system*) @@ -40,8 +40,10 @@ object @(sto/put-object! storage {::sto/content content :content-type "text/plain" :other "data"})] - (t/is (sto/storage-object? object)) + + (t/is (sto/object? object)) (t/is (fs/path? @(sto/get-object-path storage object))) + (t/is (nil? (:expired-at object))) (t/is (= :assets-fs (:backend object))) (t/is (= "data" (:other (meta object)))) @@ -58,7 +60,8 @@ ::sto/expired-at (dt/in-future {:seconds 1}) :content-type "text/plain" })] - (t/is (sto/storage-object? object)) + + (t/is (sto/object? object)) (t/is (dt/instant? (:expired-at object))) (t/is (dt/is-after? (:expired-at object) (dt/now))) (t/is (= object @(sto/get-object storage (:id object)))) @@ -77,7 +80,7 @@ object @(sto/put-object! storage {::sto/content content :content-type "text/plain" :expired-at (dt/in-future {:seconds 1})})] - (t/is (sto/storage-object? object)) + (t/is (sto/object? object)) (t/is (true? @(sto/del-object! storage object))) ;; retrieving the same object should be not nil because the diff --git a/backend/test/backend_tests/tasks_telemetry_test.clj b/backend/test/backend_tests/tasks_telemetry_test.clj index 43e8a59eb..70a2a6c91 100644 --- a/backend/test/backend_tests/tasks_telemetry_test.clj +++ b/backend/test/backend_tests/tasks_telemetry_test.clj @@ -8,7 +8,6 @@ (:require [backend-tests.helpers :as th] [app.db :as db] - [app.emails :as emails] [app.util.time :as dt] [clojure.pprint :refer [pprint]] [clojure.test :as t] diff --git a/common/deps.edn b/common/deps.edn index 2d94cc322..738b5144a 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -11,13 +11,13 @@ org.apache.logging.log4j/log4j-core {:mvn/version "2.19.0"} org.apache.logging.log4j/log4j-web {:mvn/version "2.19.0"} org.apache.logging.log4j/log4j-jul {:mvn/version "2.19.0"} - org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.18.0"} - org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"} + org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.19.0"} + org.slf4j/slf4j-api {:mvn/version "2.0.6"} + pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.26"} selmer/selmer {:mvn/version "1.12.55"} criterium/criterium {:mvn/version "0.4.6"} - expound/expound {:mvn/version "0.9.0"} com.cognitect/transit-clj {:mvn/version "1.0.329"} com.cognitect/transit-cljs {:mvn/version "0.8.280"} @@ -52,7 +52,7 @@ :build {:extra-deps - {io.github.clojure/tools.build {:git/tag "v0.9.0" :git/sha "8c93e0c"}} + {io.github.clojure/tools.build {:git/tag "v0.9.3" :git/sha "e537cd1"}} :ns-default build} :test diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 1a9806958..bf2ea3ced 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -8,23 +8,26 @@ "A collection if helpers for working with data structures and other data resources." (:refer-clojure :exclude [read-string hash-map merge name update-vals - parse-double group-by iteration concat mapcat]) + parse-double group-by iteration concat mapcat + parse-uuid]) #?(:cljs (:require-macros [app.common.data])) (:require - [app.common.math :as mth] - [clojure.set :as set] - [cuerdas.core :as str] #?(:cljs [cljs.reader :as r] :clj [clojure.edn :as r]) #?(:cljs [cljs.core :as c] :clj [clojure.core :as c]) + [app.common.exceptions :as ex] + [app.common.math :as mth] + [clojure.set :as set] + [cuerdas.core :as str] + [linked.map :as lkm] [linked.set :as lks]) - #?(:clj (:import linked.set.LinkedSet + linked.map.LinkedMap java.lang.AutoCloseable))) (def boolean-or-nil? @@ -39,11 +42,21 @@ ([a] (conj lks/empty-linked-set a)) ([a & xs] (apply conj lks/empty-linked-set a xs))) +(defn ordered-map + ([] lkm/empty-linked-map) + ([a] (conj lkm/empty-linked-map a)) + ([a & xs] (apply conj lkm/empty-linked-map a xs))) + (defn ordered-set? [o] #?(:cljs (instance? lks/LinkedSet o) :clj (instance? LinkedSet o))) +(defn ordered-map? + [o] + #?(:cljs (instance? lkm/LinkedMap o) + :clj (instance? LinkedMap o))) + #?(:clj (defmethod print-method clojure.lang.PersistentQueue [q, w] ;; Overload the printer for queues so they look like fish @@ -508,6 +521,10 @@ default v)))) +(defn parse-uuid + [v] + (ex/ignoring (c/parse-uuid v))) + (defn num-string? [v] ;; https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number #?(:cljs (and (string? v) diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index a26ca25c3..40d69f9fe 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -12,7 +12,12 @@ [app.common.pprint :as pp] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [expound.alpha :as expound])) + [expound.alpha :as expound]) + #?(:clj + (:import + clojure.lang.IPersistentMap))) + +#?(:clj (set! *warn-on-reflection* true)) (defmacro error [& {:keys [type hint] :as params}] @@ -41,44 +46,22 @@ [& exprs] `(try* (^:once fn* [] ~@exprs) identity)) -(defn cause - "Retrieve chained cause if available of the exception." - [^Throwable throwable] - (.getCause throwable)) - (defn ex-info? [v] - (instance? #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo) v)) + (instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v)) + +(defn error? + [v] + (instance? #?(:clj clojure.lang.IExceptionInfo :cljs cljs.core.ExceptionInfo) v)) (defn exception? [v] (instance? #?(:clj java.lang.Throwable :cljs js/Error) v)) -#?(:cljs - (deftype WrappedException [cause meta] - cljs.core/IMeta - (-meta [_] meta) - - cljs.core/IDeref - (-deref [_] cause)) - :clj - (deftype WrappedException [cause meta] - clojure.lang.IMeta - (meta [_] meta) - - clojure.lang.IDeref - (deref [_] cause))) - -#?(:clj (ns-unmap 'app.common.exceptions '->WrappedException)) -#?(:clj (ns-unmap 'app.common.exceptions 'map->WrappedException)) - -(defn wrapped? - [o] - (instance? WrappedException o)) - -(defn wrap-with-context - [cause context] - (WrappedException. cause context)) +#?(:clj + (defn runtime-exception? + [v] + (instance? RuntimeException v))) (defn explain ([data] (explain data nil)) @@ -97,15 +80,17 @@ (s/explain-out (update data ::s/problems #(take max-problems %)))))))) #?(:clj -(defn print-throwable - [^Throwable cause - & {:keys [trace? data? chain? data-level data-length trace-length explain-length] - :or {trace? true - data? true - chain? true - explain-length 10 - data-length 10 - data-level 3}}] +(defn format-throwable + [^Throwable cause & {:keys [summary? detail? header? data? explain? chain? data-level data-length trace-length] + :or {summary? true + detail? true + header? true + data? true + explain? true + chain? true + data-length 10 + data-level 3}}] + (letfn [(print-trace-element [^StackTraceElement e] (let [class (.getClassName e) method (.getMethodName e)] @@ -132,28 +117,29 @@ (doseq [line lines] (println " " line))))) - (print-trace-title [cause] + (print-trace-title [^Throwable cause] (print " → ") (printf "%s: %s" (.getName (class cause)) (first (str/lines (ex-message cause)))) - (when-let [e (first (.getStackTrace cause))] + (when-let [^StackTraceElement e (first (.getStackTrace ^Throwable cause))] (printf " (%s:%d)" (or (.getFileName e) "") (.getLineNumber e))) (newline)) - (print-summary [cause] - (let [causes (loop [cause (.getCause cause) + (print-summary [^Throwable cause] + (let [causes (loop [cause (ex-cause cause) result []] (if cause - (recur (.getCause cause) + (recur (ex-cause cause) (conj result cause)) result))] - (println "TRACE:") + (when header? + (println "SUMMARY:")) (print-trace-title cause) (doseq [cause causes] (print-trace-title cause)))) - (print-trace [cause] + (print-trace [^Throwable cause] (print-trace-title cause) (let [st (.getStackTrace cause)] (print " at: ") @@ -167,35 +153,35 @@ (print-trace-element e) (newline)))) - (print-all [cause] - (print-summary cause) - (println "DETAIL:") - (when trace? - (print-trace cause)) - - (when data? - (when-let [data (ex-data cause)] + (print-detail [^Throwable cause] + (print-trace cause) + (when-let [data (ex-data cause)] + (when data? + (print-data (dissoc data ::s/problems ::s/spec ::s/value))) + (when explain? (if-let [explain (explain data)] - (print-explain explain) - (print-data data)))) + (print-explain explain))))) - (when chain? - (loop [cause cause] - (when-let [cause (.getCause cause)] - (newline) - (print-trace cause) + (print-all [^Throwable cause] + (when summary? + (print-summary cause)) - (when data? - (when-let [data (ex-data cause)] - (if-let [explain (explain data)] - (print-explain explain) - (print-data data)))) + (when detail? + (when header? + (println "DETAIL:")) - (recur cause))))) + (print-detail cause) + (when chain? + (loop [cause cause] + (when-let [cause (ex-cause cause)] + (newline) + (print-detail cause) + (recur cause)))))) ] + (with-out-str + (print-all cause))))) - (println - (with-out-str - (print-all cause)))))) - - +#?(:clj +(defn print-throwable + [cause & {:as opts}] + (println (format-throwable cause opts)))) diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index 01ee53c9d..6e9c1d46f 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -187,20 +187,28 @@ (defn scale-matrix ([pt center] - (-> (matrix) - (multiply! (translate-matrix center)) - (multiply! (scale-matrix pt)) - (multiply! (translate-matrix (gpt/negate center))))) + (let [sx (dm/get-prop pt :x) + sy (dm/get-prop pt :y) + cx (dm/get-prop center :x) + cy (dm/get-prop center :y)] + (Matrix. sx 0 0 sy (- cx (* cx sx)) (- cy (* cy sy))))) ([pt] (assert (gpt/point? pt)) (Matrix. (dm/get-prop pt :x) 0 0 (dm/get-prop pt :y) 0 0))) (defn rotate-matrix ([angle point] - (-> (matrix) - (multiply! (translate-matrix point)) - (multiply! (rotate-matrix angle)) - (multiply! (translate-matrix (gpt/negate point))))) + (let [cx (dm/get-prop point :x) + cy (dm/get-prop point :y) + nx (- cx) + ny (- cy) + a (mth/radians angle) + c (mth/cos a) + s (mth/sin a) + ns (- s) + tx (+ (* c nx) (* ns ny) cx) + ty (+ (* s nx) (* c ny) cy)] + (Matrix. c s ns c tx ty))) ([angle] (let [a (mth/radians angle)] (Matrix. (mth/cos a) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index 5158ed32e..fabefe36b 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -17,6 +17,7 @@ [app.common.geom.shapes.modifiers :as gsm] [app.common.geom.shapes.path :as gsp] [app.common.geom.shapes.rect :as gpr] + [app.common.geom.shapes.text :as gst] [app.common.geom.shapes.transforms :as gtr] [app.common.math :as mth])) @@ -195,3 +196,6 @@ ;; Modifiers (dm/export gsm/set-objects-modifiers) + +;; Text +(dm/export gst/position-data-selrect) diff --git a/common/src/app/common/geom/shapes/bounds.cljc b/common/src/app/common/geom/shapes/bounds.cljc index 065ff5e5d..81a4f56c7 100644 --- a/common/src/app/common/geom/shapes/bounds.cljc +++ b/common/src/app/common/geom/shapes/bounds.cljc @@ -133,27 +133,38 @@ (-> (get-shape-filter-bounds shape) (add-padding (calculate-padding shape true)))) - bounds (if (or (:masked-group? shape) (cph/frame-shape? shape)) - [(calculate-base-bounds shape)] - (cph/reduce-objects - objects - (fn [shape] - (and (d/not-empty? (:shapes shape)) - (or (not (cph/frame-shape? shape)) - (:show-content shape)) + bounds + (cond + (empty? (:shapes shape)) + [(calculate-base-bounds shape)] - (or (not (cph/group-shape? shape)) - (not (:masked-group? shape))))) + (:masked-group? shape) + [(calculate-base-bounds shape)] - (:id shape) + (and (cph/frame-shape? shape) (not (:show-content shape))) + [(calculate-base-bounds shape)] - (fn [result shape] - (conj result (get-object-bounds objects shape))) + :else + (cph/reduce-objects + objects + (fn [shape] + (and (d/not-empty? (:shapes shape)) + (or (not (cph/frame-shape? shape)) + (:show-content shape)) - [(calculate-base-bounds shape)])) + (or (not (cph/group-shape? shape)) + (not (:masked-group? shape))))) - children-bounds (cond->> (gsr/join-selrects bounds) - (not (cph/frame-shape? shape)) (or (:children-bounds shape))) + (:id shape) + + (fn [result child] + (conj result (calculate-base-bounds child))) + + [(calculate-base-bounds shape)])) + + children-bounds + (cond->> (gsr/join-selrects bounds) + (not (cph/frame-shape? shape)) (or (:children-bounds shape))) filters (shape->filters shape) blur-value (or (-> shape :blur :value) 0)] diff --git a/common/src/app/common/geom/shapes/constraints.cljc b/common/src/app/common/geom/shapes/constraints.cljc index 6c4f24cc3..6a9885e69 100644 --- a/common/src/app/common/geom/shapes/constraints.cljc +++ b/common/src/app/common/geom/shapes/constraints.cljc @@ -288,25 +288,25 @@ constraints-h (cond - (ctl/layout? parent) + ignore-constraints + :scale + + (and (ctl/any-layout? parent) (not (ctl/layout-absolute? child))) :left - (not ignore-constraints) - (:constraints-h child (default-constraints-h child)) - :else - :scale) + (:constraints-h child (default-constraints-h child))) constraints-v (cond - (ctl/layout? parent) + ignore-constraints + :scale + + (and (ctl/any-layout? parent) (not (ctl/layout-absolute? child))) :top - (not ignore-constraints) - (:constraints-v child (default-constraints-v child)) - :else - :scale)] + (:constraints-v child (default-constraints-v child)))] (if (and (= :scale constraints-h) (= :scale constraints-v)) modifiers diff --git a/common/src/app/common/geom/shapes/corners.cljc b/common/src/app/common/geom/shapes/corners.cljc index ec7a825b7..553d66136 100644 --- a/common/src/app/common/geom/shapes/corners.cljc +++ b/common/src/app/common/geom/shapes/corners.cljc @@ -54,3 +54,27 @@ (if (and (some? r1) (some? r2) (some? r3) (some? r4)) (fix-radius width height r1 r2 r3 r4) [r1 r2 r3 r4])) + +(defn update-corners-scale-1 + "Scales round corners (using a single value)" + [shape scale] + (update shape :rx * scale)) + +(defn update-corners-scale-4 + "Scales round corners (using four values)" + [shape scale] + (-> shape + (update :r1 * scale) + (update :r2 * scale) + (update :r3 * scale) + (update :r4 * scale))) + +(defn update-corners-scale + "Scales round corners" + [shape scale] + (cond-> shape + (and (some? (:rx shape)) (> (:rx shape) 0)) + (update-corners-scale-1 scale) + + (and (some? (:r1 shape)) (> (:r1 shape) 0)) + (update-corners-scale-4 scale))) diff --git a/common/src/app/common/geom/shapes/effects.cljc b/common/src/app/common/geom/shapes/effects.cljc new file mode 100644 index 000000000..912b2de6c --- /dev/null +++ b/common/src/app/common/geom/shapes/effects.cljc @@ -0,0 +1,20 @@ +(ns app.common.geom.shapes.effects) + +(defn update-shadow-scale + [shadow scale] + (-> shadow + (update :offset-x * scale) + (update :offset-y * scale) + (update :spread * scale) + (update :blur * scale))) + +(defn update-shadows-scale + [shape scale] + (update shape :shadow + (fn [shadow] + (mapv #(update-shadow-scale % scale) shadow)))) + +(defn update-blur-scale + [shape scale] + (update-in shape [:blur :value] * scale)) + diff --git a/common/src/app/common/geom/shapes/flex_layout/bounds.cljc b/common/src/app/common/geom/shapes/flex_layout/bounds.cljc index d37dc32f3..358dbe34e 100644 --- a/common/src/app/common/geom/shapes/flex_layout/bounds.cljc +++ b/common/src/app/common/geom/shapes/flex_layout/bounds.cljc @@ -117,7 +117,9 @@ child-bounds))] - (->> children (map get-child-bounds)))) + (->> children + (remove ctl/layout-absolute?) + (map get-child-bounds)))) (defn layout-content-bounds [bounds {:keys [layout-padding] :as parent} children] diff --git a/common/src/app/common/geom/shapes/flex_layout/lines.cljc b/common/src/app/common/geom/shapes/flex_layout/lines.cljc index b5d89c378..3692e9fb4 100644 --- a/common/src/app/common/geom/shapes/flex_layout/lines.cljc +++ b/common/src/app/common/geom/shapes/flex_layout/lines.cljc @@ -104,8 +104,10 @@ (if (and (some? line-data) (or (not wrap?) - (and row? (<= next-line-min-width layout-width)) - (and col? (<= next-line-min-height layout-height)))) + (and row? (or (< next-line-min-width layout-width) + (mth/close? next-line-min-width layout-width 0.5))) + (and col? (or (< next-line-min-height layout-height) + (mth/close? next-line-min-height layout-height 0.5))))) (recur {:line-min-width (if row? (+ line-min-width next-min-width) (max line-min-width next-min-width)) :line-max-width (if row? (+ line-max-width next-max-width) (max line-max-width next-max-width)) diff --git a/common/src/app/common/geom/shapes/grid_layout.cljc b/common/src/app/common/geom/shapes/grid_layout.cljc new file mode 100644 index 000000000..eb45960f8 --- /dev/null +++ b/common/src/app/common/geom/shapes/grid_layout.cljc @@ -0,0 +1,19 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.shapes.grid-layout + (:require + [app.common.data.macros :as dm] + [app.common.geom.shapes.grid-layout.layout-data :as glld] + [app.common.geom.shapes.grid-layout.positions :as glp])) + +(dm/export glld/calc-layout-data) +(dm/export glld/get-cell-data) +(dm/export glp/child-modifiers) + +(defn get-drop-index + [frame objects _position] + (dec (count (get-in objects [frame :shapes])))) diff --git a/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc b/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc new file mode 100644 index 000000000..e797b1c64 --- /dev/null +++ b/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc @@ -0,0 +1,140 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.shapes.grid-layout.layout-data + (:require + [app.common.geom.point :as gpt] + [app.common.geom.shapes.points :as gpo])) + +#_(defn set-sample-data + [parent children] + + (let [parent (assoc parent + :layout-grid-columns + [{:type :percent :value 25} + {:type :percent :value 25} + {:type :fixed :value 100} + ;;{:type :auto} + ;;{:type :flex :value 1} + ] + + :layout-grid-rows + [{:type :percent :value 50} + {:type :percent :value 50} + ;;{:type :fixed :value 100} + ;;{:type :auto} + ;;{:type :flex :value 1} + ]) + + num-rows (count (:layout-grid-rows parent)) + num-columns (count (:layout-grid-columns parent)) + + layout-grid-cells + (into + {} + (for [[row-idx _row] (d/enumerate (:layout-grid-rows parent)) + [col-idx _col] (d/enumerate (:layout-grid-columns parent))] + (let [[_bounds shape] (nth children (+ (* row-idx num-columns) col-idx) nil) + cell-data {:id (uuid/next) + :row (inc row-idx) + :column (inc col-idx) + :row-span 1 + :col-span 1 + :shapes (when shape [(:id shape)])}] + [(:id cell-data) cell-data]))) + + parent (assoc parent :layout-grid-cells layout-grid-cells)] + + [parent children])) + +(defn calculate-initial-track-values + [{:keys [type value]} total-value] + + (case type + :percent + (let [value (/ (* total-value value) 100) ] + value) + + :fixed + value + + :auto + 0 + )) + +(defn calc-layout-data + [parent _children transformed-parent-bounds] + + (let [height (gpo/height-points transformed-parent-bounds) + width (gpo/width-points transformed-parent-bounds) + + ;; Initialize tracks + column-tracks + (->> (:layout-grid-columns parent) + (map (fn [track] + (let [initial (calculate-initial-track-values track width)] + (assoc track :value initial))))) + + row-tracks + (->> (:layout-grid-rows parent) + (map (fn [track] + (let [initial (calculate-initial-track-values track height)] + (assoc track :value initial))))) + + ;; Go through cells to adjust auto sizes + + + ;; Once auto sizes have been calculated we get calculate the `fr` with the remainining size and adjust the size + + + ;; Adjust final distances + + acc-track-distance + (fn [[result next-distance] data] + (let [result (conj result (assoc data :distance next-distance)) + next-distance (+ next-distance (:value data))] + [result next-distance])) + + column-tracks + (->> column-tracks + (reduce acc-track-distance [[] 0]) + first) + + row-tracks + (->> row-tracks + (reduce acc-track-distance [[] 0]) + first) + + shape-cells + (into {} + (mapcat (fn [[_ cell]] + (->> (:shapes cell) + (map #(vector % cell))))) + (:layout-grid-cells parent)) + ] + + {:row-tracks row-tracks + :column-tracks column-tracks + :shape-cells shape-cells})) + +(defn get-cell-data + [{:keys [row-tracks column-tracks shape-cells]} transformed-parent-bounds [_child-bounds child]] + + (let [origin (gpo/origin transformed-parent-bounds) + hv #(gpo/start-hv transformed-parent-bounds %) + vv #(gpo/start-vv transformed-parent-bounds %) + + grid-cell (get shape-cells (:id child))] + + (when (some? grid-cell) + (let [column (nth column-tracks (dec (:column grid-cell)) nil) + row (nth row-tracks (dec (:row grid-cell)) nil) + + start-p (-> origin + (gpt/add (hv (:distance column))) + (gpt/add (vv (:distance row))))] + + (assoc grid-cell :start-p start-p))))) diff --git a/common/src/app/common/geom/shapes/grid_layout/positions.cljc b/common/src/app/common/geom/shapes/grid_layout/positions.cljc new file mode 100644 index 000000000..3f81928b4 --- /dev/null +++ b/common/src/app/common/geom/shapes/grid_layout/positions.cljc @@ -0,0 +1,16 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.geom.shapes.grid-layout.positions + (:require + [app.common.geom.point :as gpt] + [app.common.geom.shapes.points :as gpo] + [app.common.types.modifiers :as ctm])) + +(defn child-modifiers + [_parent _transformed-parent-bounds _child child-bounds cell-data] + (ctm/move-modifiers + (gpt/subtract (:start-p cell-data) (gpo/origin child-bounds)))) diff --git a/common/src/app/common/geom/shapes/modifiers.cljc b/common/src/app/common/geom/shapes/modifiers.cljc index cd94fb11a..17879b765 100644 --- a/common/src/app/common/geom/shapes/modifiers.cljc +++ b/common/src/app/common/geom/shapes/modifiers.cljc @@ -11,7 +11,8 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.constraints :as gct] - [app.common.geom.shapes.flex-layout :as gcl] + [app.common.geom.shapes.flex-layout :as gcfl] + [app.common.geom.shapes.grid-layout :as gcgl] [app.common.geom.shapes.pixel-precision :as gpp] [app.common.geom.shapes.points :as gpo] [app.common.geom.shapes.transforms :as gtr] @@ -47,58 +48,61 @@ :expr (or (nil? ids) (set? ids)) :hint (dm/str "tree sequence from not set: " ids)) - (letfn [(get-tree-root ;; Finds the tree root for the current id - [id] + (let [get-tree-root + (fn ;; Finds the tree root for the current id + [id] - (loop [current id - result id] - (let [shape (get objects current) - parent (get objects (:parent-id shape))] - (cond - (or (not shape) (= uuid/zero current)) + (loop [current id + result id] + (let [shape (get objects current) + parent (get objects (:parent-id shape))] + (cond + (or (not shape) (= uuid/zero current)) + result + + ;; Frame found, but not layout we return the last layout found (or the id) + (and (= :frame (:type parent)) + (not (ctl/any-layout? parent))) + result + + ;; Layout found. We continue upward but we mark this layout + (ctl/any-layout? parent) + (recur (:id parent) (:id parent)) + + ;; If group or boolean or other type of group we continue with the last result + :else + (recur (:id parent) result))))) + + is-child? #(cph/is-child? objects %1 %2) + + calculate-common-roots + (fn ;; Given some roots retrieves the minimum number of tree roots + [result id] + (if (= id uuid/zero) + result + (let [root (get-tree-root id) + + ;; Remove the children from the current root result + (if (cph/has-children? objects root) + (into #{} (remove #(is-child? root %)) result) + result) - ;; Frame found, but not layout we return the last layout found (or the id) - (and (= :frame (:type parent)) - (not (ctl/layout? parent))) - result + root-parents (cph/get-parent-ids objects root) + contains-parent? (some #(contains? result %) root-parents)] + (cond-> result + (not contains-parent?) + (conj root))))) - ;; Layout found. We continue upward but we mark this layout - (ctl/layout? parent) - (recur (:id parent) (:id parent)) - - ;; If group or boolean or other type of group we continue with the last result - :else - (recur (:id parent) result))))) - - (calculate-common-roots ;; Given some roots retrieves the minimum number of tree roots - [result id] - (if (= id uuid/zero) - result - (let [root (get-tree-root id) - - ;; Remove the children from the current root - result - (into #{} (remove #(cph/is-child? objects root %)) result) - - contains-parent? - (some #(cph/is-child? objects % root) result)] - - (cond-> result - (not contains-parent?) - (conj root)))))] - - (let [roots (->> ids (reduce calculate-common-roots #{}))] - (concat - (when (contains? ids uuid/zero) [(get objects uuid/zero)]) - (mapcat #(children-sequence % objects) roots))))) + roots (->> ids (reduce calculate-common-roots #{}))] + (concat + (when (contains? ids uuid/zero) [(get objects uuid/zero)]) + (mapcat #(children-sequence % objects) roots)))) (defn- set-children-modifiers "Propagates the modifiers from a parent too its children applying constraints if necesary" - [modif-tree objects bounds parent transformed-parent-bounds ignore-constraints] - (let [children (:shapes parent) - modifiers (dm/get-in modif-tree [(:id parent) :modifiers])] - + [modif-tree children objects bounds parent transformed-parent-bounds ignore-constraints] + (let [modifiers (dm/get-in modif-tree [(:id parent) :modifiers])] ;; Move modifiers don't need to calculate constraints (if (ctm/only-move? modifiers) (loop [modif-tree modif-tree @@ -155,8 +159,8 @@ (not (ctm/empty? modifiers)) (gtr/transform-bounds modifiers))))) -(defn- set-layout-modifiers - [modif-tree objects bounds parent transformed-parent-bounds] +(defn- set-flex-layout-modifiers + [modif-tree children objects bounds parent transformed-parent-bounds] (letfn [(apply-modifiers [child] [(-> (get-group-bounds objects bounds modif-tree child) @@ -165,7 +169,7 @@ (set-child-modifiers [[layout-line modif-tree] [child-bounds child]] (let [[modifiers layout-line] - (gcl/layout-child-modifiers parent transformed-parent-bounds child child-bounds layout-line) + (gcfl/layout-child-modifiers parent transformed-parent-bounds child child-bounds layout-line) modif-tree (cond-> modif-tree @@ -174,11 +178,12 @@ [layout-line modif-tree]))] - (let [children (->> (cph/get-immediate-children objects (:id parent)) + (let [children (->> children + (map (d/getf objects)) (remove :hidden) (remove gco/invalid-geometry?) (map apply-modifiers)) - layout-data (gcl/calc-layout-data parent children @transformed-parent-bounds) + layout-data (gcfl/calc-layout-data parent children @transformed-parent-bounds) children (into [] (cond-> children (not (:reverse? layout-data)) reverse)) max-idx (dec (count children)) layout-lines (:layout-lines layout-data)] @@ -196,6 +201,36 @@ modif-tree))))) +(defn- set-grid-layout-modifiers + [modif-tree objects bounds parent transformed-parent-bounds] + + (letfn [(apply-modifiers [child] + [(-> (get-group-bounds objects bounds modif-tree child) + (gpo/parent-coords-bounds @transformed-parent-bounds)) + child]) + (set-child-modifiers [modif-tree cell-data [child-bounds child]] + (let [modifiers (gcgl/child-modifiers parent transformed-parent-bounds child child-bounds cell-data) + modif-tree + (cond-> modif-tree + (d/not-empty? modifiers) + (update-in [(:id child) :modifiers] ctm/add-modifiers modifiers))] + modif-tree))] + (let [children (->> (cph/get-immediate-children objects (:id parent)) + (remove :hidden) + (remove gco/invalid-geometry?) + (map apply-modifiers)) + grid-data (gcgl/calc-layout-data parent children @transformed-parent-bounds)] + (loop [modif-tree modif-tree + child (first children) + pending (rest children)] + (if (some? child) + (let [cell-data (gcgl/get-cell-data grid-data @transformed-parent-bounds child) + modif-tree (cond-> modif-tree + (some? cell-data) + (set-child-modifiers cell-data child))] + (recur modif-tree (first pending) (rest pending))) + modif-tree))))) + (defn- calc-auto-modifiers "Calculates the modifiers to adjust the bounds for auto-width/auto-height shapes" [objects bounds parent] @@ -207,14 +242,14 @@ (let [origin (gpo/origin @parent-bounds) scale-width (/ auto-width (gpo/width-points @parent-bounds))] (-> modifiers - (ctm/resize-parent (gpt/point scale-width 1) origin (:transform parent) (:transform-inverse parent))))) + (ctm/resize (gpt/point scale-width 1) origin (:transform parent) (:transform-inverse parent))))) set-parent-auto-height (fn [modifiers auto-height] (let [origin (gpo/origin @parent-bounds) scale-height (/ auto-height (gpo/height-points @parent-bounds))] (-> modifiers - (ctm/resize-parent (gpt/point 1 scale-height) origin (:transform parent) (:transform-inverse parent))))) + (ctm/resize (gpt/point 1 scale-height) origin (:transform parent) (:transform-inverse parent))))) children (->> (cph/get-immediate-children objects parent-id) (remove :hidden) @@ -222,7 +257,7 @@ content-bounds (when (and (d/not-empty? children) (or (ctl/auto-height? parent) (ctl/auto-width? parent))) - (gcl/layout-content-bounds bounds parent children)) + (gcfl/layout-content-bounds bounds parent children)) auto-width (when content-bounds (gpo/width-points content-bounds)) auto-height (when content-bounds (gpo/height-points content-bounds))] @@ -238,6 +273,7 @@ "Propagate modifiers to its children" [objects bounds ignore-constraints modif-tree parent] (let [parent-id (:id parent) + children (:shapes parent) root? (= uuid/zero parent-id) modifiers (-> (dm/get-in modif-tree [parent-id :modifiers]) (ctm/select-geometry)) @@ -247,7 +283,7 @@ (cond-> modif-tree (and has-modifiers? parent? (not root?)) - (set-children-modifiers objects bounds parent transformed-parent-bounds ignore-constraints)))) + (set-children-modifiers children objects bounds parent transformed-parent-bounds ignore-constraints)))) (defn- propagate-modifiers-layout "Propagate modifiers to its children" @@ -257,28 +293,53 @@ modifiers (-> (dm/get-in modif-tree [parent-id :modifiers]) (ctm/select-geometry)) has-modifiers? (ctm/child-modifiers? modifiers) - layout? (ctl/layout? parent) + flex-layout? (ctl/flex-layout? parent) + grid-layout? (ctl/grid-layout? parent) auto? (or (ctl/auto-height? parent) (ctl/auto-width? parent)) parent? (or (cph/group-like-shape? parent) (cph/frame-shape? parent)) - transformed-parent-bounds (delay (gtr/transform-bounds @(get bounds parent-id) modifiers))] + transformed-parent-bounds (delay (gtr/transform-bounds @(get bounds parent-id) modifiers)) + + children-modifiers + (if flex-layout? + (->> (:shapes parent) + (filter #(ctl/layout-absolute? objects %))) + (:shapes parent)) + + children-layout + (when flex-layout? + (->> (:shapes parent) + (remove #(ctl/layout-absolute? objects %))))] [(cond-> modif-tree - (and (not layout?) has-modifiers? parent? (not root?)) - (set-children-modifiers objects bounds parent transformed-parent-bounds ignore-constraints) + (and has-modifiers? parent? (not root?)) + (set-children-modifiers children-modifiers objects bounds parent transformed-parent-bounds ignore-constraints) - layout? - (set-layout-modifiers objects bounds parent transformed-parent-bounds)) + flex-layout? + (set-flex-layout-modifiers children-layout objects bounds parent transformed-parent-bounds) + + grid-layout? + (set-grid-layout-modifiers objects bounds parent transformed-parent-bounds)) ;; Auto-width/height can change the positions in the parent so we need to recalculate (cond-> autolayouts auto? (conj (:id parent)))])) (defn- apply-structure-modifiers [objects modif-tree] - (letfn [(apply-shape [objects [id {:keys [modifiers]}]] + (letfn [(update-children-structure-modifiers + [objects ids modifiers] + (reduce #(update %1 %2 ctm/apply-structure-modifiers modifiers) objects ids)) + + (apply-shape [objects [id {:keys [modifiers]}]] (cond-> objects (ctm/has-structure? modifiers) - (update id ctm/apply-structure-modifiers modifiers)))] + (update id ctm/apply-structure-modifiers modifiers) + + (and (ctm/has-structure? modifiers) + (ctm/has-structure-child? modifiers)) + (update-children-structure-modifiers + (cph/get-children-ids objects id) + (ctm/select-child-structre-modifiers modifiers))))] (reduce apply-shape objects modif-tree))) (defn merge-modif-tree @@ -364,7 +425,7 @@ to-reflow (cond-> to-reflow - (and (ctl/layout-descent? objects parent-base) + (and (ctl/flex-layout-descent? objects parent-base) (not= uuid/zero (:frame-id parent-base))) (conj (:frame-id parent-base)))] (recur modif-tree @@ -396,6 +457,7 @@ ([old-modif-tree modif-tree objects {:keys [ignore-constraints snap-pixel? snap-precision snap-ignore-axis] :or {ignore-constraints false snap-pixel? false snap-precision 1 snap-ignore-axis nil}}] + (let [objects (-> objects (cond-> (some? old-modif-tree) (apply-structure-modifiers old-modif-tree)) diff --git a/common/src/app/common/geom/shapes/pixel_precision.cljc b/common/src/app/common/geom/shapes/pixel_precision.cljc index e85c70893..9994b0978 100644 --- a/common/src/app/common/geom/shapes/pixel_precision.cljc +++ b/common/src/app/common/geom/shapes/pixel_precision.cljc @@ -80,7 +80,7 @@ (fn [modif-tree shape] (let [modifiers (dm/get-in modif-tree [(:id shape) :modifiers])] (cond-> modif-tree - (ctm/has-geometry? modifiers) + (and (some? modifiers) (ctm/has-geometry? modifiers)) (update-in [(:id shape) :modifiers] set-pixel-precision shape precision ignore-axis))))] (->> (keys modif-tree) diff --git a/common/src/app/common/geom/shapes/strokes.cljc b/common/src/app/common/geom/shapes/strokes.cljc new file mode 100644 index 000000000..e155dde7b --- /dev/null +++ b/common/src/app/common/geom/shapes/strokes.cljc @@ -0,0 +1,11 @@ +(ns app.common.geom.shapes.strokes) + +(defn update-stroke-width + [stroke scale] + (update stroke :stroke-width * scale)) + +(defn update-strokes-width + [shape scale] + (update shape :strokes + (fn [strokes] + (mapv #(update-stroke-width % scale) strokes)))) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 864167f1b..1c7bc9b8e 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -382,10 +382,11 @@ (defn update-group-selrect [group children] - (let [shape-center (gco/center-shape group) - ;; Points for every shape inside the group + (let [;; Points for every shape inside the group points (->> children (mapcat :points)) + shape-center (gco/center-points points) + ;; Fixed problem with empty groups. Should not happen (but it does) points (if (empty? points) (:points group) points) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index de8501d38..efd473d54 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -5,375 +5,362 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.logging + "A lightweight and multiplaform (clj & cljs) asynchronous by default + logging API. + + On the CLJ side it backed by SLF4J API, so the user can route + logging output to any implementation that SLF4J supports. And on the + CLJS side, it is backed by printing logs using console.log. + + Simple example of logging API: + + (require '[funcool.tools.logging :as l]) + (l/info :hint \"hello funcool logging\" + :tname (.getName (Thread/currentThread))) + + The log records are ordered key-value pairs (instead of plain + strings) and by default are formatted usin custom, human readable + but also easy parseable format; but it can be extended externally + to use JSON or whatever format user prefers. + + The format can be set at compile time (externaly), passing a JVM + property or closure compiler compile-time constant. Example: + + -Dpenpot.logging.props-format=':default' + + The exception formating is customizable in the same way as the props + formatter. + + All messages are evaluated lazily, in a different thread, only if + the message can be logged (logger level is loggable). This means + that you should take care of lazy values on loging props. For cases + where you strictly need syncrhonous message evaluation, you can use + the special `::sync?` prop. + + The formatting of the message and the exception is handled on this + library and it doesn't rely on the underlying implementation (aka + SLF4J). + " + #?(:cljs (:require-macros [app.common.logging :as l])) (:require + #?(:clj [clojure.edn :as edn] + :cljs [cljs.reader :as edn]) [app.common.data :as d] [app.common.exceptions :as ex] - [app.common.uuid :as uuid] + [app.common.pprint :as pp] [app.common.spec :as us] - [cuerdas.core :as str] + [app.common.uuid :as uuid] [clojure.spec.alpha :as s] - [fipp.edn :as fpp] - #?(:cljs [goog.log :as glog])) - #?(:cljs (:require-macros [app.common.logging]) - :clj (:import - org.apache.logging.log4j.Level - org.apache.logging.log4j.LogManager - org.apache.logging.log4j.Logger - org.apache.logging.log4j.ThreadContext - org.apache.logging.log4j.CloseableThreadContext - org.apache.logging.log4j.spi.LoggerContext))) + [cuerdas.core :as str] + [promesa.exec :as px] + [promesa.util :as pu]) + #?(:clj + (:import + org.slf4j.LoggerFactory + org.slf4j.Logger))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; CLJ Specific -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(def ^:dynamic *context* nil) #?(:clj (set! *warn-on-reflection* true)) -(def ^:private reserved-props - #{:level :cause ::logger ::async ::raw ::context}) +(defonce ^{:doc "A global log-record atom instance; stores last logged record."} + log-record + (atom nil)) -(defn build-message-kv - [props] - (loop [pairs (remove (fn [[k]] (contains? reserved-props k)) props) - result []] - (if-let [[k v] (first pairs)] - (recur (rest pairs) - (conj result (str/concat (d/name k) "=" (pr-str v)))) - (str/join ", " result)))) +(defonce + ^{:doc "Default executor instance used for processing logs." + :dynamic true} + *default-executor* + (delay + #?(:clj (px/single-executor :factory (px/thread-factory :name "penpot/logger")) + :cljs (px/microtask-executor)))) -(defn build-message-cause - [props] - #?(:clj (when-let [[_ cause] (d/seek (fn [[k]] (= k :cause)) props)] - (when cause - (with-out-str - (ex/print-throwable cause)))) - :cljs nil)) +#?(:cljs + (defonce loggers (js/Map.))) + +#?(:cljs + (declare level->int)) + +#?(:cljs + (defn- get-parent-logger + [^string logger] + (let [lindex (.lastIndexOf logger ".")] + (.slice logger 0 (max lindex 0))))) + +#?(:cljs + (defn- get-logger-level + "Get the current level set for the specified logger. Returns int." + [^string logger] + (let [val (.get ^js/Map loggers logger)] + (if (pos? val) + val + (loop [logger' (get-parent-logger logger)] + (let [val (.get ^js/Map loggers logger')] + (if (some? val) + (do + (.set ^js/Map loggers logger val) + val) + (if (= "" logger') + (do + (.set ^js/Map loggers logger 100) + 100) + (recur (get-parent-logger logger')))))))))) + +(defn enabled? + "Check if logger has enabled logging for given level." + [logger level] + #?(:clj + (let [logger (LoggerFactory/getLogger ^String logger)] + (case level + :trace (and (.isTraceEnabled ^Logger logger) logger) + :debug (and (.isDebugEnabled ^Logger logger) logger) + :info (and (.isInfoEnabled ^Logger logger) logger) + :warn (and (.isWarnEnabled ^Logger logger) logger) + :error (and (.isErrorEnabled ^Logger logger) logger) + :fatal (and (.isErrorEnabled ^Logger logger) logger) + (throw (IllegalArgumentException. (str "invalid level:" level))))) + :cljs + (>= (level->int level) + (get-logger-level logger)))) + +(defn- level->color + [level] + (case level + :error "#c82829" + :warn "#f5871f" + :info "#4271ae" + :debug "#969896" + :trace "#8e908c")) + +(defn- level->name + [level] + (case level + :debug "DBG" + :trace "TRC" + :info "INF" + :warn "WRN" + :error "ERR")) + +(defn level->int + [level] + (case level + :debug 10 + :trace 20 + :info 30 + :warn 40 + :error 50)) (defn build-message [props] - (let [props (sequence (comp (partition-all 2) (map vec)) props) - message-kv (build-message-kv props) - message-ex (build-message-cause props)] - (cond-> message-kv - (some? message-ex) - (str "\n" message-ex)))) + (loop [props (seq props) + result []] + (if-let [[k v] (first props)] + (if (simple-ident? k) + (recur (next props) + (conj result (str (name k) "=" (pr-str v)))) + (recur (next props) + result)) + (str/join ", " result)))) -#?(:clj - (def logger-context - (LogManager/getContext false))) +(defn build-stack-trace + [cause] + #?(:clj (ex/format-throwable cause) + :cljs (.-stack ^js cause))) -#?(:clj - (def logging-agent - (agent nil :error-mode :continue))) +#?(:cljs + (defn- get-special-props + [props] + (->> (seq props) + (keep (fn [[k v]] + (when (qualified-ident? k) + (cond + (= "js" (namespace k)) + [:js (name k) (if (object? v) v (clj->js v))] -#?(:clj - (defn stringify-data - [val] - (cond - (string? val) - val + (= "error" (namespace k)) + [:error (name k) v]))))))) - (instance? clojure.lang.Named val) - (name val) +(def ^:private reserved-props + #{::level :cause ::logger ::sync? ::context}) - (coll? val) - (binding [*print-level* 8 - *print-length* 25] - (with-out-str (fpp/pprint val {:width 200}))) +(def ^:no-doc msg-props-xf + (comp (partition-all 2) + (map vec) + (remove (fn [[k _]] (contains? reserved-props k))))) - :else - (str val)))) +(s/def ::id ::us/uuid) +(s/def ::props any? #_d/ordered-map?) +(s/def ::context (s/nilable (s/map-of keyword? any?))) +(s/def ::level #{:trace :debug :info :warn :error :fatal}) +(s/def ::logger string?) +(s/def ::timestamp ::us/integer) +(s/def ::cause (s/nilable ex/exception?)) +(s/def ::message delay?) +(s/def ::record + (s/keys :req [::id ::props ::logger ::level] + :opt [::cause ::context])) -#?(:clj - (defn data->context-map - ^java.util.Map - [data] - (into {} - (comp (filter second) - (map (fn [[key val]] - [(stringify-data key) - (stringify-data val)]))) - data))) +(defn current-timestamp + [] + #?(:clj (inst-ms (java.time.Instant/now)) + :cljs (js/Date.now))) -#?(:clj - (defmacro with-context - [data & body] - `(let [data# (data->context-map ~data)] - (with-open [closeable# (CloseableThreadContext/putAll data#)] - ~@body)))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Common -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn get-logger - [lname] - #?(:clj (.getLogger ^LoggerContext logger-context ^String lname) - :cljs (glog/getLogger - (cond - (string? lname) lname - (= lname :root) "" - (simple-ident? lname) (name lname) - (qualified-ident? lname) (str (namespace lname) "." (name lname)) - :else (str lname))))) - -(defn get-level - [level] - #?(:clj - (case level - :trace Level/TRACE - :debug Level/DEBUG - :info Level/INFO - :warn Level/WARN - :error Level/ERROR - :fatal Level/FATAL) - :cljs - (case level - :off (.-OFF ^js glog/Level) - :shout (.-SHOUT ^js glog/Level) - :error (.-SEVERE ^js glog/Level) - :severe (.-SEVERE ^js glog/Level) - :warning (.-WARNING ^js glog/Level) - :warn (.-WARNING ^js glog/Level) - :info (.-INFO ^js glog/Level) - :config (.-CONFIG ^js glog/Level) - :debug (.-FINE ^js glog/Level) - :fine (.-FINE ^js glog/Level) - :finer (.-FINER ^js glog/Level) - :trace (.-FINER ^js glog/Level) - :finest (.-FINEST ^js glog/Level) - :all (.-ALL ^js glog/Level)))) - -(defn write-log! - [logger level exception message] - #?(:clj - (let [message (if (string? message) message (str/join ", " message))] - (if exception - (.log ^Logger logger - ^Level level - ^Object message - ^Throwable exception) - (.log ^Logger logger - ^Level level - ^Object message))) - :cljs - (when glog/ENABLED - (let [logger (get-logger logger) - level (get-level level)] - (when (and logger (glog/isLoggable logger level)) - (let [message (if (fn? message) (message) message) - message (if (string? message) message (str/join ", " message)) - record (glog/LogRecord. level message (.getName ^js logger))] - (when exception (.setException record exception)) - (glog/publishLogRecord logger record))))))) - -#?(:clj - (defn enabled? - [logger level] - (.isEnabled ^Logger logger ^Level level))) - -#?(:clj - (defn get-error-context - [error] - (merge - {:hint (ex-message error)} - (when-let [data (ex-data error)] - (merge - {: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))} - (when-let [explain (ex/explain data)] - {:spec-explain explain})))))) - -(defmacro log +(defmacro log! + "Emit a new log record to the global log-record state (asynchronously). " [& props] - (if (:ns &env) ; CLJS - (let [{:keys [level cause ::logger ::raw]} props] - `(write-log! ~(or logger (str *ns*)) ~level ~cause (or ~raw (fn [] (build-message ~(vec props)))))) + (let [{:keys [::level ::logger ::context ::sync? cause] :or {sync? false}} props + props (into [] msg-props-xf props)] + `(when (enabled? ~logger ~level) + (let [props# (cond-> (delay ~props) ~sync? deref) + ts# (current-timestamp) + context# *context*] + (px/run! *default-executor* + (fn [] + (let [props# (if ~sync? props# (deref props#)) + props# (into (d/ordered-map) props#) + cause# ~cause + context# (d/without-nils + (merge context# ~context)) + lrecord# {::id (uuid/next) + ::timestamp ts# + ::message (delay (build-message props#)) + ::props props# + ::context context# + ::level ~level + ::logger ~logger} + lrecord# (cond-> lrecord# + (some? cause#) + (assoc ::cause cause# + ::trace (delay (build-stack-trace cause#))))] + (swap! log-record (constantly lrecord#))))))))) - (let [{:keys [level cause ::logger ::async ::raw ::context] :or {async true}} props - logger (or logger (str *ns*)) - logger-sym (gensym "log") - level-sym (gensym "log")] - `(let [~logger-sym (get-logger ~logger) - ~level-sym (get-level ~level)] - (when (enabled? ~logger-sym ~level-sym) - ~(if async - `(do - (send-off logging-agent - (fn [_#] - (let [message# (or ~raw (build-message ~(vec props)))] - (with-context (-> {:id (uuid/next)} - (into ~context) - (into (get-error-context ~cause))) - (try - (write-log! ~logger-sym ~level-sym ~cause message#) - (catch Throwable cause# - (write-log! ~logger-sym (get-level :error) cause# - "unexpected error on writing log"))))))) - nil) - `(let [message# (or ~raw (build-message ~(vec props)))] - (write-log! ~logger-sym ~level-sym ~cause message#) - nil))))))) +#?(:clj + (defn slf4j-log-handler + {:no-doc true} + [_ _ _ {:keys [::logger ::level ::props ::cause ::trace ::message]}] + (when-let [logger (enabled? logger level)] + (let [message (cond-> @message + (some? trace) + (str "\n" @trace))] + (case level + :trace (.trace ^Logger logger ^String message ^Throwable cause) + :debug (.debug ^Logger logger ^String message ^Throwable cause) + :info (.info ^Logger logger ^String message ^Throwable cause) + :warn (.warn ^Logger logger ^String message ^Throwable cause) + :error (.error ^Logger logger ^String message ^Throwable cause) + :fatal (.error ^Logger logger ^String message ^Throwable cause) + (throw (IllegalArgumentException. (str "invalid level:" level)))))))) + +#?(:cljs + (defn console-log-handler + {:no-doc true} + [_ _ _ {:keys [::logger ::props ::level ::cause ::trace ::message]}] + (when (enabled? logger level) + (let [hstyles (str/ffmt "font-weight: 600; color: %" (level->color level)) + mstyles (str/ffmt "font-weight: 300; color: %" "#282a2e") + header (str/concat "%c" (level->name level) " [" logger "] ") + message (str/concat header "%c" @message)] + + (js/console.group message hstyles mstyles) + (doseq [[type n v] (get-special-props props)] + (case type + :js (js/console.log n v) + :error (if (ex/error? v) + (js/console.error n (pr-str v)) + (js/console.error n v)))) + + (when cause + (let [data (ex-data cause) + explain (ex/explain data)] + (when explain + (js/console.log "Explain:") + (js/console.log explain)) + + (when (and data (not explain)) + (js/console.log "Data:") + (js/console.log (pp/pprint-str data))) + + (js/console.log @trace #_(.-stack cause)))) + + (js/console.groupEnd message))))) + +#?(:clj (add-watch log-record ::default slf4j-log-handler) + :cljs (add-watch log-record ::default console-log-handler)) + +(defmacro set-level! + "A CLJS-only macro for set logging level to current (that matches the + current namespace) or user specified logger." + ([level] + (when (:ns &env) + `(.set ^js/Map loggers ~(str *ns*) (level->int ~level)))) + ([name level] + (when (:ns &env) + `(.set ^js/Map loggers ~name (level->int ~level))))) + +#?(:cljs + (defn setup! + [{:as config}] + (run! (fn [[logger level]] + (let [logger (if (keyword? logger) (name logger) logger)] + (l/set-level! logger level))) + config))) (defmacro info [& params] - `(log :level :info ~@params)) + `(do + (log! ::logger ~(str *ns*) ::level :info ~@params) + nil)) + +(defmacro inf + [& params] + `(do + (log! ::logger ~(str *ns*) ::level :info ~@params) + nil)) (defmacro error [& params] - `(log :level :error ~@params)) + `(do + (log! ::logger ~(str *ns*) ::level :error ~@params) + nil)) + +(defmacro err + [& params] + `(do + (log! ::logger ~(str *ns*) ::level :error ~@params) + nil)) (defmacro warn [& params] - `(log :level :warn ~@params)) + `(do + (log! ::logger ~(str *ns*) ::level :warn ~@params) + nil)) + +(defmacro wrn + [& params] + `(do + (log! ::logger ~(str *ns*) ::level :warn ~@params) + nil)) (defmacro debug [& params] - `(log :level :debug ~@params)) + `(do + (log! ::logger ~(str *ns*) ::level :debug ~@params) + nil)) + +(defmacro dbg + [& params] + `(do + (log! ::logger ~(str *ns*) ::level :debug ~@params) + nil)) (defmacro trace [& params] - `(log :level :trace ~@params)) + `(do + (log! ::logger ~(str *ns*) ::level :trace ~@params) + nil)) -(defmacro set-level! - ([level] - (when (:ns &env) - `(set-level* ~(str *ns*) ~level))) - ([n level] - (when (:ns &env) - `(set-level* ~n ~level)))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; CLJS Specific -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -#?(:cljs - (def ^:private colors - {:gray3 "#8e908c" - :gray4 "#969896" - :gray5 "#4d4d4c" - :gray6 "#282a2e" - :black "#1d1f21" - :red "#c82829" - :blue "#4271ae" - :orange "#f5871f"})) - -#?(:cljs - (defn- level->color - [level] - (letfn [(get-level-value [l] (.-value ^js (get-level l)))] - (condp <= (get-level-value level) - (get-level-value :error) (get colors :red) - (get-level-value :warn) (get colors :orange) - (get-level-value :info) (get colors :blue) - (get-level-value :debug) (get colors :gray4) - (get-level-value :trace) (get colors :gray3) - (get colors :gray2))))) - -#?(:cljs - (defn- level->short-name - [l] - (case l - :fine "DBG" - :debug "DBG" - :finer "TRC" - :trace "TRC" - :info "INF" - :warn "WRN" - :warning "WRN" - :error "ERR" - (subs (.-name ^js (get-level l)) 0 3)))) - -#?(:cljs - (defn set-level* - "Set the level (a keyword) of the given logger, identified by name." - [name lvl] - (some-> (get-logger name) - (glog/setLevel (get-level lvl))))) - -#?(:cljs - (defn set-levels! - [lvls] - (doseq [[logger level] lvls - :let [level (if (string? level) (keyword level) level)]] - (set-level* logger level)))) - -#?(:cljs - (defn- prepare-message - [message] - (loop [kvpairs (seq message) - message [] - specials []] - (if (nil? kvpairs) - [message specials] - (let [[k v] (first kvpairs)] - (cond - (= k :err) - (recur (next kvpairs) - message - (conj specials [:error nil v])) - - (and (qualified-ident? k) - (= "js" (namespace k))) - (recur (next kvpairs) - message - (conj specials [:js (name k) (if (object? v) v (clj->js v))])) - - :else - (recur (next kvpairs) - (conj message (str/concat (d/name k) "=" (pr-str v))) - specials))))))) - -#?(:cljs - (defn default-handler - [{: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) - header (str "%c" level-name " [" logger-name "] ")] - - (if (string? message) - (let [message (str header "%c" message)] - (js/console.log message header-styles normal-styles)) - (let [[message specials] (prepare-message message)] - (if (seq specials) - (let [message (str header "%c" message)] - (js/console.group message header-styles normal-styles) - (doseq [[type n v] specials] - (case type - :js (js/console.log n v) - :error (if (ex/ex-info? v) - (js/console.error (pr-str v)) - (js/console.error v)))) - (js/console.groupEnd message)) - (let [message (str header "%c" message)] - (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 - [^js record] - {:seqn (.-sequenceNumber_ record) - :time (.-time_ record) - :level (keyword (str/lower (.-name (.-level_ record)))) - :message (.-msg_ record) - :logger-name (.-loggerName_ record) - :exception (.-exception_ record)})) - -#?(:cljs - (defonce default-console-handler - (comp default-handler record->map))) - -#?(:cljs - (defn initialize! - [] - (let [l (get-logger :root)] - (glog/removeHandler l default-console-handler) - (glog/addHandler l default-console-handler) - nil))) +(defmacro trc + [& params] + `(do + (log! ::logger ~(str *ns*) ::level :trace ~@params) + nil)) diff --git a/common/src/app/common/math.cljc b/common/src/app/common/math.cljc index d3f724ee9..0adc340be 100644 --- a/common/src/app/common/math.cljc +++ b/common/src/app/common/math.cljc @@ -185,8 +185,10 @@ (defn close? "Equality for float numbers. Check if the difference is within a range" - [num1 num2] - (<= (abs (- num1 num2)) float-equal-precision)) + ([num1 num2] + (close? num1 num2 float-equal-precision)) + ([num1 num2 precision] + (<= (abs (- num1 num2)) precision))) (defn lerp "Calculates a the linear interpolation between two values and a given percent" diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index b45ab53f1..3cf5c6100 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -35,6 +35,10 @@ [changes save-undo?] (assoc changes :save-undo? save-undo?)) +(defn set-stack-undo? + [changes stack-undo?] + (assoc changes :stack-undo? stack-undo?)) + (defn with-page [changes page] (vary-meta changes assoc @@ -665,3 +669,13 @@ :id id}) (update :undo-changes d/preconj {:type :del-component :id id}))) + +(defn ignore-remote + [changes] + (letfn [(add-ignore-remote + [change-list] + (->> change-list + (mapv #(assoc % :ignore-remote? true))))] + (-> changes + (update :redo-changes add-ignore-remote) + (update :undo-changes add-ignore-remote)))) diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc index 255e00a70..e49c0eed1 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -202,7 +202,9 @@ :layout-item-min-h :layout-item-max-w :layout-item-min-w - :layout-item-align-self} + :layout-item-align-self + :layout-item-absolute + :layout-item-z-index} :rect #{:proportion-lock :width :height diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index f3f6f0dbb..b5ec13c21 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.spec :as us] + [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [cuerdas.core :as str])) @@ -23,9 +24,11 @@ (and (= type :frame) (= id uuid/zero))) (defn root-frame? - [{:keys [frame-id type]}] - (and (= type :frame) - (= frame-id uuid/zero))) + ([objects id] + (root-frame? (get objects id))) + ([{:keys [frame-id type]}] + (and (= type :frame) + (= frame-id uuid/zero)))) (defn frame-shape? ([objects id] @@ -34,8 +37,10 @@ (= type :frame))) (defn group-shape? - [{:keys [type]}] - (= type :group)) + ([objects id] + (group-shape? (get objects id))) + ([{:keys [type]}] + (= type :group))) (defn mask-shape? [{:keys [type masked-group?]}] @@ -53,6 +58,10 @@ [{:keys [type]}] (= type :text)) +(defn rect-shape? + [{:keys [type]}] + (= type :rect)) + (defn image-shape? [{:keys [type]}] (= type :image)) @@ -71,6 +80,12 @@ (and (not (frame-shape? shape)) (= (:frame-id shape) uuid/zero))) +(defn has-children? + ([objects id] + (has-children? (get objects id))) + ([shape] + (d/not-empty? (:shapes shape)))) + (defn get-children-ids [objects id] (letfn [(get-children-ids-rec @@ -109,6 +124,16 @@ (recur (conj result parent-id) parent-id) result)))) +(defn hidden-parent? + "Checks the parent for the hidden property" + [objects shape-id] + (let [parent-id (dm/get-in objects [shape-id :parent-id])] + (cond + (or (nil? parent-id) (nil? shape-id) (= shape-id uuid/zero) (= parent-id uuid/zero)) false + (dm/get-in objects [parent-id :hidden]) true + :else + (recur objects parent-id)))) + (defn get-parent-ids-with-index "Returns a tuple with the list of parents and a map with the position within each parent" [objects shape-id] @@ -148,6 +173,25 @@ (get objects) (get-frame objects))))) +(defn get-root-frame + [objects shape-id] + + (let [frame-id + (if (frame-shape? objects shape-id) + shape-id + (dm/get-in objects [shape-id :frame-id])) + + frame (get objects frame-id)] + (cond + (or (root? frame) (nil? frame)) + nil + + (root-frame? frame) + frame + + :else + (get-root-frame objects (:frame-id frame))))) + (defn valid-frame-target? [objects parent-id shape-id] (let [shape (get objects shape-id)] @@ -482,8 +526,17 @@ (defn is-child? [objects parent-id candidate-child-id] - (let [parents (get-parent-ids objects candidate-child-id)] - (some? (d/seek #(= % parent-id) parents)))) + (loop [cur-id candidate-child-id] + (let [cur-parent-id (dm/get-in objects [cur-id :parent-id])] + (cond + (= parent-id cur-parent-id) + true + + (or (= cur-parent-id uuid/zero) (nil? cur-parent-id)) + false + + :else + (recur cur-parent-id))))) (defn reduce-objects ([objects reducer-fn init-val] @@ -499,19 +552,22 @@ (loop [current-val init-val current-id (first root-children) - pending-ids (rest root-children)] + pending-ids (rest root-children) + processed #{}] + (if (contains? processed current-id) + (recur current-val (first pending-ids) (rest pending-ids) processed) + (let [current-shape (get objects current-id) + processed (conj processed current-id) + next-val (reducer-fn current-val current-shape) + next-pending-ids + (if (or (nil? check-children?) (check-children? current-shape)) + (concat (or (:shapes current-shape) []) pending-ids) + pending-ids)] - (let [current-shape (get objects current-id) - next-val (reducer-fn current-val current-shape) - next-pending-ids - (if (or (nil? check-children?) (check-children? current-shape)) - (concat (or (:shapes current-shape) []) pending-ids) - pending-ids)] - - (if (empty? next-pending-ids) - next-val - (recur next-val (first next-pending-ids) (rest next-pending-ids))))))))) + (if (empty? next-pending-ids) + next-val + (recur next-val (first next-pending-ids) (rest next-pending-ids) processed))))))))) (defn selected-with-children [objects selected] @@ -527,3 +583,40 @@ (d/seek root-frame?) :id)) +(defn comparator-layout-z-index + [[idx-a child-a] [idx-b child-b]] + (cond + (> (ctl/layout-z-index child-a) (ctl/layout-z-index child-b)) 1 + (< (ctl/layout-z-index child-a) (ctl/layout-z-index child-b)) -1 + (> idx-a idx-b) 1 + (< idx-a idx-b) -1 + :else 0)) + +(defn sort-layout-children-z-index + [children] + (->> children + (d/enumerate) + (sort comparator-layout-z-index) + (mapv second))) + +(defn common-parent-frame + "Search for the common frame for the selected shapes. Otherwise returns the root frame" + [objects selected] + + (loop [frame-id (get-in objects [(first selected) :frame-id]) + frame-parents (get-parent-ids objects frame-id) + selected (rest selected)] + (if (empty? selected) + frame-id + + (let [current (first selected) + parent? (into #{} (get-parent-ids objects current)) + + [frame-id frame-parents] + (if (parent? frame-id) + [frame-id frame-parents] + + (let [frame-id (d/seek parent? frame-parents)] + [frame-id (get-parent-ids objects frame-id)]))] + + (recur frame-id frame-parents (rest selected)))))) diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index be64cde76..65926a005 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -201,18 +201,18 @@ (letfn [(conformer-fn [dest v] (cond - (coll? v) (into dest non-empty-strings-xf v) + (coll? v) (into dest non-empty-strings-xf v) (string? v) (into dest non-empty-strings-xf (str/split v #"[\s,]+")) :else ::s/invalid)) (unformer-fn [v] (str/join "," v))] (s/def ::set-of-strings - (s/with-gen (s/conformer (partial conformer-fn #{}) unformer-fn) - #(tgen/set (s/gen ::not-empty-string)))) + (-> (s/conformer (partial conformer-fn #{}) unformer-fn) + (s/with-gen #(tgen/set (s/gen ::not-empty-string))))) (s/def ::vector-of-strings - (s/with-gen (s/conformer (partial conformer-fn []) unformer-fn) - #(tgen/vector (s/gen ::not-empty-string))))) + (-> (s/conformer (partial conformer-fn []) unformer-fn) + (s/with-gen #(tgen/vector (s/gen ::not-empty-string)))))) ;; --- SPEC: set-of-valid-emails @@ -435,6 +435,6 @@ [cause] (if (and (map? cause) (= :spec-validation (:type cause))) cause - (when (ex/ex-info? cause) + (when (ex/error? cause) (validation-error? (ex-data cause))))) diff --git a/common/src/app/common/types/modifiers.cljc b/common/src/app/common/types/modifiers.cljc index 5d2764bb7..fb336ae3b 100644 --- a/common/src/app/common/types/modifiers.cljc +++ b/common/src/app/common/types/modifiers.cljc @@ -12,10 +12,14 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.corners :as gsc] + [app.common.geom.shapes.effects :as gse] + [app.common.geom.shapes.strokes :as gss] [app.common.math :as mth] [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.text :as txt] + [app.common.types.shape.layout :as ctl] #?(:cljs [cljs.core :as c] :clj [clojure.core :as c]))) @@ -252,7 +256,7 @@ (defn resize ([modifiers vector origin] - (assert (valid-vector? vector) (dm/str "Invalid move vector: " (:x vector) "," (:y vector))) + (assert (valid-vector? vector) (dm/str "Invalid resize vector: " (:x vector) "," (:y vector))) (let [modifiers (or modifiers (empty)) order (inc (dm/get-prop modifiers :last-order)) modifiers (assoc modifiers :last-order order)] @@ -260,12 +264,12 @@ (resize-vec? vector) (update :geometry-child maybe-add-resize (resize-op order vector origin))))) - ([modifiers vector origin transform transform-inverse] + ([modifiers vector origin transform transform-inverse] (resize modifiers vector origin transform transform-inverse nil)) ;; `precise?` works so we don't remove almost empty resizes. This will be used in the pixel-precision ([modifiers vector origin transform transform-inverse {:keys [precise?]}] - (assert (valid-vector? vector) (dm/str "Invalid move vector: " (:x vector) "," (:y vector))) + (assert (valid-vector? vector) (dm/str "Invalid resize vector: " (:x vector) "," (:y vector))) (let [modifiers (or modifiers (empty)) order (inc (dm/get-prop modifiers :last-order)) modifiers (assoc modifiers :last-order order)] @@ -305,12 +309,12 @@ (-> (or modifiers (empty)) (update :structure-child conj (scale-content-op value)))) -(defn change-property +(defn change-recursive-property [modifiers property value] (-> (or modifiers (empty)) (update :structure-child conj (change-property-op property value)))) -(defn change-parent-property +(defn change-property [modifiers property value] (-> (or modifiers (empty)) (update :structure-parent conj (change-property-op property value)))) @@ -539,6 +543,10 @@ (or (d/not-empty? structure-parent) (d/not-empty? structure-child))) +(defn has-structure-child? + [modifiers] + (d/not-empty? (dm/get-prop modifiers :structure-child))) + ;; Extract subsets of modifiers (defn select-child @@ -561,6 +569,10 @@ [modifiers] (-> modifiers select-child select-geometry)) +(defn select-child-structre-modifiers + [modifiers] + (-> modifiers select-child select-structure)) + (defn added-children-frames "Returns the frames that have an 'add-children' operation" [modif-tree] @@ -632,28 +644,53 @@ matrix)))] (recur matrix (next modifiers))))))) +(defn transform-text-node [value attrs] + (let [font-size (-> (get attrs :font-size 14) d/parse-double (* value) str) + letter-spacing (-> (get attrs :letter-spacing 0) d/parse-double (* value) str)] + (d/txt-merge attrs {:font-size font-size + :letter-spacing letter-spacing}))) + +(defn transform-paragraph-node [value attrs] + (let [font-size (-> (get attrs :font-size 14) d/parse-double (* value) str)] + (d/txt-merge attrs {:font-size font-size}))) + + +(defn update-text-content + [shape scale-text-content value] + (update shape :content scale-text-content value)) + (defn apply-structure-modifiers "Apply structure changes to a shape" [shape modifiers] (letfn [(scale-text-content [content value] - (->> content - (txt/transform-nodes - txt/is-text-node? - (fn [attrs] - (let [font-size (-> (get attrs :font-size 14) - (d/parse-double) - (* value) - (str)) ] - (d/txt-merge attrs {:font-size font-size})))))) + (txt/transform-nodes txt/is-text-node? (partial transform-text-node value)) + (txt/transform-nodes txt/is-paragraph-node? (partial transform-paragraph-node value)))) (apply-scale-content [shape value] - (cond-> shape (cph/text-shape? shape) - (update :content scale-text-content value)))] + (update-text-content scale-text-content value) + + :always + (gsc/update-corners-scale value) + + (d/not-empty? (:strokes shape)) + (gss/update-strokes-width value) + + (d/not-empty? (:shadow shape)) + (gse/update-shadows-scale value) + + (some? (:blur shape)) + (gse/update-blur-scale value) + + (ctl/flex-layout? shape) + (ctl/update-flex-scale value) + + :always + (ctl/update-flex-child value)))] (let [remove-children (fn [shapes children-to-remove] @@ -683,7 +720,6 @@ (let [value (dm/get-prop operation :value)] (update shape :shapes remove-children value)) - :scale-content (let [value (dm/get-prop operation :value)] (apply-scale-content shape value)) diff --git a/common/src/app/common/types/shape/interactions.cljc b/common/src/app/common/types/shape/interactions.cljc index 048e963d9..d06a059bc 100644 --- a/common/src/app/common/types/shape/interactions.cljc +++ b/common/src/app/common/types/shape/interactions.cljc @@ -8,6 +8,8 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.geom.shapes.bounds :as gsb] + [app.common.pages.helpers :as cph] [app.common.spec :as us] [clojure.spec.alpha :as s])) @@ -362,6 +364,8 @@ (defn calc-overlay-position [interaction ;; interaction data + shape ;; Shape with the interaction + objects ;; the objects tree relative-to-shape ;; the interaction position is realtive to this sape base-frame ;; the base frame of the current interaction dest-frame ;; the frame to display with this interaction @@ -369,56 +373,68 @@ (us/verify ::interaction interaction) (assert (has-overlay-opts interaction)) - (if (nil? dest-frame) - (gpt/point 0 0) - (let [overlay-size (:selrect dest-frame) - base-frame-size (:selrect base-frame) - relative-to-shape-size (:selrect relative-to-shape) - relative-to-adjusted-to-base-frame {:x (- (:x relative-to-shape-size) (:x base-frame-size)) - :y (- (:y relative-to-shape-size) (:y base-frame-size))} - relative-to-is-auto? (and (nil? (:position-relative-to interaction)) (not= :manual (:overlay-pos-type interaction))) - base-position (if relative-to-is-auto? - {:x 0 :y 0} - {:x (+ (:x frame-offset) - (:x relative-to-adjusted-to-base-frame)) - :y (+ (:y frame-offset) - (:y relative-to-adjusted-to-base-frame))}) - overlay-position (:overlay-position interaction) - overlay-position (if (= (:type relative-to-shape) :frame) - overlay-position - {:x (- (:x overlay-position) (:x relative-to-adjusted-to-base-frame)) - :y (- (:y overlay-position) (:y relative-to-adjusted-to-base-frame))})] - (case (:overlay-pos-type interaction) - :center - (gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2)) - (+ (:y base-position) (/ (- (:height relative-to-shape-size) (:height overlay-size)) 2))) + (let [ + ;; When the interactive item is inside a nested frame we need to add to the offset the position + ;; of the parent-frame otherwise the position won't match + shape-frame (cph/get-frame objects shape) - :top-left - (gpt/point (:x base-position) (:y base-position)) + frame-offset (if (or (not= :manual (:overlay-pos-type interaction)) + (nil? shape-frame) + (cph/root-frame? shape-frame) + (cph/root? shape-frame)) + frame-offset + (gpt/add frame-offset (gpt/point shape-frame))) + ] + (if (nil? dest-frame) + (gpt/point 0 0) + (let [overlay-size (gsb/get-object-bounds objects dest-frame) + base-frame-size (:selrect base-frame) + relative-to-shape-size (:selrect relative-to-shape) + relative-to-adjusted-to-base-frame {:x (- (:x relative-to-shape-size) (:x base-frame-size)) + :y (- (:y relative-to-shape-size) (:y base-frame-size))} + relative-to-is-auto? (and (nil? (:position-relative-to interaction)) (not= :manual (:overlay-pos-type interaction))) + base-position (if relative-to-is-auto? + {:x 0 :y 0} + {:x (+ (:x frame-offset) + (:x relative-to-adjusted-to-base-frame)) + :y (+ (:y frame-offset) + (:y relative-to-adjusted-to-base-frame))}) + overlay-position (:overlay-position interaction) + overlay-position (if (= (:type relative-to-shape) :frame) + overlay-position + {:x (- (:x overlay-position) (:x relative-to-adjusted-to-base-frame)) + :y (- (:y overlay-position) (:y relative-to-adjusted-to-base-frame))})] + (case (:overlay-pos-type interaction) + :center + (gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2)) + (+ (:y base-position) (/ (- (:height relative-to-shape-size) (:height overlay-size)) 2))) - :top-right - (gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size))) - (:y base-position)) + :top-left + (gpt/point (:x base-position) (:y base-position)) - :top-center - (gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2)) - (:y base-position)) + :top-right + (gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size))) + (:y base-position)) - :bottom-left - (gpt/point (:x base-position) - (+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size)))) + :top-center + (gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2)) + (:y base-position)) - :bottom-right - (gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size))) - (+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size)))) + :bottom-left + (gpt/point (:x base-position) + (+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size)))) - :bottom-center - (gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2)) - (+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size)))) + :bottom-right + (gpt/point (+ (:x base-position) (- (:width relative-to-shape-size) (:width overlay-size))) + (+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size)))) - :manual - (gpt/point (+ (:x base-position) (:x overlay-position)) - (+ (:y base-position) (:y overlay-position))))))) + :bottom-center + (gpt/point (+ (:x base-position) (/ (- (:width relative-to-shape-size) (:width overlay-size)) 2)) + (+ (:y base-position) (- (:height relative-to-shape-size) (:height overlay-size)))) + + :manual + (gpt/point (+ (:x base-position) (:x overlay-position)) + (+ (:y base-position) (:y overlay-position)))))))) (defn has-animation? [interaction] diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index d7ecec795..6ae3ca0ed 100644 --- a/common/src/app/common/types/shape/layout.cljc +++ b/common/src/app/common/types/shape/layout.cljc @@ -6,14 +6,17 @@ (ns app.common.types.shape.layout (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.spec :as us] + [app.common.uuid :as uuid] [clojure.spec.alpha :as s])) ;; :layout ;; :flex, :grid in the future ;; :layout-flex-dir ;; :row, :row-reverse, :column, :column-reverse ;; :layout-gap-type ;; :simple, :multiple ;; :layout-gap ;; {:row-gap number , :column-gap number} + ;; :layout-align-items ;; :start :end :center :stretch ;; :layout-justify-content ;; :start :center :end :space-between :space-around :space-evenly ;; :layout-align-content ;; :start :center :end :space-between :space-around :space-evenly :stretch (by default) @@ -21,6 +24,10 @@ ;; :layout-padding-type ;; :simple, :multiple ;; :layout-padding ;; {:p1 num :p2 num :p3 num :p4 num} number could be negative +;; layout-grid-rows +;; layout-grid-columns +;; layout-justify-items + ;; ITEMS ;; :layout-item-margin ;; {:m1 0 :m2 0 :m3 0 :m4 0} ;; :layout-item-margin-type ;; :simple :multiple @@ -30,17 +37,53 @@ ;; :layout-item-min-h ;; num ;; :layout-item-max-w ;; num ;; :layout-item-min-w ;; num +;; :layout-item-absolute +;; :layout-item-z-index (s/def ::layout #{:flex :grid}) + (s/def ::layout-flex-dir #{:row :reverse-row :row-reverse :column :reverse-column :column-reverse}) ;;TODO remove reverse-column and reverse-row after script +(s/def ::layout-grid-dir #{:row :column}) (s/def ::layout-gap-type #{:simple :multiple}) (s/def ::layout-gap ::us/safe-number) + (s/def ::layout-align-items #{:start :end :center :stretch}) +(s/def ::layout-justify-items #{:start :end :center :stretch}) (s/def ::layout-align-content #{:start :end :center :space-between :space-around :space-evenly :stretch}) (s/def ::layout-justify-content #{:start :center :end :space-between :space-around :space-evenly}) (s/def ::layout-wrap-type #{:wrap :nowrap :no-wrap}) ;;TODO remove no-wrap after script (s/def ::layout-padding-type #{:simple :multiple}) +(s/def :grid/type #{:percent :flex :auto :fixed}) +(s/def :grid/value (s/nilable ::us/safe-number)) +(s/def ::grid-definition (s/keys :req-un [:grid/type] + :opt-un [:grid/value])) +(s/def ::layout-grid-rows (s/coll-of ::grid-definition :kind vector?)) +(s/def ::layout-grid-columns (s/coll-of ::grid-definition :kind vector?)) + +(s/def :grid-cell/id uuid?) +(s/def :grid-cell/area-name ::us/string) +(s/def :grid-cell/row-start ::us/safe-integer) +(s/def :grid-cell/row-span ::us/safe-integer) +(s/def :grid-cell/column-start ::us/safe-integer) +(s/def :grid-cell/column-span ::us/safe-integer) +(s/def :grid-cell/position #{:auto :manual :area}) +(s/def :grid-cell/align-self #{:auto :start :end :center :stretch}) +(s/def :grid-cell/justify-self #{:auto :start :end :center :stretch}) +(s/def :grid-cell/shapes (s/coll-of uuid?)) + +(s/def ::grid-cell (s/keys :opt-un [:grid-cell/id + :grid-cell/area-name + :grid-cell/row-start + :grid-cell/row-span + :grid-cell/column-start + :grid-cell/column-span + :grid-cell/position ;; auto, manual, area + :grid-cell/align-self + :grid-cell/justify-self + :grid-cell/shapes])) +(s/def ::layout-grid-cells (s/map-of uuid? ::grid-cell)) + (s/def ::p1 ::us/safe-number) (s/def ::p2 ::us/safe-number) (s/def ::p3 ::us/safe-number) @@ -65,7 +108,15 @@ ::layout-padding ::layout-justify-content ::layout-align-items - ::layout-align-content])) + ::layout-align-content + + ;; grid + ::layout-grid-dir + ::layout-justify-items + ::layout-grid-rows + ::layout-grid-columns + ::layout-grid-cells + ])) (s/def ::m1 ::us/safe-number) (s/def ::m2 ::us/safe-number) @@ -82,6 +133,8 @@ (s/def ::layout-item-min-h ::us/safe-number) (s/def ::layout-item-max-w ::us/safe-number) (s/def ::layout-item-min-w ::us/safe-number) +(s/def ::layout-item-absolute boolean?) +(s/def ::layout-item-z-index ::us/safe-integer) (s/def ::layout-child-props (s/keys :opt-un [::layout-item-margin @@ -92,28 +145,65 @@ ::layout-item-min-h ::layout-item-max-w ::layout-item-min-w - ::layout-item-align-self])) + ::layout-item-align-self + ::layout-item-absolute + ::layout-item-z-index])) -(defn layout? +(defn flex-layout? ([objects id] - (layout? (get objects id))) + (flex-layout? (get objects id))) ([shape] - (and (= :frame (:type shape)) (= :flex (:layout shape))))) + (and (= :frame (:type shape)) + (= :flex (:layout shape))))) -(defn layout-immediate-child? [objects shape] +(defn grid-layout? + ([objects id] + (grid-layout? (get objects id))) + ([shape] + (and (= :frame (:type shape)) + (= :grid (:layout shape))))) + +(defn any-layout? + ([objects id] + (any-layout? (get objects id))) + + ([shape] + (or (flex-layout? shape) (grid-layout? shape)))) + +(defn flex-layout-immediate-child? [objects shape] (let [parent-id (:parent-id shape) parent (get objects parent-id)] - (layout? parent))) + (flex-layout? parent))) -(defn layout-immediate-child-id? [objects id] +(defn any-layout-immediate-child? [objects shape] + (let [parent-id (:parent-id shape) + parent (get objects parent-id)] + (any-layout? parent))) + +(defn flex-layout-immediate-child-id? [objects id] (let [parent-id (dm/get-in objects [id :parent-id]) parent (get objects parent-id)] - (layout? parent))) + (flex-layout? parent))) -(defn layout-descent? [objects shape] +(defn any-layout-immediate-child-id? [objects id] + (let [parent-id (dm/get-in objects [id :parent-id]) + parent (get objects parent-id)] + (any-layout? parent))) + +(defn flex-layout-descent? [objects shape] (let [frame-id (:frame-id shape) frame (get objects frame-id)] - (layout? frame))) + (flex-layout? frame))) + +(defn grid-layout-descent? [objects shape] + (let [frame-id (:frame-id shape) + frame (get objects frame-id)] + (grid-layout? frame))) + +(defn any-layout-descent? [objects shape] + (let [frame-id (:frame-id shape) + frame (get objects frame-id)] + (any-layout? frame))) (defn inside-layout? "Check if the shape is inside a layout" @@ -340,20 +430,182 @@ (defn align-self-stretch? [{:keys [layout-item-align-self]}] (= :stretch layout-item-align-self)) +(defn layout-absolute? + ([objects id] + (layout-absolute? (get objects id))) + ([shape] + (true? (:layout-item-absolute shape)))) + +(defn layout-z-index + ([objects id] + (layout-z-index (get objects id))) + ([shape] + (or (:layout-item-z-index shape) 0))) + (defn change-h-sizing? [frame-id objects children-ids] - (and (layout? objects frame-id) + (and (flex-layout? objects frame-id) (auto-width? objects frame-id) (or (and (col? objects frame-id) - (every? (partial fill-width? objects) children-ids)) + (->> children-ids + (remove (partial layout-absolute? objects)) + (every? (partial fill-width? objects)))) (and (row? objects frame-id) - (some (partial fill-width? objects) children-ids))))) + (->> children-ids + (remove (partial layout-absolute? objects)) + (some (partial fill-width? objects))))))) (defn change-v-sizing? [frame-id objects children-ids] - (and (layout? objects frame-id) + (and (flex-layout? objects frame-id) (auto-height? objects frame-id) (or (and (col? objects frame-id) (some (partial fill-height? objects) children-ids)) (and (row? objects frame-id) (every? (partial fill-height? objects) children-ids))))) + +(defn remove-layout-container-data + [shape] + (dissoc shape + :layout + :layout-flex-dir + :layout-gap + :layout-gap-type + :layout-wrap-type + :layout-padding-type + :layout-padding + :layout-justify-content + :layout-align-items + :layout-align-content + :layout-grid-dir + :layout-justify-items + :layout-grid-columns + :layout-grid-rows + )) + +(defn remove-layout-item-data + [shape] + (dissoc shape + :layout-item-margin + :layout-item-margin-type + :layout-item-h-sizing + :layout-item-v-sizing + :layout-item-max-h + :layout-item-min-h + :layout-item-max-w + :layout-item-min-w + :layout-item-align-self + :layout-item-absolute + :layout-item-z-index)) + +(defn update-flex-scale + [shape scale] + (-> shape + (d/update-in-when [:layout-gap :row-gap] * scale) + (d/update-in-when [:layout-gap :column-gap] * scale) + (d/update-in-when [:layout-padding :p1] * scale) + (d/update-in-when [:layout-padding :p2] * scale) + (d/update-in-when [:layout-padding :p3] * scale) + (d/update-in-when [:layout-padding :p4] * scale))) + +(defn update-flex-child + [shape scale] + (-> shape + (d/update-when :layout-item-max-h * scale) + (d/update-when :layout-item-min-h * scale) + (d/update-when :layout-item-max-w * scale) + (d/update-when :layout-item-min-w * scale) + (d/update-in-when [:layout-item-margin :m1] * scale) + (d/update-in-when [:layout-item-margin :m2] * scale) + (d/update-in-when [:layout-item-margin :m3] * scale) + (d/update-in-when [:layout-item-margin :m4] * scale))) + + +(declare assign-cells) + +(def grid-cell-defaults + {:row-span 1 + :column-span 1 + :position :auto + :align-self :auto + :justify-self :auto + :shapes []}) + +;; TODO: GRID ASSIGNMENTS + +;; Adding a track creates the cells. We should check the shapes that are not tracked (with default values) and assign to the correct tracked values +(defn add-grid-column + [parent value] + (us/assert ::grid-definition value) + (let [rows (:layout-grid-rows parent) + new-col-num (count (:layout-grid-columns parent)) + + layout-grid-cells + (->> (d/enumerate rows) + (reduce (fn [result [row-idx _row]] + (let [id (uuid/next)] + (assoc result id + (merge {:id id + :row (inc row-idx) + :column new-col-num + :track? true} + grid-cell-defaults)))) + (:layout-grid-cells parent)))] + (-> parent + (update :layout-grid-columns (fnil conj []) value) + (assoc :layout-grid-cells layout-grid-cells)))) + +(defn add-grid-row + [parent value] + (us/assert ::grid-definition value) + (let [cols (:layout-grid-columns parent) + new-row-num (inc (count (:layout-grid-rows parent))) + + layout-grid-cells + (->> (d/enumerate cols) + (reduce (fn [result [col-idx _col]] + (let [id (uuid/next)] + (assoc result id + (merge {:id id + :column (inc col-idx) + :row new-row-num + :track? true} + grid-cell-defaults)))) + (:layout-grid-cells parent)))] + (-> parent + (update :layout-grid-rows (fnil conj []) value) + (assoc :layout-grid-cells layout-grid-cells)))) + +;; TODO: Remove a track and its corresponding cells. We need to reassign the orphaned shapes into not-tracked cells +(defn remove-grid-column + [parent _index] + parent) + +(defn remove-grid-row + [parent _index] + parent) + +;; TODO: Mix the cells given as arguments leaving only one. It should move all the shapes in those cells in the direction for the grid +;; and lastly use assign-cells to reassing the orphaned shapes +(defn merge-cells + [parent _cells] + parent) + + +;; TODO +;; Assign cells takes the children and move them into the allotted cells. If there are not enough cells it creates +;; not-tracked rows/columns and put the shapes there +;; Should be caled each time a child can be added like: +;; - On shape creation +;; - When moving a child from layers +;; - Moving from the transform into a cell and there are shapes without cell +;; - Shape duplication +;; - (maybe) create group/frames. This case will assigna a cell that had one of its children +(defn assign-cells + [parent] + #_(let [allocated-shapes + (into #{} (mapcat :shapes) (:layout-grid-cells parent)) + + no-cell-shapes + (->> (:shapes parent) (remove allocated-shapes))]) + parent) diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index 1968eb1ac..5f281a890 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -14,6 +14,7 @@ [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.types.shape :as cts] + [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [clojure.spec.alpha :as s])) @@ -147,45 +148,48 @@ [base idx-a idx-b])) (defn is-shape-over-shape? - [objects base-shape-id over-shape-id] + [objects base-shape-id over-shape-id bottom-frames?] (let [[base index-a index-b] (get-base objects base-shape-id over-shape-id)] (cond - ;; The base the base shape, so the other item is bellow + ;; The base the base shape, so the other item is bellow (if not bottom-frames) (= base base-shape-id) - false + (and bottom-frames? (cph/frame-shape? objects base)) - ;; The base is the testing over, so it's over + ;; The base is the testing over, so it's over (if not bottom-frames) (= base over-shape-id) - true + (or (not bottom-frames?) (not (cph/frame-shape? objects base))) ;; Check which index is lower :else - (< index-a index-b)))) + ;; If the base is a layout we should check if the z-index property is set + (let [[z-index-a z-index-b] + (if (ctl/any-layout? objects base) + [(ctl/layout-z-index objects (dm/get-in objects [base :shapes index-a])) + (ctl/layout-z-index objects (dm/get-in objects [base :shapes index-b]))] + [0 0])] + + (if (= z-index-a z-index-b) + (< index-a index-b) + (< z-index-a z-index-b)))))) (defn sort-z-index ([objects ids] (sort-z-index objects ids nil)) - ([objects ids {:keys [bottom-frames?] :as options}] - (letfn [(comp [id-a id-b] - (let [type-a (dm/get-in objects [id-a :type]) - type-b (dm/get-in objects [id-b :type])] - (cond - (and (not= :frame type-a) (= :frame type-b)) - (if bottom-frames? -1 1) + ([objects ids {:keys [bottom-frames?] :as options + :or {bottom-frames? false}}] + (letfn [ + (comp [id-a id-b] + (cond + (= id-a id-b) + 0 - (and (= :frame type-a) (not= :frame type-b)) - (if bottom-frames? 1 -1) + (is-shape-over-shape? objects id-a id-b bottom-frames?) + 1 - (= id-a id-b) - 0 - - (is-shape-over-shape? objects id-b id-a) - -1 - - :else - 1)))] + :else + -1))] (sort comp ids)))) (defn frame-id-by-position diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index baef0d5f3..0e713976e 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -59,3 +59,10 @@ (.putLong buf (.getMostSignificantBits o)) (.putLong buf (.getLeastSignificantBits o)) (.array buf)))) + +#?(:clj + (defn from-bytes + [^bytes o] + (let [buf (ByteBuffer/wrap o)] + (UUID. ^long (.getLong buf) + ^long (.getLong buf))))) diff --git a/common/test/common_tests/types_shape_interactions_test.cljc b/common/test/common_tests/types_shape_interactions_test.cljc index c3202394b..4411d86f1 100644 --- a/common/test/common_tests/types_shape_interactions_test.cljc +++ b/common/test/common_tests/types_shape_interactions_test.cljc @@ -6,6 +6,7 @@ (ns common-tests.types-shape-interactions-test (:require + [app.common.geom.shapes :as gsh] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] [app.common.types.shape :as cts] @@ -275,26 +276,38 @@ new-interaction (ctsi/set-position-relative-to i3 relative-to-id)] (t/is (= relative-to-id (:position-relative-to new-interaction))))))) +(defn setup-selrect [{:keys [x y width height] :as obj}] + (let [rect (gsh/make-rect x y width height) + center (gsh/center-rect rect) + selrect (gsh/rect->selrect rect) + points (gsh/rect->points rect)] + (-> obj + (assoc :selrect selrect) + (assoc :points points)))) (t/deftest calc-overlay-position (let [base-frame (-> (cts/make-minimal-shape :frame) - (assoc-in [:selrect :width] 100) - (assoc-in [:selrect :height] 100)) + (assoc :width 100) + (assoc :height 100) + (setup-selrect)) popup (-> (cts/make-minimal-shape :frame) - (assoc-in [:selrect :width] 50) - (assoc-in [:selrect :height] 50) - (assoc-in [:selrect :x] 10) - (assoc-in [:selrect :y] 10)) + (assoc :width 50) + (assoc :height 50) + (assoc :x 10) + (assoc :y 10) + (setup-selrect)) rect (-> (cts/make-minimal-shape :rect) - (assoc-in [:selrect :width] 50) - (assoc-in [:selrect :height] 50) - (assoc-in [:selrect :x] 10) - (assoc-in [:selrect :y] 10)) + (assoc :width 50) + (assoc :height 50) + (assoc :x 10) + (assoc :y 10) + (setup-selrect)) overlay-frame (-> (cts/make-minimal-shape :frame) - (assoc-in [:selrect :width] 30) - (assoc-in [:selrect :height] 20)) + (assoc :width 30) + (assoc :height 20) + (setup-selrect)) objects {(:id base-frame) base-frame (:id popup) popup @@ -311,49 +324,49 @@ interaction-rect (ctsi/set-position-relative-to interaction (:id rect))] (t/testing "Overlay top-left relative to auto" (let [i2 (ctsi/set-overlay-pos-type interaction-auto :top-left base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 0)) (t/is (= (:y overlay-pos) 0)))) (t/testing "Overlay top-center relative to auto" (let [i2 (ctsi/set-overlay-pos-type interaction-auto :top-center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 35)) (t/is (= (:y overlay-pos) 0)))) (t/testing "Overlay top-right relative to auto" (let [i2 (ctsi/set-overlay-pos-type interaction-auto :top-right base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 70)) (t/is (= (:y overlay-pos) 0)))) (t/testing "Overlay bottom-left relative to auto" (let [i2 (ctsi/set-overlay-pos-type interaction-auto :bottom-left base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 0)) (t/is (= (:y overlay-pos) 80)))) (t/testing "Overlay bottom-center relative to auto" (let [i2 (ctsi/set-overlay-pos-type interaction-auto :bottom-center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 35)) (t/is (= (:y overlay-pos) 80)))) (t/testing "Overlay bottom-right relative to auto" (let [i2 (ctsi/set-overlay-pos-type interaction-auto :bottom-right base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 70)) (t/is (= (:y overlay-pos) 80)))) (t/testing "Overlay center relative to auto" (let [i2 (ctsi/set-overlay-pos-type interaction-auto :center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 35)) (t/is (= (:y overlay-pos) 40)))) (t/testing "Overlay manual relative to auto" (let [i2 (ctsi/set-overlay-pos-type interaction-auto :center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 35)) (t/is (= (:y overlay-pos) 40)))) @@ -361,49 +374,49 @@ (let [i2 (-> interaction-auto (ctsi/set-overlay-pos-type :manual base-frame objects) (ctsi/set-overlay-position (gpt/point 12 62))) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 17)) (t/is (= (:y overlay-pos) 67)))) (t/testing "Overlay top-left relative to base-frame" (let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :top-left base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 5)) (t/is (= (:y overlay-pos) 5)))) (t/testing "Overlay top-center relative to base-frame" (let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :top-center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 40)) (t/is (= (:y overlay-pos) 5)))) (t/testing "Overlay top-right relative to base-frame" (let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :top-right base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 75)) (t/is (= (:y overlay-pos) 5)))) (t/testing "Overlay bottom-left relative to base-frame" (let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :bottom-left base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 5)) (t/is (= (:y overlay-pos) 85)))) (t/testing "Overlay bottom-center relative to base-frame" (let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :bottom-center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 40)) (t/is (= (:y overlay-pos) 85)))) (t/testing "Overlay bottom-right relative to base-frame" (let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :bottom-right base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 75)) (t/is (= (:y overlay-pos) 85)))) (t/testing "Overlay center relative to base-frame" (let [i2 (ctsi/set-overlay-pos-type interaction-base-frame :center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 40)) (t/is (= (:y overlay-pos) 45)))) @@ -411,49 +424,49 @@ (let [i2 (-> interaction-base-frame (ctsi/set-overlay-pos-type :manual base-frame objects) (ctsi/set-overlay-position (gpt/point 12 62))) - overlay-pos (ctsi/calc-overlay-position i2 base-frame base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects base-frame base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 17)) (t/is (= (:y overlay-pos) 67)))) (t/testing "Overlay top-left relative to popup" (let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-left base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 popup base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 15)) (t/is (= (:y overlay-pos) 15)))) (t/testing "Overlay top-center relative to popup" (let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 popup base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 25)) (t/is (= (:y overlay-pos) 15)))) (t/testing "Overlay top-right relative to popup" (let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-right base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 popup base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 35)) (t/is (= (:y overlay-pos) 15)))) (t/testing "Overlay bottom-left relative to popup" (let [i2 (ctsi/set-overlay-pos-type interaction-popup :bottom-left base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 popup base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 15)) (t/is (= (:y overlay-pos) 45)))) (t/testing "Overlay bottom-center relative to popup" (let [i2 (ctsi/set-overlay-pos-type interaction-popup :bottom-center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 popup base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 25)) (t/is (= (:y overlay-pos) 45)))) (t/testing "Overlay bottom-right relative to popup" (let [i2 (ctsi/set-overlay-pos-type interaction-popup :bottom-right base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 popup base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 35)) (t/is (= (:y overlay-pos) 45)))) (t/testing "Overlay center relative to popup" (let [i2 (ctsi/set-overlay-pos-type interaction-popup :center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 popup base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 25)) (t/is (= (:y overlay-pos) 30)))) @@ -461,49 +474,49 @@ (let [i2 (-> interaction-popup (ctsi/set-overlay-pos-type :manual base-frame objects) (ctsi/set-overlay-position (gpt/point 12 62))) - overlay-pos (ctsi/calc-overlay-position i2 popup base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 27)) (t/is (= (:y overlay-pos) 77)))) (t/testing "Overlay top-left relative to popup" (let [i2 (ctsi/set-overlay-pos-type interaction-popup :top-left base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 popup base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects popup base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 15)) (t/is (= (:y overlay-pos) 15)))) (t/testing "Overlay top-center relative to rect" (let [i2 (ctsi/set-overlay-pos-type interaction-rect :top-center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 rect base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 25)) (t/is (= (:y overlay-pos) 15)))) (t/testing "Overlay top-right relative to rect" (let [i2 (ctsi/set-overlay-pos-type interaction-rect :top-right base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 rect base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 35)) (t/is (= (:y overlay-pos) 15)))) (t/testing "Overlay bottom-left relative to rect" (let [i2 (ctsi/set-overlay-pos-type interaction-rect :bottom-left base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 rect base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 15)) (t/is (= (:y overlay-pos) 45)))) (t/testing "Overlay bottom-center relative to rect" (let [i2 (ctsi/set-overlay-pos-type interaction-rect :bottom-center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 rect base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 25)) (t/is (= (:y overlay-pos) 45)))) (t/testing "Overlay bottom-right relative to rect" (let [i2 (ctsi/set-overlay-pos-type interaction-rect :bottom-right base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 rect base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 35)) (t/is (= (:y overlay-pos) 45)))) (t/testing "Overlay center relative to rect" (let [i2 (ctsi/set-overlay-pos-type interaction-rect :center base-frame objects) - overlay-pos (ctsi/calc-overlay-position i2 rect base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 25)) (t/is (= (:y overlay-pos) 30)))) @@ -511,7 +524,7 @@ (let [i2 (-> interaction-rect (ctsi/set-overlay-pos-type :manual base-frame objects) (ctsi/set-overlay-position (gpt/point 12 62))) - overlay-pos (ctsi/calc-overlay-position i2 rect base-frame overlay-frame frame-offset)] + overlay-pos (ctsi/calc-overlay-position i2 rect objects rect base-frame overlay-frame frame-offset)] (t/is (= (:x overlay-pos) 17)) (t/is (= (:y overlay-pos) 67)))))) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 15c007e2e..c46e0d21d 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -108,12 +108,16 @@ RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ - ESUM='5f9c1ea91000a271afad3726149a6aefbca3c3b9e0fa790e9aa7fbf0f38aa9ed'; \ - BINARY_URL='https://cdn.azul.com/zulu/bin/zulu19.30.11-ca-jdk19.0.1-linux_aarch64.tar.gz'; \ + ESUM='1c4be9aa173cb0deb0d215643d9509c8900e5497290b29eee4bee335fa57984f'; \ + BINARY_URL='https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jdk_aarch64_linux_hotspot_19.0.2_7.tar.gz'; \ + ;; \ + armhf|armv7l) \ + ESUM='6a51cb3868b5a3b81848a0d276267230ff3f8639f20ba9ae9ef1d386440bf1fd'; \ + BINARY_URL='https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jdk_arm_linux_hotspot_19.0.2_7.tar.gz'; \ ;; \ amd64|x86_64) \ - ESUM='2ac8cd9e7e1e30c8fba107164a2ded9fad698326899564af4b1254815adfaa8a'; \ - BINARY_URL='https://cdn.azul.com/zulu/bin/zulu19.30.11-ca-jdk19.0.1-linux_x64.tar.gz'; \ + ESUM='3a3ba7a3f8c3a5999e2c91ea1dca843435a0d1c43737bd2f6822b2f02fc52165'; \ + BINARY_URL='https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jdk_x64_linux_hotspot_19.0.2_7.tar.gz'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ @@ -139,7 +143,7 @@ RUN set -ex; \ curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -; \ echo "deb http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" >> /etc/apt/sources.list.d/postgresql.list; \ apt-get -qq update; \ - apt-get -qqy install postgresql-client-14; \ + apt-get -qqy install postgresql-client-15; \ rm -rf /var/lib/apt/lists/*; RUN set -eux; \ diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index b37f0ac6e..3cbcf2b98 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -100,7 +100,8 @@ (def browser-pool-factory (letfn [(create [] - (p/let [browser (.launch pw/chromium) + (p/let [opts #js {:args #js ["--font-render-hinting=none"]} + browser (.launch pw/chromium opts) id (swap! pool-browser-id inc)] (l/info :origin "factory" :action "create" :browser-id id) (unchecked-set browser "__id" id) diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs index 2efc846ba..29455bec2 100644 --- a/exporter/src/app/core.cljs +++ b/exporter/src/app/core.cljs @@ -15,7 +15,7 @@ [promesa.core :as p])) (enable-console-print!) -(l/initialize!) +(l/setup! {:app :info}) (defn start [& _] diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index 4dfd8bc86..a445e1bf6 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -12,6 +12,8 @@ const gulpSass = require("gulp-sass")(require("sass")); const svgSprite = require("gulp-svg-sprite"); const autoprefixer = require("autoprefixer") +const modules = require("postcss-modules"); + const clean = require("postcss-clean"); const mkdirp = require("mkdirp"); const rimraf = require("rimraf"); @@ -159,7 +161,7 @@ function templatePipeline(options) { const tmpl = gulpMustache({ ts: ts, - th: th, + // th: th, manifest: manifest, translations: JSON.stringify(locales), themes: JSON.stringify(themes), @@ -179,16 +181,36 @@ function templatePipeline(options) { gulpSass.compiler = sass; -gulp.task("scss", function() { +gulp.task("scss:modules", function() { + return gulp.src(["src/**/**.scss"]) + .pipe(gulpSass.sync().on('error', gulpSass.logError)) + .pipe(gulpPostcss([ + modules({ + generateScopedName: "[folder]_[name]_[local]_[hash:base64:5]", + }), + autoprefixer(), + ])) + .pipe(gulp.dest(paths.output + "css/")); +}); + +gulp.task("scss:main", function() { return gulp.src(paths.resources + "styles/main-default.scss") .pipe(gulpSass.sync().on('error', gulpSass.logError)) .pipe(gulpPostcss([ autoprefixer, - // clean({format: "keep-breaks", level: 1}) ])) .pipe(gulp.dest(paths.output + "css/")); }); +gulp.task("scss:concat", function() { + return gulp.src([paths.output + "css/main-default.css", + paths.output + "css/app/**/*.css"]) + .pipe(gulpConcat("main.css")) + .pipe(gulp.dest(paths.output + "css/")); +}); + +gulp.task("scss", gulp.series("scss:main", "scss:modules", "scss:concat")); + gulp.task("svg:sprite:icons", function() { return gulp.src(paths.resources + "images/icons/*.svg") .pipe(gulpRename({prefix: "icon-"})) @@ -250,6 +272,7 @@ gulp.task("dev:dirs", async function(next) { }); gulp.task("watch:main", function() { + gulp.watch("src/**/**.scss", gulp.series("scss")); gulp.watch(paths.resources + "styles/**/**.scss", gulp.series("scss")); gulp.watch(paths.resources + "images/**/*", gulp.series("copy:assets:images")); diff --git a/frontend/package.json b/frontend/package.json index 7bc107fd6..4a3d1353e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,6 +62,7 @@ "luxon": "^3.1.1", "mousetrap": "^1.6.5", "opentype.js": "^1.3.4", + "postcss-modules": "^6.0.0", "randomcolor": "^0.6.2", "react": "~17.0.2", "react-dom": "~17.0.2", diff --git a/frontend/resources/images/cursors/scale-diagonal.svg b/frontend/resources/images/cursors/scale-diagonal.svg new file mode 100644 index 000000000..169c1bce2 --- /dev/null +++ b/frontend/resources/images/cursors/scale-diagonal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/images/cursors/scale-h.svg b/frontend/resources/images/cursors/scale-h.svg new file mode 100644 index 000000000..4a1e56df7 --- /dev/null +++ b/frontend/resources/images/cursors/scale-h.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/images/features/1.18-absolute.gif b/frontend/resources/images/features/1.18-absolute.gif new file mode 100644 index 000000000..7e74681b8 Binary files /dev/null and b/frontend/resources/images/features/1.18-absolute.gif differ diff --git a/frontend/resources/images/features/1.18-scale.gif b/frontend/resources/images/features/1.18-scale.gif new file mode 100644 index 000000000..b51c6c5c2 Binary files /dev/null and b/frontend/resources/images/features/1.18-scale.gif differ diff --git a/frontend/resources/images/features/1.18-spacing.gif b/frontend/resources/images/features/1.18-spacing.gif new file mode 100644 index 000000000..ed888a4f7 Binary files /dev/null and b/frontend/resources/images/features/1.18-spacing.gif differ diff --git a/frontend/resources/images/features/1.18-z-index.gif b/frontend/resources/images/features/1.18-z-index.gif new file mode 100644 index 000000000..78d90699d Binary files /dev/null and b/frontend/resources/images/features/1.18-z-index.gif differ diff --git a/frontend/resources/images/icons/grid-justify-content-column-around.svg b/frontend/resources/images/icons/grid-justify-content-column-around.svg new file mode 100644 index 000000000..65cd17d58 --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-column-around.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-justify-content-column-between.svg b/frontend/resources/images/icons/grid-justify-content-column-between.svg new file mode 100644 index 000000000..783d91a21 --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-column-between.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-justify-content-column-center.svg b/frontend/resources/images/icons/grid-justify-content-column-center.svg new file mode 100644 index 000000000..fc52ce5ed --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-column-center.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-justify-content-column-end.svg b/frontend/resources/images/icons/grid-justify-content-column-end.svg new file mode 100644 index 000000000..18825a412 --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-column-end.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-justify-content-column-start.svg b/frontend/resources/images/icons/grid-justify-content-column-start.svg new file mode 100644 index 000000000..823ef4ebf --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-column-start.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-justify-content-row-around.svg b/frontend/resources/images/icons/grid-justify-content-row-around.svg new file mode 100644 index 000000000..41a980b7f --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-row-around.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-justify-content-row-between.svg b/frontend/resources/images/icons/grid-justify-content-row-between.svg new file mode 100644 index 000000000..bfc38460a --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-row-between.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-justify-content-row-center.svg b/frontend/resources/images/icons/grid-justify-content-row-center.svg new file mode 100644 index 000000000..402b8dba2 --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-row-center.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-justify-content-row-end.svg b/frontend/resources/images/icons/grid-justify-content-row-end.svg new file mode 100644 index 000000000..3515d5a95 --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-row-end.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-justify-content-row-start.svg b/frontend/resources/images/icons/grid-justify-content-row-start.svg new file mode 100644 index 000000000..6539a9314 --- /dev/null +++ b/frontend/resources/images/icons/grid-justify-content-row-start.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/resources/images/icons/grid-layout-mode.svg b/frontend/resources/images/icons/grid-layout-mode.svg new file mode 100644 index 000000000..c90cf5c85 --- /dev/null +++ b/frontend/resources/images/icons/grid-layout-mode.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/position-absolute.svg b/frontend/resources/images/icons/position-absolute.svg new file mode 100644 index 000000000..067d77864 --- /dev/null +++ b/frontend/resources/images/icons/position-absolute.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/set-thumbnail.svg b/frontend/resources/images/icons/set-thumbnail.svg index c90cf5c85..e82fa55b1 100644 --- a/frontend/resources/images/icons/set-thumbnail.svg +++ b/frontend/resources/images/icons/set-thumbnail.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/polyfills/scrollIntoViewIfNeeded.js b/frontend/resources/polyfills/scrollIntoViewIfNeeded.js index 16763f504..9460fd2fb 100644 --- a/frontend/resources/polyfills/scrollIntoViewIfNeeded.js +++ b/frontend/resources/polyfills/scrollIntoViewIfNeeded.js @@ -1,29 +1,46 @@ +/* + MIT License + +Copyright (c) 2021 Tobias Buschor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Polyfill for `scrollIntoViewIfNeeded` function not existing in Firefox. +// https://github.com/nuxodin/lazyfill + + ;(function() { if (!Element.prototype.scrollIntoViewIfNeeded) { - Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) { - centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; - - var parent = this.parentNode; - if (parent) { - var parentComputedStyle = window.getComputedStyle(parent, null), - parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')), - parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')), - overTop = this.offsetTop - parent.offsetTop < parent.scrollTop, - overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight), - overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft, - overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth), - alignWithTop = overTop && !overBottom; - - if ((overTop || overBottom) && centerIfNeeded) { - parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2; + Element.prototype.scrollIntoViewIfNeeded = function ( centerIfNeeded = true ) { + const el = this; + new IntersectionObserver( function( [entry] ) { + const ratio = entry.intersectionRatio; + if (ratio < 1) { + let place = ratio <= 0 && centerIfNeeded ? 'center' : 'nearest'; + el.scrollIntoView( { + block: place, + inline: place, + } ); } - if ((overLeft || overRight) && centerIfNeeded) { - parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2; - } - if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { - this.scrollIntoView(alignWithTop); - } - } + this.disconnect(); + } ).observe(this); }; } })() diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss index 4dafb79d5..c961d3954 100644 --- a/frontend/resources/styles/common/base.scss +++ b/frontend/resources/styles/common/base.scss @@ -56,7 +56,7 @@ svg { a { cursor: pointer; - font-weight: 500; + font-weight: $fw500; color: $color-gray-50; &:hover { @@ -64,6 +64,10 @@ a { } } +button { + font-family: "worksans", sans-serif; +} + p { font-size: $fs12; margin-bottom: 1rem; @@ -88,7 +92,7 @@ ul { } strong { - font-weight: bold; + font-weight: $fw700; } .relative { @@ -97,7 +101,7 @@ strong { h1 { font-size: $fs34; - font-weight: 500; + font-weight: $fw500; line-height: $title-lh-sm; @include bp(baby-bear) { @@ -107,7 +111,7 @@ h1 { &.supertitle { font-size: $fs44; - font-weight: 300; + font-weight: $fw300; line-height: $title-lh-sm; @include bp(baby-bear) { @@ -118,7 +122,7 @@ h1 { } h2 { font-size: $fs24; - font-weight: 300; + font-weight: $fw300; line-height: $title-lh-sm; @include bp(baby-bear) { @@ -129,13 +133,13 @@ h2 { h3 { font-size: $fs24; - font-weight: 300; + font-weight: $fw300; padding: 0.5rem 0; } h4 { font-size: $fs18; - font-weight: 300; + font-weight: $fw300; } @-webkit-keyframes rotation { @@ -225,7 +229,7 @@ h4 { } .bold { - font-weight: bold !important; + font-weight: $fw700 !important; } .nopd { diff --git a/frontend/resources/styles/common/dependencies/fonts.scss b/frontend/resources/styles/common/dependencies/fonts.scss index 21685554d..f8022583d 100644 --- a/frontend/resources/styles/common/dependencies/fonts.scss +++ b/frontend/resources/styles/common/dependencies/fonts.scss @@ -14,6 +14,7 @@ $fs13: 0.8125rem; $fs14: 0.875rem; $fs15: 0.9375rem; $fs16: 1rem; +$fs17: 1.0625rem; $fs18: 1.125rem; $fs19: 1.1875rem; $fs20: 1.25rem; @@ -30,8 +31,19 @@ $fs38: 2.375rem; $fs40: 2.5rem; $fs42: 2.675rem; $fs44: 2.75rem; +$fs80: 5rem; -$extrabold: 900; +// Font weight +// Taken from https://fonts.google.com/specimen/Work+Sans +$fw100: 100; // Thin +$fw200: 200; // Extra Light +$fw300: 300; // Light +$fw400: 400; // Regular (CSS value: 'normal') +$fw500: 500; // Medium +$fw600: 600; // Semi Bold +$fw700: 700; // Bold (CSS value: 'bold') +$fw800: 800; // Extra Bold +$fw900: 900; // Black // Line height $base-lh: 1.43; diff --git a/frontend/resources/styles/common/dependencies/helpers.scss b/frontend/resources/styles/common/dependencies/helpers.scss index 0027118f8..bad304b97 100644 --- a/frontend/resources/styles/common/dependencies/helpers.scss +++ b/frontend/resources/styles/common/dependencies/helpers.scss @@ -14,19 +14,20 @@ $size-5: 1.5rem; $size-6: 2rem; // Border radius -$br-small: 3px; -$br-medium: 5px; -$br-big: 8px; -$br-huge: 12px; - -// Alignments -.text-left { - text-align: left; -} - -.text-right { - text-align: right; -} +$br0: 0px; +$br2: 2px; +$br3: 3px; +$br4: 4px; +$br5: 5px; +$br6: 6px; +$br7: 7px; +$br8: 8px; +$br10: 10px; +$br12: 12px; +$br25: 25px; +$br50: 50px; +$br99: 99px; +$br-circle: 50%; // Need to be investigated, before we can use variable .row-flex { align-items: center; diff --git a/frontend/resources/styles/common/dependencies/highlightjs-theme.scss b/frontend/resources/styles/common/dependencies/highlightjs-theme.scss index ba9963ff5..8d8fbd6f9 100644 --- a/frontend/resources/styles/common/dependencies/highlightjs-theme.scss +++ b/frontend/resources/styles/common/dependencies/highlightjs-theme.scss @@ -37,7 +37,7 @@ Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-lic } .hljs-strong { - font-weight: bold; + font-weight: $fw700; } .hljs-emphasis { diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index df56e139d..1e8abb2df 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -11,7 +11,7 @@ appearance: none; align-items: center; border: none; - border-radius: 3px; + border-radius: $br3; cursor: pointer; display: flex; font-family: "worksans", sans-serif; @@ -209,28 +209,28 @@ input[type="button"][disabled], .text-error { background-color: $color-danger; - border-radius: 3px; + border-radius: $br3; color: $color-danger-lighter; padding: 3px 6px; } .text-success { background-color: $color-success; - border-radius: 3px; + border-radius: $br3; color: $color-white; padding: 3px 6px; } .text-warning { background-color: $color-warning; - border-radius: 3px; + border-radius: $br3; color: $color-white; padding: 3px 6px; } .text-info { background-color: $color-complete; - border-radius: 3px; + border-radius: $br3; color: $color-white; padding: 3px 6px; } @@ -305,11 +305,11 @@ ul.slider-dots { .tag { background-color: $color-gray-20; - border-radius: 3px; + border-radius: $br3; color: $color-white; cursor: pointer; font-size: $fs14; - font-weight: bold; + font-weight: $fw700; margin: 0 $size-2 $size-2 0; padding: 4px 8px; text-transform: uppercase; @@ -553,7 +553,7 @@ input[type="checkbox"] { input.element-name { background-color: $color-white; border: 1px solid $color-gray-40; - border-radius: $br-small; + border-radius: $br3; color: $color-gray-60; font-size: $fs12; margin: 0px; @@ -618,7 +618,7 @@ input.element-name { margin-bottom: 6px; &:before { - border-radius: 99px; + border-radius: $br99; transition: box-shadow 0.2s linear 0s, color 0.2s linear 0s; } } @@ -684,7 +684,7 @@ input[type="radio"]:checked + label:before { &:before { top: 1.4px; - border-radius: 3px; + border-radius: $br3; transition: border 0.2s linear 0s, color 0.2s linear 0s; } @@ -700,7 +700,7 @@ input[type="radio"]:checked + label:before { } &:after { - border-radius: 3px; + border-radius: $br3; } } @@ -711,11 +711,11 @@ input[type="radio"]:checked + label:before { &.checkbox-circle { label { &:after { - border-radius: 99px; + border-radius: $br99; } &:before { - border-radius: 99px; + border-radius: $br99; } } } @@ -833,7 +833,7 @@ input[type="range"]::-webkit-slider-runnable-track { animate: 0.2s; box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; background: $color-gray-60; - border-radius: 25px; + border-radius: $br25; border: 0px solid #000101; } input[type="range"]::-webkit-slider-thumb { @@ -841,7 +841,7 @@ input[type="range"]::-webkit-slider-thumb { border: 0px solid #000000; height: 18px; width: 6px; - border-radius: 7px; + border-radius: $br7; background: $color-gray-20; cursor: pointer; -webkit-appearance: none; @@ -857,7 +857,7 @@ input[type="range"]::-moz-range-track { animate: 0.2s; box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; background: $color-gray-60; - border-radius: 25px; + border-radius: $br25; border: 0px solid #000101; } input[type="range"]::-moz-range-thumb { @@ -865,7 +865,7 @@ input[type="range"]::-moz-range-thumb { border: 0px solid #000000; height: 24px; width: 8px; - border-radius: 7px; + border-radius: $br7; background: $color-gray-20; cursor: pointer; } @@ -882,13 +882,13 @@ input[type="range"]::-ms-track { input[type="range"]::-ms-fill-lower { background: $color-gray-60; border: 0px solid #000101; - border-radius: 50px; + border-radius: $br50; box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; } input[type="range"]::-ms-fill-upper { background: $color-gray-60; border: 0px solid #000101; - border-radius: 50px; + border-radius: $br50; box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; } input[type="range"]::-ms-thumb { @@ -896,7 +896,7 @@ input[type="range"]::-ms-thumb { border: 0px solid #000000; height: 24px; width: 8px; - border-radius: 7px; + border-radius: $br7; background: $color-gray-20; cursor: pointer; } @@ -935,11 +935,11 @@ input[type="range"]:focus::-ms-fill-upper { &:hover { &::after { background-color: $color-white; - border-radius: $br-small; + border-radius: $br3; color: $color-gray-60; content: attr(alt); font-size: $fs12; - font-weight: bold; + font-weight: $fw700; padding: $size-1; position: absolute; left: 130%; @@ -1013,7 +1013,7 @@ input[type="range"]:focus::-ms-fill-upper { align-items: center; background-color: $color-white; box-sizing: border-box; - border-radius: 0; + border-radius: $br0; color: $color-gray-60; display: flex; height: 100%; @@ -1115,7 +1115,7 @@ input[type="range"]:focus::-ms-fill-upper { } &.fixed { - border-radius: $br-small; + border-radius: $br3; box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.2); height: 48px; max-width: 1000px; diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss index 0c89db043..6b77a0ecf 100644 --- a/frontend/resources/styles/main/layouts/login.scss +++ b/frontend/resources/styles/main/layouts/login.scss @@ -73,6 +73,40 @@ form { margin: 2rem 0 0.5rem 0; + .accept-terms-and-privacy-wrapper { + position: relative; + .input-checkbox { + margin-bottom: 0; + } + .input-checkbox input[type="checkbox"] { + position: absolute; + display: block; + width: 20px; + height: 20px; + opacity: 0; + top: 22px; + } + label { + margin-left: 40px; + } + label:before { + position: absolute; + top: 15px; + left: -36px; + } + label:after { + position: absolute; + top: 15px; + left: -33px; + } + .input-checkbox input[type="checkbox"]:focus { + opacity: 100%; + } + .auth-links { + margin-left: 40px; + font-size: 0.75rem; + } + } } } @@ -92,9 +126,9 @@ .btn-large { flex-grow: 1; - font-size: 14px; + font-size: $fs14; font-style: normal; - font-weight: normal; + font-weight: $fw400; } .btn-google-auth { @@ -194,7 +228,7 @@ margin-bottom: 10px; a { font-size: $fs14; - font-weight: 500; + font-weight: $fw500; color: $color-gray-50; &:hover, &:focus { diff --git a/frontend/resources/styles/main/layouts/not-found.scss b/frontend/resources/styles/main/layouts/not-found.scss index 99060839d..4aa400cbb 100644 --- a/frontend/resources/styles/main/layouts/not-found.scss +++ b/frontend/resources/styles/main/layouts/not-found.scss @@ -46,23 +46,23 @@ .main-message { color: $color-black; - font-size: 5rem; + font-size: $fs80; line-height: 150px; text-align: center; } .desc-message { color: $color-black; - font-size: 1.6rem; - font-weight: 300; + font-size: $fs26; + font-weight: $fw300; text-align: center; } .sign-info { margin-top: 20px; color: $color-black; - font-size: 1rem; - font-weight: 200; + font-size: $fs16; + font-weight: $fw200; text-align: center; display: flex; @@ -70,7 +70,7 @@ align-items: center; b { - font-weight: 400; + font-weight: $fw400; } .btn-primary { diff --git a/frontend/resources/styles/main/partials/activity-bar.scss b/frontend/resources/styles/main/partials/activity-bar.scss index 44b8ca28e..5e5ad957b 100644 --- a/frontend/resources/styles/main/partials/activity-bar.scss +++ b/frontend/resources/styles/main/partials/activity-bar.scss @@ -23,7 +23,7 @@ h4 { color: $color-gray-40; font-size: $fs16; - font-weight: bold; + font-weight: $fw700; margin-bottom: $size-1; } @@ -31,7 +31,7 @@ background-color: lighten($color-gray-20, 12%); color: $color-white; font-size: $fs12; - font-weight: bold; + font-weight: $fw700; padding: 2px; text-align: center; width: 100%; @@ -62,7 +62,7 @@ flex-wrap: wrap; a { - font-weight: bold; + font-weight: $fw700; margin: 0 3px; } } diff --git a/frontend/resources/styles/main/partials/af-signup-questions.scss b/frontend/resources/styles/main/partials/af-signup-questions.scss index 268a86371..468d76e85 100644 --- a/frontend/resources/styles/main/partials/af-signup-questions.scss +++ b/frontend/resources/styles/main/partials/af-signup-questions.scss @@ -18,7 +18,7 @@ h3 { font-family: "worksans", sans-serif !important; margin-bottom: 0.8rem; - font-weight: 500 !important; + font-weight: $fw500 !important; } h1 { @@ -30,7 +30,7 @@ } strong { - font-weight: 500; + font-weight: $fw500; } p, @@ -109,7 +109,7 @@ .step-number { background-color: $color-gray-10; border: none; - border-radius: 1rem; + border-radius: 1rem; // Need to be investigated, before we can use variable color: $color-gray-40; float: right; font-family: "worksans", sans-serif !important; @@ -147,7 +147,7 @@ .af-step-next span, .af-step-previous span { - font-weight: normal !important; + font-weight: $fw400 !important; } .af-step-button { @@ -229,7 +229,7 @@ &::before { background-color: transparent; - border-radius: 4px; + border-radius: $br4; min-width: 100%; min-height: 100%; position: absolute; @@ -309,7 +309,7 @@ .af-field-use_of_penpot .af-choice-option:nth-child(5) label { &::before { - border-radius: 3px; + border-radius: $br3; } } diff --git a/frontend/resources/styles/main/partials/color-bullet.scss b/frontend/resources/styles/main/partials/color-bullet.scss index 9d42a2703..2f621b9a6 100644 --- a/frontend/resources/styles/main/partials/color-bullet.scss +++ b/frontend/resources/styles/main/partials/color-bullet.scss @@ -190,7 +190,7 @@ ul.palette-menu .color-bullet { } .color-bullet.is-not-library-color { - border-radius: $br-small; + border-radius: $br3; overflow: hidden; & .color-bullet-wrapper { diff --git a/frontend/resources/styles/main/partials/color-palette.scss b/frontend/resources/styles/main/partials/color-palette.scss index 7ce6367f9..2b622490c 100644 --- a/frontend/resources/styles/main/partials/color-palette.scss +++ b/frontend/resources/styles/main/partials/color-palette.scss @@ -168,11 +168,11 @@ &.current { .color-text { color: $color-gray-50; - font-weight: bold; + font-weight: $fw700; } &::before { background-color: $color-gray-50; - border-radius: 3px; + border-radius: $br3; color: $color-white; content: "selected"; font-size: $fs10; @@ -194,7 +194,7 @@ margin-left: 1.5rem; .color-text { - font-weight: bold; + font-weight: $fw700; } &:hover { .color-text { @@ -224,7 +224,7 @@ .color-tooltip { background-color: $color-gray-50; border: 1px solid $color-gray-10; - border-radius: 3px; + border-radius: $br3; left: -102px; padding: 1rem; position: absolute; diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index d63929126..268afc00b 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -49,7 +49,7 @@ padding: 0; margin: 0; border: 1px solid $color-gray-20; - border-radius: 2px; + border-radius: $br2; margin-left: $size-1; } @@ -97,7 +97,7 @@ position: absolute; width: 15px; height: 15px; - border-radius: 2px; + border-radius: $br2; border: 1px solid $color-gray-20; margin-top: -2px; margin-left: -7px; @@ -120,7 +120,7 @@ width: 14px; height: 14px; border: 2px solid $color-white; - border-radius: 8px; + border-radius: $br8; position: absolute; left: 50%; top: 50%; @@ -211,7 +211,7 @@ position: absolute; width: 12px; height: 12px; - border-radius: 6px; + border-radius: $br6; z-index: 1; } @@ -230,7 +230,7 @@ position: absolute; width: 12px; height: 12px; - border-radius: 6px; + border-radius: $br6; z-index: 1; border: 1px solid $color-white; box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset, rgb(0 0 0 / 0.25) 0px 4px 4px inset, @@ -296,7 +296,7 @@ width: 100%; margin: 0; border: 1px solid $color-gray-10; - border-radius: 2px; + border-radius: $br2; font-size: $fs12; height: 1.5rem; padding: 0 $size-1; @@ -323,11 +323,11 @@ margin-bottom: $size-2; width: 100%; padding: $size-1 0.25rem; - font-size: 0.75rem; + font-size: $fs12; color: $color-gray-40; cursor: pointer; border: 1px solid $color-gray-10; - border-radius: 2px; + border-radius: $br2; option { padding: 0; @@ -379,7 +379,7 @@ position: absolute; width: 12px; height: 12px; - border-radius: 6px; + border-radius: $br6; z-index: 1; border: 1px solid $color-white; box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset, rgb(0 0 0 / 0.25) 0px 4px 4px inset, @@ -421,7 +421,7 @@ .saturation, .value, .opacity { - border-radius: 10px; + border-radius: $br10; } .hsva-selector-label { @@ -432,7 +432,7 @@ } .colorpicker-tooltip { - border-radius: $br-small; + border-radius: $br3; display: flex; flex-direction: column; left: 1400px; @@ -459,7 +459,7 @@ .colorpicker-tabs { display: flex; margin-bottom: $size-2; - border-radius: 5px; + border-radius: $br5; border: 1px solid $color-gray-10; height: 2rem; @@ -511,7 +511,7 @@ input { background-color: $color-gray-50; border: 1px solid $color-gray-30; - border-radius: $br-small; + border-radius: $br3; color: $color-white; height: 20px; margin: 5px 0 0 0; diff --git a/frontend/resources/styles/main/partials/comments.scss b/frontend/resources/styles/main/partials/comments.scss index fab594179..fa1f85b87 100644 --- a/frontend/resources/styles/main/partials/comments.scss +++ b/frontend/resources/styles/main/partials/comments.scss @@ -42,7 +42,7 @@ border: 1px solid $color-gray-20; box-sizing: border-box; box-shadow: 0px 2px 8px rgba($color-black, 0.25); - border-radius: 2px; + border-radius: $br2; min-width: 280px; max-width: 280px; user-select: text; @@ -79,7 +79,7 @@ padding: $size-2; resize: none; width: 100%; - border-radius: 2px; + border-radius: $br2; border: 1px solid $color-gray-20; max-height: 4rem; } @@ -121,7 +121,7 @@ flex-direction: column; .fullname { - font-weight: 700; + font-weight: $fw700; color: $color-gray-60; font-size: $fs12; @@ -271,7 +271,7 @@ .label { &.filename { - font-weight: 700; + font-weight: $fw700; } } @@ -365,7 +365,7 @@ align-items: center; justify-content: center; background-color: $color-dashboard; - border-radius: 3px; + border-radius: $br3; position: relative; .button { @@ -375,7 +375,7 @@ align-items: center; justify-content: center; background-color: $color-dashboard; - border-radius: 3px; + border-radius: $br3; svg { width: 15px; @@ -398,7 +398,7 @@ width: 280px; bottom: 35px; left: 0px; - border-radius: 3px; + border-radius: $br3; } .header { @@ -408,7 +408,7 @@ padding: 0px 11px; h3 { - font-weight: 400; + font-weight: $fw400; color: $color-black; font-size: $fs14; line-height: $fs18; diff --git a/frontend/resources/styles/main/partials/context-menu.scss b/frontend/resources/styles/main/partials/context-menu.scss index 616cdfa89..e7a302655 100644 --- a/frontend/resources/styles/main/partials/context-menu.scss +++ b/frontend/resources/styles/main/partials/context-menu.scss @@ -24,7 +24,7 @@ .context-menu-items { background: $color-white; - border-radius: $br-small; + border-radius: $br3; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); left: -$size-4; max-height: 30rem; @@ -48,7 +48,7 @@ color: $color-black; display: block; font-size: $fs14; - font-weight: 400; + font-weight: $fw400; padding: $size-2 $size-4; text-align: left; white-space: nowrap; @@ -77,7 +77,7 @@ &.submenu-back { color: $color-black; display: flex; - font-weight: bold; + font-weight: $fw700; align-items: center; & svg { @@ -99,6 +99,6 @@ background-repeat: no-repeat; background-position: 5% 48%; background-size: 10px; - font-weight: bold; + font-weight: $fw700; } } diff --git a/frontend/resources/styles/main/partials/dashboard-fonts.scss b/frontend/resources/styles/main/partials/dashboard-fonts.scss index edf1d2017..cdb1327e7 100644 --- a/frontend/resources/styles/main/partials/dashboard-fonts.scss +++ b/frontend/resources/styles/main/partials/dashboard-fonts.scss @@ -47,7 +47,7 @@ input { font-size: $fs12; border: 1px solid $color-gray-30; - border-radius: $br-small; + border-radius: $br3; width: 130px; padding: $size-1; margin: 0px; @@ -73,7 +73,7 @@ input { border: 1px solid $color-gray-30; - border-radius: $br-small; + border-radius: $br3; margin: 0px; padding: $size-2; font-size: $fs12; diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index 501d87b9c..7419ce4e4 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -27,9 +27,14 @@ margin: $size-3 $size-4 $size-4 $size-2; position: relative; text-align: center; - a { + a, + button { width: 100%; - font-weight: normal; + font-weight: $fw400; + } + button { + background-color: transparent; + border: none; } @media #{$bp-max-1366} { height: 200px; @@ -43,13 +48,13 @@ } .grid-item-th { - border-radius: $br-small; + border-radius: $br3; border: 2px solid lighten($color-gray-20, 15%); text-align: initial; } &.dragged { - border-radius: $br-small; + border-radius: $br3; border: 2px solid lighten($color-gray-20, 15%); text-align: initial; max-height: 160px; @@ -78,7 +83,7 @@ } &.overlay { - border-radius: 4px; + border-radius: $br4; border: 2px solid $color-primary; height: 100%; opacity: 0; @@ -120,7 +125,7 @@ border: 1px solid transparent; color: $color-gray-60; font-size: $fs14; - font-weight: 500; + font-weight: $fw500; overflow: hidden; padding: 0; height: 27px; @@ -156,7 +161,7 @@ height: 25px; color: $color-gray-60; font-size: $fs14; - font-weight: 400; + font-weight: $fw400; } } } @@ -164,7 +169,7 @@ .item-badge { background-color: $color-white; border: 1px solid $color-gray-20; - border-radius: 2px; + border-radius: $br2; position: absolute; top: $size-2; right: $size-2; @@ -289,8 +294,8 @@ } .color-swatch { - border-top-left-radius: $br-medium; - border-top-right-radius: $br-medium; + border-top-left-radius: $br5; + border-top-right-radius: $br5; height: 25%; left: 0; position: absolute; @@ -323,8 +328,8 @@ background-position: center; background-size: auto 80%; background-repeat: no-repeat; - border-top-left-radius: $br-small; - border-top-right-radius: $br-small; + border-top-left-radius: $br3; + border-top-right-radius: $br3; height: 230px; max-height: 160px; overflow: hidden; @@ -389,7 +394,7 @@ display: flex; align-items: center; border: 1px solid transparent; - border-radius: $br-small; + border-radius: $br3; margin-top: $size-1; padding: 2px; font-size: $fs12; @@ -410,7 +415,7 @@ & svg { background-color: $color-canvas; - border-radius: 4px; + border-radius: $br4; border: 2px solid transparent; height: 24px; width: 24px; @@ -438,7 +443,7 @@ } .grid-empty-placeholder { - border-radius: $br-huge; + border-radius: $br12; display: grid; background-color: rgba(227, 227, 227, 0.3); padding: 13px; @@ -458,7 +463,7 @@ background-repeat: no-repeat; align-items: center; border: 1px dashed #b1b2b5; - border-radius: 3px; + border-radius: $br3; display: flex; flex-direction: column; height: 200px; @@ -476,7 +481,7 @@ .create-new { background-color: white; border: 2px solid $color-gray-10; - border-radius: 3px; + border-radius: $br3; color: $color-black; cursor: pointer; height: 158px; @@ -495,7 +500,7 @@ height: 200px; background: $color-white; border: 1px dashed #e3e3e3; - border-radius: 0; + border-radius: $br0; } svg { diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss index 23fcc9a6d..157a44005 100644 --- a/frontend/resources/styles/main/partials/dashboard-header.scss +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -58,7 +58,7 @@ color: $color-gray-30; height: 40px; padding: $size-1 $size-5; - font-weight: 400; + font-weight: $fw400; &:hover { color: $color-black; text-decoration: none; @@ -84,7 +84,7 @@ display: flex; flex-shrink: 0; font-size: $fs22; - font-weight: 600; + font-weight: $fw600; z-index: 10; user-select: all; } diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index ba6a9f3f5..646d53a30 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -52,7 +52,7 @@ display: flex; width: 100%; border: 1px solid $color-gray-10; - border-radius: $br-medium; + border-radius: $br5; align-items: center; } @@ -204,7 +204,7 @@ flex-shrink: 0; padding: $size-2; a { - font-weight: 400; + font-weight: $fw400; width: 100%; &:hover { text-decoration: none; @@ -228,7 +228,7 @@ &::before { background-color: transparent; - border-radius: $br-small; + border-radius: $br3; content: ""; height: 26px; margin-right: $size-2; @@ -243,7 +243,7 @@ & .edit-wrapper { border: 1px solid $color-gray-10; - border-radius: $br-small; + border-radius: $br3; display: flex; width: 100%; } @@ -260,7 +260,7 @@ .close { background-color: $color-white; cursor: pointer; - padding: 3px 5px; + padding-left: 5px; svg { fill: $color-gray-30; @@ -284,7 +284,7 @@ &.current { a { - font-weight: bold; + font-weight: $fw700; } &::before { @@ -302,7 +302,7 @@ align-items: center; background-color: $color-white; border: 1px solid $color-gray-10; - border-radius: $br-medium; + border-radius: $br5; display: flex; margin: 5px 15px; @@ -388,7 +388,7 @@ .team-form-modal { h2 { - font-weight: 400; + font-weight: $fw400; color: $color-gray-40; font-size: 28px; margin-bottom: 30px; @@ -468,9 +468,9 @@ .primary-badge { border: 1px solid $color-primary; - border-radius: 2px; + border-radius: $br2; font-size: $fs9 !important; - font-weight: bold; + font-weight: $fw500; color: $color-primary !important; padding: 2px 4px; } diff --git a/frontend/resources/styles/main/partials/dashboard-team.scss b/frontend/resources/styles/main/partials/dashboard-team.scss index edc8d4f5a..384b748ed 100644 --- a/frontend/resources/styles/main/partials/dashboard-team.scss +++ b/frontend/resources/styles/main/partials/dashboard-team.scss @@ -3,7 +3,7 @@ right: 13px; padding: 32px; box-shadow: 0px 4px 8px rgba($color-black, 0.25); - border-radius: 8px; + border-radius: $br8; width: 400px; position: fixed; z-index: 16; @@ -79,12 +79,12 @@ .title { color: $color-black; - font-weight: bold; + font-weight: $fw700; margin-bottom: 16px; } .hint { - font-size: 12px; + font-size: $fs12; &.hidden { display: none; @@ -97,12 +97,11 @@ fill: $color-gray-20; } - .error { - background-color: #ffd9e0; + .error, + .warning { width: 100%; display: flex; .icon { - background-color: $color-danger; text-align: center; padding: 5px; svg { @@ -115,7 +114,23 @@ .text { color: $color-black; padding: 5px; - font-size: 12px; + font-size: $fs12; + } + } + + .error { + background-color: #ffd9e0; + + .icon { + background-color: $color-danger; + } + } + + .warning { + background-color: #ffeaca; + + .icon { + background-color: $color-warning; } } } @@ -194,7 +209,7 @@ display: flex; justify-content: space-between; align-items: center; - border-radius: 2px; + border-radius: $br2; padding: 3px 8px; font-size: $fs14; } @@ -211,7 +226,7 @@ &.status { .status-badge { color: $color-white; - border-radius: 12px; + border-radius: $br12; min-width: 74px; height: 24px; display: flex; @@ -276,7 +291,7 @@ text-align: center; .label { - border-radius: 3px; + border-radius: $br3; color: $color-white; background-color: $color-black; white-space: nowrap; @@ -305,7 +320,7 @@ max-height: 30rem; overflow-y: auto; background-color: $color-white; - border-radius: 4px; + border-radius: $br4; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); z-index: 12; top: 30px; @@ -327,7 +342,7 @@ padding: 5px 16px; &.title { - font-weight: 600; + font-weight: $fw600; cursor: default; } @@ -365,7 +380,7 @@ padding: 12px; .label { - font-size: 13px; + font-size: $fs13; color: $color-gray-30; } } @@ -535,7 +550,7 @@ width: 80%; color: $color-gray-40; p { - font-size: 16px; + font-size: $fs16; } } @@ -567,7 +582,7 @@ .cancel-button { border: 1px solid $color-gray-30; background: $color-canvas; - border-radius: 3px; + border-radius: $br3; padding: 0.5rem 1rem; cursor: pointer; margin-right: 8px; @@ -578,12 +593,12 @@ } } .input-checkbox label { - font-size: 14px; + font-size: $fs14; color: $color-black; } .explain { - font-size: 12px; + font-size: $fs12; color: $color-gray-40; } } diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index 619acabad..34c733725 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -8,7 +8,7 @@ display: flex; position: relative; border: 2px solid $color-gray-10; - border-radius: 8px; + border-radius: $br8; padding: 20px; margin: 0 1rem 0 21px; height: 154px; @@ -18,7 +18,7 @@ padding-left: 20px; .title { font-size: $fs24; - font-weight: bold; + font-weight: $fw700; color: $color-black; } .info { @@ -69,12 +69,12 @@ grid-template-columns: 1fr 1fr; position: relative; border: 2px solid $color-gray-10; - border-radius: 8px; + border-radius: $br8; min-height: 211px; .thumbnail { - border-top-left-radius: 6px; - border-bottom-left-radius: 6px; + border-top-left-radius: $br6; + border-bottom-left-radius: $br6; padding: 30px; display: block; background-color: #e0e4e9; @@ -85,7 +85,7 @@ .title { color: $color-black; font-size: $fs24; - font-weight: bold; + font-weight: $fw700; margin-bottom: 8px; } .info { @@ -183,11 +183,12 @@ .dashboard-project-row { margin-bottom: $size-5; + position: relative; .project { align-items: center; background: $color-white; - border-radius: $br-small; + border-radius: $br3; display: flex; flex-direction: row; justify-content: space-between; @@ -211,6 +212,8 @@ font-size: $fs14; justify-content: space-between; cursor: pointer; + background-color: transparent; + border: none; .placeholder-icon { transform: rotate(-90deg); margin-left: 10px; @@ -237,7 +240,7 @@ cursor: pointer; font-size: $fs18; line-height: 1rem; - font-weight: 600; + font-weight: $fw600; color: $color-black; margin-right: $size-1; } @@ -249,7 +252,7 @@ .info { font-size: $fs14; line-height: 1rem; - font-weight: 400; + font-weight: $fw400; color: $color-gray-60; margin-left: 0.75rem; @media (max-width: 760px) { @@ -297,13 +300,42 @@ opacity: 1; } } + + .show-more { + align-items: center; + color: $color-gray-30; + display: flex; + font-size: $fs14; + justify-content: space-between; + cursor: pointer; + background-color: transparent; + border: none; + position: absolute; + top: 9px; + right: 53px; + .placeholder-icon { + transform: rotate(-90deg); + margin-left: 10px; + svg { + height: 14px; + width: 14px; + fill: $color-gray-30; + } + } + &:hover { + color: $color-primary-dark; + svg { + fill: $color-primary-dark; + } + } + } } .recent-files-row-title-info { color: $color-gray-60; line-height: 1rem; font-size: $fs14; - font-weight: 400; + font-weight: $fw400; @media (max-width: 880px) { display: none; } @@ -374,7 +406,7 @@ .edit-wrapper { border: 1px solid $color-gray-10; - border-radius: $br-small; + border-radius: $br3; display: flex; padding-right: $size-5; position: relative; @@ -417,7 +449,7 @@ background: none; border: 1px solid $color-gray-20; - border-radius: 2px; + border-radius: $br2; cursor: pointer; transition: all 0.4s; margin-left: 1rem; @@ -458,8 +490,8 @@ border-top: 2px solid #e4e4e4; border-left: 2px solid #e4e4e4; border-right: 2px solid #e4e4e4; - border-top-left-radius: 10px; - border-top-right-radius: 10px; + border-top-left-radius: $br10; + border-top-right-radius: $br10; margin-right: 30px; background-color: $color-white; position: relative; @@ -468,8 +500,8 @@ display: inline-block; vertical-align: middle; line-height: normal; - font-size: 18px; - font-weight: 600; + font-size: $fs18; + font-weight: $fw600; color: $color-black; margin-left: 18px; margin-right: 10px; @@ -540,14 +572,14 @@ .template-card { display: inline-block; width: 255px; - font-size: 16px; + font-size: $fs16; color: #181a22; cursor: pointer; .img-container { width: 100%; height: 135px; margin-bottom: 15px; - border-radius: 5px; + border-radius: $br5; border: 2px solid #e0e4e9; display: flex; justify-content: center; @@ -564,7 +596,7 @@ height: 16px; } span { - font-weight: 500; + font-weight: $fw500; font-size: $fs14; } } @@ -576,13 +608,13 @@ } .template-link-title { - font-size: 14px; - font-weight: 600; + font-size: $fs14; + font-weight: $fw600; color: $color-gray-60; } .template-link-text { - font-size: 12px; + font-size: $fs12; margin-top: $size-2; color: $color-gray-50; } diff --git a/frontend/resources/styles/main/partials/dropdown.scss b/frontend/resources/styles/main/partials/dropdown.scss index 9b21ab220..0abd62a4f 100644 --- a/frontend/resources/styles/main/partials/dropdown.scss +++ b/frontend/resources/styles/main/partials/dropdown.scss @@ -2,7 +2,7 @@ position: absolute; max-height: 30rem; background-color: $color-white; - border-radius: 2px; + border-radius: $br2; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); z-index: 12; @@ -31,7 +31,7 @@ } &.title { - font-weight: 600; + font-weight: $fw600; cursor: default; } diff --git a/frontend/resources/styles/main/partials/exception-page.scss b/frontend/resources/styles/main/partials/exception-page.scss index 59610ad65..310fc7753 100644 --- a/frontend/resources/styles/main/partials/exception-page.scss +++ b/frontend/resources/styles/main/partials/exception-page.scss @@ -49,23 +49,23 @@ .main-message { color: $color-black; - font-size: 5rem; + font-size: $fs80; line-height: 150px; text-align: center; } .desc-message { color: $color-black; - font-size: 1.6rem; - font-weight: 300; + font-size: $fs26; + font-weight: $fw300; text-align: center; } .sign-info { margin-top: 20px; color: $color-black; - font-size: 1rem; - font-weight: 200; + font-size: $fs16; + font-weight: $fw200; text-align: center; display: flex; @@ -73,7 +73,7 @@ align-items: center; b { - font-weight: 400; + font-weight: $fw400; } .btn-primary { diff --git a/frontend/resources/styles/main/partials/forms.scss b/frontend/resources/styles/main/partials/forms.scss index 47891adbc..d45d719e8 100644 --- a/frontend/resources/styles/main/partials/forms.scss +++ b/frontend/resources/styles/main/partials/forms.scss @@ -81,10 +81,10 @@ textarea { .notification-text-email { background: $color-gray-10; - border-radius: $br-small; + border-radius: $br3; color: $color-gray-60; font-size: $fs18; - font-weight: 500; + font-weight: $fw500; margin: 1.5rem 0 2.5rem 0; padding: 1rem; text-align: center; @@ -121,7 +121,7 @@ textarea { input, textarea { background-color: $color-white; - border-radius: 2px; + border-radius: $br2; border: 1px solid $color-gray-20; color: $color-gray-60; font-size: $fs14; @@ -231,7 +231,7 @@ textarea { } .custom-multi-input { - border-radius: 2px; + border-radius: $br2; border: 1px solid $color-gray-20; max-height: 300px; overflow-y: auto; @@ -267,10 +267,13 @@ textarea { .around { border: 1px solid $color-gray-20; padding-left: 5px; - border-radius: 4px; + border-radius: $br4; &.invalid { border: 1px solid $color-danger; } + &.caution { + border: 1px solid $color-warning; + } .text { display: inline-block; @@ -278,7 +281,7 @@ textarea { overflow: hidden; text-overflow: ellipsis; line-height: 16px; - font-size: 14px; + font-size: $fs14; color: $color-black; } .icon { @@ -331,7 +334,7 @@ textarea { flex-direction: row; background-color: $color-white; - border-radius: 2px; + border-radius: $br2; border: 1px solid $color-gray-20; height: 40px; diff --git a/frontend/resources/styles/main/partials/inspect.scss b/frontend/resources/styles/main/partials/inspect.scss index 28d4fb9fc..4a3d41c50 100644 --- a/frontend/resources/styles/main/partials/inspect.scss +++ b/frontend/resources/styles/main/partials/inspect.scss @@ -196,7 +196,7 @@ overflow-y: auto; max-height: 5rem; background: $color-gray-60; - border-radius: 4px; + border-radius: $br4; padding: 1rem 0.5rem; color: $color-gray-10; white-space: pre-wrap; @@ -216,7 +216,7 @@ justify-content: center; margin: 0.5rem; background: $color-gray-60; - border-radius: 4px; + border-radius: $br4; width: calc(100% - 1rem); min-height: 5rem; @@ -257,7 +257,7 @@ position: relative; margin-top: 0.5rem; border: 1px solid $color-black; - border-radius: 4px; + border-radius: $br4; margin: 0.5rem; display: flex; flex-direction: row; @@ -287,7 +287,7 @@ padding: 0.5rem 1rem; color: $color-gray-10; width: calc(100% - 1rem); - border-radius: 4px; + border-radius: $br4; margin: 0.5rem; cursor: pointer; @@ -360,7 +360,7 @@ font-size: $fs14; .code-display { - border-radius: 4px; + border-radius: $br4; padding: 1rem; overflow: hidden; white-space: pre-wrap; diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index 06c7210c3..95f8e4eb2 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -60,7 +60,7 @@ // NEW GEN MODALS .modal-container { - border-radius: $br-medium; + border-radius: $br5; display: flex; flex-direction: column; width: 448px; @@ -70,7 +70,7 @@ .modal-header { align-items: center; background-color: $color-white; - border-radius: 8px 8px 0px 0px; + border-radius: $br8 $br8 0 0; color: $color-black; display: flex; height: 63px; @@ -85,7 +85,7 @@ h2 { font-size: $fs18; - font-weight: 400; + font-weight: $fw400; margin: 0; } } @@ -138,7 +138,7 @@ h3 { color: $color-gray-40; font-size: $fs16; - font-weight: 400; + font-weight: $fw400; } &.delete-shared { padding: 15px 32px; @@ -211,7 +211,7 @@ .cancel-button { border: 1px solid $color-gray-30; background: $color-canvas; - border-radius: 3px; + border-radius: $br3; padding: 0.5rem 1rem; cursor: pointer; margin-right: 8px; @@ -222,7 +222,7 @@ } .accept-button { - border-radius: 3px; + border-radius: $br3; cursor: pointer; padding: 0.5rem 1rem; @@ -276,7 +276,7 @@ .cancel-button { border: 1px solid $color-gray-20; background: $color-white; - border-radius: 3px; + border-radius: $br3; padding: 0.5rem 2.25rem; cursor: pointer; margin-right: 18px; @@ -288,7 +288,7 @@ .accept-button { background: $color-primary; - border-radius: 3px; + border-radius: $br3; border: 1px solid $color-primary; color: $color-black; cursor: pointer; @@ -470,7 +470,7 @@ font-size: $fs10; color: $color-black; background: #d8f7fe; - border-radius: 3px; + border-radius: $br3; padding: 2px 4px; display: flex; align-items: center; @@ -509,12 +509,12 @@ .export-dialog { .export-option { - border-radius: 4px; + border-radius: $br4; border: 1px solid $color-gray-10; margin-bottom: 0.5rem; h3 { - font-weight: 700; + font-weight: $fw700; } h3, @@ -624,7 +624,7 @@ } .libraries-dialog { - border-radius: $br-medium; + border-radius: $br5; height: 664px; width: 920px; max-height: 100%; @@ -682,7 +682,7 @@ color: $color-black; font-size: $fs14; padding: 0 $size-4; - font-weight: 500; + font-weight: $fw500; } .section-list { @@ -713,7 +713,7 @@ top: $size-4; right: 0; border: 1px solid $color-primary; - border-radius: 2px; + border-radius: $br2; min-width: 4.5rem; background: $color-primary; color: $color-black; @@ -812,8 +812,8 @@ .modal-left { align-items: center; background-color: $color-pink; - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; + border-top-left-radius: $br5; + border-bottom-left-radius: $br5; display: flex; flex-shrink: 0; justify-content: center; @@ -827,8 +827,8 @@ } .modal-right { - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; + border-top-right-radius: $br5; + border-bottom-right-radius: $br5; display: flex; flex-direction: column; padding: $size-6; @@ -836,14 +836,14 @@ .modal-title h2 { color: $color-black; font-size: $fs24; - font-weight: 900; + font-weight: $fw900; } .release { background-color: $color-primary; color: $color-black; font-size: $fs12; - font-weight: bold; + font-weight: $fw700; margin-top: $size-2; padding: 2px $size-1; width: max-content; @@ -855,7 +855,7 @@ p { color: $color-black; - font-size: 16px; + font-size: $fs16; margin-top: $size-2; } } @@ -911,7 +911,7 @@ height: 464px; position: absolute; bottom: 0; - border-radius: 5px; + border-radius: $br5; } .modal-right { @@ -927,7 +927,7 @@ .release { background-color: $color-primary; - border-radius: 4px; + border-radius: $br4; color: #1f1f1f; padding: 4px $size-1; display: inline-block; @@ -952,7 +952,7 @@ width: 90%; .title a { - font-weight: bold; + font-weight: $fw700; color: $color-black; text-decoration: none; &:hover { @@ -961,7 +961,7 @@ } .description { - font-size: 12px; + font-size: $fs12; text-decoration: none; text-transform: none; } @@ -1000,8 +1000,8 @@ padding: 0; img { - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; + border-top-left-radius: $br5; + border-bottom-left-radius: $br5; height: 100%; width: 115%; } @@ -1023,14 +1023,14 @@ h1 { font-family: "worksans", sans-serif; - font-weight: 700; - font-size: 27px; + font-weight: $fw700; + font-size: $fs26; margin-bottom: $size-3; text-align: center; } p { font-family: "worksans", sans-serif; - font-weight: 500; + font-weight: $fw500; font-size: $fs18; text-align: center; } @@ -1065,7 +1065,7 @@ .modal-right, .modal-left { background-repeat: no-repeat; - border-radius: $br-medium; + border-radius: $br5; transition: all ease 0.3s; &:hover { background-color: $color-primary; @@ -1090,13 +1090,13 @@ text-align: center; border: 1px solid $color-gray-10; - border-radius: 2px; + border-radius: $br2; min-height: 180px; width: 233px; cursor: pointer; h2 { - font-weight: 700; + font-weight: $fw700; margin-bottom: $size-5; font-size: $fs24; } @@ -1107,7 +1107,7 @@ img { box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25); - border-radius: $br-medium; + border-radius: $br5; margin-bottom: $size-6; margin-top: -90px; width: 150px; @@ -1127,14 +1127,14 @@ h1 { font-family: sourcesanspro; - font-weight: bold; + font-weight: $fw700; font-size: $fs36; margin-bottom: 0.75rem; } p { font-family: sourcesanspro; - font-weight: 500; + font-weight: $fw500; font-size: $fs16; margin-bottom: 1.5rem; } @@ -1295,7 +1295,7 @@ h3 { font-size: $fs18; - font-weight: bold; + font-weight: $fw700; } p { @@ -1316,7 +1316,7 @@ display: flex; flex-direction: column; text-align: left; - border-radius: $br-small; + border-radius: $br3; &:not(:last-child) { margin-bottom: 22px; @@ -1328,7 +1328,7 @@ flex-grow: 1; img { - border-radius: $br-small $br-small 0 0; + border-radius: $br3 $br3 0 0; } } @@ -1348,7 +1348,7 @@ color: $color-primary-dark; cursor: pointer; font-size: $fs14; - font-weight: 600; + font-weight: $fw600; display: flex; justify-content: flex-end; margin-top: $size-2; @@ -1376,10 +1376,10 @@ display: flex; flex-direction: column; justify-content: space-between; - border-radius: 0 5px 5px 0; + border-radius: 0 $br5 $br5 0; .subtitle { - font-weight: bold; - font-size: 20px; + font-weight: $fw700; + font-size: $fs20; color: $color-gray-10; text-transform: uppercase; margin-bottom: 8px; @@ -1433,8 +1433,8 @@ flex-direction: column; justify-content: space-between; .title { - font-size: 27px; - font-weight: bold; + font-size: $fs26; + font-weight: $fw700; color: $color-gray-60; margin-bottom: 6px; } @@ -1622,10 +1622,10 @@ --dropdown-background-color: #ffffff; --primary-font-color: #000; --input-border-color: rgb(224, 230, 240); - --input-border-radius: 3px; - --button-border-radius: 3px; - --message-border-radius: 3px; - --checkbox-border-radius: 3px; + --input-border-radius: $br3; + --button-border-radius: $br3; + --message-border-radius: $br3; + --checkbox-border-radius: $br3; --dropdown-option-background-color: rgba(0, 195, 139, 1); --dropdown-option-active-background-color: rgba(0, 138, 98, 1); --invalid-field-background-color: rgba(238.51780000000002, 205.7178, 204.11780000000002, 1); @@ -1726,7 +1726,7 @@ padding: 16px 18px; background-color: $color-white; border: 1px solid $color-gray-20; - border-radius: 3px; + border-radius: $br3; z-index: 1000; &.transparent { @@ -1811,7 +1811,7 @@ &.image { align-items: center; border: 1px solid $color-gray-10; - border-radius: 3px; + border-radius: $br3; display: flex; justify-content: center; height: 32px; @@ -1969,7 +1969,7 @@ .close { border: 1px solid $color-gray-30; background: $color-canvas; - border-radius: 3px; + border-radius: $br3; padding: 0.5rem 1rem; cursor: pointer; margin-right: 8px; @@ -1982,7 +1982,7 @@ .button-primary { background: $color-primary; border: 1px solid $color-primary; - border-radius: 3px; + border-radius: $br3; color: $color-black; cursor: pointer; padding: 0.5rem 1rem; @@ -1995,7 +1995,7 @@ .button-secondary { border: 1px solid $color-gray-30; background: $color-white; - border-radius: 3px; + border-radius: $br3; padding: 0.5rem 1rem; cursor: pointer; margin-right: 8px; @@ -2022,7 +2022,7 @@ .title { margin-left: 32px; h2 { - font-size: 24px; + font-size: $fs24; color: $color-black; line-height: $fs36; letter-spacing: 0px; diff --git a/frontend/resources/styles/main/partials/project-bar.scss b/frontend/resources/styles/main/partials/project-bar.scss index e59d43c16..6ebb76fd2 100644 --- a/frontend/resources/styles/main/partials/project-bar.scss +++ b/frontend/resources/styles/main/partials/project-bar.scss @@ -29,7 +29,7 @@ .project-name { border-bottom: 1px solid $color-gray-10; font-size: $fs14; - font-weight: bold; + font-weight: $fw700; padding: 0 $size-2; width: 100%; } diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss index 9fc607e51..4671381f1 100644 --- a/frontend/resources/styles/main/partials/sidebar-assets.scss +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -265,7 +265,7 @@ .grid-cell { background-color: $color-canvas; - border-radius: 4px; + border-radius: $br4; border: 2px solid transparent; overflow: hidden; display: flex; @@ -303,7 +303,7 @@ .editable-label-input { border: 1px solid $color-gray-20; - border-radius: 3px; + border-radius: $br3; font-size: $fs12; padding: 2px; margin: 0; @@ -330,7 +330,7 @@ .grid-placeholder { border: 2px solid $color-gray-20; - border-radius: 4px; + border-radius: $br4; } .drop-space { @@ -339,6 +339,10 @@ .typography-container { position: relative; + + &:last-child { + padding-bottom: 0.5em; + } } .drag-counter { @@ -371,7 +375,7 @@ & > svg, & > img { background-color: $color-canvas; - border-radius: 4px; + border-radius: $br4; border: 2px solid transparent; height: 24px; width: 24px; @@ -422,7 +426,7 @@ display: flex; align-items: center; border: 1px solid transparent; - border-radius: $br-small; + border-radius: $br3; margin-top: $size-1; padding: 2px; font-size: $fs12; @@ -481,7 +485,7 @@ .chrome-picker { box-shadow: none !important; border: 1px solid $color-gray-10 !important; - border-radius: 0 !important; + border-radius: $br0 !important; & input { background-color: $color-white; @@ -514,8 +518,8 @@ .modal-create-color-title { color: $color-black; - font-size: 24px; - font-weight: normal; + font-size: $fs24; + font-weight: $fw400; } .libraries-wrapper { diff --git a/frontend/resources/styles/main/partials/sidebar-document-history.scss b/frontend/resources/styles/main/partials/sidebar-document-history.scss index 02d786450..a8e00d41c 100644 --- a/frontend/resources/styles/main/partials/sidebar-document-history.scss +++ b/frontend/resources/styles/main/partials/sidebar-document-history.scss @@ -57,7 +57,7 @@ .history-entry { border: 1px solid $color-gray-60; - border-radius: 4px; + border-radius: $br4; margin: 0.5rem; display: flex; flex-direction: column; diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 5e4498844..ef3608c28 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -13,14 +13,14 @@ .element-icons { background-color: $color-gray-60; border: 1px solid $color-gray-60; - border-radius: $br-small; + border-radius: $br3; display: flex; margin: $size-1; li { align-items: center; border-right: 1px solid $color-gray-60; - border-radius: $br-small; + border-radius: $br3; cursor: pointer; display: flex; flex: 1; @@ -145,7 +145,7 @@ span { color: $color-primary; - font-weight: bold; + font-weight: $fw700; } } } @@ -195,6 +195,10 @@ } .input-select { + /* This padding is so the text won't overlap the arrow*/ + padding-right: 1rem; + overflow: hidden; + text-overflow: ellipsis; color: $color-gray-10; &:focus { @@ -234,7 +238,7 @@ } input:checked + label::after { - font-size: 0.8rem; + font-size: $fs13; } } @@ -288,7 +292,7 @@ .custom-select { border: 1px solid $color-gray-40; - border-radius: $br-small; + border-radius: $br3; cursor: pointer; padding: $size-1 $size-5 $size-1 $size-1; position: relative; @@ -324,7 +328,7 @@ .custom-select-dropdown { background-color: $color-white; - border-radius: $br-small; + border-radius: $br3; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); left: 0; max-height: 30rem; @@ -662,7 +666,7 @@ .margin-options { align-items: center; border: 1px solid $color-gray-60; - border-radius: 4px; + border-radius: $br4; display: flex; justify-content: space-between; padding: 8px; @@ -808,7 +812,7 @@ display: flex; padding: 0.3rem 0; border: 1px solid $color-black; - border-radius: 4px; + border-radius: $br4; height: 48px; &:hover { @@ -881,7 +885,7 @@ .advanced-options { border: 1px solid $color-gray-60; background-color: $color-gray-50; - border-radius: 4px; + border-radius: $br4; padding: 8px; position: relative; top: 2px; @@ -892,9 +896,9 @@ cursor: pointer; border: 1px solid $color-black; background: $color-gray-60; - border-radius: 2px; + border-radius: $br2; color: $color-gray-20; - font-size: 11px; + font-size: $fs11; line-height: 16px; flex-grow: 1; padding: 0.25rem 0; @@ -919,7 +923,7 @@ justify-content: space-between; padding: 3px; border: 1px solid $color-black; - border-radius: 4px; + border-radius: $br4; &:hover { background: #1f1f1f; @@ -1078,18 +1082,18 @@ } .typography-sample { - font-size: 17px; + font-size: $fs17; color: $color-white; margin: 0 0.5rem; font-family: sourcesanspro; font-style: normal; - font-weight: normal; + font-weight: $fw400; } .typography-name { flex-grow: 1; - font-size: 11px; + font-size: $fs11; margin-top: 4px; white-space: nowrap; } @@ -1120,7 +1124,7 @@ } .row-flex input.adv-typography-name { - font-size: 14px; + font-size: $fs14; color: $color-gray-10; width: 100%; max-width: none; @@ -1140,11 +1144,11 @@ } .typography-read-only-data { - font-size: 12px; + font-size: $fs12; color: $color-white; .typography-name { - font-size: 14px; + font-size: $fs14; } .row-flex { @@ -1166,9 +1170,9 @@ text-align: center; background: $color-gray-50; padding: 0.5rem; - border-radius: 2px; + border-radius: $br2; cursor: pointer; - font-size: 14px; + font-size: $fs14; margin-top: 1rem; &:hover { @@ -1182,7 +1186,7 @@ margin: 0.5rem; padding: 0.5rem; border: 1px dashed $color-gray-30; - border-radius: 4px; + border-radius: $br4; display: flex; justify-content: space-between; @@ -1252,7 +1256,7 @@ width: $size-4; height: $size-4; border: 1px solid $color-gray-30; - border-radius: $br-small; + border-radius: $br3; svg { width: 8px; @@ -1282,7 +1286,7 @@ padding: 4px; font-size: $fs12; background: $color-gray-50; - border-radius: $br-small; + border-radius: $br3; color: $color-gray-20; border: 1px solid $color-gray-30; width: 88%; @@ -1364,7 +1368,7 @@ } .label { - font-size: 12px; + font-size: $fs12; } svg { @@ -1588,7 +1592,7 @@ .text { color: $color-gray-30; - font-size: 0.75rem; + font-size: $fs12; padding-left: 10px; } @@ -1624,7 +1628,8 @@ } .layout-menu, -.layout-item-menu { +.layout-item-menu, +.layout-grid-item-menu { font-family: "worksans", sans-serif; svg { height: 16px; @@ -1649,11 +1654,18 @@ align-items: start; margin-top: 4px; } + + &.align-items-grid, + &.jusfiy-content-grid { + align-items: start; + margin-top: 11px; + } } .btn-wrapper { display: flex; width: 100%; max-width: 185px; + &.wrap { flex-direction: column; gap: 5px; @@ -1672,9 +1684,10 @@ .align-self-style, .justify-content-style, .align-content-style, - .layout-behavior { + .layout-behavior, + .absolute { display: flex; - border-radius: 4px; + border-radius: $br4; border: 1px solid $color-gray-60; height: 26px; margin-right: 5px; @@ -1693,8 +1706,13 @@ border: none; cursor: pointer; border-right: 1px solid $color-gray-60; + color: $color-gray-20; &.active, &:hover { + color: $color-primary; + &.dir { + color: $color-primary; + } svg { fill: $color-primary; } @@ -1736,6 +1754,69 @@ border-right: none; } } + .z-index { + display: flex; + align-items: center; + margin-left: 2px; + margin-top: -4px; + svg { + width: 30px; + } + } + + .edit-mode { + display: flex; + justify-content: center; + align-items: center; + margin-left: 5px; + + button { + display: flex; + justify-content: center; + align-items: center; + background: transparent; + border: none; + cursor: pointer; + &.active, + &:hover { + svg { + fill: $color-primary; + } + } + } + } + + &.align-grid { + flex-direction: column; + gap: 7px; + margin: 7px 0; + } + } + .position-wrapper { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + width: 100%; + max-width: 185px; + height: 26px; + border-radius: 4px; + border: 1px solid $color-gray-60; + .position-btn { + display: flex; + justify-content: center; + align-items: center; + background: transparent; + border: none; + cursor: pointer; + color: $color-gray-20; + border-right: 1px solid $color-gray-60; + &:last-child { + border-right: none; + } + &.active, + &:hover { + color: $color-primary; + } + } } } .no-wrap { @@ -1785,7 +1866,7 @@ } } input { - font-size: 0.75rem; + font-size: $fs12; min-width: 0; padding: 0.25rem; height: 20px; @@ -1799,11 +1880,11 @@ background: none; border: none; cursor: pointer; - border-radius: $br-small; + border-radius: $br3; &.lock { border: 1px solid $color-gray-60; - border-radius: $br-small; + border-radius: $br3; width: 30px; height: 30px; } @@ -1856,7 +1937,7 @@ } } input { - font-size: 0.75rem; + font-size: $fs12; min-width: 0; padding: 0.25rem; width: 70px; @@ -1871,11 +1952,11 @@ background: none; border: none; cursor: pointer; - border-radius: $br-small; + border-radius: $br3; &.lock { border: 1px solid $color-gray-60; - border-radius: $br-small; + border-radius: $br3; width: 30px; height: 30px; } @@ -1896,9 +1977,9 @@ .margin-item-icons { padding: 0; border: 1px solid $color-gray-60; - border-radius: 3px; + border-radius: $br3; margin-bottom: 8px; - margin-top: 3px; + margin-top: $br3; margin-right: 1px; height: 30px; width: 30px; @@ -1945,7 +2026,7 @@ .layout-container { border: 1px solid $color-gray-60; - border-radius: 3px; + border-radius: $br3; margin: 5px 0; .layout-entry { display: flex; @@ -2064,6 +2145,147 @@ } } } + + .grid-columns { + border: 1px solid $color-gray-60; + padding: 5px; + min-height: 38px; + display: flex; + flex-direction: column; + align-items: center; + &:not(:first-child) { + margin-top: 5px; + } + .grid-columns-header { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + flex-grow: 1; + min-height: 36px; + .columns-info { + flex-grow: 1; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .expand-icon, + .add-column { + cursor: pointer; + background-color: transparent; + border: none; + display: flex; + justify-content: center; + align-items: center; + + &.active, + &:hover { + svg { + fill: $color-primary; + } + } + } + + .add-column svg { + height: 12px; + width: 12px; + fill: $color-gray-20; + } + } + .columns-info-wrapper { + .column-info { + display: grid; + grid-template-columns: 35px 1fr 1fr auto; + background-color: $color-gray-60; + padding: 3px; + &:not(:first-child) { + margin-top: 3px; + } + .direction-grid-icon { + display: flex; + justify-content: center; + align-items: center; + padding: 5px; + } + input { + background-color: $color-gray-60; + } + .grid-column-value, + .grid-column-unit { + display: flex; + justify-content: center; + align-items: center; + height: 30px; + &.active, + &:focus, + &:focus-within { + border-bottom: 1px solid $color-primary; + } + } + .grid-column-unit-selector { + border: none; + border-bottom: 1px solid $color-gray-30; + margin: 0.25rem 0; + height: 23px; + width: 100%; + &:hover { + border-bottom: 1px solid $color-gray-20; + } + } + + .remove-grid-column { + cursor: pointer; + background-color: transparent; + border: none; + display: flex; + justify-content: center; + align-items: center; + margin-left: 40px; + svg { + height: 12px; + width: 12px; + fill: $color-gray-20; + } + &.active, + &:hover { + svg { + fill: $color-primary; + } + } + } + } + } + } + .manage-grid-columns { + margin-left: 60px; + margin-bottom: 10px; + .grid-auto, + .grid-manual { + display: grid; + grid-template-columns: 1fr 1fr; + .grid-columns-auto, + .grid-rows-auto { + display: grid; + grid-template-columns: 20px 1fr; + .icon { + display: flex; + justify-content: center; + align-items: center; + } + input { + width: 80%; + } + } + } + .grid-manual { + .input-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + } + } + } } .advanced-ops { diff --git a/frontend/resources/styles/main/partials/sidebar-icons.scss b/frontend/resources/styles/main/partials/sidebar-icons.scss index 524064f82..0b140d4b1 100644 --- a/frontend/resources/styles/main/partials/sidebar-icons.scss +++ b/frontend/resources/styles/main/partials/sidebar-icons.scss @@ -19,7 +19,7 @@ .figure-btn { align-items: center; background-color: $color-gray-60; - border-radius: 3px; + border-radius: $br3; border: 1px solid transparent; cursor: pointer; display: flex; diff --git a/frontend/resources/styles/main/partials/sidebar-interactions.scss b/frontend/resources/styles/main/partials/sidebar-interactions.scss index 7a1dc3d93..05cfd0f38 100644 --- a/frontend/resources/styles/main/partials/sidebar-interactions.scss +++ b/frontend/resources/styles/main/partials/sidebar-interactions.scss @@ -182,7 +182,7 @@ & .content { align-items: center; background-color: $color-gray-50; - border-radius: 4px; + border-radius: $br4; display: flex; height: 24px; diff --git a/frontend/resources/styles/main/partials/sidebar-layers.scss b/frontend/resources/styles/main/partials/sidebar-layers.scss index 1bb42703b..bd3d30a0d 100644 --- a/frontend/resources/styles/main/partials/sidebar-layers.scss +++ b/frontend/resources/styles/main/partials/sidebar-layers.scss @@ -12,13 +12,52 @@ padding: $size-1 $size-2; transition: none; width: 100%; + .toggle-content { + svg { + display: flex; + height: 13px; + width: 13px; + } + } - svg { - fill: $color-gray-20; - height: 13px; - flex-shrink: 0; - margin-right: 8px; - width: 13px; + .icon { + position: relative; + display: flex; + align-items: center; + justify-content: center; + margin-right: 4px; + margin-bottom: 3px; + width: 18px; + height: 18px; + + svg { + fill: $color-gray-20; + height: 12px; + width: 12px; + } + + & .absolute { + position: absolute; + top: 0; + left: 0; + width: 18px; + height: 18px; + + svg { + width: 18px; + height: 18px; + fill: $color-gray-20; + fill-opacity: 0.5; + } + } + } + + &.selected .icon .absolute svg { + fill: $color-primary; + } + + &:hover .icon .absolute svg { + fill: $color-gray-60; } &.group { @@ -303,7 +342,7 @@ span.element-name { .icon-layer { > svg { background-color: rgba(255, 255, 255, 0.6); - border-radius: $br-small; + border-radius: $br3; flex-shrink: 0; fill: $color-black !important; padding: 1px; @@ -320,7 +359,7 @@ span.element-name { &.search { .search-box { border: 1px solid $color-gray-20; - border-radius: 4px; + border-radius: $br4; height: 32px; width: 100%; display: flex; @@ -334,7 +373,7 @@ span.element-name { width: 100%; background-color: $color-gray-50; color: $color-white; - font-size: 12px; + font-size: $fs12; height: 16px; &:focus { outline: none; @@ -367,14 +406,14 @@ span.element-name { .active-filters { margin-top: 5px; line-height: 26px; - font-size: 11px; + font-size: $fs11; margin: 0 0.5rem; span { background-color: $color-primary; color: $color-black; padding: 3px 5px; margin: 0 2px; - border-radius: 4px; + border-radius: $br4; cursor: pointer; svg { width: 7px; @@ -393,10 +432,11 @@ span.element-name { left: 8px; background-color: $color-white; color: $color-gray-50; - border-radius: 4px; + border-radius: $br4; + z-index: 1; span { padding: 10px 20px 10px 10px; - border-radius: 4px; + border-radius: $br4; svg { width: 16px; height: 16px; diff --git a/frontend/resources/styles/main/partials/sidebar-sitemap.scss b/frontend/resources/styles/main/partials/sidebar-sitemap.scss index 44706d500..0d0546a3a 100644 --- a/frontend/resources/styles/main/partials/sidebar-sitemap.scss +++ b/frontend/resources/styles/main/partials/sidebar-sitemap.scss @@ -133,7 +133,7 @@ .collapse-pages { align-items: center; background-color: transparent; - border-radius: $br-small; + border-radius: $br3; border: 1px solid transparent; cursor: pointer; display: flex; @@ -180,7 +180,7 @@ } button { background-color: transparent; - border-radius: $br-small; + border-radius: $br3; border: 1px solid transparent; cursor: pointer; color: $color-gray-20; diff --git a/frontend/resources/styles/main/partials/sidebar-tools.scss b/frontend/resources/styles/main/partials/sidebar-tools.scss index c2c881790..819f1c378 100644 --- a/frontend/resources/styles/main/partials/sidebar-tools.scss +++ b/frontend/resources/styles/main/partials/sidebar-tools.scss @@ -12,7 +12,7 @@ .tool-btn { align-items: center; background-color: $color-gray-60; - border-radius: 3px; + border-radius: $br4; cursor: pointer; display: flex; flex-shrink: 0; diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index c5765c444..fd41ce521 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -58,7 +58,7 @@ span.tool-badge { border: 1px solid $color-primary; - border-radius: 2px; + border-radius: $br2; font-size: $fs10; color: $color-primary; padding: 2px 4px; @@ -123,8 +123,8 @@ & .view-only-mode { color: $color-primary; border: 1px solid $color-primary; - border-radius: 3px; - font-size: 10px; + border-radius: $br3; + font-size: $fs10; text-transform: uppercase; display: flex; align-items: center; @@ -171,8 +171,8 @@ & .focus-mode { color: $color-primary; border: 1px solid $color-primary; - border-radius: 3px; - font-size: 10px; + border-radius: $br3; + font-size: $fs10; text-transform: uppercase; padding: 0px 4px; display: flex; @@ -189,7 +189,7 @@ .empty { color: $color-gray-20; - font-size: 12px; + font-size: $fs12; line-height: 1.5; text-align: center; padding: 0 15px; @@ -246,6 +246,7 @@ height: 100%; width: 100%; overflow-y: auto; + overflow-x: hidden; &.inspect { .tab-container-tabs { @@ -256,7 +257,7 @@ } .tab-container-tab-title { - border-radius: 4px; + border-radius: $br4; &.current { background-color: $color-primary; @@ -339,7 +340,7 @@ button.collapse-sidebar { width: 28px; height: 48px; padding: 0; - border-radius: 0px 4px 4px 0px; + border-radius: 0 $br4 $br4 0; border-left: 1px solid $color-gray-50; & svg { @@ -415,7 +416,7 @@ button.collapse-sidebar { justify-content: space-between; align-items: center; border: 1px solid $color-gray-30; - border-radius: 2px; + border-radius: $br2; width: 100%; &:focus-within { border: 1px solid $color-primary; @@ -516,7 +517,7 @@ button.collapse-sidebar { .shortcut-name { border: 1px solid $color-gray-60; - border-radius: 4px; + border-radius: $br4; padding: 7px; display: flex; justify-content: space-between; @@ -537,10 +538,10 @@ button.collapse-sidebar { min-width: 15px; background-color: $color-white; color: $color-black; - border-radius: 3px; + border-radius: $br3; padding: 2px 5px; font-size: $fs11; - font-weight: 600; + font-weight: $fw600; margin: 0 2px; text-transform: capitalize; display: inline-block; diff --git a/frontend/resources/styles/main/partials/tab-container.scss b/frontend/resources/styles/main/partials/tab-container.scss index be52565e5..41490f896 100644 --- a/frontend/resources/styles/main/partials/tab-container.scss +++ b/frontend/resources/styles/main/partials/tab-container.scss @@ -10,7 +10,7 @@ cursor: pointer; display: flex; flex-direction: row; - font-size: 12px; + font-size: $fs12; height: 2.5rem; padding: 0 0.25rem; } @@ -18,7 +18,7 @@ .tab-container-tab-title { align-items: center; background: $color-gray-60; - border-radius: 2px 2px 0px 0px; + border-radius: $br2 $br2 0 0; color: $color-white; display: flex; justify-content: center; diff --git a/frontend/resources/styles/main/partials/text-palette.scss b/frontend/resources/styles/main/partials/text-palette.scss index b1c223da1..4bb667f3d 100644 --- a/frontend/resources/styles/main/partials/text-palette.scss +++ b/frontend/resources/styles/main/partials/text-palette.scss @@ -13,7 +13,7 @@ & .typography-font, & .typography-data { - font-size: 16px; + font-size: $fs16; color: $color-gray-30; } diff --git a/frontend/resources/styles/main/partials/user-settings.scss b/frontend/resources/styles/main/partials/user-settings.scss index f7cdbfb29..8e2f78651 100644 --- a/frontend/resources/styles/main/partials/user-settings.scss +++ b/frontend/resources/styles/main/partials/user-settings.scss @@ -70,7 +70,7 @@ display: flex; flex-grow: 1; font-size: $fs24; - font-weight: normal; + font-weight: $fw400; justify-content: center; } @@ -181,7 +181,7 @@ h2 { font-size: $fs14; - font-weight: normal; + font-weight: $fw400; margin-bottom: $size-4; } } diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index d8e15b3c3..550148aa5 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -3,7 +3,7 @@ background-color: $color-gray-50; border-bottom: 1px solid $color-gray-60; display: grid; - grid-template-columns: 45% 10% 45%; + grid-template-columns: 1fr 130px 1fr; height: 48px; padding: 0 $size-4 0 55px; top: 0; diff --git a/frontend/resources/styles/main/partials/viewer-thumbnails.scss b/frontend/resources/styles/main/partials/viewer-thumbnails.scss index 8d9067ed0..2ba7a859f 100644 --- a/frontend/resources/styles/main/partials/viewer-thumbnails.scss +++ b/frontend/resources/styles/main/partials/viewer-thumbnails.scss @@ -146,7 +146,7 @@ min-height: 120px; height: 120px; border: 1px solid $color-gray-20; - border-radius: 2px; + border-radius: $br2; padding: 4px; display: flex; diff --git a/frontend/resources/styles/main/partials/viewer.scss b/frontend/resources/styles/main/partials/viewer.scss index b5a5e0365..1ed55ff1c 100644 --- a/frontend/resources/styles/main/partials/viewer.scss +++ b/frontend/resources/styles/main/partials/viewer.scss @@ -57,7 +57,7 @@ display: flex; align-items: center; justify-content: center; - border-radius: 12px; + border-radius: $br12; background: $color-gray-50; width: 24px; height: 24px; @@ -117,7 +117,7 @@ .reset { display: flex; align-items: center; - border-radius: 12px; + border-radius: $br12; background: $color-gray-50; width: 24px; height: 24px; @@ -141,7 +141,7 @@ display: flex; align-items: center; justify-content: center; - border-radius: 12px; + border-radius: $br12; background: $color-gray-50; width: 67px; height: 25px; diff --git a/frontend/resources/styles/main/partials/workspace-header.scss b/frontend/resources/styles/main/partials/workspace-header.scss index 755a442c5..2fdc61e83 100644 --- a/frontend/resources/styles/main/partials/workspace-header.scss +++ b/frontend/resources/styles/main/partials/workspace-header.scss @@ -149,7 +149,7 @@ &:focus, &:focus-within { outline: none; - border-radius: 3px; + border-radius: $br3; border: 1px solid $color-primary; } } @@ -163,17 +163,17 @@ z-index: 12; background-color: $color-white; - border-radius: $br-small; + border-radius: $br3; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); :first-child { &:hover { - border-radius: $br-small $br-small 0px 0px; + border-radius: $br3 $br3 0 0; } } :last-child { &:hover { - border-radius: 0px 0px $br-small $br-small; + border-radius: 0 0 $br3 $br3; } } @@ -211,17 +211,17 @@ width: 270px; z-index: 12; background-color: $color-white; - border-radius: $br-small; + border-radius: $br3; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); :first-child { &:hover { - border-radius: $br-small $br-small 0px 0px; + border-radius: $br3 $br3 0 0; } } :last-child { &:hover { - border-radius: 0px 0px $br-small $br-small; + border-radius: 0 0 $br3 $br3; } } @@ -294,7 +294,7 @@ & button.document-history { background: $color-gray-60; - border-radius: 3px; + border-radius: $br3; border: none; cursor: pointer; height: 24px; diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index da78e187a..e2bd7767e 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -77,7 +77,7 @@ $height-palette-max: 80px; .workspace-context-menu { background-color: $color-white; - border-radius: $br-small; + border-radius: $br3; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); left: 740px; position: absolute; @@ -198,7 +198,7 @@ $height-palette-max: 80px; .coordinates { background-color: $color-dark-bg; - border-radius: $br-small; + border-radius: $br3; bottom: 0px; padding-left: 5px; position: fixed; @@ -223,7 +223,7 @@ $height-palette-max: 80px; .cursor-tooltip { background-color: $color-dark-bg; - border-radius: $br-small; + border-radius: $br3; color: $color-white; font-size: $fs12; padding: 3px 8px; @@ -348,11 +348,11 @@ $height-palette-max: 80px; width: fit-content; font-family: worksans; padding: 2px 12px; - border-radius: 4px; + border-radius: $br4; display: flex; align-items: center; height: 20px; - font-size: 12px; + font-size: $fs12; line-height: 1.5; } @@ -373,7 +373,7 @@ $height-palette-max: 80px; display: flex; flex-direction: row; background: white; - border-radius: 3px; + border-radius: $br3; padding: 0.5rem; border: 1px solid $color-gray-20; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); @@ -393,7 +393,7 @@ $height-palette-max: 80px; display: flex; justify-content: center; align-items: center; - border-radius: 3px; + border-radius: $br3; svg { pointer-events: none; diff --git a/frontend/resources/styles/main/partials/zoom-widget.scss b/frontend/resources/styles/main/partials/zoom-widget.scss index c6ea675ba..1edb1da56 100644 --- a/frontend/resources/styles/main/partials/zoom-widget.scss +++ b/frontend/resources/styles/main/partials/zoom-widget.scss @@ -23,7 +23,7 @@ z-index: 12; background-color: $color-white; - border-radius: $br-small; + border-radius: $br3; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); li { @@ -77,7 +77,7 @@ background-color: $color-white; border: none; &:hover { - color: $color-primary; + color: $color-primary-darker; } } .reset-btn { diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index da7083dd3..14f26d76a 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -16,7 +16,7 @@ - + @@ -37,7 +37,7 @@ {{>../public/images/sprites/symbol/icons.svg}} {{>../public/images/sprites/symbol/cursors.svg}} -
+
{{# manifest}} diff --git a/frontend/resources/templates/render.mustache b/frontend/resources/templates/render.mustache index ed1fd7c37..5221030ae 100644 --- a/frontend/resources/templates/render.mustache +++ b/frontend/resources/templates/render.mustache @@ -17,7 +17,7 @@ {{/manifest}} -
+
{{# manifest}} diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 678eaf3aa..b81ccc63c 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -30,9 +30,7 @@ [potok.core :as ptk] [rumext.v2 :as mf])) -(log/initialize!) -(log/set-level! :root :warn) -(log/set-level! :app :info) +(log/setup! {:app :info}) (when (= :browser @cf/target) (log/info :message "Welcome to penpot" diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index aced66cdc..f7904d520 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -105,7 +105,7 @@ (defn created-thread-on-viewer [{:keys [id comment page-id] :as thread}] - (ptk/reify ::created-thread-on-workspace + (ptk/reify ::created-thread-on-viewer ptk/UpdateEvent (update [_ state] (-> state @@ -279,15 +279,19 @@ (-> (assoc-in (conj path :position) (:position comment-thread)) (assoc-in (conj path :frame-id) (:frame-id comment-thread)))))) - (fetched [data state] - (let [state (assoc state :comment-threads (d/index-by :id data))] - (reduce set-comment-threds state data)))] + (fetched [[users comments] state] + (let [state (-> state + (assoc :comment-threads (d/index-by :id comments)) + (assoc :current-file-comments-users (d/index-by :id users)))] + (reduce set-comment-threds state comments)))] (ptk/reify ::retrieve-comment-threads ptk/WatchEvent (watch [_ state _] (let [share-id (-> state :viewer-local :share-id)] - (->> (rp/cmd! :get-comment-threads {:file-id file-id :share-id share-id}) + (->> (rx/zip (rp/cmd! :get-team-users {:file-id file-id}) + (rp/cmd! :get-comment-threads {:file-id file-id :share-id share-id})) + (rx/take 1) (rx/map #(partial fetched %)) (rx/catch #(rx/throw {:type :comment-error})))))))) diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index e5072d13c..2b10703a4 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -27,7 +27,7 @@ (ptk/reify ::create-share-link ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation! :create-share-link params) + (->> (rp/cmd! :create-share-link params) (rx/map share-link-created))))) (defn delete-share-link @@ -41,6 +41,6 @@ ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation! :delete-share-link {:id id}) + (->> (rp/cmd! :delete-share-link {:id id}) (rx/ignore))))) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 659b2afe9..0d812e4d2 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -186,7 +186,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/query! :projects {:team-id team-id}) + (->> (rp/cmd! :get-projects {:team-id team-id}) (rx/map projects-fetched)))))) ;; --- EVENT: search @@ -674,7 +674,7 @@ {:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] - (->> (rp/mutation! :create-project params) + (->> (rp/cmd! :create-project params) (rx/tap on-success) (rx/map project-created) (rx/catch on-error)))))) @@ -736,7 +736,7 @@ (watch [_ state _] (let [project (get-in state [:dashboard-projects id]) params (select-keys project [:id :is-pinned :team-id])] - (->> (rp/mutation :update-project-pin params) + (->> (rp/cmd! :update-project-pin params) (rx/ignore)))))) ;; --- EVENT: rename-project @@ -754,7 +754,7 @@ ptk/WatchEvent (watch [_ _ _] (let [params {:id id :name name}] - (->> (rp/mutation :rename-project params) + (->> (rp/cmd! :rename-project params) (rx/ignore)))))) ;; --- EVENT: delete-project @@ -769,7 +769,7 @@ ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation :delete-project {:id id}) + (->> (rp/cmd! :delete-project {:id id}) (rx/ignore))))) ;; --- EVENT: delete-file @@ -925,6 +925,13 @@ {:num-files (count ids) :project-id project-id}) + ptk/UpdateEvent + (update [_ state] + (let [origin-project (get-in state [:dashboard-files (first ids) :project-id])] + (-> state + (update-in [:dashboard-projects origin-project :count] #(- % (count ids))) + (update-in [:dashboard-projects project-id :count] #(+ % (count ids)))))) + ptk/WatchEvent (watch [_ _ _] (let [{:keys [on-success on-error] @@ -1086,7 +1093,7 @@ action-name (if in-project? :create-file :create-project) action (if in-project? file-created project-created)] - (->> (rp/mutation! action-name params) + (->> (rp/cmd! action-name params) (rx/map action)))))) (defn open-selected-file diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs index dc7cf3784..5e45a4db0 100644 --- a/frontend/src/app/main/data/fonts.cljs +++ b/frontend/src/app/main/data/fonts.cljs @@ -76,7 +76,7 @@ (ptk/reify ::load-team-fonts ptk/WatchEvent (watch [_ _ _] - (->> (rp/query :font-variants {:team-id team-id}) + (->> (rp/cmd! :get-font-variants {:team-id team-id}) (rx/map fonts-fetched))))) (defn process-upload @@ -264,7 +264,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/mutation! :update-font {:id id :name name :team-id team-id}) + (->> (rp/cmd! :update-font {:id id :name name :team-id team-id}) (rx/ignore)))))) (defn delete-font @@ -281,7 +281,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/mutation! :delete-font {:id font-id :team-id team-id}) + (->> (rp/cmd! :delete-font {:id font-id :team-id team-id}) (rx/ignore)))))) (defn delete-font-variant @@ -298,7 +298,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/mutation! :delete-font-variant {:id id :team-id team-id}) + (->> (rp/cmd! :delete-font-variant {:id id :team-id team-id}) (rx/ignore)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs index 383aa4e1f..ff4577f9b 100644 --- a/frontend/src/app/main/data/shortcuts.cljs +++ b/frontend/src/app/main/data/shortcuts.cljs @@ -7,7 +7,7 @@ (ns app.main.data.shortcuts (:refer-clojure :exclude [meta reset!]) (:require - ["mousetrap" :as mousetrap] + ["./shortcuts_impl.js$default" :as mousetrap] [app.common.logging :as log] [app.common.spec :as us] [app.config :as cf] @@ -32,6 +32,7 @@ (def up-arrow "\u2191") (def right-arrow "\u2192") (def down-arrow "\u2193") +(def tab "tab") (defn c-mod "Adds the control/command modifier to a shortcuts depending on the diff --git a/frontend/src/app/main/data/shortcuts_impl.js b/frontend/src/app/main/data/shortcuts_impl.js new file mode 100644 index 000000000..d72137ed0 --- /dev/null +++ b/frontend/src/app/main/data/shortcuts_impl.js @@ -0,0 +1,33 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ +"use strict"; + +import Mousetrap from 'mousetrap' + +if (Mousetrap.addKeycodes) { + Mousetrap.addKeycodes({ + 219: '219' + }); +} + +const target = Mousetrap.prototype || Mousetrap; +target.stopCallback = function(e, element, combo) { + // if the element has the class "mousetrap" then no need to stop + if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { + return false; + } + + // stop for input, select, textarea and button + return element.tagName == 'INPUT' || + element.tagName == 'SELECT' || + element.tagName == 'TEXTAREA' || + (element.tagName == 'BUTTON' && combo.includes("tab")) || + (element.contentEditable && element.contentEditable == 'true'); +} + +export default Mousetrap; diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 906ddfd7a..676e5cec0 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -38,7 +38,7 @@ (s/def ::created-at ::us/inst) (s/def ::password-1 ::us/string) (s/def ::password-2 ::us/string) -(s/def ::password-old ::us/string) +(s/def ::password-old (s/nilable ::us/string)) (s/def ::profile (s/keys :req-un [::id] @@ -124,7 +124,7 @@ (ptk/reify ::fetch-profile ptk/WatchEvent (watch [_ _ _] - (->> (rp/query! :profile) + (->> (rp/cmd! :get-profile) (rx/map profile-fetched))))) ;; --- EVENT: INITIALIZE PROFILE @@ -207,7 +207,7 @@ ;; the returned profile is an NOT authenticated profile, we ;; proceed to logout and show an error message. - (->> (rp/command! :login-with-password (d/without-nils params)) + (->> (rp/cmd! :login-with-password (d/without-nils params)) (rx/merge-map (fn [data] (rx/merge (rx/of (fetch-profile)) @@ -293,7 +293,7 @@ (ptk/reify ::logout ptk/WatchEvent (watch [_ _ _] - (->> (rp/command! :logout) + (->> (rp/cmd! :logout) (rx/delay-at-least 300) (rx/catch (constantly (rx/of 1))) (rx/map #(logged-out params))))))) @@ -309,7 +309,7 @@ (let [mdata (meta data) on-success (:on-success mdata identity) on-error (:on-error mdata rx/throw)] - (->> (rp/mutation :update-profile (dissoc data :props)) + (->> (rp/cmd! :update-profile (dissoc data :props)) (rx/catch on-error) (rx/mapcat (fn [_] @@ -333,7 +333,7 @@ (let [{:keys [on-error on-success] :or {on-error identity on-success identity}} (meta data)] - (->> (rp/mutation :request-email-change data) + (->> (rp/cmd! :request-email-change data) (rx/tap on-success) (rx/catch on-error)))))) @@ -343,7 +343,7 @@ (ptk/reify ::cancel-email-change ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation :cancel-email-change {}) + (->> (rp/cmd! :cancel-email-change {}) (rx/map (constantly (fetch-profile))))))) ;; --- Update Password (Form) @@ -364,7 +364,7 @@ on-success identity}} (meta data) params {:old-password (:password-old data) :password (:password-1 data)}] - (->> (rp/mutation :update-profile-password params) + (->> (rp/cmd! :update-profile-password params) (rx/tap on-success) (rx/catch (fn [err] (on-error err) @@ -382,7 +382,7 @@ ;; the response value of update-profile-props RPC call ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation :update-profile-props {:props props}) + (->> (rp/cmd! :update-profile-props {:props props}) (rx/map (constantly (fetch-profile))))))) (defn mark-onboarding-as-viewed @@ -394,7 +394,7 @@ (let [version (or version (:main @cf/version)) props {:onboarding-viewed true :release-notes-viewed version}] - (->> (rp/mutation :update-profile-props {:props props}) + (->> (rp/cmd! :update-profile-props {:props props}) (rx/map (constantly (fetch-profile))))))))) (defn mark-questions-as-answered @@ -407,7 +407,7 @@ ptk/WatchEvent (watch [_ _ _] (let [props {:onboarding-questions-answered true}] - (->> (rp/mutation :update-profile-props {:props props}) + (->> (rp/cmd! :update-profile-props {:props props}) (rx/map (constantly (fetch-profile)))))))) @@ -431,7 +431,7 @@ (->> (rx/of file) (rx/map di/validate-file) (rx/map prepare) - (rx/mapcat #(rp/mutation :update-profile-photo %)) + (rx/mapcat #(rp/cmd! :update-profile-photo %)) (rx/do on-success) (rx/map (constantly (fetch-profile))) (rx/catch on-error)))))) @@ -460,7 +460,7 @@ ptk/WatchEvent (watch [_ state _] (let [share-id (-> state :viewer-local :share-id)] - (->> (rp/command! :get-profiles-for-file-comments {:team-id team-id :share-id share-id}) + (->> (rp/cmd! :get-profiles-for-file-comments {:team-id team-id :share-id share-id}) (rx/map #(partial fetched %)))))))) ;; --- EVENT: request-account-deletion @@ -473,7 +473,7 @@ (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta params)] - (->> (rp/mutation :delete-profile {}) + (->> (rp/cmd! :delete-profile {}) (rx/tap on-success) (rx/delay-at-least 300) (rx/catch (constantly (rx/of 1))) @@ -495,7 +495,7 @@ :or {on-error rx/throw on-success identity}} (meta data)] - (->> (rp/command! :request-profile-recovery data) + (->> (rp/cmd! :request-profile-recovery data) (rx/tap on-success) (rx/catch on-error)))))) @@ -514,7 +514,7 @@ (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta data)] - (->> (rp/command! :recover-profile data) + (->> (rp/cmd! :recover-profile data) (rx/tap on-success) (rx/catch on-error)))))) @@ -525,7 +525,7 @@ (ptk/reify ::create-demo-profile ptk/WatchEvent (watch [_ _ _] - (->> (rp/command! :create-demo-profile {}) + (->> (rp/cmd! :create-demo-profile {}) (rx/map login))))) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 4ae8f1263..7d6246109 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -129,22 +129,22 @@ (rx/map :content) (rx/map #(vector key %)))))] - (->> (rp/query! :view-only-bundle params') - (rx/mapcat - (fn [bundle] - (->> (rx/from (-> bundle :file :data :pages-index seq)) - (rx/merge-map - (fn [[_ page :as kp]] - (if (t/pointer? page) - (resolve kp) - (rx/of kp)))) - (rx/reduce conj {}) - (rx/map (fn [pages-index] - (update-in bundle [:file :data] assoc :pages-index pages-index)))))) - (rx/mapcat - (fn [{:keys [fonts] :as bundle}] - (rx/of (df/fonts-fetched fonts) - (bundle-fetched (merge bundle params)))))))))) + (->> (rp/cmd! :get-view-only-bundle params') + (rx/mapcat + (fn [bundle] + (->> (rx/from (-> bundle :file :data :pages-index seq)) + (rx/merge-map + (fn [[_ page :as kp]] + (if (t/pointer? page) + (resolve kp) + (rx/of kp)))) + (rx/reduce conj {}) + (rx/map (fn [pages-index] + (update-in bundle [:file :data] assoc :pages-index pages-index)))))) + (rx/mapcat + (fn [{:keys [fonts] :as bundle}] + (rx/of (df/fonts-fetched fonts) + (bundle-fetched (merge bundle params)))))))))) (declare go-to-frame-auto) @@ -300,6 +300,13 @@ (update [_ state] (update-in state [:viewer-local :fullscreen?] not)))) +(defn exit-fullscreen + [] + (ptk/reify ::exit-fullscreen + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:viewer-local :fullscreen?] false)))) + (defn set-viewport-size [{:keys [size]}] (ptk/reify ::set-viewport-size diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index c4c1ad6d9..1121279d7 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -277,7 +277,7 @@ (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features}) (rp/cmd! :get-file-object-thumbnails {:file-id file-id}) - (rp/query! :project {:id project-id}) + (rp/cmd! :get-project {:id project-id}) (rp/cmd! :get-team-users {:file-id file-id}) (rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id})) (rx/take 1) @@ -659,6 +659,14 @@ (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) + ;; Remove layout-item properties when moving a shape outside a layout + (cond-> (not (ctl/any-layout? objects parent-id)) + (pcb/update-shapes ordered-indexes ctl/remove-layout-item-data)) + + ;; Remove the hide in viewer flag + (cond-> (and (not= uuid/zero parent-id) (cph/frame-shape? objects parent-id)) + (pcb/update-shapes ordered-indexes #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true)))) + ;; Move the shapes (pcb/change-parent parent-id shapes @@ -710,7 +718,7 @@ ;; Fix the sizing when moving a shape (pcb/update-shapes parents (fn [parent] - (if (ctl/layout? parent) + (if (ctl/flex-layout? parent) (cond-> parent (ctl/change-h-sizing? (:id parent) objects (:shapes parent)) (assoc :layout-item-h-sizing :fix) @@ -1004,6 +1012,13 @@ ;; Navigation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn workspace-focus-lost + [] + (ptk/reify ::workspace-focus-lost + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-global :show-distances?] false)))) + (defn navigate-to-project [project-id] (ptk/reify ::navigate-to-project @@ -1290,8 +1305,8 @@ ;; Prepare the shape object. Mainly needed for image shapes ;; for retrieve the image data and convert it to the ;; data-url. - (prepare-object [objects selected+children {:keys [type] :as obj}] - (let [obj (maybe-translate obj objects selected+children)] + (prepare-object [objects parent-frame-id {:keys [type] :as obj}] + (let [obj (maybe-translate obj objects parent-frame-id)] (if (= type :image) (let [url (cf/resolve-file-media (:metadata obj))] (->> (http/send! {:method :get @@ -1314,15 +1329,11 @@ (update res :images conj img-part)) res))) - (maybe-translate [shape objects selected+children] - (let [root-frame-id (cph/get-shape-id-root-frame objects (:id shape))] - (if (and (not (cph/root-frame? shape)) - (not (contains? selected+children root-frame-id))) - ;; When the parent frame is not selected we change to relative - ;; coordinates - (let [frame (get objects root-frame-id)] - (gsh/translate-to-frame shape frame)) - shape))) + (maybe-translate [shape objects parent-frame-id] + (if (= parent-frame-id uuid/zero) + shape + (let [frame (get objects parent-frame-id)] + (gsh/translate-to-frame shape frame)))) (on-copy-error [error] (js/console.error "Clipboard blocked:" error) @@ -1335,7 +1346,7 @@ selected (->> (wsh/lookup-selected state) (cph/clean-loops objects)) - selected+children (cph/selected-with-children objects selected) + parent-frame-id (cph/common-parent-frame objects selected) pdata (reduce (partial collect-object-ids objects) {} selected) initial {:type :copied-shapes :file-id (:current-file-id state) @@ -1350,7 +1361,7 @@ (catch :default e (on-copy-error e))) (->> (rx/from (seq (vals pdata))) - (rx/merge-map (partial prepare-object objects selected+children)) + (rx/merge-map (partial prepare-object objects parent-frame-id)) (rx/reduce collect-data initial) (rx/map (partial sort-selected state)) (rx/map t/encode-str) @@ -1488,7 +1499,7 @@ :file-id file-id :content blob :is-local true})) - (rx/mapcat #(rp/mutation! :upload-file-media-object %)) + (rx/mapcat #(rp/cmd! :upload-file-media-object %)) (rx/map (fn [media] (assoc media :prev-id (:id imgpart)))))) @@ -1925,6 +1936,32 @@ (rx/empty))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Measurements +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn set-paddings-selected + [paddings-selected] + (ptk/reify ::set-paddings-selected + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-global :paddings-selected] paddings-selected)))) + +(defn set-gap-selected + [gap-selected] + (ptk/reify ::set-gap-selected + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-global :gap-selected] gap-selected)))) + +(defn set-margins-selected + [margins-selected] + (ptk/reify ::set-margins-selected + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-global :margins-selected] margins-selected)))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Orphan Shapes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2009,6 +2046,8 @@ (dm/export dws/select-all) (dm/export dws/select-inside-group) (dm/export dws/select-shape) +(dm/export dws/select-prev-shape) +(dm/export dws/select-next-shape) (dm/export dws/shift-select-shapes) ;; Highlight diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs index b9a5be4d8..2b7871261 100644 --- a/frontend/src/app/main/data/workspace/bool.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -11,6 +11,7 @@ [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.path.shapes-to-path :as stp] + [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] @@ -98,6 +99,7 @@ changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) (pcb/add-object boolean-data {:index index}) + (pcb/update-shapes (map :id shapes) ctl/remove-layout-item-data) (pcb/change-parent shape-id shapes))] (rx/of (dch/commit-changes changes) (dws/select-shapes (d/ordered-set shape-id))))))))) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs index 6623b0821..771b873ce 100644 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -55,9 +55,8 @@ (defn update-shapes ([ids update-fn] (update-shapes ids update-fn nil)) - ([ids update-fn {:keys [reg-objects? save-undo? attrs ignore-tree page-id] - :or {reg-objects? false save-undo? true}}] - + ([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-remote?] + :or {reg-objects? false save-undo? true stack-undo? false ignore-remote? false}}] (us/assert ::coll-of-uuid ids) (us/assert fn? update-fn) @@ -80,12 +79,14 @@ (pcb/update-shapes changes [id] update-fn opts))) (-> (pcb/empty-changes it page-id) (pcb/set-save-undo? save-undo?) + (pcb/set-stack-undo? stack-undo?) (pcb/with-objects objects)) ids) changes (add-group-id changes state)] (rx/concat (if (seq (:redo-changes changes)) - (let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))] + (let [changes (cond-> changes reg-objects? (pcb/resize-parents ids)) + changes (cond-> changes ignore-remote? (pcb/ignore-remote))] (rx/of (commit-changes changes))) (rx/empty)) @@ -165,8 +166,8 @@ (defn commit-changes [{:keys [redo-changes undo-changes - origin save-undo? file-id group-id] - :or {save-undo? true}}] + origin save-undo? file-id group-id stack-undo?] + :or {save-undo? true stack-undo? false}}] (log/debug :msg "commit-changes" :js/redo-changes redo-changes :js/undo-changes undo-changes) @@ -183,6 +184,7 @@ :page-id page-id :frames frames :save-undo? save-undo? + :stack-undo? stack-undo? :group-id group-id}) ptk/UpdateEvent @@ -233,4 +235,4 @@ (let [entry {:undo-changes undo-changes :redo-changes redo-changes :group-id group-id}] - (rx/of (dwu/append-undo entry))))))))))) + (rx/of (dwu/append-undo entry stack-undo?))))))))))) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index cbae33ba3..04abaad50 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -8,6 +8,7 @@ (:require [app.common.colors :as colors] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.main.broadcast :as mbc] [app.main.data.modal :as md] @@ -16,6 +17,7 @@ [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.texts :as dwt] + [app.main.data.workspace.undo :as dwu] [app.util.color :as uc] [beicon.core :as rx] [potok.core :as ptk])) @@ -80,6 +82,8 @@ text-ids (filter is-text? ids) shape-ids (remove is-text? ids) + undo-id (js/Symbol) + attrs (cond-> {} (contains? color :color) @@ -103,8 +107,10 @@ transform-attrs #(transform % attrs)] (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) (rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids)) - (rx/of (dch/update-shapes shape-ids transform-attrs))))) + (rx/of (dch/update-shapes shape-ids transform-attrs)) + (rx/of (dwu/commit-undo-transaction undo-id))))) (defn swap-attrs [shape attr index new-index] (let [first (get-in shape [attr index]) @@ -338,7 +344,8 @@ (defn change-text-color [old-color new-color index node] (let [fills (map #(dissoc % :fill-color-ref-id :fill-color-ref-file) (:fills node)) - parsed-color (d/without-nils (color-att->text old-color)) + parsed-color (-> (d/without-nils (color-att->text old-color)) + (dissoc :fill-color-ref-id :fill-color-ref-file)) parsed-new-color (d/without-nils (color-att->text new-color)) has-color? (d/index-of fills parsed-color)] (cond-> node @@ -350,33 +357,47 @@ (ptk/reify ::change-color-in-selected ptk/WatchEvent (watch [_ _ _] - (->> (rx/from shapes-by-color) - (rx/map (fn [shape] (case (:prop shape) - :fill (change-fill [(:shape-id shape)] new-color (:index shape)) - :stroke (change-stroke [(:shape-id shape)] new-color (:index shape)) - :shadow (change-shadow [(:shape-id shape)] new-color (:index shape)) - :content (dwt/update-text-with-function - (:shape-id shape) - (partial change-text-color old-color new-color (:index shape)))))))))) + (let [undo-id (js/Symbol)] + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (->> (rx/from shapes-by-color) + (rx/map (fn [shape] (case (:prop shape) + :fill (change-fill [(:shape-id shape)] new-color (:index shape)) + :stroke (change-stroke [(:shape-id shape)] new-color (:index shape)) + :shadow (change-shadow [(:shape-id shape)] new-color (:index shape)) + :content (dwt/update-text-with-function + (:shape-id shape) + (partial change-text-color old-color new-color (:index shape))))))) + (rx/of (dwu/commit-undo-transaction undo-id))))))) (defn apply-color-from-palette - [color is-alt?] + [color stroke?] (ptk/reify ::apply-color-from-palette ptk/WatchEvent (watch [_ state _] (let [objects (wsh/lookup-page-objects state) selected (->> (wsh/lookup-selected state) (cph/clean-loops objects)) - selected-obj (keep (d/getf objects) selected) - select-shapes-for-color (fn [shape objects] - (let [shapes (case (:type shape) - :group (cph/get-children objects (:id shape)) - [shape])] - (->> shapes - (remove cph/group-shape?) - (map :id)))) - ids (mapcat #(select-shapes-for-color % objects) selected-obj)] - (if is-alt? + + ids + (loop [pending (seq selected) + result []] + (if (empty? pending) + result + (let [cur (first pending) + ;; We treat frames with no fill the same as groups + group? (or (cph/group-shape? objects cur) + (and (cph/frame-shape? objects cur) + (empty? (dm/get-in objects [cur :fills])))) + + pending + (if group? + (concat pending (dm/get-in objects [cur :shapes])) + pending) + + result (cond-> result (not group?) (conj cur))] + (recur (rest pending) result))))] + (if stroke? (rx/of (change-stroke ids (merge uc/empty-color color) 0)) (rx/of (change-fill ids (merge uc/empty-color color) 0))))))) diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index 140d58bbd..9517047d7 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -82,8 +82,9 @@ focus (:workspace-focus-selected state) fid (ctst/top-nested-frame objects initial) - layout? (ctl/layout? objects fid) - drop-index (when layout? (gsl/get-drop-index fid objects initial)) + + flex-layout? (ctl/flex-layout? objects fid) + drop-index (when flex-layout? (gsl/get-drop-index fid objects initial)) shape (get-in state [:workspace-drawing :object]) shape (-> shape diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs index 1f3000eb9..307f0973a 100644 --- a/frontend/src/app/main/data/workspace/drawing/curve.cljs +++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs @@ -47,12 +47,13 @@ ptk/UpdateEvent (update [_ state] - (let [objects (wsh/lookup-page-objects state) - content (get-in state [:workspace-drawing :object :content] []) - position (gpt/point (get-in content [0 :params] nil)) - frame-id (ctst/top-nested-frame objects position) - layout? (ctl/layout? objects frame-id) - drop-index (when layout? (gsl/get-drop-index frame-id objects position))] + (let [objects (wsh/lookup-page-objects state) + content (get-in state [:workspace-drawing :object :content] []) + start (get-in content [0 :params] nil) + position (when start (gpt/point start)) + frame-id (ctst/top-nested-frame objects position) + flex-layout? (ctl/flex-layout? objects frame-id) + drop-index (when flex-layout? (gsl/get-drop-index frame-id objects position))] (-> state (assoc-in [:workspace-drawing :object :frame-id] frame-id) (cond-> (some? drop-index) diff --git a/frontend/src/app/main/data/workspace/grid_layout/editor.cljs b/frontend/src/app/main/data/workspace/grid_layout/editor.cljs new file mode 100644 index 000000000..6f9bf9a8e --- /dev/null +++ b/frontend/src/app/main/data/workspace/grid_layout/editor.cljs @@ -0,0 +1,44 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.data.workspace.grid-layout.editor + (:require + [potok.core :as ptk])) + +(defn hover-grid-cell + [grid-id row column add-to-set] + (ptk/reify ::hover-grid-cell + ptk/UpdateEvent + (update [_ state] + (update-in + state + [:workspace-grid-edition grid-id :hover] + (fn [hover-set] + (let [hover-set (or hover-set #{})] + (if add-to-set + (conj hover-set [row column]) + (disj hover-set [row column])))))))) + +(defn select-grid-cell + [grid-id row column] + (ptk/reify ::select-grid-cell + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-grid-edition grid-id :selected] [row column])))) + +(defn remove-selection + [grid-id] + (ptk/reify ::remove-selection + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-grid-edition grid-id] dissoc :selected)))) + +(defn stop-grid-layout-editing + [grid-id] + (ptk/reify ::stop-grid-layout-editing + ptk/UpdateEvent + (update [_ state] + (update state :workspace-grid-edition dissoc grid-id)))) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index cbe0558ba..d04beadc9 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -7,11 +7,13 @@ (ns app.main.data.workspace.groups (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.types.component :as ctk] [app.common.types.shape :as cts] + [app.common.types.shape.layout :as ctl] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] @@ -98,6 +100,7 @@ changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) (pcb/add-object group {:index group-idx}) + (pcb/update-shapes (map :id shapes) ctl/remove-layout-item-data) (pcb/change-parent (:id group) (reverse shapes)) (pcb/update-shapes (map :id shapes-to-detach) ctk/detach-shape) (pcb/remove-objects ids-to-delete))] @@ -142,6 +145,8 @@ (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) + (cond-> (ctl/any-layout? frame) + (pcb/update-shapes (:shapes frame) ctl/remove-layout-item-data)) (pcb/change-parent parent-id children idx-in-parent) (pcb/remove-objects [(:id frame)])))) @@ -191,6 +196,11 @@ (keep :id)) selected) + child-ids + (into (d/ordered-set) + (mapcat #(dm/get-in objects [% :shapes])) + selected) + changes {:redo-changes (vec (mapcat :redo-changes changes-list)) :undo-changes (vec (mapcat :undo-changes changes-list)) :origin it} @@ -199,7 +209,8 @@ (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (ptk/data-event :layout/update parents) - (dwu/commit-undo-transaction undo-id)))))) + (dwu/commit-undo-transaction undo-id) + (dws/select-shapes child-ids)))))) (def mask-group (ptk/reify ::mask-group diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index 9a141f22d..b9f080a8a 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -112,7 +112,7 @@ (watch [_ state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - frame (cph/get-frame objects shape) + frame (cph/get-root-frame objects (:id shape)) flows (get-in state [:workspace-data :pages-index page-id diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 78aaad5c9..6f23a0487 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -176,10 +176,11 @@ (ptk/reify ::rename-color ptk/WatchEvent (watch [it state _] - (let [data (get state :workspace-data) - object (get-in data [:colors id]) - new-object (assoc object :name new-name)] - (do-update-color it state new-object file-id))))) + (when (and (some? new-name) (not= "" new-name)) + (let [data (get state :workspace-data) + object (get-in data [:colors id]) + new-object (assoc object :name new-name)] + (do-update-color it state new-object file-id)))))) (defn delete-color [{:keys [id] :as params}] @@ -211,14 +212,15 @@ (ptk/reify ::rename-media ptk/WatchEvent (watch [it state _] - (let [data (get state :workspace-data) - [path name] (cph/parse-path-name new-name) - object (get-in data [:media id]) - new-object (assoc object :path path :name name) - changes (-> (pcb/empty-changes it) - (pcb/with-library-data data) - (pcb/update-media new-object))] - (rx/of (dch/commit-changes changes)))))) + (when (and (some? new-name) (not= "" new-name)) + (let [data (get state :workspace-data) + [path name] (cph/parse-path-name new-name) + object (get-in data [:media id]) + new-object (assoc object :path path :name name) + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/update-media new-object))] + (rx/of (dch/commit-changes changes))))))) (defn delete-media @@ -281,11 +283,12 @@ (ptk/reify ::rename-typography ptk/WatchEvent (watch [it state _] - (let [data (get state :workspace-data) - [path name] (cph/parse-path-name new-name) - object (get-in data [:typographies id]) - new-object (assoc object :path path :name name)] - (do-update-tipography it state new-object file-id))))) + (when (and (some? new-name) (not= "" new-name)) + (let [data (get state :workspace-data) + [path name] (cph/parse-path-name new-name) + object (get-in data [:typographies id]) + new-object (assoc object :path path :name name)] + (do-update-tipography it state new-object file-id)))))) (defn delete-typography [id] @@ -342,27 +345,28 @@ (ptk/reify ::rename-component ptk/WatchEvent (watch [it state _] - (let [data (get state :workspace-data) - [path name] (cph/parse-path-name new-name) + (when (and (some? new-name) (not= "" new-name)) + (let [data (get state :workspace-data) + [path name] (cph/parse-path-name new-name) - update-fn - (fn [component] - ;; NOTE: we need to ensure the component exists, - ;; because there are small possibilities of race - ;; conditions with component deletion. - (when component - (-> component - (assoc :path path) - (assoc :name name) - (update :objects - ;; Give the same name to the root shape - #(assoc-in % [id :name] name))))) + update-fn + (fn [component] + ;; NOTE: we need to ensure the component exists, + ;; because there are small possibilities of race + ;; conditions with component deletion. + (when component + (-> component + (assoc :path path) + (assoc :name name) + (update :objects + ;; Give the same name to the root shape + #(assoc-in % [id :name] name))))) - changes (-> (pcb/empty-changes it) - (pcb/with-library-data data) - (pcb/update-component id update-fn))] + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/update-component id update-fn))] - (rx/of (dch/commit-changes changes)))))) + (rx/of (dch/commit-changes changes))))))) (defn duplicate-component "Create a new component copied from the one with the given id." diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 766b29406..73f175cfd 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -126,7 +126,7 @@ (rx/map dmm/validate-file) (rx/filter (comp not svg-blob?)) (rx/map prepare-blob) - (rx/mapcat #(rp/mutation! :upload-file-media-object %)) + (rx/mapcat #(rp/cmd! :upload-file-media-object %)) (rx/do on-image)) (->> (rx/from blobs) @@ -362,7 +362,7 @@ :type :info :timeout nil :tag :media-loading})) - (->> (rp/mutation! :clone-file-media-object params) + (->> (rp/cmd! :clone-file-media-object params) (rx/do on-success) (rx/catch on-error) (rx/finalize #(st/emit! (dm/hide-tag :media-loading))))))))) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 1eaf6a688..8c3a6dfca 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -179,8 +179,8 @@ (let [origin-frame-ids (->> selected (group-by #(get-in objects [% :frame-id]))) child-set (set (get-in objects [target-frame-id :shapes])) - target-frame (get objects target-frame-id) - target-layout? (ctl/layout? target-frame) + target-frame (get objects target-frame-id) + target-flex-layout? (ctl/flex-layout? target-frame) children-ids (concat (:shapes target-frame) selected) @@ -201,7 +201,7 @@ (fn [modif-tree [original-frame shapes]] (let [shapes (->> shapes (d/removev #(= target-frame-id %))) shapes (cond->> shapes - (and target-layout? (= original-frame target-frame-id)) + (and target-flex-layout? (= original-frame target-frame-id)) ;; When movining inside a layout frame remove the shapes that are not immediate children (filterv #(contains? child-set %))) children-ids (->> (dm/get-in objects [original-frame :shapes]) @@ -219,7 +219,7 @@ (cond-> v-sizing? (update-in [original-frame :modifiers] ctm/change-property :layout-item-v-sizing :fix))) - (and target-layout? (= original-frame target-frame-id)) + (and target-flex-layout? (= original-frame target-frame-id)) (update-in [target-frame-id :modifiers] ctm/add-children shapes drop-index))))] (as-> modif-tree $ @@ -374,7 +374,7 @@ ([] (apply-modifiers nil)) - ([{:keys [undo-transation? modifiers] :or {undo-transation? true}}] + ([{:keys [modifiers undo-transation? stack-undo?] :or {undo-transation? true stack-undo? false}}] (ptk/reify ::apply-modifiers ptk/WatchEvent (watch [_ state _] @@ -412,6 +412,7 @@ (cond-> text-shape? (update-grow-type shape))))) {:reg-objects? true + :stack-undo? stack-undo? :ignore-tree ignore-tree ;; Attributes that can change in the transform. This way we don't have to check ;; all the attributes @@ -419,6 +420,15 @@ :points :x :y + :rx + :ry + :r1 + :r2 + :r3 + :r4 + :shadow + :blur + :strokes :width :height :content @@ -428,9 +438,20 @@ :flip-x :flip-y :grow-type - :layout-item-h-sizing - :layout-item-v-sizing :position-data + :layout-gap + :layout-padding + :layout-item-h-sizing + :layout-item-margin + :layout-item-max-h + :layout-item-max-w + :layout-item-min-h + :layout-item-min-w + :layout-item-v-sizing + :layout-padding-type + :layout-gap + :layout-item-margin + :layout-item-margin-type ]}) ;; We've applied the text-modifier so we can dissoc the temporary data (fn [state] diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 7c1900dd8..3a70a1b88 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -14,6 +14,8 @@ [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.persistence :as dwp] [app.main.streams :as ms] + [app.util.globals :refer [global]] + [app.util.object :as obj] [app.util.time :as dt] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -37,7 +39,8 @@ profile-id (:profile-id state) initmsg [{:type :subscribe-file - :file-id file-id} + :file-id file-id + :version (obj/get global "penpotVersion")} {:type :subscribe-team :team-id team-id}] @@ -130,7 +133,7 @@ }) (defn handle-presence - [{:keys [type session-id profile-id] :as message}] + [{:keys [type session-id profile-id version] :as message}] (letfn [(get-next-color [presence] (let [xfm (comp (map second) (map :color) @@ -149,6 +152,7 @@ (assoc :id session-id) (assoc :profile-id profile-id) (assoc :updated-at (dt/now)) + (assoc :version version) (update :color update-color presence) (assoc :text-color (if (contains? ["#00fa9a" "#ffd700" "#dda0dd" "#ffafda"] (update-color (:color presence) presence)) @@ -197,29 +201,38 @@ (-deref [_] {:changes changes}) ptk/WatchEvent - (watch [_ _ _] - (let [position-data-operation? + (watch [_ state _] + (let [page-id (:current-page-id state) + position-data-operation? (fn [{:keys [type attr]}] (and (= :set type) (= attr :position-data))) - add-origin-session-id - (fn [{:keys [] :as op}] - (cond-> op - (position-data-operation? op) - (update :val with-meta {:session-id (:session-id msg)}))) + ;;add-origin-session-id + ;;(fn [{:keys [] :as op}] + ;; (cond-> op + ;; (position-data-operation? op) + ;; (update :val with-meta {:session-id (:session-id msg)}))) update-position-data (fn [change] + ;; Remove the position data from remote operations. Will be changed localy, otherwise + ;; creates a strange "out-of-sync" behaviour. (cond-> change - (= :mod-obj (:type change)) - (update :operations #(mapv add-origin-session-id %)))) + (and (= page-id (:page-id change)) + (= :mod-obj (:type change))) + (update :operations #(d/removev position-data-operation? %)))) process-page-changes (fn [[page-id changes]] (dch/update-indices page-id changes)) ;; We update `position-data` from the incoming message - changes (->> changes (mapv update-position-data)) + changes (->> changes + (mapv update-position-data) + (d/removev (fn [change] + (and (= page-id (:page-id change)) + (:ignore-remote? change))))) + changes-by-pages (group-by :page-id changes)] (rx/merge diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index 307c6e290..873d9d95d 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -240,12 +240,12 @@ (ptk/reify ::setup-frame-path ptk/UpdateEvent (update [_ state] - (let [objects (wsh/lookup-page-objects state) - content (get-in state [:workspace-drawing :object :content] []) - position (gpt/point (get-in content [0 :params] nil)) - frame-id (ctst/top-nested-frame objects position) - layout? (ctl/layout? objects frame-id) - drop-index (when layout? (gsl/get-drop-index frame-id objects position))] + (let [objects (wsh/lookup-page-objects state) + content (get-in state [:workspace-drawing :object :content] []) + position (gpt/point (get-in content [0 :params] nil)) + frame-id (ctst/top-nested-frame objects position) + flex-layout? (ctl/flex-layout? objects frame-id) + drop-index (when flex-layout? (gsl/get-drop-index frame-id objects position))] (-> state (assoc-in [:workspace-drawing :object :frame-id] frame-id) (cond-> (some? drop-index) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 318f1dbf3..7ba97c4df 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -153,6 +153,8 @@ selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points] #{}) + start-position (apply min #(gpt/distance start-position %) selected-points) + content (st/get-path state :content) points (upg/content->points content)] @@ -241,7 +243,7 @@ (let [id (dm/get-in state [:workspace-local :edition]) cx (d/prefix-keyword prefix :x) cy (d/prefix-keyword prefix :y) - start-point @ms/mouse-position + modifiers (dm/get-in state [:workspace-local :edit-path id :content-modifiers]) start-delta-x (dm/get-in modifiers [index cx] 0) start-delta-y (dm/get-in modifiers [index cy] 0) @@ -258,7 +260,7 @@ (streams/drag-stream (rx/concat (rx/of (dch/update-shapes [id] upsp/convert-to-path)) - (->> (streams/move-handler-stream start-point point handler opposite points) + (->> (streams/move-handler-stream handler point handler opposite points) (rx/take-until (->> stream (rx/filter #(or (ms/mouse-up? %) (streams/finish-edition? %))))) (rx/map @@ -269,8 +271,8 @@ id index prefix - (+ start-delta-x (- (:x pos) (:x start-point))) - (+ start-delta-y (- (:y pos) (:y start-point))) + (+ start-delta-x (- (:x pos) (:x handler))) + (+ start-delta-y (- (:y pos) (:y handler))) (not alt?)))))) (rx/concat (rx/of (apply-content-modifiers))))))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index e2f79a913..61942fd5d 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -214,7 +214,7 @@ :features features}] (when (:id params) - (->> (rp/mutation :update-file params) + (->> (rp/cmd! :update-file params) (rx/ignore))))))) (defn update-persistence-status diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 02e78eae7..c3de20ec9 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -131,6 +131,55 @@ objects (wsh/lookup-page-objects state page-id)] (rx/of (dwc/expand-all-parents [id] objects))))))) +(defn select-prev-shape + ([] + (ptk/reify ::select-prev-shape + ptk/WatchEvent + (watch [_ state _] + (let [selected (wsh/lookup-selected state) + count-selected (count selected) + first-selected (first selected) + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + current (get objects first-selected) + parent (get objects (:parent-id current)) + sibling-ids (:shapes parent) + current-index (d/index-of sibling-ids first-selected) + sibling (if (= (dec (count sibling-ids)) current-index) + (first sibling-ids) + (nth sibling-ids (inc current-index)))] + + (cond + (= 1 count-selected) + (rx/of (select-shape sibling)) + + (> count-selected 1) + (rx/of (select-shape first-selected)))))))) + +(defn select-next-shape + ([] + (ptk/reify ::select-next-shape + ptk/WatchEvent + (watch [_ state _] + (let [selected (wsh/lookup-selected state) + count-selected (count selected) + first-selected (first selected) + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + current (get objects first-selected) + parent (get objects (:parent-id current)) + sibling-ids (:shapes parent) + current-index (d/index-of sibling-ids first-selected) + sibling (if (= 0 current-index) + (last sibling-ids) + (nth sibling-ids (dec current-index)))] + (cond + (= 1 count-selected) + (rx/of (select-shape sibling)) + + (> count-selected 1) + (rx/of (select-shape first-selected)))))))) + (defn deselect-shape [id] (us/verify ::us/uuid id) @@ -140,22 +189,14 @@ (update-in state [:workspace-local :selected] disj id)))) (defn shift-select-shapes + ([id] + (shift-select-shapes id nil)) + ([id objects] (ptk/reify ::shift-select-shapes ptk/UpdateEvent (update [_ state] - (let [selection (-> state - wsh/lookup-selected - (conj id))] - (-> state - (assoc-in [:workspace-local :selected] - (cph/expand-region-selection objects selection))))))) - ([id] - (ptk/reify ::shift-select-shapes - ptk/UpdateEvent - (update [_ state] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (let [objects (or objects (wsh/lookup-page-objects state)) selection (-> state wsh/lookup-selected (conj id))] @@ -255,7 +296,9 @@ :ignore-groups? ignore-groups? :full-frame? true}) (rx/map #(cph/clean-loops objects %)) - (rx/map #(into initial-set (filter (comp not blocked?)) %)) + (rx/map #(into initial-set (comp + (filter (complement blocked?)) + (remove (partial cph/hidden-parent? objects))) %)) (rx/map select-shapes))))))) (defn select-inside-group diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index c708bb93a..fe798b11f 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -1,4 +1,4 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public +; 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/. ;; @@ -53,28 +53,138 @@ :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}}) (def initial-grid-layout ;; TODO - {:layout :grid}) + {:layout :grid + :layout-grid-dir :row + :layout-gap-type :multiple + :layout-gap {:row-gap 0 :column-gap 0} + :layout-align-items :start + :layout-align-content :stretch + :layout-justify-items :start + :layout-justify-content :start + :layout-padding-type :simple + :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0} + :layout-grid-rows [] + :layout-grid-columns []}) (defn get-layout-initializer [type from-frame?] - (let [initial-layout-data (if (= type :flex) initial-flex-layout initial-grid-layout)] + (let [initial-layout-data + (case type + :flex initial-flex-layout + :grid initial-grid-layout)] (fn [shape] (-> shape (merge initial-layout-data) + (cond-> (= type :grid) ctl/assign-cells) ;; If the original shape is not a frame we set clip content and show-viewer to false (cond-> (not from-frame?) (assoc :show-content true :hide-in-viewer true)))))) + +(defn shapes->flex-params + "Given the shapes calculate its flex parameters (horizontal vs vertical, gaps, etc)" + ([objects shapes] + (shapes->flex-params objects shapes nil)) + ([objects shapes parent] + (let [points + (->> shapes + (map :id) + (ctt/sort-z-index objects) + (map (comp gsh/center-shape (d/getf objects)))) + + start (first points) + end (reduce (fn [acc p] (gpt/add acc (gpt/to-vec start p))) points) + + angle (gpt/signed-angle-with-other + (gpt/to-vec start end) + (gpt/point 1 0)) + + angle (mod angle 360) + + t1 (min (abs (- angle 0)) (abs (- angle 360))) + t2 (abs (- angle 90)) + t3 (abs (- angle 180)) + t4 (abs (- angle 270)) + + tmin (min t1 t2 t3 t4) + + direction + (cond + (mth/close? tmin t1) :row + (mth/close? tmin t2) :column-reverse + (mth/close? tmin t3) :row-reverse + (mth/close? tmin t4) :column) + + selrects (->> shapes + (mapv :selrect)) + min-x (->> selrects + (mapv #(min (:x1 %) (:x2 %))) + (apply min)) + max-x (->> selrects + (mapv #(max (:x1 %) (:x2 %))) + (apply max)) + all-width (->> selrects + (map :width) + (reduce +)) + column-gap (if (or (= direction :row) (= direction :row-reverse)) + (/ (- (- max-x min-x) all-width) (dec (count shapes))) + 0) + + min-y (->> selrects + (mapv #(min (:y1 %) (:y2 %))) + (apply min)) + max-y (->> selrects + (mapv #(max (:y1 %) (:y2 %))) + (apply max)) + all-height (->> selrects + (map :height) + (reduce +)) + row-gap (if (or (= direction :column) (= direction :column-reverse)) + (/ (- (- max-y min-y) all-height) (dec (count shapes))) + 0) + + layout-gap {:row-gap (max row-gap 0) :column-gap (max column-gap 0)} + + parent-selrect (:selrect parent) + padding (when (and (not (nil? parent)) (> (count shapes) 0)) + {:p1 (min (- min-y (:y1 parent-selrect)) (- (:y2 parent-selrect) max-y)) + :p2 (min (- min-x (:x1 parent-selrect)) (- (:x2 parent-selrect) max-x))})] + + (cond-> {:layout-flex-dir direction :layout-gap layout-gap} + (not (nil? padding)) + (assoc :layout-padding {:p1 (:p1 padding) :p2 (:p2 padding) :p3 (:p1 padding) :p4 (:p2 padding)}))))) + +(defn shapes->grid-params + "Given the shapes calculate its flex parameters (horizontal vs vertical, gaps, etc)" + ([objects shapes] + (shapes->flex-params objects shapes nil)) + ([_objects _shapes _parent] + {})) + (defn create-layout-from-id [ids type from-frame?] (ptk/reify ::create-layout-from-id ptk/WatchEvent (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - children-ids (into [] (mapcat #(get-in objects [% :shapes])) ids) - undo-id (js/Symbol)] + (let [objects (wsh/lookup-page-objects state) + children-ids (into [] (mapcat #(get-in objects [% :shapes])) ids) + children-shapes (map (d/getf objects) children-ids) + parent (get objects (first ids)) + layout-params (when (d/not-empty? children-shapes) + (case type + :flex (shapes->flex-params objects children-shapes parent) + :grid (shapes->grid-params objects children-shapes parent))) + undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) (dwc/update-shapes ids (get-layout-initializer type from-frame?)) + (dwc/update-shapes + ids + (fn [shape] + (-> shape + (cond-> (not from-frame?) + (assoc :layout-item-h-sizing :auto + :layout-item-v-sizing :auto)) + (merge layout-params)))) (ptk/data-event :layout/update ids) (dwc/update-shapes children-ids #(dissoc % :constraints-h :constraints-v)) (dwu/commit-undo-transaction undo-id)))))) @@ -91,7 +201,8 @@ ids (->> ids (filter #(contains? objects %)))] (if (d/not-empty? ids) (let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))] - (rx/of (dwm/apply-modifiers {:modifiers modif-tree}))) + (rx/of (dwm/apply-modifiers {:modifiers modif-tree + :stack-undo? true}))) (rx/empty)))))) (defn initialize @@ -110,40 +221,6 @@ [] (ptk/reify ::finalize)) -(defn shapes->flex-params - "Given the shapes calculate its flex parameters (horizontal vs vertical etc)" - [objects shapes] - - (let [points - (->> shapes - (map :id) - (ctt/sort-z-index objects) - (map (comp gsh/center-shape (d/getf objects)))) - - start (first points) - end (reduce (fn [acc p] (gpt/add acc (gpt/to-vec start p))) points) - - angle (gpt/signed-angle-with-other - (gpt/to-vec start end) - (gpt/point 1 0)) - - angle (mod angle 360) - - t1 (min (abs (- angle 0)) (abs (- angle 360))) - t2 (abs (- angle 90)) - t3 (abs (- angle 180)) - t4 (abs (- angle 270)) - - tmin (min t1 t2 t3 t4) - - direction - (cond - (mth/close? tmin t1) :row - (mth/close? tmin t2) :column-reverse - (mth/close? tmin t3) :row-reverse - (mth/close? tmin t4) :column)] - - {:layout-flex-dir direction})) (defn create-layout-from-selection [type] @@ -235,7 +312,7 @@ (dwu/commit-undo-transaction undo-id)))))) (defn create-layout - [] + [type] (ptk/reify ::create-layout ptk/WatchEvent (watch [_ state _] @@ -247,15 +324,12 @@ is-frame? (= :frame (:type (first selected-shapes))) undo-id (js/Symbol)] - (if (and single? is-frame?) - (rx/of - (dwu/start-undo-transaction undo-id) - (create-layout-from-id [(first selected)] :flex true) - (dwu/commit-undo-transaction undo-id)) - (rx/of - (dwu/start-undo-transaction undo-id) - (create-layout-from-selection :flex) - (dwu/commit-undo-transaction undo-id))))))) + (rx/of + (dwu/start-undo-transaction undo-id) + (if (and single? is-frame?) + (create-layout-from-id [(first selected)] type true) + (create-layout-from-selection type)) + (dwu/commit-undo-transaction undo-id)))))) (defn toggle-layout-flex [] @@ -267,12 +341,12 @@ selected (wsh/lookup-selected state) selected-shapes (map (d/getf objects) selected) single? (= (count selected-shapes) 1) - has-flex-layout? (and single? (ctl/layout? objects (:id (first selected-shapes))))] + has-flex-layout? (and single? (ctl/flex-layout? objects (:id (first selected-shapes))))] (when (not= 0 (count selected)) (if has-flex-layout? (rx/of (remove-layout selected)) - (rx/of (create-layout)))))))) + (rx/of (create-layout :flex)))))))) (defn update-layout [ids changes] @@ -285,6 +359,101 @@ (ptk/data-event :layout/update ids) (dwu/commit-undo-transaction undo-id)))))) +#_(defn update-grid-cells + [parent objects] + (let [children (cph/get-immediate-children objects (:id parent)) + layout-grid-rows (:layout-grid-rows parent) + layout-grid-columns (:layout-grid-columns parent) + num-rows (count layout-grid-columns) + num-columns (count layout-grid-columns) + layout-grid-cells (:layout-grid-cells parent) + + allocated-shapes + (into #{} (mapcat :shapes) (:layout-grid-cells parent)) + + no-cell-shapes + (->> children (:shapes parent) (remove allocated-shapes)) + + layout-grid-cells + (for [[row-idx row] (d/enumerate layout-grid-rows) + [col-idx col] (d/enumerate layout-grid-columns)] + + (let [shape (nth children (+ (* row-idx num-columns) col-idx) nil) + cell-data {:id (uuid/next) + :row (inc row-idx) + :column (inc col-idx) + :row-span 1 + :col-span 1 + :shapes (when shape [(:id shape)])}] + [(:id cell-data) cell-data]))] + (assoc parent :layout-grid-cells (into {} layout-grid-cells)))) + +#_(defn check-grid-cells-update + [ids] + (ptk/reify ::check-grid-cells-update + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + undo-id (js/Symbol)] + (rx/of (dwc/update-shapes + ids + (fn [shape] + (-> shape + (update-grid-cells objects))))))))) + +(defn add-layout-track + [ids type value] + (assert (#{:row :column} type)) + (ptk/reify ::add-layout-column + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/of (dwu/start-undo-transaction undo-id) + (dwc/update-shapes + ids + (fn [shape] + (case type + :row (ctl/add-grid-row shape value) + :column (ctl/add-grid-column shape value)))) + (ptk/data-event :layout/update ids) + (dwu/commit-undo-transaction undo-id)))))) + +(defn remove-layout-track + [ids type index] + (assert (#{:row :column} type)) + + (ptk/reify ::remove-layout-column + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol)] + (rx/of (dwu/start-undo-transaction undo-id) + (dwc/update-shapes + ids + (fn [shape] + (case type + :row (ctl/remove-grid-row shape index) + :column (ctl/remove-grid-column shape index)))) + (ptk/data-event :layout/update ids) + (dwu/commit-undo-transaction undo-id)))))) + +(defn change-layout-track + [ids type index props] + (assert (#{:row :column} type)) + (ptk/reify ::change-layout-column + ptk/WatchEvent + (watch [_ _ _] + (let [undo-id (js/Symbol) + property (case :row :layout-grid-rows + :column :layout-grid-columns)] + (rx/of (dwu/start-undo-transaction undo-id) + (dwc/update-shapes + ids + (fn [shape] + (-> shape + (update-in [property index] merge props)))) + (ptk/data-event :layout/update ids) + (dwu/commit-undo-transaction undo-id)))))) + (defn fix-child-sizing [objects parent-changes shape] @@ -296,7 +465,10 @@ col? (ctl/col? parent) row? (ctl/row? parent) - all-children (->> parent :shapes (map (d/getf objects)))] + all-children (->> parent + :shapes + (map (d/getf objects)) + (remove ctl/layout-absolute?))] (cond-> shape ;; If the parent is hug width and the direction column diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 356724254..ea0f38a09 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -95,6 +95,7 @@ selected) index (:index (meta attrs)) + changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) (cond-> (some? index) @@ -102,7 +103,10 @@ (cond-> (nil? index) (pcb/add-object shape)) (cond-> (some? (:parent-id attrs)) - (pcb/change-parent (:parent-id attrs) [shape]))) + (pcb/change-parent (:parent-id attrs) [shape])) + (cond-> (ctl/grid-layout? objects (:parent-id shape)) + (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)) + ) undo-id (js/Symbol)] (rx/concat @@ -131,7 +135,12 @@ (when (d/not-empty? to-move-shapes) (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) - (pcb/change-parent frame-id to-move-shapes 0)))] + (cond-> (not (ctl/any-layout? objects frame-id)) + (pcb/update-shapes ordered-indexes ctl/remove-layout-item-data)) + (pcb/update-shapes ordered-indexes #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true))) + (pcb/change-parent frame-id to-move-shapes 0) + (cond-> (ctl/grid-layout? objects frame-id) + (pcb/update-shapes [frame-id] ctl/assign-cells))))] (if (some? changes) (rx/of (dch/commit-changes changes)) @@ -192,10 +201,6 @@ [file page objects ids it components-v2] (let [lookup (d/getf objects) - layout-ids (->> ids - (mapcat (partial cph/get-parent-ids objects)) - (filter (partial ctl/layout? objects))) - groups-to-unmask (reduce (fn [group-ids id] ;; When the shape to delete is the mask of a masked group, @@ -317,7 +322,6 @@ (dc/detach-comment-thread ids) (ptk/data-event :layout/update all-parents) (dch/commit-changes changes) - (ptk/data-event :layout/update layout-ids) (dwu/commit-undo-transaction undo-id)))) (defn create-and-add-shape diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index cef4b74a1..5ec6448e1 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -67,8 +67,8 @@ :cut {:tooltip (ds/meta "X") :command (ds/c-mod "x") :subsections [:edit] - :fn #(emit-when-no-readonly (dw/copy-selected) - (dw/delete-selected))} + :fn #(emit-when-no-readonly (dw/copy-selected) + (dw/delete-selected))} :paste {:tooltip (ds/meta "V") :disabled true @@ -110,7 +110,7 @@ ;; MODIFY LAYERS - + :group {:tooltip (ds/meta "G") :command (ds/c-mod "g") @@ -222,7 +222,7 @@ :fn #(emit-when-no-readonly (dwsl/toggle-layout-flex))} ;; TOOLS - + :draw-frame {:tooltip "B" :command ["b" "a"] :subsections [:tools :basics] @@ -247,7 +247,7 @@ :command "t" :subsections [:tools] :fn #(emit-when-no-readonly dwtxt/start-edit-if-selected - (dwd/select-for-drawing :text))} + (dwd/select-for-drawing :text))} :draw-path {:tooltip "P" :command "p" @@ -300,7 +300,7 @@ :fn #(emit-when-no-readonly (dw/toggle-focus-mode))} ;; ITEM ALIGNMENT - + :align-left {:tooltip (ds/alt "A") :command "alt+a" :subsections [:alignment] @@ -342,7 +342,7 @@ :fn #(emit-when-no-readonly (dw/distribute-objects :vertical))} ;; MAIN MENU - + :toggle-rules {:tooltip (ds/meta-shift "R") :command (ds/c-mod "shift+r") :subsections [:main-menu] @@ -354,12 +354,14 @@ :fn #(st/emit! (dw/select-all))} :toggle-grid {:tooltip (ds/meta "'") - :command (ds/c-mod "'") + ;;https://github.com/ccampbell/mousetrap/issues/85 + :command [(ds/c-mod "'") (ds/c-mod "219")] :subsections [:main-menu] :fn #(st/emit! (toggle-layout-flag :display-grid))} :toggle-snap-grid {:tooltip (ds/meta-shift "'") - :command (ds/c-mod "shift+'") + ;;https://github.com/ccampbell/mousetrap/issues/85 + :command [(ds/c-mod "shift+'") (ds/c-mod "shift+219")] :subsections [:main-menu] :fn #(st/emit! (toggle-layout-flag :snap-grid))} @@ -387,7 +389,7 @@ :command (ds/c-mod "shift+e") :subsections [:basics :main-menu] :fn #(st/emit! - (de/show-workspace-export-dialog))} + (de/show-workspace-export-dialog))} :toggle-snap-guide {:tooltip (ds/meta-shift "G") :command (ds/c-mod "shift+g") @@ -400,7 +402,7 @@ :fn #(st/emit! (toggle-layout-flag :shortcuts))} ;; PANELS - + :toggle-layers {:tooltip (ds/alt "L") :command (ds/a-mod "l") :subsections [:panels] @@ -420,15 +422,15 @@ :command (ds/a-mod "p") :subsections [:panels] :fn #(do (r/set-resize-type! :bottom) - (emit-when-no-readonly (dw/remove-layout-flag :textpalette) - (toggle-layout-flag :colorpalette)))} + (emit-when-no-readonly (dw/remove-layout-flag :textpalette) + (toggle-layout-flag :colorpalette)))} :toggle-textpalette {:tooltip (ds/alt "T") :command (ds/a-mod "t") :subsections [:panels] :fn #(do (r/set-resize-type! :bottom) - (emit-when-no-readonly (dw/remove-layout-flag :colorpalette) - (toggle-layout-flag :textpalette)))} + (emit-when-no-readonly (dw/remove-layout-flag :colorpalette) + (toggle-layout-flag :textpalette)))} :hide-ui {:tooltip "\\" :command "\\" @@ -436,16 +438,16 @@ :fn #(st/emit! (toggle-layout-flag :hide-ui))} ;; ZOOM-WORKSPACE - + :increase-zoom {:tooltip "+" :command ["+" "="] :subsections [:zoom-workspace] - :fn #(st/emit! (dw/increase-zoom nil))} + :fn #(st/emit! (dw/increase-zoom))} :decrease-zoom {:tooltip "-" :command ["-" "_"] :subsections [:zoom-workspace] - :fn #(st/emit! (dw/decrease-zoom nil))} + :fn #(st/emit! (dw/decrease-zoom))} :reset-zoom {:tooltip (ds/shift "0") :command "shift+0" @@ -473,7 +475,7 @@ :fn identity} ;; NAVIGATION - + :open-viewer {:tooltip "G V" :command "g v" @@ -495,7 +497,18 @@ :subsections [:navigation-workspace] :fn #(st/emit! (dw/go-to-dashboard))} + :select-prev {:tooltip (ds/shift "tab") + :command "shift+tab" + :subsections [:navigation-workspace] + :fn #(st/emit! (dw/select-prev-shape))} + + :select-next {:tooltip ds/tab + :command "tab" + :subsections [:navigation-workspace] + :fn #(st/emit! (dw/select-next-shape))} + ;; SHAPE + :bool-union {:tooltip (ds/meta (ds/alt "U")) :command (ds/c-mod "alt+u") diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index e807d1937..ae5354b15 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -62,7 +62,9 @@ height (get-in data [:attrs :height] 100) viewbox (get-in data [:attrs :viewBox] (str "0 0 " width " " height)) [x y width height] (->> (str/split viewbox #"\s+") - (map d/parse-double))] + (map d/parse-double)) + width (if (= width 0) 1 width) + height (if (= height 0) 1 height)] [(assert-valid-num :x x) (assert-valid-num :y y) (assert-valid-pos-num :width width) @@ -162,11 +164,13 @@ (cond-> shape (get-in shape [:svg-attrs :opacity]) (-> (update :svg-attrs dissoc :opacity) - (assoc :opacity (get-in shape [:svg-attrs :opacity]))) + (assoc :opacity (-> (get-in shape [:svg-attrs :opacity]) + (d/parse-double)))) (get-in shape [:svg-attrs :style :opacity]) (-> (update-in [:svg-attrs :style] dissoc :opacity) - (assoc :opacity (get-in shape [:svg-attrs :style :opacity]))) + (assoc :opacity (-> (get-in shape [:svg-attrs :style :opacity]) + (d/parse-double)))) (get-in shape [:svg-attrs :mix-blend-mode]) @@ -408,7 +412,8 @@ (assoc :strokes []) (assoc :svg-defs (select-keys (:defs svg-data) references)) (setup-fill) - (setup-stroke)) + (setup-stroke) + (setup-opacity)) shape (cond-> shape hidden (assoc :hidden true)) @@ -483,117 +488,118 @@ (defn create-svg-shapes [svg-data {:keys [x y]} objects frame-id parent-id selected center?] - (try - (let [[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data) - x (mth/round - (if center? - (- x vb-x (/ vb-width 2)) - x)) - y (mth/round - (if center? - (- y vb-y (/ vb-height 2)) - y)) + (let [[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data) + x (mth/round + (if center? + (- x vb-x (/ vb-width 2)) + x)) + y (mth/round + (if center? + (- y vb-y (/ vb-height 2)) + y)) - unames (cp/retrieve-used-names objects) + unames (cp/retrieve-used-names objects) - svg-name (str/replace (:name svg-data) ".svg" "") + svg-name (str/replace (:name svg-data) ".svg" "") - svg-data (-> svg-data - (assoc :x x - :y y - :offset-x vb-x - :offset-y vb-y - :width vb-width - :height vb-height - :name svg-name)) + svg-data (-> svg-data + (assoc :x x + :y y + :offset-x vb-x + :offset-y vb-y + :width vb-width + :height vb-height + :name svg-name)) - [def-nodes svg-data] (-> svg-data - (usvg/fix-default-values) - (usvg/fix-percents) - (usvg/extract-defs)) + [def-nodes svg-data] (-> svg-data + (usvg/fix-default-values) + (usvg/fix-percents) + (usvg/extract-defs)) - svg-data (assoc svg-data :defs def-nodes) + svg-data (assoc svg-data :defs def-nodes) - root-shape (create-svg-root frame-id parent-id svg-data) - root-id (:id root-shape) + root-shape (create-svg-root frame-id parent-id svg-data) + root-id (:id root-shape) - ;; In penpot groups have the size of their children. To respect the imported svg size and empty space let's create a transparent shape as background to respect the imported size - base-background-shape {:tag :rect - :attrs {:x (str vb-x) - :y (str vb-y) - :width (str vb-width) - :height (str vb-height) - :fill "none" - :id "base-background"} - :hidden true - :content []} + ;; In penpot groups have the size of their children. To respect the imported svg size and empty space let's create a transparent shape as background to respect the imported size + base-background-shape {:tag :rect + :attrs {:x (str vb-x) + :y (str vb-y) + :width (str vb-width) + :height (str vb-height) + :fill "none" + :id "base-background"} + :hidden true + :content []} - svg-data (-> svg-data - (assoc :defs def-nodes) - (assoc :content (into [base-background-shape] (:content svg-data)))) + svg-data (-> svg-data + (assoc :defs def-nodes) + (assoc :content (into [base-background-shape] (:content svg-data)))) - ;; Create the root shape - new-shape (dwsh/make-new-shape root-shape objects selected) + ;; Create the root shape + new-shape (dwsh/make-new-shape root-shape objects selected) - root-attrs (-> (:attrs svg-data) - (usvg/format-styles)) + root-attrs (-> (:attrs svg-data) + (usvg/format-styles)) - [_ new-children] - (reduce (partial create-svg-children objects selected frame-id root-id svg-data) - [unames []] - (d/enumerate (->> (:content svg-data) - (mapv #(usvg/inherit-attributes root-attrs %)))))] + [_ new-children] + (reduce (partial create-svg-children objects selected frame-id root-id svg-data) + [unames []] + (d/enumerate (->> (:content svg-data) + (mapv #(usvg/inherit-attributes root-attrs %)))))] - [new-shape new-children]) - - (catch :default e - (.error js/console "Error SVG" e) - (rx/throw {:type :svg-parser - :data e})))) + [new-shape new-children])) (defn add-svg-shapes [svg-data position] (ptk/reify ::add-svg-shapes ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - frame-id (ctst/top-nested-frame objects position) - selected (wsh/lookup-selected state) - page-objects (wsh/lookup-page-objects state) - base (cph/get-base-shape page-objects selected) - selected-frame? (and (= 1 (count selected)) - (= :frame (get-in objects [(first selected) :type]))) + (try + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + frame-id (ctst/top-nested-frame objects position) + selected (wsh/lookup-selected state) + page-objects (wsh/lookup-page-objects state) + base (cph/get-base-shape page-objects selected) + selected-frame? (and (= 1 (count selected)) + (= :frame (get-in objects [(first selected) :type]))) - parent-id (if - (or selected-frame? (empty? selected)) frame-id - (:parent-id base)) + parent-id + (if (or selected-frame? (empty? selected)) + frame-id + (:parent-id base)) - [new-shape new-children] - (create-svg-shapes svg-data position objects frame-id parent-id selected true) - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/add-object new-shape)) + [new-shape new-children] + (create-svg-shapes svg-data position objects frame-id parent-id selected true) + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/add-object new-shape)) - changes - (reduce (fn [changes [index new-child]] - (-> changes - (pcb/add-object new-child) - (pcb/change-parent (:parent-id new-child) [new-child] index))) - changes - (d/enumerate new-children)) + changes + (reduce (fn [changes [index new-child]] + (-> changes + (pcb/add-object new-child) + (pcb/change-parent (:parent-id new-child) [new-child] index))) + changes + (d/enumerate new-children)) - changes (pcb/resize-parents changes - (->> changes - :redo-changes - (filter #(= :add-obj (:type %))) - (map :id) - reverse - vec)) - undo-id (js/Symbol)] + changes (pcb/resize-parents changes + (->> changes + :redo-changes + (filter #(= :add-obj (:type %))) + (map :id) + reverse + vec)) + undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set (:id new-shape))) - (ptk/data-event :layout/update [(:id new-shape)]) - (dwu/commit-undo-transaction undo-id)))))) + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (dws/select-shapes (d/ordered-set (:id new-shape))) + (ptk/data-event :layout/update [(:id new-shape)]) + (dwu/commit-undo-transaction undo-id))) + + (catch :default e + (.error js/console "Error SVG" e) + (rx/throw {:type :svg-parser + :data e})))))) diff --git a/frontend/src/app/main/data/workspace/text/shortcuts.cljs b/frontend/src/app/main/data/workspace/text/shortcuts.cljs index 5ef7850ee..61f2c7908 100644 --- a/frontend/src/app/main/data/workspace/text/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/text/shortcuts.cljs @@ -202,22 +202,22 @@ (st/emit! (dwu/commit-undo-transaction undo-id))))) (def shortcuts - {:align-left {:tooltip (ds/meta (ds/alt "l")) - :command (ds/c-mod "alt+l") - :subsections [:text-editor] - :fn #(update-attrs-when-no-readonly {:text-align "left"})} - :align-right {:tooltip (ds/meta (ds/alt "r")) - :command (ds/c-mod "alt+r") - :subsections [:text-editor] - :fn #(update-attrs-when-no-readonly {:text-align "right"})} - :align-center {:tooltip (ds/meta (ds/alt "t")) - :command (ds/c-mod "alt+t") - :subsections [:text-editor] - :fn #(update-attrs-when-no-readonly {:text-align "center"})} - :align-justify {:tooltip (ds/meta (ds/alt "j")) - :command (ds/c-mod "alt+j") - :subsections [:text-editor] - :fn #(update-attrs-when-no-readonly {:text-align "justify"})} + {:text-align-left {:tooltip (ds/meta (ds/alt "l")) + :command (ds/c-mod "alt+l") + :subsections [:text-editor] + :fn #(update-attrs-when-no-readonly {:text-align "left"})} + :text-align-right {:tooltip (ds/meta (ds/alt "r")) + :command (ds/c-mod "alt+r") + :subsections [:text-editor] + :fn #(update-attrs-when-no-readonly {:text-align "right"})} + :text-align-center {:tooltip (ds/meta (ds/alt "t")) + :command (ds/c-mod "alt+t") + :subsections [:text-editor] + :fn #(update-attrs-when-no-readonly {:text-align "center"})} + :text-align-justify {:tooltip (ds/meta (ds/alt "j")) + :command (ds/c-mod "alt+j") + :subsections [:text-editor] + :fn #(update-attrs-when-no-readonly {:text-align "justify"})} :underline {:tooltip (ds/meta "u") :command (ds/c-mod "u") diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index dd3d79cb0..4475f95a0 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -377,7 +377,7 @@ (assoc-in [:workspace-local :edition] (-> selected first :id))))))) (defn not-changed? [old-dim new-dim] - (> (mth/abs (- old-dim new-dim)) 1)) + (> (mth/abs (- old-dim new-dim)) 0.1)) (defn commit-resize-text [] @@ -414,7 +414,7 @@ (let [ids (->> (keys props) (filter changed-text?))] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes ids update-fn {:reg-objects? true :save-undo? true}) + (dch/update-shapes ids update-fn {:reg-objects? true :stack-undo? true :ignore-remote? true}) (ptk/data-event :layout/update ids) (dwu/commit-undo-transaction undo-id)))))))) @@ -539,7 +539,7 @@ ptk/WatchEvent (watch [_ _ _] - (rx/of (dwm/apply-modifiers))))) + (rx/of (dwm/apply-modifiers {:stack-undo? true}))))) (defn commit-position-data [] @@ -558,7 +558,7 @@ (fn [shape] (-> shape (assoc :position-data (get position-data (:id shape))))) - {:save-undo? false :reg-objects? false})) + {:stack-undo? true :reg-objects? false :ignore-remote? true})) (rx/of (fn [state] (dissoc state ::update-position-data-debounce ::update-position-data)))))))) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 5a62a135f..be254bb69 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -12,7 +12,8 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.flex-layout :as gsl] + [app.common.geom.shapes.flex-layout :as gslf] + [app.common.geom.shapes.grid-layout :as gslg] [app.common.math :as mth] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] @@ -103,7 +104,8 @@ (defn start-resize "Enter mouse resize mode, until mouse button is released." [handler ids shape] - (letfn [(resize [shape initial layout [point lock? center? point-snap]] + (letfn [(resize + [shape initial layout [point lock? center? point-snap]] (let [{:keys [width height]} (:selrect shape) {:keys [rotation]} shape @@ -182,16 +184,16 @@ (ctm/resize scalev resize-origin shape-transform shape-transform-inverse) (cond-> set-fix-width? - (ctm/change-parent-property :layout-item-h-sizing :fix)) + (ctm/change-property :layout-item-h-sizing :fix)) (cond-> set-fix-height? - (ctm/change-parent-property :layout-item-v-sizing :fix)) + (ctm/change-property :layout-item-v-sizing :fix)) (cond-> scale-text (ctm/scale-content (:x scalev)))) modif-tree (dwm/create-modif-tree ids modifiers)] - (rx/of (dwm/set-modifiers modif-tree)))) + (rx/of (dwm/set-modifiers modif-tree scale-text)))) ;; Unifies the instantaneous proportion lock modifier ;; activated by Shift key and the shapes own proportion @@ -208,7 +210,7 @@ ptk/WatchEvent (watch [_ state stream] (let [initial-position @ms/mouse-position - stoper (rx/filter ms/mouse-up? stream) + stopper (rx/filter ms/mouse-up? stream) layout (:workspace-layout state) page-id (:current-page-id state) focus (:workspace-focus-selected state) @@ -225,7 +227,7 @@ (->> (snap/closest-snap-point page-id resizing-shapes objects layout zoom focus point) (rx/map #(conj current %))))) (rx/mapcat (partial resize shape initial-position layout)) - (rx/take-until stoper)) + (rx/take-until stopper)) (rx/of (dwm/apply-modifiers) (finish-transform)))))))) @@ -440,7 +442,7 @@ exclude-frames-siblings (into exclude-frames (comp (mapcat (partial cph/get-siblings-ids objects)) - (filter (partial ctl/layout-immediate-child-id? objects))) + (filter (partial ctl/any-layout-immediate-child-id? objects))) selected) position (->> ms/mouse-position @@ -469,11 +471,14 @@ (rx/map (fn [[move-vector mod?]] - (let [position (gpt/add from-position move-vector) + (let [position (gpt/add from-position move-vector) exclude-frames (if mod? exclude-frames exclude-frames-siblings) - target-frame (ctst/top-nested-frame objects position exclude-frames) - layout? (ctl/layout? objects target-frame) - drop-index (when layout? (gsl/get-drop-index target-frame objects position))] + target-frame (ctst/top-nested-frame objects position exclude-frames) + flex-layout? (ctl/flex-layout? objects target-frame) + grid-layout? (ctl/grid-layout? objects target-frame) + drop-index (cond + flex-layout? (gslf/get-drop-index target-frame objects position) + grid-layout? (gslg/get-drop-index target-frame objects position))] [move-vector target-frame drop-index]))) (rx/take-until stopper))] @@ -529,7 +534,8 @@ get-new-position (fn [parent-id position] (let [parent (get objects parent-id)] - (when (ctl/layout? parent) + (cond + (ctl/flex-layout? parent) (if (or (and (ctl/reverse? parent) (or (= direction :left) @@ -538,7 +544,12 @@ (or (= direction :right) (= direction :down)))) (dec position) - (+ position 2))))) + (+ position 2)) + + ;; TODO: GRID + (ctl/grid-layout? parent) + nil + ))) add-children-position (fn [[parent-id children]] @@ -643,7 +654,9 @@ (let [objects (wsh/lookup-page-objects state) selected (wsh/lookup-selected state {:omit-blocked? true}) selected-shapes (->> selected (map (d/getf objects)))] - (if (every? (partial ctl/layout-immediate-child? objects) selected-shapes) + (if (every? #(and (ctl/any-layout-immediate-child? objects %) + (not (ctl/layout-absolute? %))) + selected-shapes) (rx/of (reorder-selected-layout-child direction)) (rx/of (nudge-selected-shapes direction shift?))))))) @@ -726,16 +739,28 @@ #{} (into (d/ordered-set) (find-all-empty-parents #{}))) + ;; Not move absolute shapes that won't change parent + moving-shapes + (->> moving-shapes + (remove (fn [shape] + (and (ctl/layout-absolute? shape) + (= frame-id (:parent-id shape)))))) + moving-shapes-ids + (map :id moving-shapes) + changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) + ;; Remove layout-item properties when moving a shape outside a layout + (cond-> (not (ctl/any-layout? objects frame-id)) + (pcb/update-shapes moving-shapes-ids ctl/remove-layout-item-data)) + (pcb/update-shapes moving-shapes-ids #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true))) (pcb/change-parent frame-id moving-shapes drop-index) (pcb/remove-objects empty-parents))] (when (and (some? frame-id) (d/not-empty? changes)) (rx/of (dch/commit-changes changes) (dwc/expand-collapse frame-id))))))) - (defn- get-displacement "Retrieve the correct displacement delta point for the provided direction speed and distances thresholds." diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs index b0872fa47..d17b6de88 100644 --- a/frontend/src/app/main/data/workspace/undo.cljs +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -6,6 +6,7 @@ (ns app.main.data.workspace.undo (:require + [app.common.data :as d] [app.common.pages.changes-spec :as pcs] [app.common.spec :as us] [cljs.spec.alpha :as s] @@ -54,6 +55,17 @@ (dec MAX-UNDO-SIZE))))) state)) +(defn- stack-undo-entry + [state {:keys [undo-changes redo-changes] :as entry}] + (let [index (get-in state [:workspace-undo :index] -1)] + (if (>= index 0) + (update-in state [:workspace-undo :items index] + (fn [item] + (-> item + (update :undo-changes #(into undo-changes %)) + (update :redo-changes #(into % redo-changes))))) + (add-undo-entry state entry)))) + (defn- accumulate-undo-entry [state {:keys [undo-changes redo-changes group-id]}] (-> state @@ -62,13 +74,22 @@ (assoc-in [:workspace-undo :transaction :group-id] group-id))) (defn append-undo - [entry] + [entry stack?] (us/assert ::undo-entry entry) (ptk/reify ::append-undo ptk/UpdateEvent (update [_ state] - (if (get-in state [:workspace-undo :transaction]) + (cond + (and (get-in state [:workspace-undo :transaction]) + (or (not stack?) + (d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes])) + (d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes])))) (accumulate-undo-entry state entry) + + stack? + (stack-undo-entry state entry) + + :else (add-undo-entry state entry))))) (def empty-tx @@ -103,16 +124,6 @@ (update :workspace-undo dissoc :transaction)) state))))) -(def pop-undo-into-transaction - (ptk/reify ::last-undo-into-transaction - ptk/UpdateEvent - (update [_ state] - (let [index (get-in state [:workspace-undo :index] -1)] - - (cond-> state - (>= index 0) (accumulate-undo-entry (get-in state [:workspace-undo :items index])) - (>= index 0) (update-in [:workspace-undo :index] dec)))))) - (def reinitialize-undo (ptk/reify ::reset-undo ptk/UpdateEvent diff --git a/frontend/src/app/main/data/workspace/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs index 97b360472..8a8d64c41 100644 --- a/frontend/src/app/main/data/workspace/zoom.cljs +++ b/frontend/src/app/main/data/workspace/zoom.cljs @@ -28,20 +28,26 @@ (update :vbox merge (select-keys vbox' [:x :y :width :height]))))) (defn increase-zoom - [center] - (ptk/reify ::increase-zoom - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local - #(impl-update-zoom % center (fn [z] (min (* z 1.3) 200))))))) + ([] + (increase-zoom ::auto)) + ([center] + (ptk/reify ::increase-zoom + ptk/UpdateEvent + (update [_ state] + (let [center (if (= center ::auto) @ms/mouse-position center)] + (update state :workspace-local + #(impl-update-zoom % center (fn [z] (min (* z 1.3) 200))))))))) (defn decrease-zoom - [center] - (ptk/reify ::decrease-zoom - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local - #(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01))))))) + ([] + (decrease-zoom ::auto)) + ([center] + (ptk/reify ::decrease-zoom + ptk/UpdateEvent + (update [_ state] + (let [center (if (= center ::auto) @ms/mouse-position center)] + (update state :workspace-local + #(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01))))))))) (defn set-zoom [center scale] diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index 82709468e..a9386ca2f 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -194,47 +194,43 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn ensure-loaded! - ([id] - (p/create (fn [resolve] - (ensure-loaded! id resolve)))) - ([id on-loaded] - (log/debug :action "try-ensure-loaded!" :font-id id) - (if-not (exists? js/window) - ;; If we are in the worker environment, we just mark it as loaded - ;; without really loading it. - (do - (swap! loaded conj id) - (p/resolved id)) + [id] + (log/debug :action "try-ensure-loaded!" :font-id id) + (if-not (exists? js/window) + ;; If we are in the worker environment, we just mark it as loaded + ;; without really loading it. + (do + (swap! loaded conj id) + (p/resolved id)) - (when-let [font (get @fontsdb id)] - (cond - ;; Font already loaded, we just continue - (contains? @loaded id) - (p/do - (on-loaded id) - id) + (let [font (get @fontsdb id)] + (cond + (nil? font) + (p/resolved id) - ;; Font is currently downloading. We attach the caller to the promise - (contains? @loading id) - (-> (get @loading id) - (p/then #(do (on-loaded id) id))) + ;; Font already loaded, we just continue + (contains? @loaded id) + (p/resolved id) - ;; First caller, we create the promise and then wait - :else - (let [on-load (fn [resolve] - (swap! loaded conj id) - (swap! loading dissoc id) - (on-loaded id) - (resolve id)) + ;; Font is currently downloading. We attach the caller to the promise + (contains? @loading id) + (p/resolved (get @loading id)) - load-p (p/create - (fn [resolve _] - (-> font - (assoc ::on-loaded (partial on-load resolve)) - (load-font))))] + ;; First caller, we create the promise and then wait + :else + (let [on-load (fn [resolve] + (swap! loaded conj id) + (swap! loading dissoc id) + (resolve id)) - (swap! loading assoc id load-p) - load-p)))))) + load-p (p/create + (fn [resolve _] + (-> font + (assoc ::on-loaded (partial on-load resolve)) + (load-font))))] + + (swap! loading assoc id load-p) + load-p))))) (defn ready [cb] diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 59c91bbd6..86ca9d33d 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -279,6 +279,15 @@ (def workspace-read-only? (l/derived :read-only? workspace-global)) +(def workspace-paddings-selected + (l/derived :paddings-selected workspace-global)) + +(def workspace-gap-selected + (l/derived :gap-selected workspace-global)) + +(def workspace-margins-selected + (l/derived :margins-selected workspace-global)) + (defn object-by-id [id] (l/derived #(get % id) workspace-page-objects)) @@ -402,7 +411,7 @@ (let [objects (wsh/lookup-page-objects state)] (into [] (comp (map (d/getf objects)) - (filter (partial ctl/layout-immediate-child? objects))) + (filter (partial ctl/flex-layout-immediate-child? objects))) ids))) st/state =)) @@ -475,22 +484,22 @@ (defn workspace-text-modifier-by-id [id] (l/derived #(get % id) workspace-text-modifier =)) -(defn is-layout-child? +(defn is-flex-layout-child? [ids] (l/derived (fn [objects] (->> ids (map (d/getf objects)) - (some (partial ctl/layout-immediate-child? objects)))) + (some (partial ctl/flex-layout-immediate-child? objects)))) workspace-page-objects)) -(defn all-layout-child? +(defn all-flex-layout-child? [ids] (l/derived (fn [objects] (->> ids (map (d/getf objects)) - (every? (partial ctl/layout-immediate-child? objects)))) + (every? (partial ctl/flex-layout-immediate-child? objects)))) workspace-page-objects)) (defn get-flex-child-viewer @@ -500,7 +509,7 @@ (let [objects (wsh/lookup-viewer-objects state page-id)] (into [] (comp (map (d/getf objects)) - (filter (partial ctl/layout-immediate-child? objects))) + (filter (partial ctl/flex-layout-immediate-child? objects))) ids))) st/state =)) @@ -519,3 +528,12 @@ (def colorpicker (l/derived :colorpicker st/state)) + + +(def workspace-grid-edition + (l/derived :workspace-grid-edition st/state)) + +(defn workspace-grid-edition-id + [id] + (l/derived #(get % id) workspace-grid-edition)) + diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 594c1f3b4..320f5a682 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -12,17 +12,23 @@ [app.util.http :as http] [beicon.core :as rx])) +(derive :get-all-projects ::query) +(derive :get-comment-threads ::query) (derive :get-file ::query) -(derive :get-file-object-thumbnails ::query) -(derive :get-file-libraries ::query) (derive :get-file-fragment ::query) -(derive :search-files ::query) -(derive :get-teams ::query) -(derive :get-team-users ::query) -(derive :get-team-members ::query) -(derive :get-team-stats ::query) +(derive :get-file-libraries ::query) +(derive :get-file-object-thumbnails ::query) +(derive :get-font-variants ::query) +(derive :get-profile ::query) +(derive :get-project ::query) (derive :get-team-invitations ::query) +(derive :get-team-members ::query) (derive :get-team-shared-files ::query) +(derive :get-team-stats ::query) +(derive :get-team-users ::query) +(derive :get-teams ::query) +(derive :get-view-only-bundle ::query) +(derive :search-files ::query) (defn handle-response [{:keys [status body] :as response}] @@ -164,15 +170,6 @@ (rx/map http/conditional-decode-transit) (rx/mapcat handle-response)))) -(defmethod command :send-feedback - [_ params] - (->> (http/send! {:method :post - :uri (u/join @cf/public-uri "api/feedback") - :credentials "include" - :body (http/transit-data params)}) - (rx/map http/conditional-decode-transit) - (rx/mapcat handle-response))) - (defn- send-export [{:keys [blob?] :as params}] (->> (http/send! {:method :post diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index cb2d99824..87fa4a050 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -46,7 +46,9 @@ (defonce state (ptk/store {:resolve ptk/resolve :on-event on-event - :on-error (fn [e] (@on-error e))})) + :on-error (fn [e] + (.log js/console "ERROR!!" e) + (@on-error e))})) (defonce stream (ptk/input-stream state)) diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs index 9f63c8572..a4dc59d69 100644 --- a/frontend/src/app/main/streams.cljs +++ b/frontend/src/app/main/streams.cljs @@ -205,3 +205,14 @@ (rx/dedupe))] (rx/subscribe-with ob sub) sub)) + +(defonce keyboard-shift + (let [sub (rx/behavior-subject nil) + ob (->> st/stream + (rx/filter keyboard-event?) + (rx/filter kbd/shift-key?) + (rx/filter (comp not kbd/editing?)) + (rx/map #(= :down (:type %))) + (rx/dedupe))] + (rx/subscribe-with ob sub) + sub)) diff --git a/frontend/src/app/main/style.clj b/frontend/src/app/main/style.clj new file mode 100644 index 000000000..82dd96a47 --- /dev/null +++ b/frontend/src/app/main/style.clj @@ -0,0 +1,21 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.style + "A fonts loading macros." + (:require + [app.common.data :as d] + [clojure.data.json :as json])) + +(defmacro css + [selector] + (let [;; Get the associated styles will be module.cljs => module.css.json + filename (:file (meta *ns*)) + styles-file (str "./src/" (subs filename 0 (- (count filename) 4)) "css.json") + data (-> (slurp styles-file) + (json/read-str)) + result (get data (d/name selector))] + `~result)) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index e5c1190f2..bfb3dca09 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -245,16 +245,16 @@ :type "text"}]] (when (contains? @cf/flags :terms-and-privacy-checkbox) - [:div.fields-row + [:div.fields-row.input-visible.accept-terms-and-privacy-wrapper [:& fm/input {:name :accept-terms-and-privacy :class "check-primary" :type "checkbox"} [:span - (tr "auth.terms-privacy-agreement") - [:div - [:a {:href "https://penpot.app/terms" :target "_blank"} (tr "auth.terms-of-service")] - [:span ",\u00A0"] - [:a {:href "https://penpot.app/privacy" :target "_blank"} (tr "auth.privacy-policy")]]]]]) + (tr "auth.terms-privacy-agreement")]] + [:div.auth-links + [:a {:href "https://penpot.app/terms" :target "_blank"} (tr "auth.terms-of-service")] + [:span ",\u00A0"] + [:a {:href "https://penpot.app/privacy" :target "_blank"} (tr "auth.privacy-policy")]]]) [:& fm/submit-button {:label (tr "auth.register-submit") diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index ea2a00ab9..c83cf0310 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -377,7 +377,7 @@ (swap! state assoc :new-position-x nil) (swap! state assoc :new-position-y nil))) - on-mouse-move + on-pointer-move (mf/use-callback (mf/deps position zoom) (fn [event] @@ -392,7 +392,7 @@ {:on-pointer-down on-pointer-down :on-pointer-up on-pointer-up - :on-mouse-move on-mouse-move + :on-pointer-move on-pointer-move :on-lost-pointer-capture on-lost-pointer-capture :state state})) @@ -408,7 +408,7 @@ {:keys [on-pointer-down on-pointer-up - on-mouse-move + on-pointer-move state on-lost-pointer-capture]} (use-buble zoom thread) @@ -438,14 +438,14 @@ (and (not (mf/ref-val was-open?)) (not (mf/ref-val drag?)))) (st/emit! (dcm/open-thread thread)))))) - on-mouse-move* + on-pointer-move* (mf/use-callback - (mf/deps origin drag? on-mouse-move) + (mf/deps origin drag? on-pointer-move) (fn [event] (when (not= origin :viewer) (mf/set-ref-val! drag? true) (dom/stop-propagation event) - (on-mouse-move event)))) + (on-pointer-move event)))) on-click* (mf/use-callback @@ -460,7 +460,7 @@ :left (str pos-x "px")} :on-pointer-down on-pointer-down* :on-pointer-up on-pointer-up* - :on-mouse-move on-mouse-move* + :on-pointer-move on-pointer-move* :on-click on-click* :on-lost-pointer-capture on-lost-pointer-capture :class (dom/classnames diff --git a/frontend/src/app/main/ui/components/color_input.cljs b/frontend/src/app/main/ui/components/color_input.cljs index eed2d2648..415472f6f 100644 --- a/frontend/src/app/main/ui/components/color_input.cljs +++ b/frontend/src/app/main/ui/components/color_input.cljs @@ -138,7 +138,6 @@ (mf/use-layout-effect (fn [] (let [keys [(events/listen globals/window EventType.POINTERDOWN on-click) - (events/listen globals/window EventType.MOUSEDOWN on-click) (events/listen globals/window EventType.CLICK on-click)]] #(doseq [key keys] (events/unlistenByKey key))))) diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs index d64ab95d8..4bf05203e 100644 --- a/frontend/src/app/main/ui/components/context_menu.cljs +++ b/frontend/src/app/main/ui/components/context_menu.cljs @@ -57,8 +57,8 @@ {node-height :height node-width :width} bounding_rect {window-height :height window-width :width} window_size target-offset-y (if (> (+ top node-height) window-height) - (- node-height) - 0) + (- node-height) + 0) target-offset-x (if (> (+ left node-width) window-width) (- node-width) 0)] @@ -85,9 +85,9 @@ props (obj/merge props #js {:on-close on-local-close})] (mf/use-effect - (mf/deps options) - #(swap! local assoc :levels [{:parent-option nil - :options options}])) + (mf/deps options) + #(swap! local assoc :levels [{:parent-option nil + :options options}])) (when (and open? (some? (:levels @local))) [:> dropdown' props diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.cljs b/frontend/src/app/main/ui/components/context_menu_a11y.cljs new file mode 100644 index 000000000..fadaacc3d --- /dev/null +++ b/frontend/src/app/main/ui/components/context_menu_a11y.cljs @@ -0,0 +1,260 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.components.context-menu-a11y + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.refs :as refs] + [app.main.ui.components.dropdown :refer [dropdown']] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [app.util.object :as obj] + [app.util.timers :as tm] + [goog.object :as gobj] + [rumext.v2 :as mf])) + +(defn generate-ids-group + [options parent-name] + (let [ids (->> options + (map :id) + (filter some?))] + (if parent-name + (cons "go-back-sub-option" ids) + ids))) + +(mf/defc context-menu-a11y-item + {::mf/wrap-props false} + [props] + + (let [children (gobj/get props "children") + on-click (gobj/get props "on-click") + on-key-down (gobj/get props "on-key-down") + id (gobj/get props "id") + klass (gobj/get props "klass") + key (gobj/get props "key") + data-test (gobj/get props "data-test")] + [:li {:id id + :class klass + :tab-index "0" + :on-key-down on-key-down + :on-click on-click + :key key + :role "menuitem" + :data-test data-test} + children])) + +(mf/defc context-menu-a11y' + {::mf/wrap-props false} + [props] + (assert (fn? (gobj/get props "on-close")) "missing `on-close` prop") + (assert (boolean? (gobj/get props "show")) "missing `show` prop") + (assert (vector? (gobj/get props "options")) "missing `options` prop") + (let [open? (gobj/get props "show") + on-close (gobj/get props "on-close") + options (gobj/get props "options") + is-selectable (gobj/get props "selectable") + selected (gobj/get props "selected") + top (gobj/get props "top" 0) + left (gobj/get props "left" 0) + fixed? (gobj/get props "fixed?" false) + min-width? (gobj/get props "min-width?" false) + origin (gobj/get props "origin") + route (mf/deref refs/route) + in-dashboard? (= :dashboard-projects (:name (:data route))) + local (mf/use-state {:offset-y 0 + :offset-x 0 + :levels nil}) + + on-local-close + (mf/use-callback + (fn [] + (swap! local assoc :levels [{:parent-option nil + :options options}]) + (on-close))) + + props (obj/merge props #js {:on-close on-local-close}) + + ids (generate-ids-group (:options (last (:levels @local))) (:parent-option (last (:levels @local)))) + check-menu-offscreen + (mf/use-callback + (mf/deps top (:offset-y @local) left (:offset-x @local)) + (fn [node] + (when (some? node) + (let [bounding_rect (dom/get-bounding-rect node) + window_size (dom/get-window-size) + {node-height :height node-width :width} bounding_rect + {window-height :height window-width :width} window_size + target-offset-y (if (> (+ top node-height) window-height) + (- node-height) + 0) + target-offset-x (if (> (+ left node-width) window-width) + (- node-width) + 0)] + + (when (or (not= target-offset-y (:offset-y @local)) (not= target-offset-x (:offset-x @local))) + (swap! local assoc :offset-y target-offset-y :offset-x target-offset-x)))))) + + enter-submenu + (mf/use-callback + (mf/deps options) + (fn [option-name sub-options] + (fn [event] + (dom/stop-propagation event) + (swap! local update :levels + conj {:parent-option option-name + :options sub-options})))) + + exit-submenu + (mf/use-callback + (fn [event] + (dom/stop-propagation event) + (swap! local update :levels pop))) + + on-key-down + (fn [options-original parent-original] + (fn [event] + (let [ids (generate-ids-group options-original parent-original) + first-id (dom/get-element (first ids)) + first-element (dom/get-element first-id) + len (count ids) + parent (dom/get-target event) + parent-id (dom/get-attribute parent "id") + option (first (filter #(= parent-id (:id %)) options-original)) + sub-options (:sub-options option) + has-suboptions? (some? (:sub-options option)) + option-handler (:option-handler option) + is-back-option (= "go-back-sub-option" parent-id)] + (when (kbd/home? event) + (when first-element + (dom/focus! first-element))) + + (when (kbd/enter? event) + (if is-back-option + (exit-submenu event) + + (if has-suboptions? + (do + (dom/stop-propagation event) + (swap! local update :levels + conj {:parent-option (:option-name option) + :options sub-options})) + + (do + (dom/stop-propagation event) + (option-handler event))))) + + (when (and is-back-option + (kbd/left-arrow? event)) + (exit-submenu event)) + + (when (and has-suboptions? (kbd/right-arrow? event)) + (dom/stop-propagation event) + (swap! local update :levels + conj {:parent-option (:option-name option) + :options sub-options})) + (when (kbd/up-arrow? event) + (let [actual-selected (dom/get-active) + actual-id (dom/get-attribute actual-selected "id") + actual-index (d/index-of ids actual-id) + previous-id (if (= 0 actual-index) + (last ids) + (nth ids (- actual-index 1)))] + (dom/focus! (dom/get-element previous-id)))) + + (when (kbd/down-arrow? event) + (let [actual-selected (dom/get-active) + actual-id (dom/get-attribute actual-selected "id") + actual-index (d/index-of ids actual-id) + next-id (if (= (- len 1) actual-index) + (first ids) + (nth ids (+ 1 actual-index)))] + (dom/focus! (dom/get-element next-id)))) + + (when (or (kbd/esc? event) (kbd/tab? event)) + (on-close) + (dom/focus! (dom/get-element origin))))))] + + (mf/with-effect [options] + (swap! local assoc :levels [{:parent-option nil + :options options}])) + + (mf/with-effect [ids] + (tm/schedule-on-idle + (dom/focus! (dom/get-element (first ids))))) + + (when (and open? (some? (:levels @local))) + [:> dropdown' props + + (let [level (-> @local :levels peek) + original-options (:options level) + parent-original (:parent-option level)] + [:div.context-menu {:class (dom/classnames :is-open open? + :fixed fixed? + :is-selectable is-selectable) + :style {:top (+ top (:offset-y @local)) + :left (+ left (:offset-x @local))} + :on-key-down (on-key-down original-options parent-original)} + (let [level (-> @local :levels peek)] + [:ul.context-menu-items {:class (dom/classnames :min-width min-width?) + :role "menu" + :ref check-menu-offscreen} + (when-let [parent-option (:parent-option level)] + [:* + [:& context-menu-a11y-item + {:id "go-back-sub-option" + :tab-index "0" + :on-key-down (fn [event] + (dom/prevent-default event))} + [:div.context-menu-action.submenu-back + {:data-no-close true + :on-click exit-submenu} + [:span i/arrow-slide] + parent-option]] + [:li.separator]]) + (for [[index option] (d/enumerate (:options level))] + + (let [option-name (:option-name option) + id (:id option) + sub-options (:sub-options option) + option-handler (:option-handler option) + data-test (:data-test option)] + (when option-name + (if (= option-name :separator) + [:li.separator {:key (dm/str "context-item-" index)}] + [:& context-menu-a11y-item + {:id id + :class (dom/classnames :is-selected (and selected (= option-name selected))) + :key (dm/str "context-item-" index) + :tab-index "0" + :on-key-down (fn [event] + (dom/prevent-default event))} + (if-not sub-options + [:a.context-menu-action {:on-click #(do (dom/stop-propagation %) + (on-close) + (option-handler %)) + :data-test data-test} + (if (and in-dashboard? (= option-name "Default")) + (tr "dashboard.default-team-name") + option-name)] + [:a.context-menu-action.submenu + {:data-no-close true + :on-click (enter-submenu option-name sub-options) + :data-test data-test} + option-name + [:span i/arrow-slide]])]))))])])]))) + +(mf/defc context-menu-a11y + {::mf/wrap-props false} + [props] + (assert (fn? (gobj/get props "on-close")) "missing `on-close` prop") + (assert (boolean? (gobj/get props "show")) "missing `show` prop") + (assert (vector? (gobj/get props "options")) "missing `options` prop") + + (when (gobj/get props "show") + (mf/element context-menu-a11y' props))) diff --git a/frontend/src/app/main/ui/components/dropdown_menu.cljs b/frontend/src/app/main/ui/components/dropdown_menu.cljs index 0123bec65..c397b5c26 100644 --- a/frontend/src/app/main/ui/components/dropdown_menu.cljs +++ b/frontend/src/app/main/ui/components/dropdown_menu.cljs @@ -25,7 +25,7 @@ on-key-down (gobj/get props "on-key-down") id (gobj/get props "id") klass (gobj/get props "klass") - key (gobj/get props "klass") + key (gobj/get props "key") data-test (gobj/get props "data-test")] [:li {:id id :class klass @@ -45,7 +45,7 @@ ref (gobj/get props "container") ids (gobj/get props "ids") list-class (gobj/get props "list-class") - + ids (filter some? ids) on-click (fn [event] (let [target (dom/get-target event) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index fab3f476c..55e63f9e4 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -251,7 +251,7 @@ (into [] (distinct) (conj coll item))) (mf/defc multi-input - [{:keys [form label class name trim valid-item-fn on-submit] :as props}] + [{:keys [form label class name trim valid-item-fn caution-item-fn on-submit] :as props}] (let [form (or form (mf/use-ctx form-ctx)) input-name (get props :name) touched? (get-in @form [:touched input-name]) @@ -309,7 +309,9 @@ (on-submit form)) (when (not (str/empty? @value)) (reset! value "") - (swap! items conj-dedup {:text val :valid (valid-item-fn val)})))) + (swap! items conj-dedup {:text val + :valid (valid-item-fn val) + :caution (caution-item-fn val)})))) (and (kbd/backspace? event) (str/empty? @value)) @@ -361,6 +363,7 @@ [:div.selected-item {:key (:text item) :tab-index "0" :on-key-down (partial manage-key-down item)} - [:span.around {:class (when-not (:valid item) "invalid")} + [:span.around {:class (dom/classnames "invalid" (not (:valid item)) + "caution" (:caution item))} [:span.text (:text item)] [:span.icon {:on-click #(remove-item! item)} i/cross]]])])])) diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index ad772f1ac..9b9034979 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -233,7 +233,6 @@ (mf/use-layout-effect (fn [] (let [keys [(events/listen globals/window EventType.POINTERDOWN on-click) - (events/listen globals/window EventType.MOUSEDOWN on-click) (events/listen globals/window EventType.CLICK on-click)]] #(doseq [key keys] (events/unlistenByKey key))))) diff --git a/frontend/src/app/main/ui/components/shape_icon.cljs b/frontend/src/app/main/ui/components/shape_icon.cljs index 9eca7fa5b..fa6d7606b 100644 --- a/frontend/src/app/main/ui/components/shape_icon.cljs +++ b/frontend/src/app/main/ui/components/shape_icon.cljs @@ -20,12 +20,14 @@ i/component-copy) (case (:type shape) :frame (cond - (and (ctl/layout? shape) (ctl/col? shape)) + (and (ctl/flex-layout? shape) (ctl/col? shape)) i/layout-columns - (and (ctl/layout? shape) (ctl/row? shape)) + (and (ctl/flex-layout? shape) (ctl/row? shape)) i/layout-rows + ;; TODO: GRID ICON + :else i/artboard) :image i/image diff --git a/frontend/src/app/main/ui/cursors.cljs b/frontend/src/app/main/ui/cursors.cljs index 4a0f323e1..6b4786c58 100644 --- a/frontend/src/app/main/ui/cursors.cljs +++ b/frontend/src/app/main/ui/cursors.cljs @@ -41,6 +41,12 @@ (def rotate (cursor-fn :rotate 90)) (def text (cursor-fn :text 0)) +;; text +(def scale-ew (cursor-fn :scale-h 0)) +(def scale-nesw (cursor-fn :scale-h 45)) +(def scale-ns (cursor-fn :scale-h 90)) +(def scale-nwse (cursor-fn :scale-h 135)) + ;; (def resize-ew-2 (cursor-fn :resize-h-2 0)) (def resize-ns-2 (cursor-fn :resize-h-2 90)) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index ea4acca7e..152859a06 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -8,6 +8,7 @@ (:require [app.common.colors :as clr] [app.common.data :as d] + [app.common.math :as mth] [app.common.spec :as us] [app.main.data.dashboard :as dd] [app.main.data.dashboard.shortcuts :as sc] @@ -83,6 +84,10 @@ container-size (* (+ 2 num-cards) card-width) ;; We need space for num-cards plus the libraries&templates link more-cards (> (+ @card-offset (* (+ 1 num-cards) card-width)) content-width) + visible-card-count (mth/floor (/ content-width 275)) + left-moves (/ @card-offset -275) + first-visible-card left-moves + last-visible-card (+ (- visible-card-count 1) left-moves) content-ref (mf/use-ref) toggle-collapse @@ -146,38 +151,78 @@ [:div.dashboard-templates-section {:class (when collapsed "collapsed")} [:div.title [:button {:tab-index "0" - :on-click toggle-collapse} + :on-click toggle-collapse + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (dom/prevent-default event) + (toggle-collapse)))} [:span (tr "dashboard.libraries-and-templates")] [:span.icon (if collapsed i/arrow-up i/arrow-down)]]] [:div.content {:ref content-ref - :style {:left @card-offset :width (str container-size "px")}} + :style {:left @card-offset :width (str container-size "px")}} + (for [num-item (range (count templates)) :let [item (nth templates num-item)]] - [:a.card-container {:tab-index "0" - :id (str/concat "card-container-" num-item) - :key (:id item) - :on-click #(import-template item) - :on-key-down (fn [event] - (when (kbd/enter? event) - (import-template item)))} + (let [is-visible? (and (>= num-item first-visible-card) (<= num-item last-visible-card))] + [:a.card-container {:tab-index (if (or (not is-visible?) collapsed) + "-1" + "0") + :id (str/concat "card-container-" num-item) + :key (:id item) + :on-click #(import-template item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (import-template item)))} + [:div.template-card + [:div.img-container + [:img {:src (:thumbnail-uri item) + :alt (:name item)}]] + [:div.card-name [:span (:name item)] [:span.icon i/download]]]])) + + (let [is-visible? (and (>= num-cards first-visible-card) (<= num-cards last-visible-card))] + [:div.card-container [:div.template-card [:div.img-container - [:img {:src (:thumbnail-uri item) - :alt (:name item)}]] - [:div.card-name [:span (:name item)] [:span.icon i/download]]]]) - - [:div.card-container - [:div.template-card - [:div.img-container - [:a {:tab-index "0" - :href "https://penpot.app/libraries-templates" :target "_blank" :on-click handle-template-link} - [:div.template-link - [:div.template-link-title (tr "dashboard.libraries-and-templates")] - [:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]]] - (when (< @card-offset 0) - [:button.button.left {:on-click move-left} i/go-prev]) + [:a {:id (str/concat "card-container-" num-cards) + :tab-index (if (or (not is-visible?) collapsed) + "-1" + "0") + :href "https://penpot.app/libraries-templates.html" + :target "_blank" + :on-click handle-template-link + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (handle-template-link)))} + [:div.template-link + [:div.template-link-title (tr "dashboard.libraries-and-templates")] + [:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]])] + (when (< @card-offset 0) + [:button.button.left {:tab-index (if collapsed + "-1" + "0") + :on-click move-left + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (move-left) + (let [first-element (dom/get-element (str/concat "card-container-" first-visible-card))] + (when first-element + (dom/focus! first-element)))))} i/go-prev]) (when more-cards - [:button.button.right {:on-click move-right - :aria-label (tr "labels.next")} i/go-next])])) + [:button.button.right {:tab-index (if collapsed + "-1" + "0") + :on-click move-right + :aria-label (tr "labels.next") + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (move-right) + (let [last-element (dom/get-element (str/concat "card-container-" last-visible-card))] + (when last-element + (dom/focus! last-element)))))} i/go-next])])) (mf/defc dashboard-content [{:keys [team projects project section search-term profile] :as props}] @@ -277,6 +322,7 @@ (let [events [(events/listen goog/global EventType.KEYDOWN (fn [event] (when (kbd/enter? event) + (dom/stop-propagation event) (st/emit! (dd/open-selected-file)))))]] (fn [] (doseq [key events] diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 70f068e06..178635c98 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -12,7 +12,7 @@ [app.main.data.modal :as modal] [app.main.repo :as rp] [app.main.store :as st] - [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] [app.main.ui.context :as ctx] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -27,6 +27,10 @@ (tr "labels.drafts") (:name project))) +(defn get-project-id + [project] + (str (:id project))) + (defn get-team-name [team] (if (:is-default team) @@ -49,13 +53,14 @@ projects)) (mf/defc file-menu - [{:keys [files show? on-edit on-menu-close top left navigate? origin] :as props}] + [{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id] :as props}] (assert (seq files) "missing `files` prop") (assert (boolean? show?) "missing `show?` prop") (assert (fn? on-edit) "missing `on-edit` prop") (assert (fn? on-menu-close) "missing `on-menu-close` prop") (assert (boolean? navigate?) "missing `navigate?` prop") - (let [is-lib-page? (= :libraries origin) + (let [is-lib-page? (= :libraries origin) + is-search-page? (= :search origin) top (or top 0) left (or left 0) @@ -65,13 +70,10 @@ current-team-id (mf/use-ctx ctx/current-team-id) teams (mf/use-state nil) - current-team (get @teams current-team-id) other-teams (remove #(= (:id %) current-team-id) (vals @teams)) - current-projects (remove #(= (:id %) (:project-id file)) (:projects current-team)) - on-new-tab (fn [_] (let [path-params {:project-id (:project-id file) @@ -207,57 +209,108 @@ (mf/deps show?) (fn [] (when show? - (->> (rp/query! :all-projects) + (->> (rp/cmd! :get-all-projects) (rx/map group-by-team) (rx/subs #(when (mf/ref-val mounted-ref) (reset! teams %))))))) - + (when current-team - (let [sub-options (conj (vec (for [project current-projects] - [(get-project-name project) - (on-move (:id current-team) - (:id project))])) - (when (seq other-teams) - [(tr "dashboard.move-to-other-team") nil - (for [team other-teams] - [(get-team-name team) nil - (for [sub-project (:projects team)] - [(get-project-name sub-project) - (on-move (:id team) - (:id sub-project))])]) - "move-to-other-team"])) + (let [sub-options (concat (vec (for [project current-projects] + {:option-name (get-project-name project) + :id (get-project-id project) + :option-handler (on-move (:id current-team) + (:id project))})) + (when (seq other-teams) + [{:option-name (tr "dashboard.move-to-other-team") + :id "move-to-other-team" + :sub-options + (for [team other-teams] + {:option-name (get-team-name team) + :id (get-project-id team) + :sub-options + (for [sub-project (:projects team)] + {:option-name (get-project-name sub-project) + :id (get-project-id sub-project) + :option-handler (on-move (:id team) + (:id sub-project))})})}])) options (if multi? - [[(tr "dashboard.duplicate-multi" file-count) on-duplicate nil "duplicate-multi"] + [{:option-name (tr "dashboard.duplicate-multi" file-count) + :id "file-duplicate-multi" + :option-handler on-duplicate + :data-test "duplicate-multi"} (when (or (seq current-projects) (seq other-teams)) - [(tr "dashboard.move-to-multi" file-count) nil sub-options "move-to-multi"]) - [(tr "dashboard.export-binary-multi" file-count) on-export-binary-files] - [(tr "dashboard.export-standard-multi" file-count) on-export-standard-files] + {:option-name (tr "dashboard.move-to-multi" file-count) + :id "file-move-multi" + :sub-options sub-options + :data-test "move-to-multi"}) + {:option-name (tr "dashboard.export-binary-multi" file-count) + :id "file-binari-export-multi" + :option-handler on-export-binary-files} + {:option-name (tr "dashboard.export-standard-multi" file-count) + :id "file-standard-export-multi" + :option-handler on-export-standard-files} (when (:is-shared file) - [(tr "labels.unpublish-multi-files" file-count) on-del-shared nil "file-del-shared"]) + {:option-name (tr "labels.unpublish-multi-files" file-count) + :id "file-unpublish-multi" + :option-handler on-del-shared + :data-test "file-del-shared"}) (when (not is-lib-page?) - [:separator] - [(tr "labels.delete-multi-files" file-count) on-delete nil "delete-multi-files"])] + {:option-name :separator} + {:option-name (tr "labels.delete-multi-files" file-count) + :id "file-delete-multi" + :option-handler on-delete + :data-test "delete-multi-files"})] - [[(tr "dashboard.open-in-new-tab") on-new-tab] - [(tr "labels.rename") on-edit nil "file-rename"] - [(tr "dashboard.duplicate") on-duplicate nil "file-duplicate"] - (when (and (not is-lib-page?) (or (seq current-projects) (seq other-teams))) - [(tr "dashboard.move-to") nil sub-options "file-move-to"]) - (if (:is-shared file) - [(tr "dashboard.unpublish-shared") on-del-shared nil "file-del-shared"] - [(tr "dashboard.add-shared") on-add-shared nil "file-add-shared"]) - [:separator] - [(tr "dashboard.download-binary-file") on-export-binary-files nil "download-binary-file"] - [(tr "dashboard.download-standard-file") on-export-standard-files nil "download-standard-file"] - (when (not is-lib-page?) - [:separator] - [(tr "labels.delete") on-delete nil "file-delete"])])] + [{:option-name (tr "dashboard.open-in-new-tab") + :id "file-open-new-tab" + :option-handler on-new-tab} + (when (not is-search-page?) + {:option-name (tr "labels.rename") + :id "file-rename" + :option-handler on-edit + :data-test "file-rename"}) + (when (not is-search-page?) + {:option-name (tr "dashboard.duplicate") + :id "file-duplicate" + :option-handler on-duplicate + :data-test "file-duplicate"}) + (when (and (not is-lib-page?) (not is-search-page?) (or (seq current-projects) (seq other-teams))) + {:option-name (tr "dashboard.move-to") + :id "file-move-to" + :sub-options sub-options + :data-test "file-move-to"}) + (when (not is-search-page?) + (if (:is-shared file) + {:option-name (tr "dashboard.unpublish-shared") + :id "file-del-shared" + :option-handler on-del-shared + :data-test "file-del-shared"} + {:option-name (tr "dashboard.add-shared") + :id "file-add-shared" + :option-handler on-add-shared + :data-test "file-add-shared"})) + {:option-name :separator} + {:option-name (tr "dashboard.download-binary-file") + :id "file-download-binary" + :option-handler on-export-binary-files + :data-test "download-binary-file"} + {:option-name (tr "dashboard.download-standard-file") + :id "file-download-standard" + :option-handler on-export-standard-files + :data-test "download-standard-file"} + (when (and (not is-lib-page?) (not is-search-page?)) + {:option-name :separator} + {:option-name (tr "labels.delete") + :id "file-delete" + :option-handler on-delete + :data-test "file-delete"})])] - [:& context-menu {:on-close on-menu-close - :show show? - :fixed? (or (not= top 0) (not= left 0)) - :min-width? true - :top top - :left left - :options options}])))) + [:& context-menu-a11y {:on-close on-menu-close + :show show? + :fixed? (or (not= top 0) (not= left 0)) + :min-width? true + :top top + :left left + :options options + :origin parent-id}])))) diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 712183d96..e67ebbfc6 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -39,7 +39,7 @@ on-menu-click (mf/use-fn (fn [event] - (let [position (dom/get-client-position event)] + (let [position (dom/get-client-position event)] (dom/prevent-default event) (swap! local assoc :menu-open true :menu-pos position)))) @@ -144,6 +144,7 @@ create-file (mf/use-fn + (mf/deps project) (fn [origin] (st/emit! (with-meta (dd/create-file {:project-id (:id project)}) {::ev/origin origin}))))] diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index cb786df45..f7236a88c 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -94,7 +94,7 @@ (mf/deps team) (fn [item] (swap! uploading conj (:id item)) - (->> (rp/mutation! :create-font-variant item) + (->> (rp/cmd! :create-font-variant item) (rx/delay-at-least 2000) (rx/subs (fn [font] (swap! fonts dissoc (:id item)) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 1bdaf8d02..c0d4b1c0a 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -8,6 +8,7 @@ (:require [app.common.data.macros :as dm] [app.common.files.features :as ffeat] + [app.common.geom.point :as gpt] [app.common.logging :as log] [app.main.data.dashboard :as dd] [app.main.data.messages :as msg] @@ -251,7 +252,14 @@ (st/emit! (dd/clear-selected-files))) (st/emit! (dd/toggle-file-select file))) - (let [position (dom/get-client-position event)] + (let [client-position (dom/get-client-position event) + position (if (and (nil? (:y client-position)) (nil? (:x client-position))) + (let [target-element (dom/get-target event) + points (dom/get-bounding-rect target-element) + y (:top points) + x (:left points)] + (gpt/point x y)) + client-position)] (swap! local assoc :menu-open true :menu-pos position)))) @@ -260,7 +268,9 @@ (mf/use-fn (mf/deps file) (fn [name] - (st/emit! (dd/rename-file (assoc file :name name))) + (let [name (str/trim name)] + (when (not= name "") + (st/emit! (dd/rename-file (assoc file :name name))))) (swap! local assoc :edition false))) on-edit @@ -277,7 +287,7 @@ (swap! local assoc :menu-open false))) [:li.grid-item.project-th - [:a + [:button {:tab-index "0" :class (dom/classnames :selected selected? :library library-view?) @@ -314,9 +324,11 @@ [:div.project-th-icon.menu {:tab-index "0" :ref menu-ref + :id (str file-id "-action-menu") :on-click on-menu-click :on-key-down (fn [event] (when (kbd/enter? event) + (dom/stop-propagation event) (on-menu-click event)))} i/actions (when selected? @@ -328,8 +340,8 @@ :on-edit on-edit :on-menu-close on-menu-close :origin origin - :dashboard-local dashboard-local}])]]]]])) - + :dashboard-local dashboard-local + :parent-id (str file-id "-action-menu")}])]]]]])) (mf/defc grid [{:keys [files project origin limit library-view? create-fn] :as props}] diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 66a481d1d..1aa8fbc73 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -22,6 +22,7 @@ [app.util.keyboard :as kbd] [app.util.webapi :as wapi] [beicon.core :as rx] + [cuerdas.core :as str] [potok.core :as ptk] [rumext.v2 :as mf])) @@ -61,9 +62,11 @@ (->> files (mapv (fn [file] - (cond-> file - (= (:file-id file) file-id) - (assoc :name new-name)))))) + (let [new-name (str/trim new-name)] + (cond-> file + (and (= (:file-id file) file-id) + (not= "" new-name)) + (assoc :name new-name))))))) (defn remove-file [files file-id] (->> files @@ -378,13 +381,13 @@ [:div.feedback-banner [:div.icon i/checkbox-checked] - [:div.message (tr "dashboard.import.import-message" (if (some? template) 1 success-files))]])) + [:div.message (tr "dashboard.import.import-message" (i18n/c (if (some? template) 1 success-files)))]])) (for [file files] (let [editing? (and (some? (:file-id file)) (= (:file-id file) (:editing @state)))] [:& import-entry {:state state - :key (dm/str (:id file)) + :key (dm/str (:uri file)) :file file :editing? editing? :can-be-deleted? (> (count files) 1)}])) diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index 8804c52cd..5c749d52d 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -12,7 +12,7 @@ [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.context-menu :refer [context-menu]] + [app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] [app.main.ui.context :as ctx] [app.main.ui.dashboard.import :as udi] [app.util.dom :as dom] @@ -67,7 +67,7 @@ (let [data {:id (:id project) :team-id team-id} mdata {:on-success #(on-move-success team-id)}] #(st/emit! (dm/success (tr "dashboard.success-move-project")) - (dd/move-project (with-meta data mdata))))) + (dd/move-project (with-meta data mdata))))) delete-fn (fn [_] @@ -77,12 +77,12 @@ on-delete #(st/emit! - (modal/show - {:type :confirm - :title (tr "modals.delete-project-confirm.title") - :message (tr "modals.delete-project-confirm.message") - :accept-label (tr "modals.delete-project-confirm.accept") - :on-accept delete-fn})) + (modal/show + {:type :confirm + :title (tr "modals.delete-project-confirm.title") + :message (tr "modals.delete-project-confirm.message") + :accept-label (tr "modals.delete-project-confirm.accept") + :on-accept delete-fn})) file-input (mf/use-ref nil) @@ -94,34 +94,54 @@ on-finish-import (mf/use-callback (fn [] - (when (fn? on-import) (on-import))))] + (when (fn? on-import) (on-import)))) + + options [(when-not (:is-default project) + {:option-name (tr "labels.rename") + :id "project-menu-rename" + :option-handler on-edit + :data-test "project-rename"}) + (when-not (:is-default project) + {:option-name (tr "dashboard.duplicate") + :id "project-menu-duplicated" + :option-handler on-duplicate + :data-test "project-duplicate"}) + (when-not (:is-default project) + {:option-name (tr "dashboard.pin-unpin") + :id "project-menu-pin" + :option-handler toggle-pin}) + + (when (and (seq teams) (not (:is-default project))) + {:option-name (tr "dashboard.move-to") + :id "project-menu-move-to" + :sub-options (for [team teams] + {:option-name (:name team) + :id (:name team) + :option-handler (on-move (:id team))}) + :data-test "project-move-to"}) + (when (some? on-import) + {:option-name (tr "dashboard.import") + :id "project-menu-import" + :option-handler on-import-files + :data-test "file-import"}) + (when-not (:is-default project) + {:option-name :separator}) + (when-not (:is-default project) + {:option-name (tr "labels.delete") + :id "project-menu-delete" + :option-handler on-delete + :data-test "project-delete"})]] [:* [:& udi/import-form {:ref file-input :project-id (:id project) :on-finish-import on-finish-import}] - [:& context-menu + [:& context-menu-a11y {:on-close on-menu-close :show show? :fixed? (or (not= top 0) (not= left 0)) :min-width? true :top top :left left - :options [(when-not (:is-default project) - [(tr "labels.rename") on-edit nil "project-rename"]) - (when-not (:is-default project) - [(tr "dashboard.duplicate") on-duplicate nil "project-duplicate"]) - (when-not (:is-default project) - [(tr "dashboard.pin-unpin") toggle-pin]) - (when (and (seq teams) (not (:is-default project))) - [(tr "dashboard.move-to") nil - (for [team teams] - [(:name team) (on-move (:id team))]) - "project-move-to"]) - (when (some? on-import) - [(tr "dashboard.import") on-import-files nil "file-import"]) - (when-not (:is-default project) - [:separator]) - (when-not (:is-default project) - [(tr "labels.delete") on-delete nil "project-delete"])]}]])) + :options options}]])) diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index dad357e67..95a9648db 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.dashboard.projects (:require [app.common.data :as d] + [app.common.geom.point :as gpt] [app.common.math :as mth] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] @@ -142,7 +143,7 @@ [:div.text [:h2.title (tr "dasboard.walkthrough-hero.title")] [:p.info (tr "dasboard.walkthrough-hero.info")] - [:a.btn-primary.action + [:a.btn-primary.action {:href " https://design.penpot.app/walkthrough" :target "_blank" :on-click handle-walkthrough-link} @@ -187,13 +188,23 @@ toggle-pin (mf/use-fn (mf/deps project) - #(st/emit! (dd/toggle-project-pin project))) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dd/toggle-project-pin project)))) on-menu-click (mf/use-fn (fn [event] - (let [position (dom/get-client-position event)] - (dom/prevent-default event) + (dom/prevent-default event) + + (let [client-position (dom/get-client-position event) + position (if (and (nil? (:y client-position)) (nil? (:x client-position))) + (let [target-element (dom/get-target event) + points (dom/get-bounding-rect target-element) + y (:top points) + x (:left points)] + (gpt/point x y)) + client-position)] (swap! local assoc :menu-open true :menu-pos position)))) @@ -276,7 +287,7 @@ [:& project-menu {:project project :show? (:menu-open @local) - :left (:x (:menu-pos @local)) + :left (+ 24 (:x (:menu-pos @local))) :top (:y (:menu-pos @local)) :on-edit on-edit-open :on-menu-close on-menu-close @@ -291,12 +302,9 @@ (when-not (:is-default project) [:button.pin-icon.tooltip.tooltip-bottom {:class (when (:is-pinned project) "active") - :on-click toggle-pin + :on-click toggle-pin :alt (tr "dashboard.pin-unpin") :aria-label (tr "dashboard.pin-unpin") - :on-key-down (fn [event] - (when (kbd/enter? event) - (toggle-pin event))) :tab-index "0"} (if (:is-pinned project) i/pin-fill @@ -321,22 +329,27 @@ :tab-index "0" :on-key-down (fn [event] (when (kbd/enter? event) + (dom/stop-propagation event) (on-menu-click event)))} - i/actions]]] - - (when (and (> limit 0) - (> file-count limit)) - [:div.show-more {:on-click on-nav} - [:div.placeholder-label - (tr "dashboard.show-all-files")] - [:div.placeholder-icon i/arrow-down]])] + i/actions]]]] [:& line-grid {:project project :team team :files files :create-fn create-file - :limit limit}]])) + :limit limit}] + + (when (and (> limit 0) + (> file-count limit)) + [:button.show-more {:on-click on-nav + :tab-index "0" + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-nav)))} + [:div.placeholder-label + (tr "dashboard.show-all-files")] + [:div.placeholder-icon i/arrow-down]])])) (def recent-files-ref @@ -349,8 +362,13 @@ (reverse)) recent-map (mf/deref recent-files-ref) props (some-> profile (get :props {})) - team-hero? (and (:team-hero? props true) - (not (:is-default team))) + you-owner? (get-in team [:permissions :is-owner]) + you-admin? (get-in team [:permissions :is-admin]) + can-invite? (or you-owner? you-admin?) + team-hero? (and can-invite? + (:team-hero? props true) + (not (:is-default team))) + tutorial-viewed? (:viewed-tutorial? props true) walkthrough-viewed? (:viewed-walkthrough? props true) diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index 8d5bf9fb3..bcbcfd710 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -87,4 +87,5 @@ :else [:& grid {:files result :hide-new? true + :origin :search :limit limit}])]])) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index da8d7c0e1..9ab16f6fa 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -39,7 +39,9 @@ go-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks))) invite-member (mf/use-fn (mf/deps team) - #(st/emit! (modal/show {:type :invite-members :team team :origin :team}))) + #(st/emit! (modal/show {:type :invite-members + :team team + :origin :team}))) members-section? (= section :dashboard-team-members) settings-section? (= section :dashboard-team-settings) @@ -98,7 +100,10 @@ {::mf/register modal/components ::mf/register-as :invite-members} [{:keys [team origin]}] - (let [perms (:permissions team) + (let [members-map (mf/deref refs/dashboard-team-members) + + perms (:permissions team) + roles (mf/use-memo (mf/deps perms) #(get-available-roles perms)) initial (mf/use-memo (constantly {:role "editor" :team-id (:id team)})) form (fm/use-form :spec ::invite-member-form @@ -111,6 +116,9 @@ (modal/hide) (dd/fetch-team-invitations))) + current-data-emails (into #{} (dm/get-in @form [:clean-data :emails])) + current-members-emails (into #{} (map (comp :email second)) members-map) + on-error (fn [{:keys [type code] :as error}] (cond @@ -148,22 +156,30 @@ [:div.error [:span.icon i/msg-error] [:span.text @error-text]]) + + (when (some current-data-emails current-members-emails) + [:div.warning + [:span.icon i/msg-warning] + [:span.text (tr "modals.invite-member.repeated-invitation")]]) + [:div.form-row [:p.label (tr "onboarding.choice.team-up.roles")] [:& fm/select {:name :role :options roles}]] + [:div.form-row - - [:& fm/multi-input {:type "email" :name :emails :auto-focus? true :trim true :valid-item-fn us/parse-email + :caution-item-fn current-members-emails :label (tr "modals.invite-member.emails") :on-submit on-submit}]] [:div.action-buttons - [:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]])) + [:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept") + :disabled (and (boolean (some current-data-emails current-members-emails)) + (empty? (remove current-members-emails current-data-emails)))}]]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MEMBERS SECTION @@ -572,7 +588,8 @@ [:div.empty-invitations [:span (tr "labels.no-invitations")] (when can-invite? - [:span (tr "labels.no-invitations-hint")])]) + [:& i18n/tr-html {:label "labels.no-invitations-hint" + :tag-name "span"}])]) (mf/defc invitation-section [{:keys [team invitations] :as props}] @@ -883,6 +900,10 @@ stats (mf/deref refs/dashboard-team-stats) + you-owner? (get-in team [:permissions :is-owner]) + you-admin? (get-in team [:permissions :is-admin]) + can-edit? (or you-owner? you-admin?) + on-image-click (mf/use-callback #(dom/click (mf/ref-val finput))) @@ -914,12 +935,14 @@ [:div.label (tr "dashboard.team-info")] [:div.name (:name team)] [:div.icon - [:span.update-overlay {:on-click on-image-click} i/image] + (when can-edit? + [:span.update-overlay {:on-click on-image-click} i/image]) [:img {:src (cfg/resolve-team-photo-url team)}] - [:& file-uploader {:accept "image/jpeg,image/png" - :multi false - :ref finput - :on-selected on-file-selected}]]] + (when can-edit? + [:& file-uploader {:accept "image/jpeg,image/png" + :multi false + :ref finput + :on-selected on-file-selected}])]] [:div.block.owner-block [:div.label (tr "dashboard.team-members")] diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs index a14ce5d8e..b01a7de45 100644 --- a/frontend/src/app/main/ui/hooks/resize.cljs +++ b/frontend/src/app/main/ui/hooks/resize.cljs @@ -52,9 +52,9 @@ (mf/set-ref-val! start-ref nil) (set! last-resize-type nil))) - on-mouse-move + on-pointer-move (mf/use-callback - (mf/deps min-val max-val) + (mf/deps min-val max-val negate?) (fn [event] (when (mf/ref-val dragging-ref) (let [start (mf/ref-val start-ref) @@ -68,7 +68,7 @@ (swap! storage assoc-in [::saved-resize current-file-id key] new-size)))))] {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture - :on-mouse-move on-mouse-move + :on-pointer-move on-pointer-move :parent-ref parent-ref :size @size-state})) @@ -81,25 +81,27 @@ callback (hooks/use-ref-callback callback) ;; We use the ref as a callback when the dom node is ready (or change) - node-ref (mf/use-fn - (fn [^js node] - (when (some? node) - (let [^js observer (mf/ref-val observer-ref) - ^js prev-val (mf/ref-val prev-val-ref)] + node-ref + (mf/use-fn + (fn [^js node] + (when (some? node) + (let [^js observer (mf/ref-val observer-ref) + ^js prev-val (mf/ref-val prev-val-ref)] - (when (and (not= prev-val node) (some? observer)) - (log/debug :action "disconnect" :js/prev-val prev-val :js/node node) - (.disconnect observer) - (mf/set-ref-val! observer-ref nil)) + (when (and (not= prev-val node) (some? observer)) + (log/debug :action "disconnect" :js/prev-val prev-val :js/node node) + (.disconnect observer) + (mf/set-ref-val! observer-ref nil)) - (when (and (not= prev-val node) (some? node)) - (let [^js observer (js/ResizeObserver. - #(callback last-resize-type (dom/get-client-size node)))] - (mf/set-ref-val! observer-ref observer) - (log/debug :action "observe" :js/node node :js/observer observer) - (.observe observer node)))) + (when (and (not= prev-val node) (some? node)) + (let [^js observer (js/ResizeObserver. + #(callback last-resize-type (dom/get-client-size node)))] + (mf/set-ref-val! observer-ref observer) + (log/debug :action "observe" :js/node node :js/observer observer) + (.observe observer node) + (callback last-resize-type (dom/get-client-size node))))) - (mf/set-ref-val! prev-val-ref node))))] + (mf/set-ref-val! prev-val-ref node))))] (mf/with-effect [] ;; On dismount we need to disconnect the current observer diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index b52b8fb41..edc033bfc 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -113,6 +113,17 @@ (def full-screen (icon-xref :full-screen)) (def full-screen-off (icon-xref :full-screen-off)) (def grid (icon-xref :grid)) +(def grid-justify-content-column-around (icon-xref :grid-justify-content-column-around)) +(def grid-justify-content-column-between (icon-xref :grid-justify-content-column-between)) +(def grid-justify-content-column-center (icon-xref :grid-justify-content-column-center)) +(def grid-justify-content-column-end (icon-xref :grid-justify-content-column-end)) +(def grid-justify-content-column-start (icon-xref :grid-justify-content-column-start)) +(def grid-justify-content-row-around (icon-xref :grid-justify-content-row-around)) +(def grid-justify-content-row-between (icon-xref :grid-justify-content-row-between)) +(def grid-justify-content-row-center (icon-xref :grid-justify-content-row-center)) +(def grid-justify-content-row-end (icon-xref :grid-justify-content-row-end)) +(def grid-justify-content-row-start (icon-xref :grid-justify-content-row-start)) +(def grid-layout-mode (icon-xref :grid-layout-mode)) (def grid-snap (icon-xref :grid-snap)) (def go-next (icon-xref :go-next)) (def go-prev (icon-xref :go-prev)) @@ -185,6 +196,7 @@ (def play (icon-xref :play)) (def plus (icon-xref :plus)) (def pointer-inner (icon-xref :pointer-inner)) +(def position-absolute (icon-xref :position-absolute)) (def position-bottom-center (icon-xref :position-bottom-center)) (def position-bottom-left (icon-xref :position-bottom-left)) (def position-bottom-right (icon-xref :position-bottom-right)) diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index 19752f0f9..26fe1bb46 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -10,9 +10,20 @@ [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.flex-layout :as gsl] + [app.common.geom.shapes.points :as gpo] [app.common.math :as mth] + [app.common.pages.helpers :as cph] + [app.common.types.modifiers :as ctm] [app.common.uuid :as uuid] + [app.main.data.workspace.modifiers :as dwm] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.cursors :as cur] [app.main.ui.formats :as fmt] + [app.main.ui.workspace.viewport.viewport-ref :refer [point->viewport]] + [app.util.dom :as dom] [rumext.v2 :as mf])) ;; ------------------------------------------------ @@ -43,6 +54,10 @@ (def distance-pill-width 50) (def distance-pill-height 16) (def distance-line-stroke 1) +(def warning-color "var(--color-warning)") +(def flex-display-pill-width 40) +(def flex-display-pill-height 20) +(def flex-display-pill-border-radius 4) ;; ------------------------------------------------ ;; HELPERS @@ -261,3 +276,600 @@ [:& distance-display {:from hover-selrect :to selected-selrect :zoom zoom :bounds bounds-selrect}]])]))) + +(mf/defc flex-display-pill [{:keys [x y width height font-size border-radius value color]}] + [:g.distance-pill + [:rect {:x x + :y y + :width width + :height height + :rx border-radius + :ry border-radius + :style {:fill color}}] + + [:text {:x (+ x (/ width 2)) + :y (+ y (/ height 2)) + :text-anchor "middle" + :text-align "center" + :dominant-baseline "central" + :style {:fill distance-text-color + :font-size font-size}} + (fmt/format-number (or value 0))]]) + + +(mf/defc padding-display [{:keys [frame-id zoom hover-all? hover-v? hover-h? padding-num padding on-pointer-enter on-pointer-leave + rect-data hover? selected? mouse-pos hover-value]}] + (let [resizing? (mf/use-var false) + start (mf/use-var nil) + original-value (mf/use-var 0) + negate? (true? (:resize-negate? rect-data)) + axis (:resize-axis rect-data) + + on-pointer-down + (mf/use-callback + (mf/deps frame-id rect-data padding-num) + (fn [event] + (dom/capture-pointer event) + (reset! resizing? true) + (reset! start (dom/get-client-position event)) + (reset! original-value (:initial-value rect-data)))) + + on-lost-pointer-capture + (mf/use-callback + (mf/deps frame-id padding-num padding) + (fn [event] + (dom/release-pointer event) + (reset! resizing? false) + (reset! start nil) + (reset! original-value 0) + (st/emit! (dwm/apply-modifiers)))) + + on-pointer-move + (mf/use-callback + (mf/deps frame-id padding-num padding hover-all? hover-v? hover-h?) + (fn [event] + (let [pos (dom/get-client-position event)] + (reset! mouse-pos (point->viewport pos)) + (when @resizing? + (let [delta (-> (gpt/to-vec @start pos) + (cond-> negate? gpt/negate) + (get axis)) + val (int (max (+ @original-value (/ delta zoom)) 0)) + layout-padding (cond + hover-all? (assoc padding :p1 val :p2 val :p3 val :p4 val) + hover-v? (assoc padding :p1 val :p3 val) + hover-h? (assoc padding :p2 val :p4 val) + :else (assoc padding padding-num val)) + + + layout-padding-type (if (= (:p1 padding) (:p2 padding) (:p3 padding) (:p4 padding)) :simple :multiple) + modifiers (dwm/create-modif-tree [frame-id] + (-> (ctm/empty) + (ctm/change-property :layout-padding layout-padding) + (ctm/change-property :layout-padding-type layout-padding-type)))] + (reset! hover-value val) + (st/emit! (dwm/set-modifiers modifiers)))))))] + + [:rect.padding-rect {:x (:x rect-data) + :y (:y rect-data) + :width (:width rect-data) + :height (:height rect-data) + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move + :style {:fill (if (or hover? selected?) distance-color "none") + :cursor (when (or hover? selected?) + (if (= (:resize-axis rect-data) :x) (cur/resize-ew 0) (cur/resize-ew 90))) + :opacity (if selected? 0.5 0.25)}}])) + +(mf/defc padding-rects [{:keys [frame zoom alt? shift?]}] + (let [frame-id (:id frame) + paddings-selected (mf/deref refs/workspace-paddings-selected) + hover-value (mf/use-var 0) + mouse-pos (mf/use-var nil) + hover (mf/use-var nil) + hover-all? (and (not (nil? @hover)) alt?) + hover-v? (and (or (= @hover :p1) (= @hover :p3)) shift?) + hover-h? (and (or (= @hover :p2) (= @hover :p4)) shift?) + padding (:layout-padding frame) + {:keys [width height x1 x2 y1 y2]} (:selrect frame) + on-pointer-enter (fn [hover-type val] + (reset! hover hover-type) + (reset! hover-value val)) + on-pointer-leave #(reset! hover nil) + pill-width (/ flex-display-pill-width zoom) + pill-height (/ flex-display-pill-height zoom) + hover? #(or hover-all? + (and (or (= % :p1) (= % :p3)) hover-v?) + (and (or (= % :p2) (= % :p4)) hover-h?) + (= @hover %)) + negate {:p1 (if (:flip-y frame) true false) + :p2 (if (:flip-x frame) true false) + :p3 (if (:flip-y frame) true false) + :p4 (if (:flip-x frame) true false)} + negate (cond-> negate + (not= :auto (:layout-item-h-sizing frame)) (assoc :p2 (not (:p2 negate))) + (not= :auto (:layout-item-v-sizing frame)) (assoc :p3 (not (:p3 negate)))) + + padding-rect-data {:p1 {:key (str frame-id "-p1") + :x x1 + :y (if (:flip-y frame) (- y2 (:p1 padding)) y1) + :width width + :height (:p1 padding) + :initial-value (:p1 padding) + :resize-type (if (:flip-y frame) :bottom :top) + :resize-axis :y + :resize-negate? (:p1 negate)} + :p2 {:key (str frame-id "-p2") + :x (if (:flip-x frame) x1 (- x2 (:p2 padding))) + :y y1 + :width (:p2 padding) + :height height + :initial-value (:p2 padding) + :resize-type :left + :resize-axis :x + :resize-negate? (:p2 negate)} + :p3 {:key (str frame-id "-p3") + :x x1 + :y (if (:flip-y frame) y1 (- y2 (:p3 padding))) + :width width + :height (:p3 padding) + :initial-value (:p3 padding) + :resize-type :bottom + :resize-axis :y + :resize-negate? (:p3 negate)} + :p4 {:key (str frame-id "-p4") + :x (if (:flip-x frame) (- x2 (:p4 padding)) x1) + :y y1 + :width (:p4 padding) + :height height + :initial-value (:p4 padding) + :resize-type (if (:flip-x frame) :right :left) + :resize-axis :x + :resize-negate? (:p4 negate)}}] + + [:g.paddings {:pointer-events "visible"} + (for [[padding-num rect-data] padding-rect-data] + [:& padding-display {:key (:key rect-data) + :frame-id frame-id + :zoom zoom + :hover-all? hover-all? + :hover-v? hover-v? + :hover-h? hover-h? + :padding padding + :mouse-pos mouse-pos + :hover-value hover-value + :padding-num padding-num + :on-pointer-enter (partial on-pointer-enter padding-num (get padding padding-num)) + :on-pointer-leave on-pointer-leave + :hover? (hover? padding-num) + :selected? (get paddings-selected padding-num) + :rect-data rect-data}]) + (when @hover + [:& flex-display-pill {:height pill-height + :width pill-width + :font-size (/ font-size zoom) + :border-radius (/ flex-display-pill-border-radius zoom) + :color distance-color + :x (:x @mouse-pos) + :y (- (:y @mouse-pos) pill-width) + :value @hover-value}])])) + +(mf/defc margin-display [{:keys [shape-id zoom hover-all? hover-v? hover-h? margin-num margin on-pointer-enter on-pointer-leave + rect-data hover? selected? mouse-pos hover-value]}] + (let [resizing? (mf/use-var false) + start (mf/use-var nil) + original-value (mf/use-var 0) + negate? (true? (:resize-negate? rect-data)) + axis (:resize-axis rect-data) + + on-pointer-down + (mf/use-callback + (mf/deps shape-id margin-num margin) + (fn [event] + (dom/capture-pointer event) + (reset! resizing? true) + (reset! start (dom/get-client-position event)) + (reset! original-value (:initial-value rect-data)))) + + on-lost-pointer-capture + (mf/use-callback + (mf/deps shape-id margin-num margin) + (fn [event] + (dom/release-pointer event) + (reset! resizing? false) + (reset! start nil) + (reset! original-value 0) + (st/emit! (dwm/apply-modifiers)))) + + on-pointer-move + (mf/use-callback + (mf/deps shape-id margin-num margin hover-all? hover-v? hover-h?) + (fn [event] + (let [pos (dom/get-client-position event)] + (reset! mouse-pos (point->viewport pos)) + (when @resizing? + (let [delta (-> (gpt/to-vec @start pos) + (cond-> negate? gpt/negate) + (get axis)) + val (int (max (+ @original-value (/ delta zoom)) 0)) + layout-item-margin (cond + hover-all? (assoc margin :m1 val :m2 val :m3 val :m4 val) + hover-v? (assoc margin :m1 val :m3 val) + hover-h? (assoc margin :m2 val :m4 val) + :else (assoc margin margin-num val)) + layout-item-margin-type (if (= (:m1 margin) (:m2 margin) (:m3 margin) (:m4 margin)) :simple :multiple) + modifiers (dwm/create-modif-tree [shape-id] + (-> (ctm/empty) + (ctm/change-property :layout-item-margin layout-item-margin) + (ctm/change-property :layout-item-margin-type layout-item-margin-type)))] + (reset! hover-value val) + (st/emit! (dwm/set-modifiers modifiers)))))))] + + [:rect.margin-rect {:x (:x rect-data) + :y (:y rect-data) + :width (:width rect-data) + :height (:height rect-data) + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move + :style {:fill (if (or hover? selected?) warning-color "none") + :cursor (when (or hover? selected?) + (if (= (:resize-axis rect-data) :x) (cur/resize-ew 0) (cur/resize-ew 90))) + :opacity (if selected? 0.5 0.25)}}])) + +(mf/defc margin-rects [{:keys [shape frame zoom alt? shift?]}] + (let [shape-id (:id shape) + pill-width (/ flex-display-pill-width zoom) + pill-height (/ flex-display-pill-height zoom) + margins-selected (mf/deref refs/workspace-margins-selected) + hover-value (mf/use-var 0) + mouse-pos (mf/use-var nil) + hover (mf/use-var nil) + hover-all? (and (not (nil? @hover)) alt?) + hover-v? (and (or (= @hover :m1) (= @hover :m3)) shift?) + hover-h? (and (or (= @hover :m2) (= @hover :m4)) shift?) + margin (:layout-item-margin shape) + {:keys [width height x1 x2 y1 y2]} (:selrect shape) + on-pointer-enter (fn [hover-type val] + (reset! hover hover-type) + (reset! hover-value val)) + on-pointer-leave #(reset! hover nil) + hover? #(or hover-all? + (and (or (= % :m1) (= % :m3)) hover-v?) + (and (or (= % :m2) (= % :m4)) hover-h?) + (= @hover %)) + margin-display-data {:m1 {:key (str shape-id "-m1") + :x x1 + :y (if (:flip-y frame) y2 (- y1 (:m1 margin))) + :width width + :height (:m1 margin) + :initial-value (:m1 margin) + :resize-type :top + :resize-axis :y + :resize-negate? (:flip-y frame)} + :m2 {:key (str shape-id "-m2") + :x (if (:flip-x frame) (- x1 (:m2 margin)) x2) + :y y1 + :width (:m2 margin) + :height height + :initial-value (:m2 margin) + :resize-type :left + :resize-axis :x + :resize-negate? (:flip-x frame)} + :m3 {:key (str shape-id "-m3") + :x x1 + :y (if (:flip-y frame) (- y1 (:m3 margin)) y2) + :width width + :height (:m3 margin) + :initial-value (:m3 margin) + :resize-type :top + :resize-axis :y + :resize-negate? (:flip-y frame)} + :m4 {:key (str shape-id "-m4") + :x (if (:flip-x frame) x2 (- x1 (:m4 margin))) + :y y1 + :width (:m4 margin) + :height height + :initial-value (:m4 margin) + :resize-type :left + :resize-axis :x + :resize-negate? (:flip-x frame)}}] + + [:g.margins {:pointer-events "visible"} + (for [[margin-num rect-data] margin-display-data] + [:& margin-display + {:key (:key rect-data) + :shape-id shape-id + :zoom zoom + :hover-all? hover-all? + :hover-v? hover-v? + :hover-h? hover-h? + :margin-num margin-num + :margin margin + :on-pointer-enter (partial on-pointer-enter margin-num (get margin margin-num)) + :on-pointer-leave on-pointer-leave + :rect-data rect-data + :hover? (hover? margin-num) + :selected? (get margins-selected margin-num) + :mouse-pos mouse-pos + :hover-value hover-value}]) + + (when @hover + [:& flex-display-pill {:height pill-height + :width pill-width + :font-size (/ font-size zoom) + :border-radius (/ flex-display-pill-border-radius zoom) + :color warning-color + :x (:x @mouse-pos) + :y (- (:y @mouse-pos) pill-width) + :value @hover-value}])])) + +(mf/defc gap-display [{:keys [frame-id zoom gap-type gap on-pointer-enter on-pointer-leave + rect-data hover? selected? mouse-pos hover-value]}] + (let [resizing (mf/use-var nil) + start (mf/use-var nil) + original-value (mf/use-var 0) + negate? (:resize-negate? rect-data) + axis (:resize-axis rect-data) + + on-pointer-down + (mf/use-callback + (mf/deps frame-id gap-type gap) + (fn [event] + (dom/capture-pointer event) + (reset! resizing gap-type) + (reset! start (dom/get-client-position event)) + (reset! original-value (:initial-value rect-data)))) + + on-lost-pointer-capture + (mf/use-callback + (mf/deps frame-id gap-type gap) + (fn [event] + (dom/release-pointer event) + (reset! resizing nil) + (reset! start nil) + (reset! original-value 0) + (st/emit! (dwm/apply-modifiers)))) + + on-pointer-move + (mf/use-callback + (mf/deps frame-id gap-type gap) + (fn [event] + (let [pos (dom/get-client-position event)] + (reset! mouse-pos (point->viewport pos)) + (when (= @resizing gap-type) + (let [delta (-> (gpt/to-vec @start pos) + (cond-> negate? gpt/negate) + (get axis)) + val (int (max (+ @original-value (/ delta zoom)) 0)) + layout-gap (assoc gap gap-type val) + modifiers (dwm/create-modif-tree [frame-id] (ctm/change-property (ctm/empty) :layout-gap layout-gap))] + + (reset! hover-value val) + (st/emit! (dwm/set-modifiers modifiers)))))))] + + [:rect.gap-rect {:x (:x rect-data) + :y (:y rect-data) + :width (:width rect-data) + :height (:height rect-data) + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move + :style {:fill (if (or hover? selected?) distance-color "none") + :cursor (when (or hover? selected?) + (if (= (:resize-axis rect-data) :x) (cur/resize-ew 0) (cur/resize-ew 90))) + :opacity (if selected? 0.5 0.25)}}])) + +(mf/defc gap-rects [{:keys [frame zoom]}] + (let [frame-id (:id frame) + saved-dir (:layout-flex-dir frame) + is-col? (or (= :column saved-dir) (= :column-reverse saved-dir)) + flip-x (:flip-x frame) + flip-y (:flip-y frame) + pill-width (/ flex-display-pill-width zoom) + pill-height (/ flex-display-pill-height zoom) + workspace-modifiers (mf/deref refs/workspace-modifiers) + gap-selected (mf/deref refs/workspace-gap-selected) + hover (mf/use-var nil) + hover-value (mf/use-var 0) + mouse-pos (mf/use-var nil) + padding (:layout-padding frame) + gap (:layout-gap frame) + {:keys [width height x1 y1]} (:selrect frame) + on-pointer-enter (fn [hover-type val] + (reset! hover hover-type) + (reset! hover-value val)) + + on-pointer-leave #(reset! hover nil) + negate {:column-gap (if flip-x true false) + :row-gap (if flip-y true false)} + + objects (wsh/lookup-page-objects @st/state) + children (->> (cph/get-immediate-children objects frame-id) + (remove :layout-item-absolute) + (remove :hidden)) + + children-to-display (if (or (= :row-reverse saved-dir) + (= :column-reverse saved-dir)) + (drop-last children) + (rest children)) + children-to-display (->> children-to-display + (map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers])))) + + wrap-blocks + (let [block-children (->> children + (map #(vector (gpo/parent-coords-bounds (:points %) (:points frame)) %))) + layout-data (gsl/calc-layout-data frame block-children (:points frame)) + layout-bounds (:layout-bounds layout-data) + xv #(gpo/start-hv layout-bounds %) + yv #(gpo/start-vv layout-bounds %)] + (for [{:keys [start-p line-width line-height layout-gap-row layout-gap-col num-children]} (:layout-lines layout-data)] + (let [line-width (if is-col? line-width (+ line-width (* (dec num-children) layout-gap-row))) + line-height (if is-col? (+ line-height (* (dec num-children) layout-gap-col)) line-height) + end-p (-> start-p (gpt/add (xv line-width)) (gpt/add (yv line-height)))] + {:x1 (min (:x start-p) (:x end-p)) + :y1 (min (:y start-p) (:y end-p)) + :x2 (max (:x start-p) (:x end-p)) + :y2 (max (:y start-p) (:y end-p))}))) + + block-contains + (fn [x y block] + (if is-col? + (<= (:x1 block) x (:x2 block)) + (<= (:y1 block) y (:y2 block)))) + + get-container-block + (fn [shape] + (let [selrect (:selrect shape) + x (/ (+ (:x1 selrect) (:x2 selrect)) 2) + y (/ (+ (:y1 selrect) (:y2 selrect)) 2)] + (->> wrap-blocks + (filter #(block-contains x y %)) + first))) + + create-cgdd + (fn [shape] + (let [block (get-container-block shape) + x (if flip-x + (- (:x1 (:selrect shape)) + (get-in shape [:layout-item-margin :m2]) + (:column-gap gap)) + (+ (:x2 (:selrect shape)) (get-in shape [:layout-item-margin :m2]))) + y (:y1 block) + h (- (:y2 block) (:y1 block))] + {:x x + :y y + :height h + :width (:column-gap gap) + :initial-value (:column-gap gap) + :resize-type :left + :resize-axis :x + :resize-negate? (:column-gap negate) + :gap-type (if is-col? :row-gap :column-gap)})) + + create-cgdd-block + (fn [block] + (let [x (if flip-x + (- (:x1 block) (:column-gap gap)) + (:x2 block)) + y (if flip-y + (+ y1 (:p3 padding)) + (+ y1 (:p1 padding))) + h (- height (+ (:p1 padding) (:p3 padding)))] + {:x x + :y y + :width (:column-gap gap) + :height h + :initial-value (:column-gap gap) + :resize-type :left + :resize-axis :x + :resize-negate? (:column-gap negate) + :gap-type (if is-col? :column-gap :row-gap)})) + + create-rgdd + (fn [shape] + (let [block (get-container-block shape) + x (:x1 block) + y (if flip-y + (- (:y1 (:selrect shape)) + (get-in shape [:layout-item-margin :m3]) + (:row-gap gap)) + (+ (:y2 (:selrect shape)) (get-in shape [:layout-item-margin :m3]))) + w (- (:x2 block) (:x1 block))] + {:x x + :y y + :width w + :height (:row-gap gap) + :initial-value (:row-gap gap) + :resize-type :bottom + :resize-axis :y + :resize-negate? (:row-gap negate) + :gap-type (if is-col? :row-gap :column-gap)})) + + create-rgdd-block + (fn [block] + (let [x (if flip-x + (+ x1 (:p2 padding)) + (+ x1 (:p4 padding))) + y (if flip-y + (- (:y1 block) (:row-gap gap)) + (:y2 block)) + w (- width (+ (:p2 padding) (:p4 padding)))] + {:x x + :y y + :width w + :height (:row-gap gap) + :initial-value (:row-gap gap) + :resize-type :bottom + :resize-axis :y + :resize-negate? (:row-gap negate) + :gap-type (if is-col? :column-gap :row-gap)})) + + display-blocks (if is-col? + (->> (drop-last wrap-blocks) + (map create-cgdd-block)) + (->> (drop-last wrap-blocks) + (map create-rgdd-block))) + + display-children (if is-col? + (->> children-to-display + (map create-rgdd)) + (->> children-to-display + (map create-cgdd)))] + + [:g.gaps {:pointer-events "visible"} + [:* + (for [[index display-item] (d/enumerate (concat display-blocks display-children))] + (let [gap-type (:gap-type display-item)] + [:& gap-display {:key (str frame-id index) + :frame-id frame-id + :zoom zoom + :gap-type gap-type + :gap gap + :on-pointer-enter (partial on-pointer-enter gap-type (get gap gap-type)) + :on-pointer-leave on-pointer-leave + :rect-data display-item + :hover? (= @hover gap-type) + :selected? (= gap-selected gap-type) + :mouse-pos mouse-pos + :hover-value hover-value}]))] + + (when @hover + [:& flex-display-pill {:height pill-height + :width pill-width + :font-size (/ font-size zoom) + :border-radius (/ flex-display-pill-border-radius zoom) + :color distance-color + :x (:x @mouse-pos) + :y (- (:y @mouse-pos) pill-width) + :value @hover-value}])])) + +(mf/defc padding + [{:keys [frame zoom alt? shift?]}] + (when frame + [:g.measurement-gaps {:pointer-events "none"} + [:g.hover-shapes + [:& padding-rects {:frame frame :zoom zoom :alt? alt? :shift? shift?}]]])) + +(mf/defc gap + [{:keys [frame zoom]}] + (when frame + [:g.measurement-gaps {:pointer-events "none"} + [:g.hover-shapes + [:& gap-rects {:frame frame :zoom zoom}]]])) + +(mf/defc margin + [{:keys [shape parent zoom alt? shift?]}] + (when shape + [:g.measurement-gaps {:pointer-events "none"} + [:g.hover-shapes + [:& margin-rects {:shape shape :frame parent :zoom zoom :alt? alt? :shift? shift?}]]])) + + diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index 47cca1645..94831f951 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -66,7 +66,7 @@ (events/listen js/document EventType.KEYDOWN handle-keydown) ;; Changing to js/document breaks the color picker - (events/listen (dom/get-root) EventType.CLICK handle-click-outside) + (events/listen (dom/get-root) EventType.POINTERDOWN handle-click-outside) (events/listen js/document EventType.CONTEXTMENU handle-click-outside)]] #(doseq [key keys] diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index b1a1e5342..f34fab29b 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -185,6 +185,7 @@ :auto-focus? true :trim true :valid-item-fn us/parse-email + :caution-item-fn #{} :on-submit on-submit :label (tr "modals.invite-member.emails")}]] diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs index 840b20c8c..a62b76a83 100644 --- a/frontend/src/app/main/ui/releases.cljs +++ b/frontend/src/app/main/ui/releases.cljs @@ -18,6 +18,7 @@ [app.main.ui.releases.v1-15] [app.main.ui.releases.v1-16] [app.main.ui.releases.v1-17] + [app.main.ui.releases.v1-18] [app.main.ui.releases.v1-4] [app.main.ui.releases.v1-5] [app.main.ui.releases.v1-6] @@ -87,4 +88,4 @@ (defmethod rc/render-release-notes "0.0" [params] - (rc/render-release-notes (assoc params :version "1.17"))) + (rc/render-release-notes (assoc params :version "1.18"))) diff --git a/frontend/src/app/main/ui/releases/v1_18.cljs b/frontend/src/app/main/ui/releases/v1_18.cljs new file mode 100644 index 000000000..22ae6b153 --- /dev/null +++ b/frontend/src/app/main/ui/releases/v1_18.cljs @@ -0,0 +1,108 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.releases.v1-18 + (:require + [app.main.ui.releases.common :as c] + [rumext.v2 :as mf])) + +(defmethod c/render-release-notes "1.18" + [{:keys [slide klass next finish navigate version]}] + (mf/html + (case @slide + :start + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/onboarding-version.jpg" :border "0" :alt "What's new release 1.18"}]] + [:div.modal-right + [:div.modal-title + [:h2 "What's new?"]] + [:span.release "Version " version] + [:div.modal-content + [:p "On this 1.18 release we make Flex Layout even more powerful with smart spacing, absolute position and z-index management."] + [:p "We also continued implementing accessibility improvements to make Penpot more inclusive and published stability and performance enhancements."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"]]] + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]] + + 0 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/1.18-spacing.gif" :border "0" :alt "Spacing management"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Spacing management for Flex layoutFlex-Layout"]] + [:div.modal-content + [:p "Managing Flex Layout spacing is much more intuitive now. Visualize paddings, margins and gaps and drag to resize them."] + [:p "And not only that, when creating Flex layouts, the spacing is predicted, helping you to maintain your design composition."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& c/navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 1 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/1.18-absolute.gif" :border "0" :alt "Position absolute feature"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Absolute position elements in Flex layout"]] + [:div.modal-content + [:p "Sometimes you need to freely position an element in a specific place regardless of the size of the layout where it belongs."] + [:p "Now you can exclude elements from the Flex layout flow using absolute position."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& c/navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 2 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/1.18-z-index.gif" :border "0" :alt "Z-index feature"}]] + [:div.modal-right + [:div.modal-title + [:h2 "More on Flex layout: z-index"]] + [:div.modal-content + [:p "With the new z-index option you can decide the order of overlapping elements while maintaining the layers order."] + [:p "This is another capability that brings Penpot Flex layout even closer to the power of CSS standards."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click next} "Continue"] + [:& c/navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]] + + 3 + [:div.modal-overlay + [:div.animated {:class @klass} + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/features/1.18-scale.gif" :border "0" :alt "Scale content proportionally"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Scale content proportionally affects strokes, shadows, blurs and corners"]] + [:div.modal-content + [:p "Now you can resize your layers and groups preserving their aspect ratio while scaling their properties proportionally, including strokes, shadows, blurs and corners."] + [:p "Activate the scale tool by pressing K and scale your elements, maintaining their visual aspect."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click finish} "Start!"] + [:& c/navigation-bullets + {:slide @slide + :navigate navigate + :total 4}]]]]]]))) diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index 0825f47e9..222ae33e2 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -95,7 +95,7 @@ ;; We just recheck with an additional profile request; this avoids ;; some race conditions that causes unexpected redirects on ;; invitations workflows (and probably other cases). - (->> (rp/query! :profile) + (->> (rp/command! :get-profile) (rx/subs (fn [{:keys [id] :as profile}] (if (= id uuid/zero) (st/emit! (rt/nav :auth-login)) diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index 10f1d88ac..e198088e6 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -55,7 +55,7 @@ (fn [form _] (reset! loading true) (let [data (:clean-data @form)] - (->> (rp/command! :send-feedback data) + (->> (rp/command! :send-user-feedback data) (rx/subs on-succes on-error)))))] [:& fm/form {:class "feedback-form" diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs index 940e4fd8f..9e792d545 100644 --- a/frontend/src/app/main/ui/settings/password.cljs +++ b/frontend/src/app/main/ui/settings/password.cljs @@ -32,7 +32,10 @@ (defn- on-success [form] (reset! form nil) - (let [msg (tr "dashboard.notifications.password-saved")] + (let [password-old-node (dom/get-element "password-old") + msg (tr "dashboard.notifications.password-saved")] + (dom/clean-value! password-old-node) + (dom/focus! password-old-node) (st/emit! (dm/success msg)))) (defn- on-submit @@ -45,7 +48,7 @@ (s/def ::password-1 ::us/not-empty-string) (s/def ::password-2 ::us/not-empty-string) -(s/def ::password-old ::us/not-empty-string) +(s/def ::password-old (s/nilable ::us/string)) (defn- password-equality [errors data] @@ -66,9 +69,10 @@ (mf/defc password-form [{:keys [locale] :as props}] - (let [form (fm/use-form :spec ::password-form - :validators [password-equality] - :initial {})] + (let [initial (mf/use-memo (constantly {:password-old nil})) + form (fm/use-form :spec ::password-form + :validators [password-equality] + :initial initial)] [:& fm/form {:class "password-form" :on-submit on-submit :form form} @@ -77,6 +81,7 @@ [:& fm/input {:type "password" :name :password-old + :auto-focus? true :label (t locale "labels.old-password")}]] [:div.fields-row diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 2c0be8a8b..ebc89b5d2 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -38,20 +38,27 @@ [:use {:href (str "#" shape-id)}]])) (mf/defc outer-stroke-mask - [{:keys [shape render-id index]}] + [{:keys [shape stroke render-id index]}] (let [suffix (if index (str "-" index) "") stroke-mask-id (str "outer-stroke-" render-id "-" (:id shape) suffix) shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix) - stroke-width (case (:stroke-alignment shape :center) - :center (/ (:stroke-width shape 0) 2) - :outer (:stroke-width shape 0) + stroke-width (case (:stroke-alignment stroke :center) + :center (/ (:stroke-width stroke 0) 2) + :outer (:stroke-width stroke 0) 0) - margin (gsb/shape-stroke-margin shape stroke-width) - bounding-box (-> (gsh/points->selrect (:points shape)) - (update :x - (+ stroke-width margin)) - (update :y - (+ stroke-width margin)) - (update :width + (* 2 (+ stroke-width margin))) - (update :height + (* 2 (+ stroke-width margin))))] + margin (gsb/shape-stroke-margin stroke stroke-width) + + selrect + (if (cph/text-shape? shape) + (gsh/position-data-selrect shape) + (gsh/points->selrect (:points shape))) + + bounding-box + (-> selrect + (update :x - (+ stroke-width margin)) + (update :y - (+ stroke-width margin)) + (update :width + (* 2 (+ stroke-width margin))) + (update :height + (* 2 (+ stroke-width margin))))] [:mask {:id stroke-mask-id :x (:x bounding-box) @@ -67,17 +74,17 @@ :stroke "none"}}]])) (mf/defc cap-markers - [{:keys [shape render-id index]}] + [{:keys [stroke render-id index]}] (let [marker-id-prefix (str "marker-" render-id) - cap-start (:stroke-cap-start shape) - cap-end (:stroke-cap-end shape) + cap-start (:stroke-cap-start stroke) + cap-end (:stroke-cap-end stroke) - stroke-color (if (:stroke-color-gradient shape) + stroke-color (if (:stroke-color-gradient stroke) (str/format "url(#%s)" (str "stroke-color-gradient_" render-id "_" index)) - (:stroke-color shape)) + (:stroke-color stroke)) - stroke-opacity (when-not (:stroke-color-gradient shape) - (:stroke-opacity shape))] + stroke-opacity (when-not (:stroke-color-gradient stroke) + (:stroke-opacity stroke))] [:* (when (or (= cap-start :line-arrow) (= cap-end :line-arrow)) @@ -169,36 +176,37 @@ [:rect {:x 3 :y 2.5 :width 0.5 :height 1}]])])) (mf/defc stroke-defs - [{:keys [shape render-id index]}] + [{:keys [shape stroke render-id index]}] (let [open-path? (and (= :path (:type shape)) (gsh/open-path? shape))] [:* - (cond (some? (:stroke-color-gradient shape)) - (case (:type (:stroke-color-gradient shape)) + (cond (some? (:stroke-color-gradient stroke)) + (case (:type (:stroke-color-gradient stroke)) :linear [:> grad/linear-gradient #js {:id (str (name :stroke-color-gradient) "_" render-id "_" index) - :gradient (:stroke-color-gradient shape) + :gradient (:stroke-color-gradient stroke) :shape shape}] :radial [:> grad/radial-gradient #js {:id (str (name :stroke-color-gradient) "_" render-id "_" index) - :gradient (:stroke-color-gradient shape) + :gradient (:stroke-color-gradient stroke) :shape shape}])) (cond (and (not open-path?) - (= :inner (:stroke-alignment shape :center)) - (> (:stroke-width shape 0) 0)) + (= :inner (:stroke-alignment stroke :center)) + (> (:stroke-width stroke 0) 0)) [:& inner-stroke-clip-path {:shape shape :render-id render-id :index index}] (and (not open-path?) - (= :outer (:stroke-alignment shape :center)) - (> (:stroke-width shape 0) 0)) + (= :outer (:stroke-alignment stroke :center)) + (> (:stroke-width stroke 0) 0)) [:& outer-stroke-mask {:shape shape + :stroke stroke :render-id render-id :index index}] - (or (some? (:stroke-cap-start shape)) - (some? (:stroke-cap-end shape))) - [:& cap-markers {:shape shape + (or (some? (:stroke-cap-start stroke)) + (some? (:stroke-cap-end stroke))) + [:& cap-markers {:stroke stroke :render-id render-id :index index}])])) @@ -216,8 +224,9 @@ base-props (obj/get child "props") elem-name (obj/get child "type") shape (obj/get props "shape") + stroke (obj/get props "stroke") index (obj/get props "index") - stroke-width (:stroke-width shape) + stroke-width (:stroke-width stroke) suffix (if index (str "-" index) "") stroke-mask-id (str "outer-stroke-" render-id "-" (:id shape) suffix) @@ -225,7 +234,7 @@ [:g.outer-stroke-shape [:defs - [:& stroke-defs {:shape shape :render-id render-id :index index}] + [:& stroke-defs {:shape shape :stroke stroke :render-id render-id :index index}] [:> elem-name (-> (obj/clone base-props) (obj/set! "id" shape-id) (obj/set! @@ -258,10 +267,11 @@ base-props (obj/get child "props") elem-name (obj/get child "type") shape (obj/get props "shape") + stroke (obj/get props "stroke") index (obj/get props "index") transform (obj/get base-props "transform") - stroke-width (:stroke-width shape 0) + stroke-width (:stroke-width stroke 0) suffix (if index (str "-" index) "") clip-id (str "inner-stroke-" render-id "-" (:id shape) suffix) @@ -275,7 +285,7 @@ [:g.inner-stroke-shape {:transform transform} [:defs - [:& stroke-defs {:shape shape :render-id render-id :index index}] + [:& stroke-defs {:shape shape :stroke stroke :render-id render-id :index index}] [:> elem-name shape-props]] [:use {:href (str "#" shape-id) @@ -290,33 +300,34 @@ {::mf/wrap-props false} [props] - (let [child (obj/get props "children") - shape (obj/get props "shape") + (let [child (obj/get props "children") + shape (obj/get props "shape") + stroke (obj/get props "stroke") + render-id (mf/use-ctx muc/render-id) index (obj/get props "index") - stroke-width (:stroke-width shape 0) - stroke-style (:stroke-style shape :none) - stroke-position (:stroke-alignment shape :center) + stroke-width (:stroke-width stroke 0) + stroke-style (:stroke-style stroke :none) + stroke-position (:stroke-alignment stroke :center) has-stroke? (and (> stroke-width 0) (not= stroke-style :none)) - closed? (or (not= :path (:type shape)) - (not (gsh/open-path? shape))) + closed? (or (not= :path (:type shape)) (not (gsh/open-path? shape))) inner? (= :inner stroke-position) outer? (= :outer stroke-position)] (cond (and has-stroke? inner? closed?) - [:& inner-stroke {:shape shape :index index} + [:& inner-stroke {:shape shape :stroke stroke :index index} child] (and has-stroke? outer? closed?) - [:& outer-stroke {:shape shape :index index} + [:& outer-stroke {:shape shape :stroke stroke :index index} child] :else [:g.stroke-shape [:defs - [:& stroke-defs {:shape shape :render-id render-id :index index}]] + [:& stroke-defs {:shape shape :stroke stroke :render-id render-id :index index}]] child]))) (defn build-fill-props [shape child position render-id] @@ -426,6 +437,7 @@ [props] (let [child (obj/get props "children") shape (obj/get props "shape") + elem-name (obj/get child "type") render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-id)) stroke-id (dm/fmt "strokes-%" (:id shape)) @@ -445,9 +457,8 @@ (d/not-empty? (:strokes shape)) [:> :g stroke-props (for [[index value] (-> (d/enumerate (:strokes shape)) reverse)] - (let [props (build-stroke-props index child value render-id) - shape (assoc value :points (:points shape))] - [:& shape-custom-stroke {:shape shape :index index :key (dm/str index "-" stroke-id)} + (let [props (build-stroke-props index child value render-id)] + [:& shape-custom-stroke {:shape shape :stroke value :index index :key (dm/str index "-" stroke-id)} [:> elem-name props]]))])])) (mf/defc shape-custom-strokes diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 1708c3640..11be6ba00 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -8,6 +8,8 @@ (:require [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as cph] + [app.common.types.shape.layout :as ctl] [app.config :as cf] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] @@ -125,7 +127,10 @@ {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - childs (unchecked-get props "childs")] + childs (unchecked-get props "childs") + childs (cond-> childs + (ctl/any-layout? shape) + (cph/sort-layout-children-z-index))] [:> frame-container props [:g.frame-children {:opacity (:opacity shape)} (for [item childs] diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index 64edf8503..e508f0bbf 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -69,16 +69,21 @@ [:> :g group-props (for [[index data] (d/enumerate position-data)] - (let [alignment-bl (when (cf/check-browser? :safari) "text-before-edge") - dominant-bl (when-not (cf/check-browser? :safari) "text-before-edge") - rtl? (= "rtl" (:direction data)) + (let [rtl? (= "rtl" (:direction data)) + + browser-props + (cond + (cf/check-browser? :safari) + #js {:dominantBaseline "hanging" + :dy "0.2em" + :y (- (:y data) (:height data))}) + props (-> #js {:key (dm/str "text-" (:id shape) "-" index) :x (if rtl? (+ (:x data) (:width data)) (:x data)) - :y (- (:y data) (:height data)) + :y (:y data) + :dominantBaseline "ideographic" :textLength (:width data) :lengthAdjust "spacingAndGlyphs" - :alignmentBaseline alignment-bl - :dominantBaseline dominant-bl :style (-> #js {:fontFamily (:font-family data) :fontSize (:font-size data) :fontWeight (:font-weight data) @@ -88,7 +93,9 @@ :fontStyle (:font-style data) :direction (:direction data) :whiteSpace "pre"} - (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))}) + (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))} + (cond-> browser-props + (obj/merge! browser-props))) shape (assoc shape :fills (:fills data))] [:& (mf/provider muc/render-id) {:key index :value (str render-id "_" (:id shape) "_" index)} diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 4b7de5729..723b927f3 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.viewer + (:import goog.events.EventType) (:require [app.common.colors :as clr] [app.common.data :as d] @@ -34,6 +35,7 @@ [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] [app.util.dom :as dom] [app.util.dom.normalize-wheel :as nw] + [app.util.globals :as globals] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.webapi :as wapi] @@ -329,7 +331,13 @@ (dom/stop-propagation event) (if shift? (dom/set-h-scroll-pos! viewer-section new-scroll-pos) - (dom/set-scroll-pos! viewer-section new-scroll-pos)))))))] + (dom/set-scroll-pos! viewer-section new-scroll-pos))))))) + + on-exit-fullscreen + (mf/use-callback + (fn [] + (when (not (dom/fullscreen?)) + (st/emit! (dv/exit-fullscreen)))))] (hooks/use-shortcuts ::viewer sc/shortcuts) (when (nil? page) @@ -348,11 +356,19 @@ (mf/with-effect [] (dom/set-html-theme-color clr/gray-50 "dark") - (let [key1 (events/listen js/window "click" on-click) - key2 (events/listen (mf/ref-val viewer-section-ref) "wheel" on-wheel #js {"passive" false})] + (let [events + [(events/listen globals/window EventType.CLICK on-click) + (events/listen (mf/ref-val viewer-section-ref) EventType.WHEEL on-wheel #js {"passive" false})]] + + (doseq [event dom/fullscreen-events] + (.addEventListener globals/document event on-exit-fullscreen false)) + (fn [] - (events/unlistenByKey key1) - (events/unlistenByKey key2)))) + (doseq [key events] + (events/unlistenByKey key)) + + (doseq [event dom/fullscreen-events] + (.removeEventListener globals/document event on-exit-fullscreen))))) (mf/use-effect (fn [] diff --git a/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs b/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs index 276032db8..4de4d9fd8 100644 --- a/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/left_sidebar.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.viewer.inspect.left-sidebar (:require [app.common.data :as d] + [app.common.types.shape.layout :as ctl] [app.main.data.viewer :as dv] [app.main.store :as st] [app.main.ui.components.shape-icon :as si] @@ -34,7 +35,7 @@ (make-collapsed-iref id)) expanded? (not (mf/deref collapsed-iref)) - + absolute? (ctl/layout-absolute? item) toggle-collapse (fn [event] (dom/stop-propagation event) @@ -71,7 +72,10 @@ [:div.element-list-body {:class (dom/classnames :selected selected? :icon-layer (= (:type item) :icon)) :on-click select-shape} - [:& si/element-icon {:shape item}] + [:div.icon + (when absolute? + [:div.absolute i/position-absolute]) + [:& si/element-icon {:shape item}]] [:& layer-name {:shape item :disabled-double-click true}] (when (and (not disable-collapse?) (:shapes item)) diff --git a/frontend/src/app/main/ui/viewer/inspect/render.cljs b/frontend/src/app/main/ui/viewer/inspect/render.cljs index 1c7053b7d..da8e79d80 100644 --- a/frontend/src/app/main/ui/viewer/inspect/render.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/render.cljs @@ -64,8 +64,8 @@ (if render-wrapper? [:> shape-container {:shape shape - :on-mouse-enter (handle-hover-shape shape true) - :on-mouse-leave (handle-hover-shape shape false) + :on-pointer-enter (handle-hover-shape shape true) + :on-pointer-leave (handle-hover-shape shape false) :on-click (select-shape shape)} [:& component {:shape shape :frame frame diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index c364d38c1..619e0a159 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -46,6 +46,7 @@ (defn- activate-interaction [interaction shape base-frame frame-offset objects overlays] + (case (:action-type interaction) :navigate (when-let [frame-id (:destination interaction)] @@ -69,6 +70,8 @@ overlays-ids (set (map :id overlays)) relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame) position (ctsi/calc-overlay-position interaction + shape + viewer-objects relative-to-shape relative-to-base-frame dest-frame @@ -90,6 +93,8 @@ overlays-ids (set (map :id overlays)) relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame) position (ctsi/calc-overlay-position interaction + shape + objects relative-to-shape relative-to-base-frame dest-frame @@ -154,6 +159,8 @@ overlays-ids (set (map :id overlays)) relative-to-base-frame (find-relative-to-base-frame relative-to-shape objects overlays-ids base-frame) position (ctsi/calc-overlay-position interaction + shape + objects relative-to-shape relative-to-base-frame dest-frame @@ -166,7 +173,7 @@ (:animation interaction))))) nil)) -(defn- on-mouse-down +(defn- on-pointer-down [event shape base-frame frame-offset objects overlays] (let [interactions (->> (:interactions shape) (filter #(or (= (:event-type %) :click) @@ -176,7 +183,7 @@ (doseq [interaction interactions] (activate-interaction interaction shape base-frame frame-offset objects overlays))))) -(defn- on-mouse-up +(defn- on-pointer-up [event shape base-frame frame-offset objects overlays] (let [interactions (->> (:interactions shape) (filter #(= (:event-type %) :mouse-press)))] @@ -185,7 +192,7 @@ (doseq [interaction interactions] (deactivate-interaction interaction shape base-frame frame-offset objects overlays))))) -(defn- on-mouse-enter +(defn- on-pointer-enter [event shape base-frame frame-offset objects overlays] (let [interactions (->> (:interactions shape) (filter #(or (= (:event-type %) :mouse-enter) @@ -195,7 +202,7 @@ (doseq [interaction interactions] (activate-interaction interaction shape base-frame frame-offset objects overlays))))) -(defn- on-mouse-leave +(defn- on-pointer-leave [event shape base-frame frame-offset objects overlays] (let [interactions (->> (:interactions shape) (filter #(= (:event-type %) :mouse-leave))) @@ -259,21 +266,21 @@ svg-element? (and (= :svg-raw (:type shape)) (not= :svg (get-in shape [:content :tag]))) - on-mouse-down + on-pointer-down (mf/use-fn (mf/deps shape base-frame frame-offset objects) - #(on-mouse-down % shape base-frame frame-offset objects overlays)) + #(on-pointer-down % shape base-frame frame-offset objects overlays)) - on-mouse-up + on-pointer-up (mf/use-fn (mf/deps shape base-frame frame-offset objects) - #(on-mouse-up % shape base-frame frame-offset objects overlays)) + #(on-pointer-up % shape base-frame frame-offset objects overlays)) - on-mouse-enter + on-pointer-enter (mf/use-fn (mf/deps shape base-frame frame-offset objects) - #(on-mouse-enter % shape base-frame frame-offset objects overlays)) + #(on-pointer-enter % shape base-frame frame-offset objects overlays)) - on-mouse-leave + on-pointer-leave (mf/use-fn (mf/deps shape base-frame frame-offset objects) - #(on-mouse-leave % shape base-frame frame-offset objects overlays))] + #(on-pointer-leave % shape base-frame frame-offset objects overlays))] (mf/with-effect [] @@ -283,10 +290,10 @@ (if-not svg-element? [:> shape-container {:shape shape :cursor (when (ctsi/actionable? interactions) "pointer") - :on-mouse-down on-mouse-down - :on-mouse-up on-mouse-up - :on-mouse-enter on-mouse-enter - :on-mouse-leave on-mouse-leave} + :on-pointer-down on-pointer-down + :on-pointer-up on-pointer-up + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} [:& component {:shape shape :frame frame diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 8aba4efbb..751ebdf8c 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace + (:import goog.events.EventType) (:require [app.common.colors :as clr] [app.common.data.macros :as dm] @@ -16,6 +17,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] + [app.main.ui.hooks :as hooks] [app.main.ui.hooks.resize :refer [use-resize-observer]] [app.main.ui.icons :as i] [app.main.ui.workspace.colorpalette :refer [colorpalette]] @@ -31,9 +33,11 @@ [app.main.ui.workspace.textpalette :refer [textpalette]] [app.main.ui.workspace.viewport :refer [viewport]] [app.util.dom :as dom] + [app.util.globals :as globals] [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] [debug :refer [debug?]] + [goog.events :as events] [okulary.core :as l] [rumext.v2 :as mf])) @@ -45,6 +49,7 @@ (let [selected (mf/deref refs/selected-shapes) file (obj/get props "file") layout (obj/get props "layout") + page-id (obj/get props "page-id") {:keys [vport] :as wlocal} (mf/deref refs/workspace-local) {:keys [options-mode] :as wglobal} (obj/get props "wglobal") @@ -68,7 +73,8 @@ (when (and textpalette? (not hide-ui?)) [:& textpalette]) - [:section.workspace-content {:ref node-ref} + [:section.workspace-content {:key (dm/str "workspace-" page-id) + :ref node-ref} [:section.workspace-viewport (when (debug? :coordinates) [:& coordinates/coordinates {:colorpalette? colorpalette?}]) @@ -100,19 +106,21 @@ (mf/defc workspace-page [{:keys [file layout page-id wglobal] :as props}] - (mf/with-effect [page-id] - (if (nil? page-id) - (st/emit! (dw/go-to-page)) - (st/emit! (dw/initialize-page page-id))) - (fn [] - (when page-id - (st/emit! (dw/finalize-page page-id))))) + (let [prev-page-id (hooks/use-previous page-id)] + (mf/with-effect + [page-id] + (when (and prev-page-id (not= prev-page-id page-id)) + (st/emit! (dw/finalize-page prev-page-id))) - (when (mf/deref trimmed-page-ref) - [:& workspace-content {:key (dm/str page-id) - :file file - :wglobal wglobal - :layout layout}])) + (if (nil? page-id) + (st/emit! (dw/go-to-page)) + (st/emit! (dw/initialize-page page-id)))) + + (when (mf/deref trimmed-page-ref) + [:& workspace-content {:page-id page-id + :file file + :wglobal wglobal + :layout layout}]))) (mf/defc workspace-loader [] @@ -126,12 +134,24 @@ project (mf/deref refs/workspace-project) layout (mf/deref refs/workspace-layout) wglobal (mf/deref refs/workspace-global) - ready? (mf/deref refs/workspace-ready?) + ready? (mf/deref refs/workspace-ready?) workspace-read-only? (mf/deref refs/workspace-read-only?) components-v2 (features/use-feature :components-v2) - background-color (:background-color wglobal)] + background-color (:background-color wglobal) + + focus-out + (mf/use-callback + (fn [] + (st/emit! (dw/workspace-focus-lost))))] + + (mf/use-effect + (mf/deps focus-out) + (fn [] + (let [keys [(events/listen globals/document EventType.FOCUSOUT focus-out)]] + #(doseq [key keys] + (events/unlistenByKey key))))) ;; Setting the layout preset by its name (mf/with-effect [layout-name] @@ -159,7 +179,8 @@ [:& (mf/provider ctx/current-page-id) {:value page-id} [:& (mf/provider ctx/components-v2) {:value components-v2} [:& (mf/provider ctx/workspace-read-only?) {:value workspace-read-only?} - [:section#workspace {:style {:background-color background-color}} + [:section#workspace {:style {:background-color background-color + :touch-action "none"}} (when (not (:hide-ui layout)) [:& header {:file file :page-id page-id @@ -169,11 +190,10 @@ [:& context-menu] (if ready? - [:& workspace-page {:key (dm/str "page-" page-id) - :page-id page-id - :file file - :wglobal wglobal - :layout layout}] + [:& workspace-page {:page-id page-id + :file file + :wglobal wglobal + :layout layout}] [:& workspace-loader])]]]]]]])) (mf/defc remove-graphics-dialog diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs index 12507ef39..5d64b9a99 100644 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs @@ -49,7 +49,7 @@ container (mf/use-ref nil) - {:keys [on-pointer-down on-lost-pointer-capture on-mouse-move parent-ref size]} + {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} (use-resize-hook :palette 72 54 80 :y true :bottom) on-left-arrow-click @@ -113,7 +113,7 @@ "--bullet-size" (dm/str (if (< size 72) (- size 15) (- size 30)) "px")}} [:div.resize-area {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture - :on-mouse-move on-mouse-move}] + :on-pointer-move on-pointer-move}] [:& dropdown {:show (:show-menu @state) :on-close #(swap! state assoc :show-menu false)} [:ul.workspace-context-menu.palette-menu diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 3f6f3a72b..b99302e36 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -65,11 +65,13 @@ handle-change-color (mf/use-fn - (mf/deps @drag?) + (mf/deps current-color @drag?) (fn [color] - (let [recent-color (merge current-color color) - recent-color (dc/materialize-color-components recent-color)] - (st/emit! (dc/update-colorpicker-color recent-color (not @drag?)))))) + (when (or (not= (str/lower (:hex color)) (str/lower (:hex current-color))) + (not= (:h color) (:h current-color))) + (let [recent-color (merge current-color color) + recent-color (dc/materialize-color-components recent-color)] + (st/emit! (dc/update-colorpicker-color recent-color (not @drag?))))))) handle-click-picker (mf/use-fn @@ -190,7 +192,8 @@ :h h :s s :v v :alpha (/ alpha 255)})))) - [:div.colorpicker {:ref node-ref} + [:div.colorpicker {:ref node-ref + :style {:touch-action "none"}} [:div.colorpicker-content [:div.top-actions [:button.picker-btn diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs index d9c8609a5..119faa933 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.cljs @@ -134,7 +134,7 @@ :on-pointer-up handle-stop-drag :on-lost-pointer-capture handle-stop-drag :on-click calculate-pos - :on-mouse-move #(when @dragging? (calculate-pos %))}] + :on-pointer-move #(when @dragging? (calculate-pos %))}] [:div.handler {:style {:pointer-events "none" :left (:x pos-current) :top (:y pos-current)}}] diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs index 63c7b467c..0a839324a 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs @@ -44,7 +44,7 @@ :on-pointer-up handle-stop-drag :on-lost-pointer-capture handle-stop-drag :on-click calculate-pos - :on-mouse-move #(when @dragging? (calculate-pos %))} + :on-pointer-move #(when @dragging? (calculate-pos %))} [:div.handler {:style {:pointer-events "none" :left (str (* 100 saturation) "%") :top (str (* 100 (- 1 (/ value 255))) "%")}}]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs index 83c56c1b6..7b5af5bae 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs @@ -55,7 +55,7 @@ :on-pointer-up handle-stop-drag :on-lost-pointer-capture handle-stop-drag :on-click calculate-pos - :on-mouse-move #(when @dragging? (calculate-pos %))} + :on-pointer-move #(when @dragging? (calculate-pos %))} (let [value-percent (* (/ (- value min-value) (- max-value min-value)) 100) diff --git a/frontend/src/app/main/ui/workspace/effects.cljs b/frontend/src/app/main/ui/workspace/effects.cljs index a124dfb45..8210ef068 100644 --- a/frontend/src/app/main/ui/workspace/effects.cljs +++ b/frontend/src/app/main/ui/workspace/effects.cljs @@ -28,7 +28,7 @@ (fn [] (st/emit! (dws/change-hover-state id false))))) -(defn use-mouse-down +(defn use-pointer-down [{:keys [id type blocked]}] (mf/use-callback (mf/deps id type blocked) diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs index 8c5418ff4..7be23d60d 100644 --- a/frontend/src/app/main/ui/workspace/header.cljs +++ b/frontend/src/app/main/ui/workspace/header.cljs @@ -15,6 +15,7 @@ [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.colors :as dc] + [app.main.data.workspace.common :as dwc] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.shortcuts :as sc] [app.main.refs :as refs] @@ -32,6 +33,7 @@ [app.util.keyboard :as kbd] [app.util.router :as rt] [beicon.core :as rx] + [cuerdas.core :as str] [okulary.core :as l] [potok.core :as ptk] [rumext.v2 :as mf])) @@ -149,10 +151,12 @@ :on-accept #(st/emit! (dwl/set-file-shared (:id file) false)) :count-libraries 1})))) - handle-blur (fn [_] - (let [value (-> edit-input-ref mf/ref-val dom/get-value)] - (st/emit! (dw/rename-file (:id file) value))) - (reset! editing? false)) + handle-blur + (fn [_] + (let [value (str/trim (-> edit-input-ref mf/ref-val dom/get-value))] + (when (not= value "") + (st/emit! (dw/rename-file (:id file) value)))) + (reset! editing? false)) handle-name-keydown (fn [event] (when (kbd/enter? event) @@ -306,12 +310,14 @@ [:li {:on-click #(st/emit! (dw/select-all))} [:span (tr "workspace.header.menu.select-all")] [:span.shortcut (sc/get-tooltip :select-all)]] - [:li {:on-click #(st/emit! (toggle-flag :scale-text))} - [:span - (if (contains? layout :scale-text) - (tr "workspace.header.menu.disable-scale-text") - (tr "workspace.header.menu.enable-scale-text"))] - [:span.shortcut (sc/get-tooltip :toggle-scale-text)]]]] + + [:li {:on-click #(st/emit! dwc/undo)} + [:span (tr "workspace.header.menu.undo")] + [:span.shortcut (sc/get-tooltip :undo)]] + + [:li {:on-click #(st/emit! dwc/redo)} + [:span (tr "workspace.header.menu.redo")] + [:span.shortcut (sc/get-tooltip :redo)]]]] [:& dropdown {:show (= @show-sub-menu? :view) :on-close #(reset! show-sub-menu? false)} @@ -374,6 +380,13 @@ [:& dropdown {:show (= @show-sub-menu? :preferences) :on-close #(reset! show-sub-menu? false)} [:ul.sub-menu.preferences + [:li {:on-click #(st/emit! (toggle-flag :scale-text))} + [:span + (if (contains? layout :scale-text) + (tr "workspace.header.menu.disable-scale-content") + (tr "workspace.header.menu.enable-scale-content"))] + [:span.shortcut (sc/get-tooltip :toggle-scale-text)]] + [:li {:on-click #(st/emit! (toggle-flag :snap-guides))} [:span (if (contains? layout :snap-guides) diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index 9ee12f976..8eddab085 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -51,7 +51,7 @@ (fn [_] (st/emit! (drp/path-pointer-leave position)))) - on-mouse-down + on-pointer-down (fn [event] (dom/stop-propagation event) (dom/prevent-default event) @@ -95,9 +95,9 @@ [:circle {:cx x :cy y :r (/ point-radius-active-area zoom) - :on-mouse-down on-mouse-down - :on-mouse-enter on-enter - :on-mouse-leave on-leave + :on-pointer-down on-pointer-down + :on-pointer-enter on-enter + :on-pointer-leave on-leave :pointer-events (when-not preview? "visible") :style {:cursor (cond (= edit-mode :draw) cur/pen-node @@ -116,7 +116,7 @@ (fn [_] (st/emit! (drp/path-handler-leave index prefix))) - on-mouse-down + on-pointer-down (fn [event] (dom/stop-propagation event) (dom/prevent-default event) @@ -157,9 +157,9 @@ [:circle {:cx x :cy y :r (/ point-radius-active-area zoom) - :on-mouse-down on-mouse-down - :on-mouse-enter on-enter - :on-mouse-leave on-leave + :on-pointer-down on-pointer-down + :on-pointer-enter on-enter + :on-pointer-leave on-leave :style {:cursor (when (= edit-mode :move) cur/pointer-move) :fill "none" :stroke-width 0}}]]))) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 39ae91b1c..d1d34b6c0 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -15,9 +15,54 @@ [app.main.refs :as refs] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text :as text] + [app.util.dom :as dom] [debug :refer [debug?]] [rumext.v2 :as mf])) +(mf/defc debug-text-bounds + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + zoom (mf/deref refs/selected-zoom) + bounding-box (gsht/position-data-selrect shape) + ctx (js* "document.createElement(\"canvas\").getContext(\"2d\")")] + [:g {:transform (gsh/transform-str shape)} + [:rect {:x (:x bounding-box) + :y (:y bounding-box) + :width (:width bounding-box) + :height (:height bounding-box) + :style {:fill "none" + :stroke "orange" + :stroke-width (/ 1 zoom)}}] + + (for [[index data] (d/enumerate (:position-data shape))] + (let [{:keys [x y width height]} data + res (dom/measure-text ctx (:font-size data) (:font-family data) (:text data))] + [:g {:key (dm/str index)} + ;; Text fragment bounding box + [:rect {:x x + :y (- y height) + :width width + :height height + :style {:fill "none" + :stroke "red" + :stroke-width (/ 1 zoom)}}] + + ;; Text baseline + [:line {:x1 (mth/round x) + :y1 (mth/round (- (:y data) (:height data))) + :x2 (mth/round (+ x width)) + :y2 (mth/round (- (:y data) (:height data))) + :style {:stroke "blue" + :stroke-width (/ 1 zoom)}}] + + [:line {:x1 (:x data) + :y1 (- (:y data) (:descent res)) + :x2 (+ (:x data) (:width data)) + :y2 (- (:y data) (:descent res)) + :style {:stroke "green" + :stroke-width (/ 2 zoom)}}]]))])) + ;; --- Text Wrapper for workspace (mf/defc text-wrapper {::mf/wrap-props false} @@ -39,28 +84,4 @@ [:& text/text-shape {:shape shape}]] (when (and (debug? :text-outline) (d/not-empty? (:position-data shape))) - [:g {:transform (gsh/transform-str shape)} - (let [bounding-box (gsht/position-data-selrect shape)] - [:rect { - :x (:x bounding-box) - :y (:y bounding-box) - :width (:width bounding-box) - :height (:height bounding-box) - :style { :fill "none" :stroke "orange"}}]) - - (for [[index data] (d/enumerate (:position-data shape))] - (let [{:keys [x y width height]} data] - [:g {:key (dm/str index)} - ;; Text fragment bounding box - [:rect {:x x - :y (- y height) - :width width - :height height - :style {:fill "none" :stroke "red"}}] - - ;; Text baseline - [:line {:x1 (mth/round x) - :y1 (mth/round (- (:y data) (:height data))) - :x2 (mth/round (+ x width)) - :y2 (mth/round (- (:y data) (:height data))) - :style {:stroke "blue"}}]]))])])) + [:& debug-text-bounds {:shape shape}])])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index a866d451a..030b79b63 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -12,6 +12,7 @@ [app.common.geom.shapes :as gsh] [app.common.geom.shapes.text :as gsht] [app.common.text :as txt] + [app.config :as cf] [app.main.data.workspace :as dw] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] @@ -200,7 +201,7 @@ (st/emit! (dwt/update-editor-state shape state))) "handled")) - on-mouse-down + on-pointer-down (mf/use-callback (fn [event] (when (dom/class? (dom/get-target event) "DraftEditor-root") @@ -228,7 +229,7 @@ ;; the underlying text. Use opacity because display or visibility won't allow to recover ;; focus afterwards. :opacity (when @blurred 0)} - :on-mouse-down on-mouse-down + :on-pointer-down on-pointer-down :class (dom/classnames :align-top (= (:vertical-align content "top") "top") :align-center (= (:vertical-align content) "center") @@ -271,6 +272,12 @@ text-modifier (mf/deref text-modifier-ref) + ;; For Safari It's necesary to scale the editor with the zoom level to fix + ;; a problem with foreignObjects not scaling correctly with the viewbox + maybe-zoom + (when (cf/check-browser? :safari) + (mf/deref refs/selected-zoom)) + shape (cond-> shape (some? text-modifier) (dwt/apply-text-modifier text-modifier) @@ -299,5 +306,7 @@ [:div {:style {:position "fixed" :left 0 :top (- (:y shape) y) - :pointer-events "all"}} + :pointer-events "all" + :transform-origin "top left" + :transform (when maybe-zoom (dm/fmt "scale(%)" maybe-zoom))}} [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs index a6433818b..a6633ccd7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs @@ -66,7 +66,8 @@ (when (contains? #{:auto-height :auto-width} grow-type) (let [{:keys [width height]} (-> (dom/query node ".paragraph-set") - (dom/get-client-size)) + (dom/get-bounding-rect)) + width (mth/ceil width) height (mth/ceil height)] (when (and (not (mth/almost-zero? width)) @@ -234,10 +235,11 @@ ;; When we have a text with grow-type :auto-height or :auto-height we need to check the correct height ;; otherwise the center alignment will break - tr-shape (when text-modifier (dwt/apply-text-modifier shape text-modifier)) - shape (cond-> shape - (and (some? text-modifier) (#{:auto-height :auto-width} (:grow-type shape))) - (assoc :width (:width tr-shape) :height (:height tr-shape))) + shape + (if (some? text-modifier) + (let [{:keys [width height]} (dwt/apply-text-modifier shape text-modifier)] + (assoc shape :width width :height height)) + shape) shape (hooks/use-equal-memo shape) diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index 02f2c6be5..bc311baf9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -37,7 +37,7 @@ shortcuts? (contains? layout :shortcuts) show-debug? (contains? layout :debug-panel) - {:keys [on-pointer-down on-lost-pointer-capture on-mouse-move parent-ref size]} + {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} (use-resize-hook :left-sidebar 255 255 500 :x false :left) handle-collapse @@ -52,7 +52,7 @@ :style #js {"--width" (str size "px")}} [:div.resize-area {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture - :on-mouse-move on-mouse-move}] + :on-pointer-move on-pointer-move}] [:div.settings-bar-inside (cond diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index ea1e450fb..6009b1613 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -1150,29 +1150,13 @@ (:color color) (:color color) :else (:value color)) - ;; TODO: looks like the first argument is not necessary - ;; TODO: this code should be out of this UI component apply-color - (fn [_ event] - (let [objects (wsh/lookup-page-objects @st/state) - selected (->> (wsh/lookup-selected @st/state) - (cph/clean-loops objects)) - selected-obj (keep (d/getf objects) selected) - select-shapes-for-color (fn [shape objects] - (let [shapes (case (:type shape) - :group (cph/get-children objects (:id shape)) - [shape])] - (->> shapes - (remove cph/group-shape?) - (map :id)))) - ids (mapcat #(select-shapes-for-color % objects) selected-obj)] - (if (kbd/alt? event) - (st/emit! (dc/change-stroke ids (merge uc/empty-color color) 0)) - (st/emit! (dc/change-fill ids (merge uc/empty-color color) 0))))) + (fn [event] + (st/emit! (dc/apply-color-from-palette (merge uc/empty-color color) (kbd/alt? event)))) rename-color (fn [name] - (st/emit! (dwl/update-color (assoc color :name name) file-id))) + (st/emit! (dwl/rename-color file-id (:id color) name))) edit-color (fn [new-color] @@ -1277,8 +1261,7 @@ :selected (contains? selected-colors (:id color))) :on-context-menu on-context-menu :on-click (when-not (:editing @state) - #(on-asset-click % (:id color) - (partial apply-color (:id color)))) + #(on-asset-click % (:id color) apply-color)) :ref item-ref :draggable (and (not workspace-read-only?) (not (:editing @state))) :on-drag-start on-color-drag-start diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.cljs b/frontend/src/app/main/ui/workspace/sidebar/history.cljs index b90834ed3..908491bf5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/history.cljs @@ -288,8 +288,8 @@ :current current? :hover @hover? :show-detail @show-detail?) - :on-mouse-enter #(reset! hover? true) - :on-mouse-leave #(reset! hover? false) + :on-pointer-enter #(reset! hover? true) + :on-pointer-leave #(reset! hover? false) :on-click #(st/emit! (dwc/undo-to-index idx-entry))} [:div.history-entry-summary [:div.history-entry-summary-icon (entry->icon entry)] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index d7f61a31d..39cfc501b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] + [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] [app.main.data.workspace.collapse :as dwc] @@ -101,6 +102,7 @@ selected? (contains? selected id) container? (or (cph/frame-shape? item) (cph/group-shape? item)) + absolute? (ctl/layout-absolute? item) components-v2 (mf/use-ctx ctx/components-v2) workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) @@ -253,9 +255,11 @@ :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave :on-double-click #(dom/stop-propagation %)} - [:div {:on-double-click #(do (dom/stop-propagation %) - (dom/prevent-default %) - (st/emit! dw/zoom-to-selected-shape))} + [:div.icon {:on-double-click #(do (dom/stop-propagation %) + (dom/prevent-default %) + (st/emit! dw/zoom-to-selected-shape))} + (when absolute? + [:div.absolute i/position-absolute]) [:& si/element-icon {:shape item :main-instance? main-instance?}]] [:& layer-name {:shape item diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 80d4fa796..9c98a763f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -23,6 +23,7 @@ [app.main.ui.workspace.sidebar.options.shapes.bool :as bool] [app.main.ui.workspace.sidebar.options.shapes.circle :as circle] [app.main.ui.workspace.sidebar.options.shapes.frame :as frame] + [app.main.ui.workspace.sidebar.options.shapes.grid-cell :as grid-cell] [app.main.ui.workspace.sidebar.options.shapes.group :as group] [app.main.ui.workspace.sidebar.options.shapes.image :as image] [app.main.ui.workspace.sidebar.options.shapes.multiple :as multiple] @@ -67,9 +68,16 @@ (let [drawing (mf/deref refs/workspace-drawing) objects (mf/deref refs/workspace-page-objects) shared-libs (mf/deref refs/workspace-libraries) + grid-edition (mf/deref refs/workspace-grid-edition) selected-shapes (into [] (keep (d/getf objects)) selected) first-selected-shape (first selected-shapes) shape-parent-frame (cph/get-frame objects (:frame-id first-selected-shape)) + + [grid-id {[row-selected col-selected] :selected}] + (d/seek (fn [[_ {:keys [selected]}]] (some? selected)) grid-edition) + + grid-cell-selected? (and (some? grid-id) (some? row-selected) (some? col-selected)) + on-change-tab (fn [options-mode] (st/emit! (udw/set-options-mode options-mode) @@ -87,6 +95,10 @@ [:& align-options] [:& bool-options] (cond + grid-cell-selected? [:& grid-cell/options {:shape (get objects grid-id) + :row row-selected + :column col-selected}] + (d/not-empty? drawing) [:& shape-options {:shape (:object drawing) :page-id page-id :file-id file-id @@ -138,4 +150,3 @@ :file-id file-id :page-id page-id :section section}])) - diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index 7a18745e8..857e60da5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -161,27 +161,37 @@ on-change (mf/use-fn - (fn [new-color old-color] + (fn [new-color old-color from-picker?] (let [old-color (-> old-color (dissoc :name) (dissoc :path) (d/without-nils)) - prev-color (-> @prev-color* - (dissoc :name) - (dissoc :path) - (d/without-nils)) + prev-color (when @prev-color* + (-> @prev-color* + (dissoc :name) + (dissoc :path) + (d/without-nils))) ;; When dragging on the color picker sometimes all the shapes hasn't updated the color to the prev value so we need this extra calculation shapes-by-old-color (get @grouped-colors* old-color) shapes-by-prev-color (get @grouped-colors* prev-color) shapes-by-color (or shapes-by-prev-color shapes-by-old-color)] - (reset! prev-color* new-color) - (st/emit! (dc/change-color-in-selected new-color shapes-by-color old-color))))) - on-open (mf/use-fn - (fn [color] - (reset! prev-color* color))) + (when from-picker? + (reset! prev-color* new-color)) + + (st/emit! (dc/change-color-in-selected new-color shapes-by-color (or prev-color old-color)))))) + + on-open + (mf/use-fn + (fn [] + (reset! prev-color* nil))) + + on-close + (mf/use-fn + (fn [] + (reset! prev-color* nil))) on-detach (mf/use-fn @@ -212,8 +222,9 @@ :index index :on-detach on-detach :select-only select-only - :on-change #(on-change % color) - :on-open on-open}]) + :on-change #(on-change %1 color %2) + :on-open on-open + :on-close on-close}]) (when (and (false? @expand-lib-color) (< 3 (count library-colors))) [:div.expand-colors {:on-click #(reset! expand-lib-color true)} [:span i/actions] @@ -225,8 +236,9 @@ :index index :on-detach on-detach :select-only select-only - :on-change #(on-change % color) - :on-open on-open}]))] + :on-change #(on-change %1 color %2) + :on-open on-open + :on-close on-close}]))] [:div.selected-colors (for [[index color] (d/enumerate (take 3 colors))] @@ -234,8 +246,9 @@ :color color :index index :select-only select-only - :on-change #(on-change % color) - :on-open on-open}]) + :on-change #(on-change %1 color %2) + :on-open on-open + :on-close on-close}]) (when (and (false? @expand-color) (< 3 (count colors))) [:div.expand-colors {:on-click #(reset! expand-color true)} [:span i/actions] @@ -246,5 +259,6 @@ :color color :index index :select-only select-only - :on-change #(on-change % color) - :on-open on-open}]))]]]))) + :on-change #(on-change %1 color %2) + :on-open on-open + :on-close on-close}]))]]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 655d1873c..eff892981 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -336,7 +336,7 @@ [:span.element-set-subtitle.wide (tr "workspace.options.interaction-delay")] [:div.input-element {:title (tr "workspace.options.interaction-ms")} [:> numeric-input {:ref ext-delay-ref - :on-click (select-text ext-delay-ref) + :on-focus (select-text ext-delay-ref) :on-change change-delay :value (:delay interaction) :title (tr "workspace.options.interaction-ms")}] @@ -523,7 +523,7 @@ [:span.element-set-subtitle.wide (tr "workspace.options.interaction-duration")] [:div.input-element {:title (tr "workspace.options.interaction-ms")} [:> numeric-input {:ref ext-duration-ref - :on-click (select-text ext-duration-ref) + :on-focus (select-text ext-duration-ref) :on-change change-duration :value (-> interaction :animation :duration) :title (tr "workspace.options.interaction-ms")}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index 3aaec9d4c..5623ed669 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -113,7 +113,7 @@ [:div.input-element {:title (tr "workspace.options.opacity") :class "percentail"} [:> numeric-input {:value (-> values :opacity opacity->string) :placeholder (tr "settings.multiple") - :on-click select-all + :on-focus select-all :on-change handle-opacity-change :min 0 :max 100}]] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs index 4e006752c..ac624f7b4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs @@ -5,96 +5,138 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.workspace.sidebar.options.menus.layout-container - (:require [app.common.data :as d] - [app.common.data.macros :as dm] - [app.main.data.workspace.shape-layout :as dwsl] - [app.main.store :as st] - [app.main.ui.components.numeric-input :refer [numeric-input]] - [app.main.ui.icons :as i] - [app.util.dom :as dom] - [cuerdas.core :as str] - [rumext.v2 :as mf])) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.data.workspace :as udw] + [app.main.data.workspace.shape-layout :as dwsl] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.components.select :refer [select]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [cuerdas.core :as str] + [rumext.v2 :as mf])) (def layout-container-flex-attrs [:layout ;; :flex, :grid in the future :layout-flex-dir ;; :row, :row-reverse, :column, :column-reverse :layout-gap-type ;; :simple, :multiple :layout-gap ;; {:row-gap number , :column-gap number} + :layout-align-items ;; :start :end :center :stretch :layout-justify-content ;; :start :center :end :space-between :space-around :space-evenly :layout-align-content ;; :start :center :end :space-between :space-around :space-evenly :stretch (by default) :layout-wrap-type ;; :wrap, :nowrap :layout-padding-type ;; :simple, :multiple :layout-padding ;; {:p1 num :p2 num :p3 num :p4 num} number could be negative - ]) + + :layout-grid-dir ;; :row :column + :layout-justify-items + :layout-grid-columns + :layout-grid-rows]) (defn get-layout-flex-icon [type val is-col?] (case type - :align-items (if is-col? - (case val - :start i/align-items-column-start - :end i/align-items-column-end - :center i/align-items-column-center - :stretch i/align-items-column-strech - :baseline i/align-items-column-baseline) - (case val - :start i/align-items-row-start - :end i/align-items-row-end - :center i/align-items-row-center - :stretch i/align-items-row-strech - :baseline i/align-items-row-baseline)) - :justify-content (if is-col? - (case val - :start i/justify-content-column-start - :end i/justify-content-column-end - :center i/justify-content-column-center - :space-around i/justify-content-column-around - :space-evenly i/justify-content-column-evenly - :space-between i/justify-content-column-between) - (case val - :start i/justify-content-row-start - :end i/justify-content-row-end - :center i/justify-content-row-center - :space-around i/justify-content-row-around - :space-evenly i/justify-content-row-evenly - :space-between i/justify-content-row-between)) + :align-items + (if is-col? + (case val + :start i/align-items-column-start + :end i/align-items-column-end + :center i/align-items-column-center + :stretch i/align-items-column-strech + :baseline i/align-items-column-baseline) + (case val + :start i/align-items-row-start + :end i/align-items-row-end + :center i/align-items-row-center + :stretch i/align-items-row-strech + :baseline i/align-items-row-baseline)) - :align-content (if is-col? - (case val - :start i/align-content-column-start - :end i/align-content-column-end - :center i/align-content-column-center - :space-around i/align-content-column-around - :space-evenly i/align-content-column-evenly - :space-between i/align-content-column-between - :stretch nil) + :justify-content + (if is-col? + (case val + :start i/justify-content-column-start + :end i/justify-content-column-end + :center i/justify-content-column-center + :space-around i/justify-content-column-around + :space-evenly i/justify-content-column-evenly + :space-between i/justify-content-column-between) + (case val + :start i/justify-content-row-start + :end i/justify-content-row-end + :center i/justify-content-row-center + :space-around i/justify-content-row-around + :space-evenly i/justify-content-row-evenly + :space-between i/justify-content-row-between)) - (case val - :start i/align-content-row-start - :end i/align-content-row-end - :center i/align-content-row-center - :space-around i/align-content-row-around - :space-evenly i/align-content-row-evenly - :space-between i/align-content-row-between - :stretch nil)) + :align-content + (if is-col? + (case val + :start i/align-content-column-start + :end i/align-content-column-end + :center i/align-content-column-center + :space-around i/align-content-column-around + :space-evenly i/align-content-column-evenly + :space-between i/align-content-column-between + :stretch nil) - :align-self (if is-col? - (case val - :start i/align-self-row-left - :end i/align-self-row-right - :center i/align-self-row-center - :stretch i/align-self-row-strech - :baseline i/align-self-row-baseline) - (case val - :start i/align-self-column-top - :end i/align-self-column-bottom - :center i/align-self-column-center - :stretch i/align-self-column-strech - :baseline i/align-self-column-baseline)))) + (case val + :start i/align-content-row-start + :end i/align-content-row-end + :center i/align-content-row-center + :space-around i/align-content-row-around + :space-evenly i/align-content-row-evenly + :space-between i/align-content-row-between + :stretch nil)) + + (case val + :start i/align-content-row-start + :end i/align-content-row-end + :center i/align-content-row-center + :space-around i/align-content-row-around + :space-between i/align-content-row-between + :stretch nil) + + :align-self + (if is-col? + (case val + :auto i/minus + :start i/align-self-row-left + :end i/align-self-row-right + :center i/align-self-row-center + :stretch i/align-self-row-strech + :baseline i/align-self-row-baseline) + (case val + :auto i/minus + :start i/align-self-column-top + :end i/align-self-column-bottom + :center i/align-self-column-center + :stretch i/align-self-column-strech + :baseline i/align-self-column-baseline)))) + +(defn get-layout-grid-icon + [type val is-col?] + (case type + :justify-items + (if is-col? + (case val + :start i/grid-justify-content-column-start + :end i/grid-justify-content-column-end + :center i/grid-justify-content-column-center + :space-around i/grid-justify-content-column-around + :space-between i/grid-justify-content-column-between) + (case val + :start i/grid-justify-content-row-start + :end i/grid-justify-content-row-end + :center i/grid-justify-content-row-center + :space-around i/grid-justify-content-row-around + :space-between i/grid-justify-content-row-between)))) (mf/defc direction-btn - [{:keys [dir saved-dir set-direction] :as props}] + [{:keys [dir saved-dir set-direction icon?] :as props}] (let [handle-on-click (mf/use-callback (mf/deps set-direction dir) @@ -111,7 +153,9 @@ :key (dm/str "direction-" dir) :alt (str/replace (str/capital (d/name dir)) "-" " ") :on-click handle-on-click} - i/auto-direction])) + (if icon? + i/auto-direction + (str/replace (str/capital (d/name dir)) "-" " "))])) (mf/defc wrap-row [{:keys [wrap-type set-wrap] :as props}] @@ -204,7 +248,19 @@ (= (dm/get-in values [:layout-padding :p2]) (dm/get-in values [:layout-padding :p4]))) (dm/get-in values [:layout-padding :p2]) - "--")] + "--") + + select-paddings + (fn [p1? p2? p3? p4?] + (st/emit! (udw/set-paddings-selected {:p1 p1? :p2 p2? :p3 p3? :p4 p4?}))) + + select-padding #(select-paddings (= % :p1) (= % :p2) (= % :p3) (= % :p4))] + + (mf/use-effect + (fn [] + (fn [] + ;;on destroy component + (select-paddings false false false false)))) [:div.padding-row (cond @@ -216,8 +272,11 @@ [:span.icon.rotated i/auto-padding-both-sides] [:> numeric-input {:placeholder "--" - :on-click #(dom/select-target %) :on-change (partial on-change :simple :p1) + :on-focus #(select-paddings true false true false) + :on-blur #(do + (dom/select-target %) + (select-paddings false false false false)) :value p1}]] [:div.padding-item.tooltip.tooltip-bottom-left @@ -225,8 +284,10 @@ [:span.icon i/auto-padding-both-sides] [:> numeric-input {:placeholder "--" - :on-click #(dom/select-target %) :on-change (partial on-change :simple :p2) + :on-focus #(do (dom/select-target %) + (select-paddings false true false true)) + :on-blur #(select-paddings false false false false) :value p2}]]] (= padding-type :multiple) @@ -242,49 +303,172 @@ [:div.input-element.auto [:> numeric-input {:placeholder "--" - :on-click #(dom/select-target %) :on-change (partial on-change :multiple num) + :on-focus #(do (dom/select-target %) + (select-padding num)) + :on-blur #(select-paddings false false false false) :value (num (:layout-padding values))}]]])]) [:div.padding-icons [:div.padding-icon.tooltip.tooltip-bottom-left {:class (dom/classnames :selected (= padding-type :multiple)) - :alt "Padding - multiple" + :alt "Independent paddings" :on-click #(on-change-style (if (= padding-type :multiple) :simple :multiple))} i/auto-padding-side]]])) (mf/defc gap-section [{:keys [is-col? wrap-type gap-selected? set-gap gap-value]}] - [:div.layout-row - [:div.gap.row-title "Gap"] - [:div.gap-group - [:div.gap-row.tooltip.tooltip-bottom-left - {:alt "Column gap"} - [:span.icon - i/auto-gap] - [:> numeric-input {:no-validate true - :placeholder "--" - :on-click (fn [event] - (reset! gap-selected? :column-gap) - (dom/select-target event)) - :on-change (partial set-gap (= :nowrap wrap-type) :column-gap) - :on-blur #(reset! gap-selected? :none) - :value (:column-gap gap-value) - :disabled (and (= :nowrap wrap-type) is-col?)}]] + (let [select-gap + (fn [gap] + (st/emit! (udw/set-gap-selected gap)))] - [:div.gap-row.tooltip.tooltip-bottom-left - {:alt "Row gap"} - [:span.icon.rotated - i/auto-gap] - [:> numeric-input {:no-validate true - :placeholder "--" - :on-click (fn [event] - (reset! gap-selected? :row-gap) - (dom/select-target event)) - :on-change (partial set-gap (= :nowrap wrap-type) :row-gap) - :on-blur #(reset! gap-selected? :none) - :value (:row-gap gap-value) - :disabled (and (= :nowrap wrap-type) (not is-col?))}]]]]) + (mf/use-effect + (fn [] + (fn [] + ;;on destroy component + (select-gap nil)))) + + [:div.layout-row + [:div.gap.row-title "Gap"] + [:div.gap-group + [:div.gap-row.tooltip.tooltip-bottom-left + {:alt "Column gap"} + [:span.icon + i/auto-gap] + [:> numeric-input {:no-validate true + :placeholder "--" + :on-focus (fn [event] + (select-gap :column-gap) + (reset! gap-selected? :column-gap) + (dom/select-target event)) + :on-change (partial set-gap (= :nowrap wrap-type) :column-gap) + :on-blur (fn [_] + (select-gap nil) + (reset! gap-selected? :none)) + :value (:column-gap gap-value) + :disabled (and (= :nowrap wrap-type) is-col?)}]] + + [:div.gap-row.tooltip.tooltip-bottom-left + {:alt "Row gap"} + [:span.icon.rotated + i/auto-gap] + [:> numeric-input {:no-validate true + :placeholder "--" + :on-focus (fn [event] + (select-gap :row-gap) + (reset! gap-selected? :row-gap) + (dom/select-target event)) + :on-change (partial set-gap (= :nowrap wrap-type) :row-gap) + :on-blur (fn [_] + (select-gap nil) + (reset! gap-selected? :none)) + :value (:row-gap gap-value) + :disabled (and (= :nowrap wrap-type) (not is-col?))}]]]])) + +(mf/defc grid-edit-mode + [{:keys [id] :as props}] + (let [edition (mf/deref refs/selected-edition) + active? (= id edition) + + toggle-edit-mode + (mf/use-callback + (mf/deps id edition) + (fn [] + (if-not active? + (st/emit! (udw/start-edition-mode id)) + (st/emit! :interrupt))))] + + [:button.tooltip.tooltip-bottom-left + {:class (dom/classnames :active active?) + :alt "Grid edit mode" + :on-click #(toggle-edit-mode) + :style {:padding 0}} + i/grid-layout-mode])) + +(mf/defc align-grid-row + [{:keys [is-col? align-items set-align] :as props}] + (let [type (if is-col? :column :row)] + [:div.align-items-style + (for [align [:start :center :end :stretch :baseline]] + [:button.align-start.tooltip + {:class (dom/classnames :active (= align-items align) + :tooltip-bottom-left (not= align :start) + :tooltip-bottom (= align :start)) + :alt (dm/str "Align items " (d/name align)) + :on-click #(set-align align type) + :key (dm/str "align-items" (d/name align))} + (get-layout-flex-icon :align-items align is-col?)])])) + +(mf/defc justify-grid-row + [{:keys [is-col? justify-items set-justify] :as props}] + (let [type (if is-col? :column :row)] + [:div.justify-content-style + (for [align [:start :center :end :space-around :space-between]] + [:button.align-start.tooltip + {:class (dom/classnames :active (= justify-items align) + :tooltip-bottom-left (not= align :start) + :tooltip-bottom (= align :start)) + :alt (dm/str "Justify content " (d/name align)) + :on-click #(set-justify align type) + :key (dm/str "justify-content" (d/name align))} + (get-layout-grid-icon :justify-items align is-col?)])])) + +(defn manage-values [{:keys [value type]}] + (case type + :auto "auto" + :percent (dm/str value "%") + :flex (dm/str value "fr") + :fixed (dm/str value "px") + value)) + +(mf/defc grid-columns-row + [{:keys [is-col? expanded? column-values toggle add-new-element set-column-value set-column-type remove-element] :as props}] + (let [column-num (count column-values) + direction (if (> column-num 1) + (if is-col? "Columns " "Rows ") + (if is-col? "Column " "Row ")) + + column-vals (str/join ", " (map manage-values column-values)) + generated-name (dm/str direction (if (= column-num 0) " - empty" (dm/str column-num " (" column-vals ")"))) + type (if is-col? :column :row)] + + [:div.grid-columns + [:div.grid-columns-header + [:button.expand-icon + {:on-click toggle} i/actions] + + [:div.columns-info {:title generated-name + :on-click toggle} generated-name] + [:button.add-column {:on-click #(do + (when-not expanded? (toggle)) + (add-new-element type {:type :fixed :value 100}))} i/plus]] + + (when expanded? + [:div.columns-info-wrapper + (for [[index column] (d/enumerate column-values)] + [:div.column-info + [:div.direction-grid-icon + (if is-col? + i/layout-rows + i/layout-columns)] + + [:div.grid-column-value + [:> numeric-input {:no-validate true + :value (:value column) + :on-change #(set-column-value type index %) + :placeholder "--"}]] + [:div.grid-column-unit + [:& select + {:class "grid-column-unit-selector" + :default-value (:type column) + :options [{:value :flex :label "fr"} + {:value :auto :label "auto"} + {:value :fixed :label "px"} + {:value :percent :label "%"}] + :on-change #(set-column-type type index %)}]] + [:button.remove-grid-column + {:on-click #(remove-element type index)} + i/minus]])])])) (mf/defc layout-container-menu {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "multiple"]))]} @@ -295,8 +479,8 @@ layout-type (:layout values) on-add-layout - (fn [_] - (st/emit! (dwsl/create-layout)) + (fn [type] + (st/emit! (dwsl/create-layout type)) (reset! open? true)) @@ -305,22 +489,20 @@ (st/emit! (dwsl/remove-layout ids)) (reset! open? false)) - ;; Uncomment when activating the grid options - ;; set-flex (fn [] - ;; (st/emit! (dwsl/remove-layout ids)) - ;; (on-add-layout :flex)) - ;; - ;; set-grid (fn [] - ;; (st/emit! (dwsl/remove-layout ids)) - ;; (on-add-layout :grid)) + _set-flex + (fn [] + (st/emit! (dwsl/remove-layout ids)) + (on-add-layout :flex)) + + _set-grid + (fn [] + (st/emit! (dwsl/remove-layout ids)) + (on-add-layout :grid)) ;; Flex-direction saved-dir (:layout-flex-dir values) is-col? (or (= :column saved-dir) (= :column-reverse saved-dir)) - set-direction - (fn [dir] - (st/emit! (dwsl/update-layout ids {:layout-flex-dir dir}))) ;; Wrap type @@ -373,7 +555,79 @@ (st/emit! (dwsl/update-layout ids {:layout-padding {:p2 val :p4 val}})) :else - (st/emit! (dwsl/update-layout ids {:layout-padding {prop val}}))))] + (st/emit! (dwsl/update-layout ids {:layout-padding {prop val}})))) + + ;; Grid-direction + + saved-grid-dir (:layout-grid-dir values) + + set-direction + (fn [dir type] + (if (= :flex type) + (st/emit! (dwsl/update-layout ids {:layout-flex-dir dir})) + (st/emit! (dwsl/update-layout ids {:layout-grid-dir dir})))) + + ;; Align grid + align-items-row (:layout-align-items values) + align-items-column (:layout-justify-items values) + + set-align-grid + (fn [value type] + (if (= type :row) + (st/emit! (dwsl/update-layout ids {:layout-align-items value})) + (st/emit! (dwsl/update-layout ids {:layout-justify-items value})))) + + ;; Justify grid + grid-justify-content-row (:layout-align-content values) + grid-justify-content-column (:layout-justify-content values) + + set-justify-grid + (mf/use-callback + (mf/deps ids) + (fn [value type] + (if (= type :row) + (st/emit! (dwsl/update-layout ids {:layout-align-content value})) + (st/emit! (dwsl/update-layout ids {:layout-justify-content value}))))) + + + ;;Grid columns + column-grid-values (:layout-grid-columns values) + grid-columns-open? (mf/use-state false) + toggle-columns-info (mf/use-callback + (fn [_] + (swap! grid-columns-open? not))) + + ; Grid rows / columns + rows-grid-values (:layout-grid-rows values) + grid-rows-open? (mf/use-state false) + toggle-rows-info + (mf/use-callback + (fn [_] + (swap! grid-rows-open? not))) + + add-new-element + (mf/use-callback + (mf/deps ids) + (fn [type value] + (st/emit! (dwsl/add-layout-track ids type value)))) + + remove-element + (mf/use-callback + (mf/deps ids) + (fn [type index] + (st/emit! (dwsl/remove-layout-track ids type index)))) + + set-column-value + (mf/use-callback + (mf/deps ids) + (fn [type index value] + (st/emit! (dwsl/change-layout-track ids type index {:value value})))) + + set-column-type + (mf/use-callback + (mf/deps ids) + (fn [type index track-type] + (st/emit! (dwsl/change-layout-track ids type index {:type track-type}))))] [:div.element-set [:div.element-set-title @@ -382,19 +636,21 @@ (if (and (not multiple) (:layout values)) [:div.title-actions #_[:div.layout-btns - [:button {:on-click set-flex - :class (dom/classnames - :active (= :flex layout-type))} "Flex"] - [:button {:on-click set-grid - :class (dom/classnames - :active (= :grid layout-type))} "Grid"]] + [:button {:on-click set-flex + :class (dom/classnames + :active (= :flex layout-type))} "Flex"] + [:button {:on-click set-grid + :class (dom/classnames + :active (= :grid layout-type))} "Grid"]] [:button.remove-layout {:on-click on-remove-layout} i/minus]] - [:button.add-page {:on-click on-add-layout} i/close])]] + [:button.add-page {:on-click #(on-add-layout :flex)} i/close])]] (when (:layout values) (when (not= :multiple layout-type) - (if (= :flex layout-type) + (case layout-type + :flex + [:div.element-set-content.layout-menu [:div.layout-row [:div.direction-wrap.row-title "Direction"] @@ -405,7 +661,8 @@ [:& direction-btn {:key (d/name dir) :dir dir :saved-dir saved-dir - :set-direction set-direction}])]] + :set-direction #(set-direction dir :flex) + :icon? true}])]] [:div.wrap-type [:& wrap-row {:wrap-type wrap-type @@ -443,4 +700,74 @@ :on-change-style change-padding-type :on-change on-padding-change}]] - [:div "GRID TO COME"])))])) + :grid + + [:div.element-set-content.layout-menu + [:div.layout-row + [:div.direction-wrap.row-title "Direction"] + [:div.btn-wrapper + [:div.direction + [:* + (for [dir [:row :column]] + [:& direction-btn {:key (d/name dir) + :dir dir + :saved-dir saved-grid-dir + :set-direction #(set-direction dir :grid) + :icon? false}])]] + + (when (= 1 (count ids)) + [:div.edit-mode + [:& grid-edit-mode {:id (first ids)}]])]] + + [:div.layout-row + [:div.align-items-grid.row-title "Align"] + [:div.btn-wrapper.align-grid + [:& align-grid-row {:is-col? false + :align-items align-items-row + :set-align set-align-grid}] + + [:& align-grid-row {:is-col? true + :align-items align-items-column + :set-align set-align-grid}]]] + + [:div.layout-row + [:div.jusfiy-content-grid.row-title "Justify"] + [:div.btn-wrapper.align-grid + [:& justify-grid-row {:is-col? true + :justify-items grid-justify-content-column + :set-justify set-justify-grid}] + [:& justify-grid-row {:is-col? false + :justify-items grid-justify-content-row + :set-justify set-justify-grid}]]] + + [:& grid-columns-row {:is-col? true + :expanded? @grid-columns-open? + :toggle toggle-columns-info + :column-values column-grid-values + :add-new-element add-new-element + :set-column-value set-column-value + :set-column-type set-column-type + :remove-element remove-element}] + + [:& grid-columns-row {:is-col? false + :expanded? @grid-rows-open? + :toggle toggle-rows-info + :column-values rows-grid-values + :add-new-element add-new-element + :set-column-value set-column-value + :set-column-type set-column-type + :remove-element remove-element}] + + [:& gap-section {:is-col? is-col? + :wrap-type wrap-type + :gap-selected? gap-selected? + :set-gap set-gap + :gap-value (:layout-gap values)}] + + [:& padding-section {:values values + :on-change-style change-padding-type + :on-change on-padding-change}]] + + + ;; Default if not grid or flex + nil)))])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index 8ce57f231..af92826ee 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.types.shape.layout :as ctl] + [app.main.data.workspace :as udw] [app.main.data.workspace.shape-layout :as dwsl] [app.main.refs :as refs] [app.main.store :as st] @@ -29,7 +30,8 @@ :layout-item-max-w ;; num :layout-item-min-w ;; num :layout-item-align-self ;; :start :end :center :stretch :baseline - ]) + :layout-item-absolute + :layout-item-z-index]) (mf/defc margin-section [{:keys [values change-margin-style on-margin-change] :as props}] @@ -45,7 +47,18 @@ (= (dm/get-in values [:layout-item-margin :m2]) (dm/get-in values [:layout-item-margin :m4]))) (dm/get-in values [:layout-item-margin :m2]) - "--")] + "--") + select-margins + (fn [m1? m2? m3? m4?] + (st/emit! (udw/set-margins-selected {:m1 m1? :m2 m2? :m3 m3? :m4 m4?}))) + + select-margin #(select-margins (= % :m1) (= % :m2) (= % :m3) (= % :m4))] + + (mf/use-effect + (fn [] + (fn [] + ;;on destroy component + (select-margins false false false false)))) [:div.margin-row (cond @@ -57,8 +70,11 @@ [:span.icon i/auto-margin-both-sides] [:> numeric-input {:placeholder "--" - :on-click #(dom/select-target %) + :on-focus (fn [event] + (select-margins true false true false) + (dom/select-target event)) :on-change (partial on-margin-change :simple :m1) + :on-blur #(select-margins false false false false) :value m1}]] [:div.margin-item.tooltip.tooltip-bottom-left @@ -66,8 +82,11 @@ [:span.icon.rotated i/auto-margin-both-sides] [:> numeric-input {:placeholder "--" - :on-click #(dom/select-target %) + :on-focus (fn [event] + (select-margins false true false true) + (dom/select-target event)) :on-change (partial on-margin-change :simple :m2) + :on-blur #(select-margins false false false false) :value m2}]]] (= margin-type :multiple) @@ -83,8 +102,11 @@ [:div.input-element.auto [:> numeric-input {:placeholder "--" - :on-click #(dom/select-target %) + :on-focus (fn [event] + (select-margin num) + (dom/select-target event)) :on-change (partial on-margin-change :multiple num) + :on-blur #(select-margins false false false false) :value (num (:layout-item-margin values))}]]])]) [:div.margin-item-icons @@ -193,58 +215,94 @@ on-size-change (fn [measure value] - (st/emit! (dwsl/update-layout-child ids {measure value})))] + (st/emit! (dwsl/update-layout-child ids {measure value}))) + + on-change-position + (fn [value] + (st/emit! (dwsl/update-layout-child ids {:layout-item-absolute (= value :absolute)}))) + + on-change-z-index + (fn [value] + (st/emit! (dwsl/update-layout-child ids {:layout-item-z-index value})))] [:div.element-set [:div.element-set-title - [:span "Flex elements"]] + [:span (if (and is-layout-container? (not is-layout-child?)) + "Flex board" + "Flex element")]] [:div.element-set-content.layout-item-menu - [:div.layout-row - [:div.row-title.sizing "Sizing"] - [:& element-behavior {:is-layout-child? is-layout-child? - :is-layout-container? is-layout-container? - :layout-item-v-sizing (or (:layout-item-v-sizing values) :fix) - :layout-item-h-sizing (or (:layout-item-h-sizing values) :fix) - :on-change-behavior on-change-behavior}]] - (when is-layout-child? [:div.layout-row - [:div.row-title "Align"] + [:div.row-title.sizing "Position"] [:div.btn-wrapper - [:& align-self-row {:is-col? is-col? - :align-self align-self - :set-align-self set-align-self}]]]) + [:div.absolute + [:button.behavior-btn.tooltip.tooltip-bottom + {:alt "Static" + :class (dom/classnames :active (not (:layout-item-absolute values))) + :on-click #(on-change-position :static)} + "Static"] + [:button.behavior-btn.tooltip.tooltip-bottom + {:alt "Absolute" + :class (dom/classnames :active (and (:layout-item-absolute values) (not= :multiple (:layout-item-absolute values)))) + :on-click #(on-change-position :absolute)} + "Absolute"]] - (when is-layout-child? - [:& margin-section {:values values - :change-margin-style change-margin-style - :on-margin-change on-margin-change}]) + [:div.tooltip.tooltip-bottom-left.z-index {:alt "z-index"} + i/layers + [:> numeric-input + {:placeholder "--" + :on-focus #(dom/select-target %) + :on-change #(on-change-z-index %) + :value (:layout-item-z-index values)}]]]]) - [:div.advanced-ops-body - [:div.input-wrapper - (for [item (cond-> [] - (= (:layout-item-h-sizing values) :fill) - (conj :layout-item-min-w :layout-item-max-w) + (when (not (:layout-item-absolute values)) + [:* + [:div.layout-row + [:div.row-title.sizing "Sizing"] + [:& element-behavior {:is-layout-child? is-layout-child? + :is-layout-container? is-layout-container? + :layout-item-v-sizing (or (:layout-item-v-sizing values) :fix) + :layout-item-h-sizing (or (:layout-item-h-sizing values) :fix) + :on-change-behavior on-change-behavior}]] - (= (:layout-item-v-sizing values) :fill) - (conj :layout-item-min-h :layout-item-max-h))] + (when is-layout-child? + [:div.layout-row + [:div.row-title "Align"] + [:div.btn-wrapper + [:& align-self-row {:is-col? is-col? + :align-self align-self + :set-align-self set-align-self}]]]) - [:div.tooltip.tooltip-bottom - {:key (d/name item) - :alt (tr (dm/str "workspace.options.layout-item.title." (d/name item))) - :class (dom/classnames "maxH" (= item :layout-item-max-h) - "minH" (= item :layout-item-min-h) - "maxW" (= item :layout-item-max-w) - "minW" (= item :layout-item-min-w))} - [:div.input-element - {:alt (tr (dm/str "workspace.options.layout-item." (d/name item)))} - [:> numeric-input - {:no-validate true - :min 0 - :data-wrap true - :placeholder "--" - :on-click #(dom/select-target %) - :on-change (partial on-size-change item) - :value (get values item) - :nillable true}]]])]]]])) + (when is-layout-child? + [:& margin-section {:values values + :change-margin-style change-margin-style + :on-margin-change on-margin-change}]) + + [:div.advanced-ops-body + [:div.input-wrapper + (for [item (cond-> [] + (= (:layout-item-h-sizing values) :fill) + (conj :layout-item-min-w :layout-item-max-w) + + (= (:layout-item-v-sizing values) :fill) + (conj :layout-item-min-h :layout-item-max-h))] + + [:div.tooltip.tooltip-bottom + {:key (d/name item) + :alt (tr (dm/str "workspace.options.layout-item.title." (d/name item))) + :class (dom/classnames "maxH" (= item :layout-item-max-h) + "minH" (= item :layout-item-min-h) + "maxW" (= item :layout-item-max-w) + "minW" (= item :layout-item-min-w))} + [:div.input-element + {:alt (tr (dm/str "workspace.options.layout-item." (d/name item)))} + [:> numeric-input + {:no-validate true + :min 0 + :data-wrap true + :placeholder "--" + :on-focus #(dom/select-target %) + :on-change (partial on-size-change item) + :value (get values item) + :nillable true}]]])]]])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 5a5c584f9..3169f5aef 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -83,14 +83,23 @@ selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) selection-parents (mf/deref selection-parents-ref) - flex-child? (->> selection-parents (some ctl/layout?)) - - flex-container? (ctl/layout? shape) + flex-child? (->> selection-parents (some ctl/flex-layout?)) + absolute? (ctl/layout-absolute? shape) + flex-container? (ctl/flex-layout? shape) flex-auto-width? (ctl/auto-width? shape) flex-fill-width? (ctl/fill-width? shape) flex-auto-height? (ctl/auto-height? shape) flex-fill-height? (ctl/fill-height? shape) + disabled-position-x? (and flex-child? (not absolute?)) + disabled-position-y? (and flex-child? (not absolute?)) + disabled-width-sizing? (and (or flex-child? flex-container?) + (or flex-auto-width? flex-fill-width?) + (not absolute?)) + disabled-height-sizing? (and (or flex-child? flex-container?) + (or flex-auto-height? flex-fill-height?) + (not absolute?)) + ;; To show interactively the measures while the user is manipulating ;; the shape with the mouse, generate a copy of the shapes applying ;; the transient transformations. @@ -307,18 +316,18 @@ [:> numeric-input {:min 0.01 :no-validate true :placeholder "--" - :on-click select-all + :on-focus select-all :on-change on-width-change - :disabled (and (or flex-child? flex-container?) (or flex-auto-width? flex-fill-width?)) + :disabled disabled-width-sizing? :value (:width values)}]] [:div.input-element.height {:title (tr "workspace.options.height")} [:> numeric-input {:min 0.01 :no-validate true :placeholder "--" - :on-click select-all + :on-focus select-all :on-change on-height-change - :disabled (and (or flex-child? flex-container?) (or flex-auto-height? flex-fill-height?)) + :disabled disabled-height-sizing? :value (:height values)}]] [:div.lock-size {:class (dom/classnames @@ -336,15 +345,15 @@ [:div.input-element.Xaxis {:title (tr "workspace.options.x")} [:> numeric-input {:no-validate true :placeholder "--" - :on-click select-all + :on-focus select-all :on-change on-pos-x-change - :disabled flex-child? + :disabled disabled-position-x? :value (:x values)}]] [:div.input-element.Yaxis {:title (tr "workspace.options.y")} [:> numeric-input {:no-validate true :placeholder "--" - :on-click select-all - :disabled flex-child? + :on-focus select-all + :disabled disabled-position-y? :on-change on-pos-y-change :value (:y values)}]]]) @@ -359,7 +368,7 @@ :max 359 :data-wrap true :placeholder "--" - :on-click select-all + :on-focus select-all :on-change on-rotation-change :value (:rotation values)}]]]) @@ -387,7 +396,7 @@ {:placeholder "--" :ref radius-input-ref :min 0 - :on-click select-all + :on-focus select-all :on-change on-radius-1-change :value (:rx values)}]] @@ -397,7 +406,7 @@ {:type "number" :placeholder "--" :min 0 - :on-click select-all + :on-focus select-all :on-change on-radius-multi-change :value ""}]] @@ -407,7 +416,7 @@ [:> numeric-input {:placeholder "--" :min 0 - :on-click select-all + :on-focus select-all :on-change on-radius-r1-change :value (:r1 values)}]] @@ -415,7 +424,7 @@ [:> numeric-input {:placeholder "--" :min 0 - :on-click select-all + :on-focus select-all :on-change on-radius-r2-change :value (:r2 values)}]] @@ -423,7 +432,7 @@ [:> numeric-input {:placeholder "--" :min 0 - :on-click select-all + :on-focus select-all :on-change on-radius-r3-change :value (:r3 values)}]] @@ -431,7 +440,7 @@ [:> numeric-input {:placeholder "--" :min 0 - :on-click select-all + :on-focus select-all :on-change on-radius-r4-change :value (:r4 values)}]]])]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs index fca3df79e..b518d9763 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs @@ -150,7 +150,7 @@ [:> numeric-input {:ref adv-offset-x-ref :no-validate true :placeholder "--" - :on-click (select-text adv-offset-x-ref) + :on-focus (select-text adv-offset-x-ref) :on-change (update-attr index :offset-x valid-number? basic-offset-x-ref) :value (:offset-x value)}] [:span.after (tr "workspace.options.shadow-options.offsetx")]] @@ -159,7 +159,7 @@ [:> numeric-input {:ref adv-offset-y-ref :no-validate true :placeholder "--" - :on-click (select-text adv-offset-y-ref) + :on-focus (select-text adv-offset-y-ref) :on-change (update-attr index :offset-y valid-number? basic-offset-y-ref) :value (:offset-y value)}] [:span.after (tr "workspace.options.shadow-options.offsety")]]] @@ -169,7 +169,7 @@ [:> numeric-input {:ref adv-blur-ref :no-validate true :placeholder "--" - :on-click (select-text adv-blur-ref) + :on-focus (select-text adv-blur-ref) :on-change (update-attr index :blur valid-number? basic-blur-ref) :min 0 :value (:blur value)}] @@ -179,9 +179,8 @@ [:> numeric-input {:ref adv-spread-ref :no-validate true :placeholder "--" - :on-click (select-text adv-spread-ref) + :on-focus (select-text adv-spread-ref) :on-change (update-attr index :spread valid-number?) - :min 0 :value (:spread value)}] [:span.after (tr "workspace.options.shadow-options.spread")]]] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 70e539d73..9cbfc528c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -11,6 +11,7 @@ [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.undo :as dwu] [app.main.fonts :as fonts] @@ -25,8 +26,6 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) - - (mf/defc text-align-options [{:keys [values on-change on-blur] :as props}] (let [{:keys [text-align]} values @@ -38,22 +37,22 @@ ;; --- Align [:div.align-icons [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.align-left") + {:alt (tr "workspace.options.text-options.align-left" (sc/get-tooltip :text-align-left)) :class (dom/classnames :current (= "left" text-align)) :on-click #(handle-change % "left")} i/text-align-left] [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.align-center") + {:alt (tr "workspace.options.text-options.align-center" (sc/get-tooltip :text-align-center)) :class (dom/classnames :current (= "center" text-align)) :on-click #(handle-change % "center")} i/text-align-center] [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.align-right") + {:alt (tr "workspace.options.text-options.align-right" (sc/get-tooltip :text-align-right)) :class (dom/classnames :current (= "right" text-align)) :on-click #(handle-change % "right")} i/text-align-right] [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.align-justify") + {:alt (tr "workspace.options.text-options.align-justify" (sc/get-tooltip :text-align-justify)) :class (dom/classnames :current (= "justify" text-align)) :on-click #(handle-change % "justify")} i/text-align-justify]])) @@ -149,13 +148,13 @@ i/minus] [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.underline") + {:alt (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline)) :class (dom/classnames :current (= "underline" text-decoration)) :on-click #(handle-change % "underline")} i/underline] [:span.tooltip.tooltip-bottom - {:alt (tr "workspace.options.text-options.strikethrough") + {:alt (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through)) :class (dom/classnames :current (= "line-through" text-decoration)) :on-click #(handle-change % "line-through")} i/strikethrough]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index e00b2ef41..4d51e1480 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -503,8 +503,8 @@ [:div.element-set-actions (when on-detach [:div.element-set-actions-button - {:on-mouse-enter #(reset! hover-detach true) - :on-mouse-leave #(reset! hover-detach false) + {:on-pointer-enter #(reset! hover-detach true) + :on-pointer-leave #(reset! hover-detach false) :on-click on-detach} (if @hover-detach i/unchain i/chain)]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 4e2cc346b..4a5653216 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -113,7 +113,8 @@ :y y :disable-gradient disable-gradient :disable-opacity disable-opacity - :on-change #(on-change (merge uc/empty-color %)) + ;; on-change second parameter means if the source is the color-picker + :on-change #(on-change (merge uc/empty-color %) true) :on-close (fn [value opacity id file-id] (when on-close (on-close value opacity id file-id))) @@ -167,8 +168,8 @@ [:div.color-name (str color-name)]] (when on-detach [:div.element-set-actions-button - {:on-mouse-enter #(reset! hover-detach true) - :on-mouse-leave #(reset! hover-detach false) + {:on-pointer-enter #(reset! hover-detach true) + :on-pointer-leave #(reset! hover-detach false) :on-click detach-value} (if @hover-detach i/unchain i/chain)]) @@ -196,7 +197,7 @@ "" (-> color :color uc/remove-hash)) :placeholder (tr "settings.multiple") - :on-click select-all + :on-focus select-all :on-blur on-blur :on-change handle-value-change}]] @@ -206,7 +207,7 @@ {:class (dom/classnames :percentail (not= (:opacity color) :multiple))} [:> numeric-input {:value (-> color :opacity opacity->string) :placeholder (tr "settings.multiple") - :on-click select-all + :on-focus select-all :on-blur on-blur :on-change handle-opacity-change :min 0 diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index fb83ff3c3..62189be57 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -100,7 +100,7 @@ :value (-> (:stroke-width stroke) width->string) :placeholder (tr "settings.multiple") :on-change (on-stroke-width-change index) - :on-click select-all + :on-focus select-all :on-blur on-blur}]] [:select#style.input-select {:value (enum->string (:stroke-alignment stroke)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs index f386b58a6..458acf5d9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/bool.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.sidebar.options.shapes.bool (:require + [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] @@ -29,8 +30,10 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref)] + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + + is-layout-child-absolute? (ctl/layout-absolute? shape)] [:* [:& measures-menu {:ids ids :type type @@ -38,7 +41,7 @@ :shape shape}] [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-layout-child? + (when is-flex-layout-child? [:& layout-item-menu {:ids ids :type type @@ -46,7 +49,7 @@ :is-layout-child? true :shape shape}]) - (when (not is-layout-child?) + (when (or (not is-flex-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) [:& layer-menu {:ids ids diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs index 6e0a78ebf..78b938ca7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/circle.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.sidebar.options.shapes.circle (:require + [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] @@ -31,8 +32,9 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref)] + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-layout-child-absolute? (ctl/layout-absolute? shape)] [:* [:& measures-menu {:ids ids :type type @@ -40,14 +42,14 @@ :shape shape}] [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-layout-child? + (when is-flex-layout-child? [:& layout-item-menu {:ids ids :type type :values layout-item-values :is-layout-child? true :is-layout-container? false :shape shape}]) - (when (not is-layout-child?) + (when (or (not is-flex-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) [:& layer-menu {:ids ids diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index 6656c66bc..2f33bd52a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -36,9 +36,10 @@ layout-item-values (select-keys shape layout-item-attrs) [comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)] - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref) - is-layout-container? (ctl/layout? shape)] + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-flex-layout-container? (ctl/flex-layout? shape) + is-layout-child-absolute? (ctl/layout-absolute? shape)] [:* [:& measures-menu {:ids [(:id shape)] :values measure-values @@ -47,18 +48,18 @@ [:& component-menu {:ids comp-ids :values comp-values :shape-name (:name shape)}] - (when (not is-layout-child?) + (when (or (not is-flex-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when (or is-layout-child? is-layout-container?) + (when (or is-flex-layout-child? is-flex-layout-container?) [:& layout-item-menu {:ids ids :type type :values layout-item-values - :is-layout-child? is-layout-child? - :is-layout-container? is-layout-container? + :is-layout-child? is-flex-layout-child? + :is-layout-container? is-flex-layout-container? :shape shape}]) [:& layer-menu {:ids ids diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/grid_cell.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/grid_cell.cljs new file mode 100644 index 000000000..5b4f96dd8 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/grid_cell.cljs @@ -0,0 +1,171 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.sidebar.options.shapes.grid-cell + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.options.menus.layout-container :as lyc] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(mf/defc set-self-alignment + [{:keys [is-col? alignment set-alignment] :as props}] + (let [dir-v [:auto :start :center :end :stretch #_:baseline]] + [:div.align-self-style + (for [align dir-v] + [:button.align-self.tooltip.tooltip-bottom + {:class (dom/classnames :active (= alignment align) + :tooltip-bottom-left (not= align :start) + :tooltip-bottom (= align :start)) + :alt (dm/str "Align self " (d/name align)) ;; TODO fix this tooltip + :on-click #(set-alignment align) + :key (str "align-self" align)} + (lyc/get-layout-flex-icon :align-self align is-col?)])])) + + +(mf/defc options + {::mf/wrap [mf/memo]} + [{:keys [_shape row column] :as props}] + + (let [position-mode (mf/use-state :auto) ;; TODO this should come from shape + + set-position-mode (fn [mode] + (reset! position-mode mode)) + + + align-self (mf/use-state :auto) ;; TODO this should come from shape + justify-alignment (mf/use-state :auto) ;; TODO this should come from shape + set-alignment (fn [value] + (reset! align-self value) + #_(if (= align-self value) + (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self nil})) + (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self value})))) + set-justify-self (fn [value] + (reset! justify-alignment value) + #_(if (= align-self value) + (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self nil})) + (st/emit! (dwsl/update-layout-child ids {:layout-item-align-self value})))) + column-start column + column-end (inc column) + row-start row + row-end (inc row) + + on-change + (fn [_side _orientation _value] + ;; TODO + #_(if (= orientation :column) + (case side + :all ((reset! column-start value) + (reset! column-end value)) + :start (reset! column-start value) + :end (reset! column-end value)) + (case side + :all ((reset! row-start value) + (reset! row-end value)) + :start (reset! row-start value) + :end (reset! row-end value)))) + + area-name (mf/use-state "header") ;; TODO this should come from shape + + on-area-name-change (fn [value] + (reset! area-name value)) + on-key-press (fn [_event])] + + [:div.element-set + [:div.element-set-title + [:span "Grid Cell"]] + + [:div.element-set-content.layout-grid-item-menu + [:div.layout-row + [:div.row-title.sizing "Position"] + [:div.position-wrapper + [:button.position-btn + {:on-click #(set-position-mode :auto) + :class (dom/classnames :active (= :auto @position-mode))} "Auto"] + [:button.position-btn + {:on-click #(set-position-mode :manual) + :class (dom/classnames :active (= :manual @position-mode))} "Manual"] + [:button.position-btn + {:on-click #(set-position-mode :area) + :class (dom/classnames :active (= :area @position-mode))} "Area"]]] + [:div.manage-grid-columns + (when (= :auto @position-mode) + [:div.grid-auto + [:div.grid-columns-auto + [:spam.icon i/layout-rows] + [:div.input-wrapper + [:> numeric-input + {:placeholder "--" + :on-click #(dom/select-target %) + :on-change (partial on-change :all :column) ;; TODO cambiar este on-change y el value + :value column-start}]]] + [:div.grid-rows-auto + [:spam.icon i/layout-columns] + [:div.input-wrapper + [:> numeric-input + {:placeholder "--" + :on-click #(dom/select-target %) + :on-change (partial on-change :all :row) ;; TODO cambiar este on-change y el value + :value row-start}]]]]) + (when (= :area @position-mode) + [:div.input-wrapper + [:input.input-text + {:key "grid-area-name" + :id "grid-area-name" + :type "text" + :aria-label "grid-area-name" + :placeholder "--" + :default-value @area-name + :auto-complete "off" + :on-change on-area-name-change + :on-key-press on-key-press}]]) + + (when (or (= :manual @position-mode) (= :area @position-mode)) + [:div.grid-manual + [:div.grid-columns-auto + [:spam.icon i/layout-rows] + [:div.input-wrapper + [:> numeric-input + {:placeholder "--" + :on-click #(dom/select-target %) + :on-change (partial on-change :start :column) + :value column-start}] + [:> numeric-input + {:placeholder "--" + :on-click #(dom/select-target %) + :on-change (partial on-change :end :column) + :value column-end}]]] + [:div.grid-rows-auto + [:spam.icon i/layout-columns] + [:div.input-wrapper + [:> numeric-input + {:placeholder "--" + :on-click #(dom/select-target %) + :on-change (partial on-change :start :row) + :value row-start}] + [:> numeric-input + {:placeholder "--" + :on-click #(dom/select-target %) + :on-change (partial on-change :end :row) + :value row-end}]]]])] + + [:div.layout-row + [:div.row-title "Align"] + [:div.btn-wrapper + [:& set-self-alignment {:is-col? false + :alignment @align-self + :set-alignment set-alignment}]]] + + + [:div.layout-row + [:div.row-title "Justify"] + [:div.btn-wrapper + [:& set-self-alignment {:is-col? true + :alignment @justify-alignment + :set-alignment set-justify-self}]]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs index 016e4742d..4d4520350 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.options.shapes.group (:require [app.common.data :as d] + [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.color-selection :refer [color-selection-menu]] @@ -28,15 +29,16 @@ {::mf/wrap [mf/memo] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - shape-with-children (unchecked-get props "shape-with-children") - shared-libs (unchecked-get props "shared-libs") - objects (->> shape-with-children (group-by :id) (d/mapm (fn [_ v] (first v)))) - file-id (unchecked-get props "file-id") - layout-container-values (select-keys shape layout-container-flex-attrs) - ids [(:id shape)] - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref) + (let [shape (unchecked-get props "shape") + shape-with-children (unchecked-get props "shape-with-children") + shared-libs (unchecked-get props "shared-libs") + objects (->> shape-with-children (group-by :id) (d/mapm (fn [_ v] (first v)))) + file-id (unchecked-get props "file-id") + layout-container-values (select-keys shape layout-container-flex-attrs) + ids [(:id shape)] + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-layout-child-absolute? (ctl/layout-absolute? shape) type :group [measure-ids measure-values] (get-attrs [shape] objects :measure) @@ -56,7 +58,7 @@ [:& component-menu {:ids comp-ids :values comp-values :shape-name (:name shape)}] [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-layout-child? + (when is-flex-layout-child? [:& layout-item-menu {:type type :ids layout-item-ids @@ -64,8 +66,9 @@ :is-layout-container? false :values layout-item-values}]) - (when (not is-layout-child?) + (when (or (not is-flex-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids constraint-ids :values constraint-values}]) + [:& layer-menu {:type type :ids layer-ids :values layer-values}] (when-not (empty? fill-ids) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs index 862dba587..7d9327304 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/image.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.sidebar.options.shapes.image (:require + [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] @@ -31,8 +32,9 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref)] + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-layout-child-absolute? (ctl/layout-absolute? shape)] [:* [:& measures-menu {:ids ids :type type @@ -40,7 +42,7 @@ :shape shape}] [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-layout-child? + (when is-flex-layout-child? [:& layout-item-menu {:ids ids :type type @@ -48,7 +50,7 @@ :is-layout-child? true :shape shape}]) - (when (not is-layout-child?) + (when (or (not is-flex-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 0e0f7008c..bdef18f32 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -294,17 +294,17 @@ all-types (into #{} (map :type shapes)) ids (->> shapes (map :id)) - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref) + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) has-text? (contains? all-types :text) - has-layout-container? (->> shapes (some ctl/layout?)) + has-flex-layout-container? (->> shapes (some ctl/flex-layout?)) - all-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/all-layout-child? ids)) - all-layout-child? (mf/deref all-layout-child-ref) + all-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/all-flex-layout-child? ids)) + all-flex-layout-child? (mf/deref all-flex-layout-child-ref) - all-layout-container? (->> shapes (every? ctl/layout?)) + all-flex-layout-container? (->> shapes (every? ctl/flex-layout?)) [measure-ids measure-values] (get-attrs shapes objects :measure) @@ -342,15 +342,15 @@ [:& layout-container-menu {:type type :ids layout-container-ids :values layout-container-values :multiple true}] - (when (or is-layout-child? has-layout-container?) + (when (or is-flex-layout-child? has-flex-layout-container?) [:& layout-item-menu {:type type :ids layout-item-ids - :is-layout-child? all-layout-child? - :is-layout-container? all-layout-container? + :is-layout-child? all-flex-layout-child? + :is-layout-container? all-flex-layout-container? :values layout-item-values}]) - (when-not (or (empty? constraint-ids) is-layout-child?) + (when-not (or (empty? constraint-ids) is-flex-layout-child?) [:& constraints-menu {:ids constraint-ids :values constraint-values}]) (when-not (empty? layer-ids) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs index c1803daa6..5a7b1bd95 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.sidebar.options.shapes.path (:require + [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] @@ -31,8 +32,9 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref)] + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-layout-child-absolute? (ctl/layout-absolute? shape)] [:* [:& measures-menu {:ids ids :type type @@ -40,14 +42,14 @@ :shape shape}] [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-layout-child? + (when is-flex-layout-child? [:& layout-item-menu {:ids ids :type type :values layout-item-values :is-layout-child? true :is-layout-container? false :shape shape}]) - (when (not is-layout-child?) + (when (or (not is-flex-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) [:& layer-menu {:ids ids diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs index e133f7a22..401ef70ef 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/rect.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.sidebar.options.shapes.rect (:require + [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] @@ -31,15 +32,16 @@ stroke-values (select-keys shape stroke-attrs) layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref)] + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-layout-child-absolute? (ctl/layout-absolute? shape)] [:* [:& measures-menu {:ids ids :type type :values measure-values :shape shape}] [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-layout-child? + (when is-flex-layout-child? [:& layout-item-menu {:ids ids :type type @@ -47,7 +49,7 @@ :is-layout-child? true :shape shape}]) - (when (not is-layout-child?) + (when (or (not is-flex-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs index bae854679..0560de549 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/svg_raw.cljs @@ -8,6 +8,7 @@ (:require [app.common.colors :as clr] [app.common.data :as d] + [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.menus.constraints :refer [constraint-attrs constraints-menu]] @@ -105,8 +106,9 @@ layout-item-values (select-keys shape layout-item-attrs) layout-container-values (select-keys shape layout-container-flex-attrs) - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref)] + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) + is-layout-child-absolute? (ctl/layout-absolute? shape)] (when (contains? svg-elements tag) [:* @@ -116,7 +118,7 @@ :shape shape}] [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-layout-child? + (when is-flex-layout-child? [:& layout-item-menu {:ids ids :type type @@ -124,7 +126,7 @@ :is-layout-child? true :shape shape}]) - (when (not is-layout-child?) + (when (or (not is-flex-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids ids :values constraint-values}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 3437d4a8a..8c93c8f9e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.options.shapes.text (:require [app.common.data :as d] + [app.common.types.shape.layout :as ctl] [app.main.data.workspace.texts :as dwt :refer [text-fill-attrs root-attrs paragraph-attrs text-attrs]] [app.main.refs :as refs] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-menu]] @@ -27,9 +28,10 @@ (let [ids [(:id shape)] type (:type shape) - is-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-layout-child? ids)) - is-layout-child? (mf/deref is-layout-child-ref) + is-flex-layout-child-ref (mf/use-memo (mf/deps ids) #(refs/is-flex-layout-child? ids)) + is-flex-layout-child? (mf/deref is-flex-layout-child-ref) layout-container-values (select-keys shape layout-container-flex-attrs) + is-layout-child-absolute? (ctl/layout-absolute? shape) state-map (mf/deref refs/workspace-editor-state) shared-libs (mf/deref refs/workspace-libraries) @@ -74,7 +76,7 @@ :shape shape}] [:& layout-container-menu {:type type :ids [(:id shape)] :values layout-container-values :multiple false}] - (when is-layout-child? + (when is-flex-layout-child? [:& layout-item-menu {:ids ids :type type @@ -82,7 +84,7 @@ :is-layout-child? true :shape shape}]) - (when (not is-layout-child?) + (when (or (not is-flex-layout-child?) is-layout-child-absolute?) [:& constraints-menu {:ids ids :values (select-keys shape constraint-attrs)}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs index d740b528a..72017a3e3 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs @@ -143,6 +143,8 @@ ;; shortcuts.open-color-picker ;; shortcuts.open-comments ;; shortcuts.open-dashboard + ;; shortcuts.select-prev + ;; shortcuts.select-next ;; shortcuts.open-inspect ;; shortcuts.open-interactions ;; shortcuts.open-viewer diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 673773162..716218f32 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -204,7 +204,7 @@ (mf/defc sitemap [] - (let [{:keys [on-pointer-down on-lost-pointer-capture on-mouse-move parent-ref size]} + (let [{:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} (use-resize-hook :sitemap 200 38 400 :y false nil) file (mf/deref refs/workspace-file) @@ -234,4 +234,4 @@ (when @show-pages? [:div.resize-area {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture - :on-mouse-move on-mouse-move}])])) + :on-pointer-move on-pointer-move}])])) diff --git a/frontend/src/app/main/ui/workspace/textpalette.cljs b/frontend/src/app/main/ui/workspace/textpalette.cljs index c7b7625cb..fac5ce536 100644 --- a/frontend/src/app/main/ui/workspace/textpalette.cljs +++ b/frontend/src/app/main/ui/workspace/textpalette.cljs @@ -93,7 +93,7 @@ (on-right-arrow-click) (on-left-arrow-click))))) - {:keys [on-pointer-down on-lost-pointer-capture on-mouse-move parent-ref size]} + {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]} (use-resize-hook :palette 72 54 80 :y true :bottom)] [:div.color-palette {:ref parent-ref @@ -101,7 +101,7 @@ :style #js {"--height" (str size "px")}} [:div.resize-area {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture - :on-mouse-move on-mouse-move}] + :on-pointer-move on-pointer-move}] [:& dropdown {:show (:show-menu @state) :on-close #(swap! state assoc :show-menu false)} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index a766ea236..f96c44b68 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -29,6 +29,7 @@ [app.main.ui.workspace.viewport.drawarea :as drawarea] [app.main.ui.workspace.viewport.frame-grid :as frame-grid] [app.main.ui.workspace.viewport.gradients :as gradients] + [app.main.ui.workspace.viewport.grid-layout-editor :as grid-layout] [app.main.ui.workspace.viewport.guides :as guides] [app.main.ui.workspace.viewport.hooks :as hooks] [app.main.ui.workspace.viewport.interactions :as interactions] @@ -51,17 +52,20 @@ (defn apply-modifiers-to-selected [selected objects text-modifiers modifiers] - (into [] - (comp - (keep (d/getf objects)) - (map (fn [{:keys [id] :as shape}] - (cond-> shape - (and (cph/text-shape? shape) (contains? text-modifiers id)) - (dwm/apply-text-modifier (get text-modifiers id)) + (reduce + (fn [objects id] + (update + objects id + (fn [shape] + (cond-> shape + (and (cph/text-shape? shape) (contains? text-modifiers id)) + (dwm/apply-text-modifier (get text-modifiers id)) - (contains? modifiers id) - (gsh/transform-shape (dm/get-in modifiers [id :modifiers])))))) - selected)) + (contains? modifiers id) + (gsh/transform-shape (dm/get-in modifiers [id :modifiers])))))) + + objects + selected)) (mf/defc viewport [{:keys [wlocal wglobal selected layout file] :as props}] @@ -97,13 +101,17 @@ modifiers (mf/deref refs/workspace-modifiers) text-modifiers (mf/deref refs/workspace-text-modifier) - objects-modified (mf/with-memo [base-objects modifiers] - (gsh/apply-objects-modifiers base-objects modifiers selected)) + objects-modified (mf/with-memo + [base-objects text-modifiers modifiers] + (apply-modifiers-to-selected selected base-objects text-modifiers modifiers)) + + selected-shapes (->> selected (keep (d/getf objects-modified))) background (get options :background clr/canvas) ;; STATE alt? (mf/use-state false) + shift? (mf/use-state false) mod? (mf/use-state false) space? (mf/use-state false) z? (mf/use-state false) @@ -126,18 +134,19 @@ ;; STREAMS move-stream (mf/use-memo #(rx/subject)) - frame-parent (mf/use-memo + guide-frame (mf/use-memo (mf/deps @hover-ids base-objects) (fn [] - (let [parent (get base-objects (last @hover-ids))] - (when (= :frame (:type parent)) - parent)))) + (let [parent-id + (->> @hover-ids + (d/seek (partial cph/root-frame? base-objects)))] + (when (some? parent-id) + (get base-objects parent-id))))) zoom (d/check-num zoom 1) drawing-tool (:tool drawing) drawing-obj (:object drawing) - selected-shapes (apply-modifiers-to-selected selected base-objects text-modifiers modifiers) selected-frames (into #{} (map :frame-id) selected-shapes) @@ -151,6 +160,7 @@ (and (some? drawing-obj) (= :path (:type drawing-obj)))) node-editing? (and edition (not= :text (get-in base-objects [edition :type]))) text-editing? (and edition (= :text (get-in base-objects [edition :type]))) + grid-editing? (and edition (ctl/grid-layout? base-objects edition)) workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) mode-inspect? (= options-mode :inspect) @@ -161,14 +171,14 @@ on-drag-enter (actions/on-drag-enter) on-drag-over (actions/on-drag-over) on-drop (actions/on-drop file) - on-mouse-down (actions/on-mouse-down @hover selected edition drawing-tool text-editing? node-editing? - drawing-path? create-comment? space? panning z? workspace-read-only?) - on-mouse-up (actions/on-mouse-up disable-paste) - on-pointer-down (actions/on-pointer-down) + on-pointer-down (actions/on-pointer-down @hover selected edition drawing-tool text-editing? node-editing? grid-editing? + drawing-path? create-comment? space? panning z? workspace-read-only?) + + on-pointer-up (actions/on-pointer-up disable-paste) + on-pointer-enter (actions/on-pointer-enter in-viewport?) on-pointer-leave (actions/on-pointer-leave in-viewport?) on-pointer-move (actions/on-pointer-move move-stream) - on-pointer-up (actions/on-pointer-up) on-move-selected (actions/on-move-selected hover hover-ids selected space? z? workspace-read-only?) on-menu-selected (actions/on-menu-selected hover hover-ids selected workspace-read-only?) @@ -192,6 +202,7 @@ show-pixel-grid? (and (contains? layout :show-pixel-grid) (>= zoom 8)) show-text-editor? (and editing-shape (= :text (:type editing-shape))) + show-grid-editor? (and editing-shape (ctl/grid-layout? editing-shape)) show-presence? page-id show-prototypes? (= options-mode :prototype) show-selection-handlers? (and (seq selected) (not show-text-editor?)) @@ -209,12 +220,24 @@ show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui))) - disabled-guides? (or drawing-tool transform)] + disabled-guides? (or drawing-tool transform) - (hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport? workspace-read-only?) - (hooks/setup-viewport-size viewport-ref) + show-padding? (and (nil? transform) + (= (count selected-shapes) 1) + (= (:type (first selected-shapes)) :frame) + (= (:layout (first selected-shapes)) :flex) + (zero? (:rotation (first selected-shapes)))) + + + show-margin? (and (nil? transform) + (= (count selected-shapes) 1) + (= (:layout selected-frame) :flex) + (zero? (:rotation (first selected-shapes))))] + + (hooks/setup-dom-events zoom disable-paste in-viewport? workspace-read-only?) + (hooks/setup-viewport-size vport viewport-ref) (hooks/setup-cursor cursor alt? mod? space? panning drawing-tool drawing-path? node-editing? z? workspace-read-only?) - (hooks/setup-keyboard alt? mod? space? z?) + (hooks/setup-keyboard alt? mod? space? z? shift?) (hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures?) (hooks/setup-viewport-modifiers modifiers base-objects) (hooks/setup-shortcuts node-editing? drawing-path? text-editing?) @@ -292,7 +315,7 @@ :view-box (utils/format-viewbox vbox) :ref on-viewport-ref :class (when drawing-tool "drawing") - :style {:cursor @cursor} + :style {:cursor @cursor :touch-action "none"} :fill "none" :on-click on-click @@ -301,8 +324,6 @@ :on-drag-enter on-drag-enter :on-drag-over on-drag-over :on-drop on-drop - :on-mouse-down on-mouse-down - :on-mouse-up on-mouse-up :on-pointer-down on-pointer-down :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave @@ -328,7 +349,7 @@ :zoom zoom :modifiers modifiers}] - (when (ctl/layout? outlined-frame) + (when (ctl/any-layout? outlined-frame) [:g.ghost-outline [:& outline/shape-outlines {:objects base-objects @@ -368,6 +389,31 @@ :hover-shape @hover :zoom zoom}]) + (when show-padding? + [:* + [:& msr/padding + {:frame (first selected-shapes) + :hover @frame-hover + :zoom zoom + :alt? @alt? + :shift? @shift?}] + + [:& msr/gap + {:frame (first selected-shapes) + :hover @frame-hover + :zoom zoom + :alt? @alt? + :shift? @shift?}]]) + + (when show-margin? + [:& msr/margin + {:shape (first selected-shapes) + :parent selected-frame + :hover @frame-hover + :zoom zoom + :alt? @alt? + :shift? @shift?}]) + [:& widgets/frame-titles {:objects base-objects :selected selected @@ -454,7 +500,7 @@ [:& guides/viewport-guides {:zoom zoom :vbox vbox - :hover-frame frame-parent + :hover-frame guide-frame :disabled-guides? disabled-guides? :modifiers modifiers}]) @@ -483,6 +529,12 @@ :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) + (when (debug? :grid-layout) + [:& wvd/debug-grid-layout {:selected-shapes selected-shapes + :objects base-objects + :hover-top-frame-id @hover-top-frame-id + :zoom zoom}]) + (when show-selection-handlers? [:g.selection-handlers {:clipPath "url(#clip-handlers)"} [:defs @@ -513,4 +565,11 @@ (when show-gradient-handlers? [:& gradients/gradient-handlers {:id (first selected) - :zoom zoom}])]]])) + :zoom zoom}]) + + (when show-grid-editor? + [:& grid-layout/editor + {:zoom zoom + :objects base-objects + :shape (get base-objects edition)}]) + ]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 8cf782224..a18d24bd3 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -32,14 +32,27 @@ (def scale-per-pixel -0.0057) -(defn on-mouse-down +(defn on-pointer-down [{:keys [id blocked hidden type]} selected edition drawing-tool text-editing? - node-editing? drawing-path? create-comment? space? panning z? workspace-read-only?] + node-editing? grid-editing? drawing-path? create-comment? space? panning z? workspace-read-only?] (mf/use-callback (mf/deps id blocked hidden type selected edition drawing-tool text-editing? - node-editing? drawing-path? create-comment? @z? @space? + node-editing? grid-editing? drawing-path? create-comment? @z? @space? panning workspace-read-only?) + (fn [bevent] + ;; We need to handle editor related stuff here because + ;; handling on editor dom node does not works properly. + (let [target (dom/get-target bevent) + editor (.closest ^js target ".public-DraftEditor-content")] + ;; Capture mouse pointer to detect the movements even if cursor + ;; leaves the viewport or the browser itself + ;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture + (if editor + (.setPointerCapture editor (.-pointerId bevent)) + (.setPointerCapture target (.-pointerId bevent)))) + + (when (or (dom/class? (dom/get-target bevent) "viewport-controls") (dom/class? (dom/get-target bevent) "viewport-selrect")) (dom/stop-propagation bevent) @@ -70,7 +83,7 @@ (do (st/emit! (ms/->MouseEvent :down ctrl? shift? alt? meta?)) - (when (and (not= edition id) text-editing?) + (when (and (not= edition id) (or text-editing? grid-editing?)) (st/emit! dw/clear-edition-mode)) (when (and (not text-editing?) @@ -80,7 +93,7 @@ (not drawing-path?)) (cond node-editing? - ;; Handle path node area selection + ;; Handle path node area selection (when-not workspace-read-only? (st/emit! (dwdp/handle-area-selection shift?))) @@ -241,12 +254,16 @@ (let [position (dom/get-client-position event)] (st/emit! (dw/show-shape-context-menu {:position position :hover-ids @hover-ids}))))))) -(defn on-mouse-up +(defn on-pointer-up [disable-paste] (mf/use-callback (fn [event] (dom/stop-propagation event) + (let [target (dom/get-target event)] + ;; Release pointer on mouse up + (.releasePointerCapture target (.-pointerId event))) + (let [event (.-nativeEvent event) ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) @@ -279,27 +296,6 @@ (fn [] (reset! in-viewport? false)))) -(defn on-pointer-down [] - (mf/use-callback - (fn [event] - ;; We need to handle editor related stuff here because - ;; handling on editor dom node does not works properly. - (let [target (dom/get-target event) - editor (.closest ^js target ".public-DraftEditor-content")] - ;; Capture mouse pointer to detect the movements even if cursor - ;; leaves the viewport or the browser itself - ;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture - (if editor - (.setPointerCapture editor (.-pointerId event)) - (.setPointerCapture target (.-pointerId event))))))) - -(defn on-pointer-up [] - (mf/use-callback - (fn [event] - (let [target (dom/get-target event)] - ; Release pointer on mouse up - (.releasePointerCapture target (.-pointerId event)))))) - (defn on-key-down [] (mf/use-callback (fn [event] @@ -333,7 +329,7 @@ (= "TEXTAREA" (obj/get target "tagName")))] (st/emit! (ms/->KeyboardEvent :up key shift? ctrl? alt? meta? editing?)))))) -(defn on-mouse-move [] +(defn on-pointer-move [move-stream] (let [last-position (mf/use-var nil)] (mf/use-callback (fn [event] @@ -345,7 +341,7 @@ delta (if @last-position (gpt/subtract raw-pt @last-position) (gpt/point 0 0))] - + (rx/push! move-stream pt) (reset! last-position raw-pt) (st/emit! (ms/->PointerEvent :delta delta (kbd/ctrl? event) @@ -358,14 +354,6 @@ (kbd/alt? event) (kbd/meta? event)))))))) -(defn on-pointer-move [move-stream] - (mf/use-callback - (mf/deps move-stream) - (fn [event] - (let [raw-pt (dom/get-client-position event) - pt (uwvv/point->viewport raw-pt)] - (rx/push! move-stream pt))))) - (defn on-mouse-wheel [zoom] (mf/use-callback (mf/deps zoom) @@ -446,7 +434,15 @@ (:id component) (gpt/point final-x final-y)))) - ;; Will trigger when the user drags an image from a browser to the viewport + ;; Will trigger when the user drags an image from a browser to the viewport (firefox and chrome do it a bit different depending on the origin) + (dnd/has-type? event "Files") + (let [files (dnd/get-files event) + params {:file-id (:id file) + :position viewport-coord + :blobs (seq files)}] + (st/emit! (dwm/upload-media-workspace params))) + + ;; Will trigger when the user drags an image from a browser to the viewport (firefox and chrome do it a bit different depending on the origin) (dnd/has-type? event "text/uri-list") (let [data (dnd/get-data event "text/uri-list") lines (str/lines data) diff --git a/frontend/src/app/main/ui/workspace/viewport/debug.cljs b/frontend/src/app/main/ui/workspace/viewport/debug.cljs index 1fb9b6d02..7f686435a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/debug.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/debug.cljs @@ -11,6 +11,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.flex-layout :as gsl] + [app.common.geom.shapes.grid-layout :as gsg] [app.common.geom.shapes.points :as gpo] [app.common.pages.helpers :as cph] [app.common.types.shape.layout :as ctl] @@ -68,7 +69,7 @@ shape (or selected-frame (get objects hover-top-frame-id))] - (when (and shape (ctl/layout? shape)) + (when (and shape (ctl/flex-layout? shape)) (let [row? (ctl/row? shape) col? (ctl/col? shape) @@ -195,3 +196,55 @@ :cy (:y point) :r (/ 2 zoom) :style {:fill "red"}}]))])])))) + +(mf/defc debug-grid-layout + {::mf/wrap-props false} + [props] + + (let [objects (unchecked-get props "objects") + zoom (unchecked-get props "zoom") + selected-shapes (unchecked-get props "selected-shapes") + hover-top-frame-id (unchecked-get props "hover-top-frame-id") + + selected-frame + (when (and (= (count selected-shapes) 1) (= :frame (-> selected-shapes first :type))) + (first selected-shapes)) + + parent (or selected-frame (get objects hover-top-frame-id)) + parent-bounds (:points parent)] + + (when (and (some? parent) (not= uuid/zero (:id parent))) + (let [children (->> (cph/get-immediate-children objects (:id parent)) + (remove :hidden) + (map #(vector (gpo/parent-coords-bounds (:points %) (:points parent)) %))) + + hv #(gpo/start-hv parent-bounds %) + vv #(gpo/start-vv parent-bounds %) + + width (gpo/width-points parent-bounds) + height (gpo/height-points parent-bounds) + origin (gpo/origin parent-bounds) + + {:keys [row-tracks column-tracks]} + (gsg/calc-layout-data parent children parent-bounds)] + + [:* + (for [row-data row-tracks] + (let [start-p (gpt/add origin (vv (:distance row-data))) + end-p (gpt/add start-p (hv width))] + [:line {:x1 (:x start-p) + :y1 (:y start-p) + :x2 (:x end-p) + :y2 (:y end-p) + :style {:stroke "red" + :stroke-width (/ 1 zoom)}}])) + + (for [column-data column-tracks] + (let [start-p (gpt/add origin (hv (:distance column-data))) + end-p (gpt/add start-p (vv height))] + [:line {:x1 (:x start-p) + :y1 (:y start-p) + :x2 (:x end-p) + :y2 (:y end-p) + :style {:stroke "red" + :stroke-width (/ 1 zoom)}}]))])))) diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index 524791604..badd79ec2 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs @@ -83,7 +83,7 @@ (mf/defc gradient-color-handler [{:keys [filter-id zoom point color angle selected - on-click on-mouse-down on-mouse-up]}] + on-click on-pointer-down on-pointer-up]}] [:g {:filter (str/fmt "url(#%s)" filter-id) :transform (gmt/rotate-matrix angle point)} @@ -100,8 +100,8 @@ :height (/ gradient-square-width zoom) :fill (:value color) :on-click (partial on-click :to-p) - :on-mouse-down (partial on-mouse-down :to-p) - :on-mouse-up (partial on-mouse-up :to-p)}] + :on-pointer-down (partial on-pointer-down :to-p) + :on-pointer-up (partial on-pointer-up :to-p)}] [:rect {:data-allow-click-modal "colorpicker" :x (- (:x point) (/ gradient-square-width 2 zoom)) @@ -114,8 +114,8 @@ :fill (:value color) :fill-opacity (:opacity color) :on-click on-click - :on-mouse-down on-mouse-down - :on-mouse-up on-mouse-up}]]) + :on-pointer-down on-pointer-down + :on-pointer-up on-pointer-up}]]) (mf/defc gradient-handler-transformed [{:keys [from-p to-p width-p from-color to-color zoom editing @@ -133,7 +133,7 @@ :from-p 0 :to-p 1))))) - on-mouse-down + on-pointer-down (fn [position event] (dom/stop-propagation event) (dom/prevent-default event) @@ -144,7 +144,7 @@ :from-p 0 :to-p 1))))) - on-mouse-up + on-pointer-up (fn [_position event] (dom/stop-propagation event) (dom/prevent-default event) @@ -203,8 +203,8 @@ :cy (:y width-p) :r (/ gradient-width-handler-radius zoom) :fill gradient-width-handler-color - :on-mouse-down (partial on-mouse-down :width-p) - :on-mouse-up (partial on-mouse-up :width-p)}]]) + :on-pointer-down (partial on-pointer-down :width-p) + :on-pointer-up (partial on-pointer-up :width-p)}]]) [:& gradient-color-handler {:selected (or (not editing) (= editing 0)) @@ -214,8 +214,8 @@ :color from-color :angle angle :on-click (partial on-click :from-p) - :on-mouse-down (partial on-mouse-down :from-p) - :on-mouse-up (partial on-mouse-up :from-p)}] + :on-pointer-down (partial on-pointer-down :from-p) + :on-pointer-up (partial on-pointer-up :from-p)}] [:& gradient-color-handler {:selected (= editing 1) @@ -225,8 +225,8 @@ :color to-color :angle angle :on-click (partial on-click :to-p) - :on-mouse-down (partial on-mouse-down :to-p) - :on-mouse-up (partial on-mouse-up :to-p)}]])) + :on-pointer-down (partial on-pointer-down :to-p) + :on-pointer-up (partial on-pointer-up :to-p)}]])) (mf/defc gradient-handlers* [{:keys [zoom stops gradient editing-stop shape]}] diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs new file mode 100644 index 000000000..938aa2802 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs @@ -0,0 +1,324 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.viewport.grid-layout-editor + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.point :as gpt] + [app.common.geom.shapes.grid-layout :as gsg] + [app.common.geom.shapes.points :as gpo] + [app.common.pages.helpers :as cph] + [app.main.data.workspace.grid-layout.editor :as dwge] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.cursors :as cur] + [app.util.dom :as dom] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn apply-to-point [result next-fn] + (conj result (next-fn (last result)))) + +(mf/defc track-marker + {::mf/wrap-props false} + [props] + + (let [center (unchecked-get props "center") + value (unchecked-get props "value") + zoom (unchecked-get props "zoom") + + marker-points + (reduce + apply-to-point + [(gpt/subtract center + (gpt/point (/ 13 zoom) (/ 16 zoom)))] + [#(gpt/add % (gpt/point (/ 26 zoom) 0)) + #(gpt/add % (gpt/point 0 (/ 24 zoom))) + #(gpt/add % (gpt/point (- (/ 13 zoom)) (/ 8 zoom))) + #(gpt/subtract % (gpt/point (/ 13 zoom) (/ 8 zoom)))]) + + text-x (:x center) + text-y (:y center)] + + [:g.grid-track-marker + [:polygon {:points (->> marker-points + (map #(dm/fmt "%,%" (:x %) (:y %))) + (str/join " ")) + + :style {:fill "var(--color-distance)" + :fill-opacity 0.3}}] + [:text {:x text-x + :y text-y + :width (/ 26.26 zoom) + :height (/ 32 zoom) + :font-size (/ 16 zoom) + :text-anchor "middle" + :dominant-baseline "middle" + :style {:fill "var(--color-distance)"}} + (dm/str value)]])) + +(mf/defc grid-editor-frame + {::mf/wrap-props false} + [props] + + (let [bounds (unchecked-get props "bounds") + zoom (unchecked-get props "zoom") + hv #(gpo/start-hv bounds %) + vv #(gpo/start-vv bounds %) + width (gpo/width-points bounds) + height (gpo/height-points bounds) + origin (gpo/origin bounds) + + frame-points + (reduce + apply-to-point + [origin] + [#(gpt/add % (hv width)) + #(gpt/subtract % (vv (/ 40 zoom))) + #(gpt/subtract % (hv (+ width (/ 40 zoom)))) + #(gpt/add % (vv (+ height (/ 40 zoom)))) + #(gpt/add % (hv (/ 40 zoom)))])] + + [:polygon {:points (->> frame-points + (map #(dm/fmt "%,%" (:x %) (:y %))) + (str/join " ")) + :style {:stroke "var(--color-distance)" + :stroke-width (/ 1 zoom)}}])) + +(mf/defc plus-btn + {::mf/wrap-props false} + [props] + + (let [start-p (unchecked-get props "start-p") + zoom (unchecked-get props "zoom") + type (unchecked-get props "type") + + [rect-x rect-y icon-x icon-y] + (if (= type :column) + [(:x start-p) + (- (:y start-p) (/ 40 zoom)) + (+ (:x start-p) (/ 12 zoom)) + (- (:y start-p) (/ 28 zoom))] + + [(- (:x start-p) (/ 40 zoom)) + (:y start-p) + (- (:x start-p) (/ 28 zoom)) + (+ (:y start-p) (/ 12 zoom))])] + + [:g.plus-button + [:rect {:x rect-x + :y rect-y + :width (/ 40 zoom) + :height (/ 40 zoom) + :style {:fill "var(--color-distance)" + :stroke "var(--color-distance)" + :stroke-width (/ 1 zoom)}}] + + [:use {:x icon-x + :y icon-y + :width (/ 16 zoom) + :height (/ 16 zoom) + :href (dm/str "#icon-plus") + :fill "white"}]])) + +(mf/defc grid-cell + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + {:keys [row-tracks column-tracks]} (unchecked-get props "layout-data") + bounds (unchecked-get props "bounds") + zoom (unchecked-get props "zoom") + + hover? (unchecked-get props "hover?") + selected? (unchecked-get props "selected?") + + row (unchecked-get props "row") + column (unchecked-get props "column") + + column-track (nth column-tracks (dec column) nil) + row-track (nth row-tracks (dec row) nil) + + + origin (gpo/origin bounds) + hv #(gpo/start-hv bounds %) + vv #(gpo/start-vv bounds %) + + start-p (-> origin + (gpt/add (hv (:distance column-track))) + (gpt/add (vv (:distance row-track)))) + + end-p (-> start-p + (gpt/add (hv (:value column-track))) + (gpt/add (vv (:value row-track))))] + + [:rect.cell-editor + {:x (:x start-p) + :y (:y start-p) + :width (- (:x end-p) (:x start-p)) + :height (- (:y end-p) (:y start-p)) + + :on-pointer-enter #(st/emit! (dwge/hover-grid-cell (:id shape) row column true)) + :on-pointer-leave #(st/emit! (dwge/hover-grid-cell (:id shape) row column false)) + + :on-click #(st/emit! (dwge/select-grid-cell (:id shape) row column)) + + :style {:fill "transparent" + :stroke "var(--color-distance)" + :stroke-dasharray (when-not (or hover? selected?) + (str/join " " (map #(/ % zoom) [0 8]) )) + :stroke-linecap "round" + :stroke-width (/ 2 zoom)}}])) + +(mf/defc resize-handler + {::mf/wrap-props false} + [props] + + (let [start-p (unchecked-get props "start-p") + type (unchecked-get props "type") + bounds (unchecked-get props "bounds") + zoom (unchecked-get props "zoom") + + width (gpo/width-points bounds) + height (gpo/height-points bounds) + + dragging-ref (mf/use-ref false) + start-ref (mf/use-ref nil) + + on-pointer-down + (mf/use-callback + (fn [event] + (dom/capture-pointer event) + (mf/set-ref-val! dragging-ref true) + (mf/set-ref-val! start-ref (dom/get-client-position event)))) + + on-lost-pointer-capture + (mf/use-callback + (fn [event] + (dom/release-pointer event) + (mf/set-ref-val! dragging-ref false) + (mf/set-ref-val! start-ref nil))) + + on-pointer-move + (mf/use-callback + (fn [event] + (when (mf/ref-val dragging-ref) + (let [start (mf/ref-val start-ref) + pos (dom/get-client-position event) + _delta (-> (gpt/to-vec start pos) + (get (if (= type :column) :x :y)))] + + ;; TODO Implement resize + #_(prn ">Delta" delta))))) + + + [x y width height] + (if (= type :column) + [(- (:x start-p) (/ 8 zoom)) + (- (:y start-p) (/ 40 zoom)) + (/ 16 zoom) + (+ height (/ 40 zoom))] + + [(- (:x start-p) (/ 40 zoom)) + (- (:y start-p) (/ 8 zoom)) + (+ width (/ 40 zoom)) + (/ 16 zoom)])] + + [:rect.resize-handler + {:x x + :y y + :height height + :width width + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move + :style {:fill "transparent" + :cursor (if (= type :column) + (cur/resize-ew 0) + (cur/resize-ns 0))}}])) + +(mf/defc editor + {::mf/wrap-props false} + [props] + + (let [shape (unchecked-get props "shape") + objects (unchecked-get props "objects") + zoom (unchecked-get props "zoom") + bounds (:points shape) + + grid-edition-id-ref (mf/use-memo #(refs/workspace-grid-edition-id (:id shape))) + grid-edition (mf/deref grid-edition-id-ref) + + hover-cells (:hover grid-edition) + selected-cells (:selected grid-edition) + + children (->> (cph/get-immediate-children objects (:id shape)) + (remove :hidden) + (map #(vector (gpo/parent-coords-bounds (:points %) (:points shape)) %))) + + hv #(gpo/start-hv bounds %) + vv #(gpo/start-vv bounds %) + width (gpo/width-points bounds) + height (gpo/height-points bounds) + origin (gpo/origin bounds) + + {:keys [row-tracks column-tracks] :as layout-data} + (gsg/calc-layout-data shape children bounds)] + + (mf/use-effect + (fn [] + #(st/emit! (dwge/stop-grid-layout-editing (:id shape))))) + + [:g.grid-editor + [:& grid-editor-frame {:zoom zoom + :bounds bounds}] + (let [start-p (-> origin (gpt/add (hv width)))] + [:& plus-btn {:start-p start-p + :zoom zoom + :type :column}]) + + (let [start-p (-> origin (gpt/add (vv height)))] + [:& plus-btn {:start-p start-p + :zoom zoom + :type :row}]) + + (for [[_ {:keys [column row]}] (:layout-grid-cells shape)] + [:& grid-cell {:shape shape + :layout-data layout-data + :row row + :column column + :bounds bounds + :zoom zoom + :hover? (contains? hover-cells [row column]) + :selected? (= selected-cells [row column]) + }]) + + (for [[idx column-data] (d/enumerate column-tracks)] + (let [start-p (-> origin (gpt/add (hv (:distance column-data)))) + marker-p (-> start-p (gpt/subtract (vv (/ 20 zoom))))] + [:* + [:& track-marker {:center marker-p + :value (dm/str (inc idx)) + :zoom zoom}] + + [:& resize-handler {:type :column + :start-p start-p + :zoom zoom + :bounds bounds}]])) + + (for [[idx row-data] (d/enumerate row-tracks)] + (let [start-p (-> origin (gpt/add (vv (:distance row-data)))) + marker-p (-> start-p (gpt/subtract (hv (/ 20 zoom))))] + [:* + [:g {:transform (dm/fmt "rotate(-90 % %)" (:x marker-p) (:y marker-p))} + [:& track-marker {:center marker-p + :value (dm/str (inc idx)) + :zoom zoom}]] + + [:& resize-handler {:type :row + :start-p start-p + :zoom zoom + :bounds bounds}]]))])) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index dd624febd..42459e7c2 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -93,7 +93,7 @@ (mf/set-ref-val! start-pos-ref nil) (swap! state assoc :new-position nil))) - on-mouse-move + on-pointer-move (mf/use-callback (mf/deps position zoom snap-pixel?) (fn [event] @@ -120,7 +120,7 @@ :on-pointer-down on-pointer-down :on-pointer-up on-pointer-up :on-lost-pointer-capture on-lost-pointer-capture - :on-mouse-move on-mouse-move + :on-pointer-move on-pointer-move :state state :frame frame})) @@ -274,7 +274,7 @@ on-pointer-down on-pointer-up on-lost-pointer-capture - on-mouse-move + on-pointer-move state frame]} (use-guide handle-change-position get-hover-frame zoom guide) @@ -310,7 +310,7 @@ :on-pointer-down on-pointer-down :on-pointer-up on-pointer-up :on-lost-pointer-capture on-lost-pointer-capture - :on-mouse-move on-mouse-move}])) + :on-pointer-move on-pointer-move}])) (if (some? frame) (let [{:keys [l1-x1 l1-y1 l1-x2 l1-y2 @@ -399,7 +399,7 @@ on-pointer-down on-pointer-up on-lost-pointer-capture - on-mouse-move + on-pointer-move state frame]} (use-guide on-guide-change get-hover-frame zoom {:axis axis})] @@ -415,7 +415,7 @@ :on-pointer-down on-pointer-down :on-pointer-up on-pointer-up :on-lost-pointer-capture on-lost-pointer-capture - :on-mouse-move on-mouse-move + :on-pointer-move on-pointer-move :style {:fill "none" :pointer-events "fill" :cursor (if (= axis :x) (cur/resize-ew 0) (cur/resize-ns 0))}}])) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index e7270e7b9..84bd54855 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.viewport.hooks (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.helpers :as cph] @@ -33,25 +34,14 @@ [rumext.v2 :as mf]) (:import goog.events.EventType)) -(defn setup-dom-events [viewport-ref zoom disable-paste in-viewport? workspace-read-only?] +(defn setup-dom-events [zoom disable-paste in-viewport? workspace-read-only?] (let [on-key-down (actions/on-key-down) on-key-up (actions/on-key-up) - on-mouse-move (actions/on-mouse-move) on-mouse-wheel (actions/on-mouse-wheel zoom) on-paste (actions/on-paste disable-paste in-viewport? workspace-read-only?)] - ;; We use the DOM listener because the goog.closure one forces reflow to generate its internal - ;; structure. As we don't need currently nothing from BrowserEvent we optimize by using the basic event (mf/use-layout-effect - (mf/deps on-mouse-move) - (fn [] - (let [node (mf/ref-val viewport-ref)] - (.addEventListener node "mousemove" on-mouse-move) - (fn [] - (.removeEventListener node "mousemove" on-mouse-move))))) - - (mf/use-layout-effect - (mf/deps on-key-down on-key-up on-mouse-move on-mouse-wheel on-paste workspace-read-only?) + (mf/deps on-key-down on-key-up on-mouse-wheel on-paste workspace-read-only?) (fn [] (let [keys [(events/listen js/document EventType.KEYDOWN on-key-down) (events/listen js/document EventType.KEYUP on-key-up) @@ -63,14 +53,16 @@ (doseq [key keys] (events/unlistenByKey key)))))))) -(defn setup-viewport-size [viewport-ref] +(defn setup-viewport-size [vport viewport-ref] (mf/use-layout-effect + (mf/deps vport) (fn [] - (let [node (mf/ref-val viewport-ref) - prnt (dom/get-parent node) - size (dom/get-client-size prnt)] - ;; We schedule the event so it fires after `initialize-page` event - (timers/schedule #(st/emit! (dw/initialize-viewport size))))))) + (when-not vport + (let [node (mf/ref-val viewport-ref) + prnt (dom/get-parent node) + size (dom/get-client-size prnt)] + ;; We schedule the event so it fires after `initialize-page` event + (timers/schedule #(st/emit! (dw/initialize-viewport size)))))))) (defn setup-cursor [cursor alt? mod? space? panning drawing-tool drawing-path? path-editing? z? workspace-read-only?] (mf/use-effect @@ -107,12 +99,13 @@ (when (not= @cursor new-cursor) (reset! cursor new-cursor)))))) -(defn setup-keyboard [alt? mod? space? z?] +(defn setup-keyboard [alt? mod? space? z? shift?] (hooks/use-stream ms/keyboard-alt #(reset! alt? %)) (hooks/use-stream ms/keyboard-mod #((reset! mod? %) (when-not % (reset! z? false)))) ;; In mac after command+z there is no event for the release of the z key (hooks/use-stream ms/keyboard-space #(reset! space? %)) - (hooks/use-stream ms/keyboard-z #(reset! z? %))) + (hooks/use-stream ms/keyboard-z #(reset! z? %)) + (hooks/use-stream ms/keyboard-shift #(reset! shift? %))) (defn group-empty-space? "Given a group `group-id` check if `hover-ids` contains any of its children. If it doesn't means @@ -166,7 +159,8 @@ ;; but the mouse has not been moved from its position. (->> mod-str (rx/observe-on :async) - (rx/map #(deref last-point-ref))) + (rx/map #(deref last-point-ref)) + (rx/merge-map query-point)) (->> move-stream (rx/tap #(reset! last-point-ref %)) @@ -238,9 +232,17 @@ remove-id? (into selected-with-parents remove-id-xf ids) + no-fill-nested-frames? + (fn [id] + (and (cph/frame-shape? objects id) + (not (cph/root-frame? objects id)) + (empty? (dm/get-in objects [id :fills])))) + hover-shape (->> ids (remove remove-id?) + (remove (partial cph/hidden-parent? objects)) + (remove #(and mod? (no-fill-nested-frames? %))) (filter #(or (empty? focus) (cp/is-in-focus? objects focus %))) (first) (get objects))] diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index de17f3fd2..6b4ba2ad4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -33,7 +33,7 @@ :move-overlay-index]) refs/workspace-local =)) -(defn- on-mouse-down +(defn- on-pointer-down [event index {:keys [id] :as shape}] (dom/stop-propagation event) (st/emit! (dw/select-shape id)) @@ -163,7 +163,7 @@ arrow-dir (if (= dest-pos :left) :right :left)] (if-not selected? - [:g {:on-mouse-down #(on-mouse-down % index orig-shape)} + [:g {:on-pointer-down #(on-pointer-down % index orig-shape)} [:path {:stroke "var(--color-gray-20)" :fill "none" :pointer-events "visible" @@ -178,7 +178,7 @@ :arrow-dir arrow-dir :zoom zoom}])] - [:g {:on-mouse-down #(on-mouse-down % index orig-shape)} + [:g {:on-pointer-down #(on-pointer-down % index orig-shape)} [:path {:stroke "var(--color-primary)" :fill "none" :pointer-events "visible" @@ -209,7 +209,7 @@ (let [shape-rect (:selrect shape) handle-x (+ (:x shape-rect) (:width shape-rect)) handle-y (+ (:y shape-rect) (/ (:height shape-rect) 2))] - [:g {:on-mouse-down #(on-mouse-down % index shape)} + [:g {:on-pointer-down #(on-pointer-down % index shape)} [:& interaction-marker {:x handle-x :y handle-y :stroke "var(--color-primary)" @@ -246,9 +246,9 @@ dest-shape (cond-> dest-shape (some? thumbnail-data) (assoc :thumbnail thumbnail-data))] - [:g {:on-mouse-down start-move-position - :on-mouse-enter #(reset! hover-disabled? true) - :on-mouse-leave #(reset! hover-disabled? false)} + [:g {:on-pointer-down start-move-position + :on-pointer-enter #(reset! hover-disabled? true) + :on-pointer-leave #(reset! hover-disabled? false)} [:g {:transform (gmt/translate-matrix (gpt/point (- marker-x dest-x) (- marker-y dest-y))) } [:& (mf/provider muc/render-thumbnails) {:value true} [:& (mf/provider embed/context) {:value false} diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs index 214c9b599..ceab081f3 100644 --- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs @@ -81,7 +81,7 @@ (st/emit! (dwc/stop-picker)) (modal/disallow-click-outside!)))) - handle-mouse-move-picker + handle-pointer-move-picker (mf/use-callback (mf/deps viewport-node) (fn [event] @@ -109,7 +109,7 @@ (.drawImage zoom-context image 0 0 200 160)))) (st/emit! (dwc/pick-color [r g b a])))))) - handle-mouse-down-picker + handle-pointer-down-picker (mf/use-callback (fn [event] (dom/prevent-default event) @@ -117,7 +117,7 @@ (st/emit! (dwu/start-undo-transaction :mouse-down-picker) (dwc/pick-color-select true (kbd/shift? event))))) - handle-mouse-up-picker + handle-pointer-up-picker (mf/use-callback (fn [event] (dom/prevent-default event) @@ -185,9 +185,9 @@ [:div.pixel-overlay {:tab-index 0 :style {:cursor cur/picker} - :on-mouse-down handle-mouse-down-picker - :on-mouse-up handle-mouse-up-picker - :on-mouse-move handle-mouse-move-picker} + :on-pointer-down handle-pointer-down-picker + :on-pointer-up handle-pointer-up-picker + :on-pointer-move handle-pointer-move-picker} [:div {:style {:display "none"}} [:img {:ref img-ref :on-load handle-image-load diff --git a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs index 75166092a..8a346f1fe 100644 --- a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs @@ -123,7 +123,7 @@ (- scrollbar-width)) h-scrollbar-x) - on-mouse-move + on-pointer-move (fn [event axis] (when-let [_ (or @v-scrolling? @h-scrolling?)] (let [start-pt (mf/ref-val start-ref) @@ -147,7 +147,7 @@ (mf/set-ref-val! h-scrollbar-x-ref new-h-scrollbar-x) (mf/set-ref-val! start-ref current-pt)))) - on-mouse-down + on-pointer-down (fn [event axis] (let [start-pt (dom/get-client-position event) viewport-point (point->viewport start-pt) @@ -180,7 +180,7 @@ (reset! v-scrolling? (= axis :y)) (reset! h-scrolling? (= axis :x)))) - on-mouse-up + on-pointer-up (fn [] (reset! v-scrolling? false) (reset! h-scrolling? false))] @@ -188,9 +188,9 @@ [:* (when show-v-scroll? [:g.v-scroll {:fill clr/black} - [:rect {:on-mouse-move #(on-mouse-move % :y) - :on-mouse-down #(on-mouse-down % :y) - :on-mouse-up on-mouse-up + [:rect {:on-pointer-move #(on-pointer-move % :y) + :on-pointer-down #(on-pointer-down % :y) + :on-pointer-up on-pointer-up :width (* inv-zoom 7) :rx (* inv-zoom 3) :ry (* inv-zoom 3) @@ -202,9 +202,9 @@ :stroke-width (/ 0.15 zoom)}}]]) (when show-h-scroll? [:g.h-scroll {:fill clr/black} - [:rect {:on-mouse-move #(on-mouse-move % :x) - :on-mouse-down #(on-mouse-down % :x) - :on-mouse-up on-mouse-up + [:rect {:on-pointer-move #(on-pointer-move % :x) + :on-pointer-down #(on-pointer-down % :x) + :on-pointer-up on-pointer-up :width scrollbar-width :rx (* inv-zoom 3) :ry (* inv-zoom 3) diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index 57fc23d8a..b0f21b6c4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -45,7 +45,7 @@ :width width :height height :transform (str transform) - :on-mouse-down on-move-selected + :on-pointer-down on-move-selected :on-context-menu on-context-menu :style {:stroke color :stroke-width (/ selection-rect-width zoom) @@ -172,12 +172,15 @@ :fill (if (debug? :handlers) "blue" "none") :stroke-width 0 :transform (dm/str transform) - :on-mouse-down on-rotate}])) + :on-pointer-down on-rotate}])) (mf/defc resize-point-handler [{:keys [cx cy zoom position on-resize transform rotation color align]}] - (let [cursor (if (#{:top-left :bottom-right} position) - (cur/resize-nesw rotation) (cur/resize-nwse rotation)) + (let [layout (mf/deref refs/workspace-layout) + scale-text (:scale-text layout) + cursor (if (#{:top-left :bottom-right} position) + (if scale-text (cur/scale-nesw rotation) (cur/resize-nesw rotation)) + (if scale-text (cur/scale-nwse rotation) (cur/resize-nwse rotation))) {cx' :x cy' :y} (gpt/transform (gpt/point cx cy) transform)] [:g.resize-handler @@ -205,9 +208,9 @@ :style {:fill (if (debug? :handlers) "red" "none") :stroke-width 0 :cursor cursor} - :on-mouse-down #(on-resize {:x cx' :y cy'} %)}]) + :on-pointer-down #(on-resize {:x cx' :y cy'} %)}]) - [:circle {:on-mouse-down #(on-resize {:x cx' :y cy'} %) + [:circle {:on-pointer-down #(on-resize {:x cx' :y cy'} %) :r (/ resize-point-circle-radius zoom) :cx cx' :cy cy' @@ -221,7 +224,8 @@ (let [res-point (if (#{:top :bottom} position) {:y y} {:x x}) - + layout (mf/deref refs/workspace-layout) + scale-text (:scale-text layout) height (/ resize-side-height zoom) offset-y (if (= align :outside) (- height) (- (/ height 2))) target-y (+ y offset-y) @@ -242,12 +246,12 @@ :width length :height height :transform transform-str - :on-mouse-down #(on-resize res-point %) + :on-pointer-down #(on-resize res-point %) :style {:fill (if (debug? :handlers) "yellow" "none") :stroke-width 0 :cursor (if (#{:left :right} position) - (cur/resize-ew rotation) - (cur/resize-ns rotation)) }}]])) + (if scale-text (cur/scale-ew rotation) (cur/resize-ew rotation)) + (if scale-text (cur/scale-ns rotation) (cur/resize-ns rotation))) }}]])) (defn minimum-selrect [{:keys [x y width height] :as selrect}] (let [final-width (max width min-selrect-side) diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs index 9ae906cc5..185d50e74 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs @@ -268,7 +268,7 @@ frame (mf/deref (refs/object-by-id frame-id)) selrect (gsh/selection-rect selected-shapes)] - (when-not (ctl/layout? frame) + (when-not (ctl/any-layout? frame) [:g.distance [:& shape-distance {:selrect selrect diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs index 88fe7bc81..a57260864 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs @@ -175,7 +175,7 @@ shapes (if drawing [drawing] shapes) frame-id (snap/snap-frame-id shapes)] - (when-not (ctl/layout? objects frame-id) + (when-not (ctl/any-layout? objects frame-id) [:& snap-feedback {:shapes shapes :page-id page-id :remove-snap? remove-snap? diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 750c79730..4768e0331 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -8,7 +8,7 @@ (:require [app.common.data.macros :as dm] [app.common.geom.point :as gpt] - [app.common.geom.shapes :as gsh] + [app.common.math :as mth] [app.main.ui.cursors :as cur] [app.main.ui.formats :refer [format-number]])) @@ -42,7 +42,58 @@ (let [inv-zoom (/ 1 zoom)] (dm/fmt "scale(%, %) translate(%, %)" inv-zoom inv-zoom (* zoom x) (* zoom y)))) -(defn title-transform [{:keys [selrect] :as shape} zoom] - (let [transform (gsh/transform-str shape {:no-flip true}) - label-pos (gpt/point (:x selrect) (- (:y selrect) (/ 10 zoom)))] - (dm/str transform " " (text-transform label-pos zoom)))) +(defn left? + [cur cand] + (let [closex? (mth/close? (:x cand) (:x cur))] + (cond + (and closex? (< (:y cand) (:y cur))) cand + closex? cur + (< (:x cand) (:x cur)) cand + :else cur))) + +(defn top? + [cur cand] + (let [closey? (mth/close? (:y cand) (:y cur))] + (cond + (and closey? (< (:x cand) (:x cur))) cand + closey? cur + (< (:y cand) (:y cur)) cand + :else cur))) + +(defn right? + [cur cand] + (let [closex? (mth/close? (:x cand) (:x cur))] + (cond + (and closex? (< (:y cand) (:y cur))) cand + closex? cur + (> (:x cand) (:x cur)) cand + :else cur))) + +(defn title-transform [{:keys [points] :as shape} zoom] + (let [leftmost (->> points (reduce left?)) + topmost (->> points (remove #{leftmost}) (reduce top?)) + rightmost (->> points (remove #{leftmost topmost}) (reduce right?)) + + left-top (gpt/to-vec leftmost topmost) + left-top-angle (gpt/angle left-top) + + top-right (gpt/to-vec topmost rightmost) + top-right-angle (gpt/angle top-right) + + ;; Choose the position that creates the less angle between left-side and top-side + [label-pos angle v-pos] + (if (< (mth/abs left-top-angle) (mth/abs top-right-angle)) + [leftmost left-top-angle (gpt/perpendicular left-top)] + [topmost top-right-angle (gpt/perpendicular top-right)]) + + + label-pos + (gpt/subtract label-pos (gpt/scale (gpt/unit v-pos) (/ 10 zoom)))] + + (dm/fmt "rotate(% %,%) scale(%, %) translate(%, %)" + ;; rotate + angle (:x label-pos) (:y label-pos) + ;; scale + (/ 1 zoom) (/ 1 zoom) + ;; translate + (* zoom (:x label-pos)) (* zoom (:y label-pos))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index be1d5372e..4503911c6 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] + [app.common.pages.helpers :as cph] [app.common.types.shape-tree :as ctt] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] @@ -55,8 +56,12 @@ drawing (mf/deref refs/workspace-drawing) drawing-obj (:object drawing) shape (or drawing-obj (-> selected first))] - (when (or (and (= (count selected) 1) (= (:id shape) edition) (not= :text (:type shape))) - (and (some? drawing-obj) (= :path (:type drawing-obj)) + (when (or (and (= (count selected) 1) + (= (:id shape) edition) + (and (not (cph/text-shape? shape)) + (not (cph/frame-shape? shape)))) + (and (some? drawing-obj) + (cph/path-shape? drawing-obj) (not= :curve (:tool drawing)))) [:div.viewport-actions [:& path-actions {:shape shape}]]))) @@ -93,7 +98,7 @@ #(mf/deferred % ts/raf)]} [{:keys [frame selected? zoom show-artboard-names? show-id? on-frame-enter on-frame-leave on-frame-select]}] (let [workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) - on-mouse-down + on-pointer-down (mf/use-callback (mf/deps (:id frame) on-frame-select workspace-read-only?) (fn [bevent] @@ -151,7 +156,7 @@ :class "workspace-frame-label" :style {:fill (when selected? "var(--color-primary-dark)")} :visibility (if show-artboard-names? "visible" "hidden") - :on-mouse-down on-mouse-down + :on-pointer-down on-pointer-down :on-double-click on-double-click :on-context-menu on-context-menu :on-pointer-enter on-pointer-enter @@ -200,7 +205,7 @@ (let [{:keys [x y]} frame flow-pos (gpt/point x (- y (/ 35 zoom))) - on-mouse-down + on-pointer-down (mf/use-callback (mf/deps (:id frame) on-frame-select) (fn [bevent] @@ -233,7 +238,7 @@ :height 24 :transform (vwu/text-transform flow-pos zoom)} [:div.flow-badge {:class (dom/classnames :selected selected?)} - [:div.content {:on-mouse-down on-mouse-down + [:div.content {:on-pointer-down on-pointer-down :on-double-click on-double-click :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave} diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index 534da2488..69ea2ecd2 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -29,9 +29,7 @@ ;; SETUP ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(l/initialize!) -(l/set-level! :root :warn) -(l/set-level! :app :info) +(l/setup! {:app :info}) (declare ^:private render-single-object) (declare ^:private render-components) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 0307321c5..8328f9025 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -147,7 +147,7 @@ [^js node selector] (loop [current node] - (if (or (nil? current) (.matches current selector) ) + (if (or (nil? current) (.matches current selector)) current (recur (.-parentElement current))))) @@ -175,10 +175,10 @@ (defn get-scroll-position [^js event] (when (some? event) - {:scroll-height (.-scrollHeight event) - :scroll-left (.-scrollLeft event) - :scroll-top (.-scrollTop event) - :scroll-width (.-scrollWidth event)})) + {:scroll-height (.-scrollHeight event) + :scroll-left (.-scrollLeft event) + :scroll-top (.-scrollTop event) + :scroll-width (.-scrollWidth event)})) (def get-target-val (comp get-value get-target)) @@ -309,6 +309,13 @@ (when (some? el) (.querySelectorAll el selector)))) +(defn get-element-offset-position + [^js node] + (when (some? node) + (let [x (.-offsetTop node) + y (.-offsetLeft node)] + (gpt/point x y)))) + (defn get-client-position [^js event] (let [x (.-clientX event) @@ -361,12 +368,22 @@ (when (some? node) (.blur node))) +;; List of dom events for different browsers to detect the exit of fullscreen mode +(def fullscreen-events + ["fullscreenchange" "mozfullscreenchange" "MSFullscreenChange" "webkitfullscreenchange"]) + (defn fullscreen? [] (cond (obj/in? globals/document "webkitFullscreenElement") (boolean (.-webkitFullscreenElement globals/document)) + (obj/in? globals/document "mozFullScreen") + (boolean (.-mozFullScreen globals/document)) + + (obj/in? globals/document "msFullscreenElement") + (boolean (.-msFullscreenElement globals/document)) + (obj/in? globals/document "fullscreenElement") (boolean (.-fullscreenElement globals/document)) @@ -441,7 +458,7 @@ (assert (even? (count params))) (str/join " " (reduce (fn [acc [k v]] (if (true? (boolean v)) - (conj acc (name k)) + (conj acc (d/name k)) acc)) [] (partition 2 params)))) @@ -567,7 +584,7 @@ (let [extension (cm/mtype->extension mtype) opts {:suggestedName (str filename "." extension) :types [{:description description - :accept { mtype [(str "." extension)]}}]}] + :accept {mtype [(str "." extension)]}}]}] (-> (p/let [file-system (.showSaveFilePicker globals/window (clj->js opts)) writable (.createWritable file-system) @@ -576,9 +593,9 @@ _ (.write writable blob)] (.close writable)) (p/catch - #(when-not (and (= (type %) js/DOMException) - (= (.-name %) "AbortError")) - (trigger-download-uri filename mtype uri))))) + #(when-not (and (= (type %) js/DOMException) + (= (.-name %) "AbortError")) + (trigger-download-uri filename mtype uri))))) (trigger-download-uri filename mtype uri))) @@ -609,9 +626,9 @@ (defn animate! ([item keyframes duration] (animate! item keyframes duration nil)) ([item keyframes duration onfinish] - (let [animation (.animate item keyframes duration)] - (when onfinish - (set! (.-onfinish animation) onfinish))))) + (let [animation (.animate item keyframes duration)] + (when onfinish + (set! (.-onfinish animation) onfinish))))) (defn is-child? [^js node ^js candidate] @@ -655,3 +672,12 @@ (defn has-children? [^js node] (> (-> node .-children .-length) 0)) + +;; WARNING: Use only for debugging. It's to costly to use for real +(defn measure-text + "Given a canvas' context 2d and the text info returns tis ascent/descent info" + [context-2d font-size font-family text] + (let [_ (set! (.-font context-2d) (str font-size " " font-family)) + measures (.measureText context-2d text)] + {:descent (.-actualBoundingBoxDescent measures) + :ascent (.-actualBoundingBoxAscent measures)})) diff --git a/frontend/src/app/util/keyboard.cljs b/frontend/src/app/util/keyboard.cljs index ccb14ddf6..7d4ecf95a 100644 --- a/frontend/src/app/util/keyboard.cljs +++ b/frontend/src/app/util/keyboard.cljs @@ -50,6 +50,7 @@ (def left-arrow? (is-key? "ArrowLeft")) (def right-arrow? (is-key? "ArrowRight")) (def alt-key? (is-key? "Alt")) +(def shift-key? (is-key? "Shift")) (def ctrl-key? (is-key? "Control")) (def meta-key? (is-key? "Meta")) (def comma? (is-key? ",")) diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs index e9c36d50d..f7628bc33 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -112,20 +112,25 @@ (update command :params assoc :x x :y y)) (defn format-path [content] - (let [result (make-array (count content))] - (reduce (fn [last-move current] - (let [point (upc/command->point current) - current-move? (= :move-to (:command current)) - last-move (if current-move? point last-move)] + (try + (let [result (make-array (count content))] + (reduce (fn [last-move current] + (let [point (upc/command->point current) + current-move? (= :move-to (:command current)) + last-move (if current-move? point last-move)] - (if (and (not current-move?) (pt= last-move point)) - (arr/conj! result (command->string (set-point current last-move))) - (arr/conj! result (command->string current))) + (if (and (not current-move?) (pt= last-move point)) + (arr/conj! result (command->string (set-point current last-move))) + (arr/conj! result (command->string current))) - (when (and (not current-move?) (pt= last-move point)) - (arr/conj! result "Z")) + (when (and (not current-move?) (pt= last-move point)) + (arr/conj! result "Z")) - last-move)) - nil - content) - (.join ^js result ""))) + last-move)) + nil + content) + (.join ^js result "")) + + (catch :default err + (.error js/console err) + nil))) diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs index deb4239fe..f9cf779fa 100644 --- a/frontend/src/app/util/snap_data.cljs +++ b/frontend/src/app/util/snap_data.cljs @@ -82,7 +82,9 @@ grid-y-data (get-grids-snap-points frame :y)] (cond-> page-data - (not (ctl/layout-descent? objects frame)) + (and (not (ctl/any-layout-descent? objects frame)) + (not (:hidden frame)) + (not (cph/hidden-parent? objects frame-id))) (-> ;; Update root frame information (assoc-in [uuid/zero :objects-data frame-id] frame-data) @@ -106,7 +108,9 @@ :id (:id shape) :pt %)))] (cond-> page-data - (not (ctl/layout-descent? objects shape)) + (and (not (ctl/any-layout-descent? objects shape)) + (not (:hidden shape)) + (not (cph/hidden-parent? objects (:id shape)))) (-> (assoc-in [frame-id :objects-data (:id shape)] shape-data) (update-in [frame-id :x] (make-insert-tree-data shape-data :x)) (update-in [frame-id :y] (make-insert-tree-data shape-data :y)))))) @@ -124,9 +128,11 @@ :pt %)))] (if-let [frame-id (:frame-id guide)] ;; Guide inside frame, we add the information only on that frame - (-> page-data - (assoc-in [frame-id :objects-data (:id guide)] guide-data) - (update-in [frame-id (:axis guide)] (make-insert-tree-data guide-data (:axis guide)))) + (cond-> page-data + (and (not (:hidden frame)) + (not (cph/hidden-parent? objects frame-id))) + (-> (assoc-in [frame-id :objects-data (:id guide)] guide-data) + (update-in [frame-id (:axis guide)] (make-insert-tree-data guide-data (:axis guide))))) ;; Guide outside the frame. We add the information in the global guides data (-> page-data diff --git a/frontend/src/app/util/strings.cljs b/frontend/src/app/util/strings.cljs index 0eb5f74c8..3f74c9e6c 100644 --- a/frontend/src/app/util/strings.cljs +++ b/frontend/src/app/util/strings.cljs @@ -46,4 +46,5 @@ (defn camelize [str] ;; str.replace(":", "-").replace(/-./g, x=>x[1].toUpperCase()) - (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", str)) + (when (not (nil? str)) + (js* "~{}.replace(\":\", \"-\").replace(/-./g, x=>x[1].toUpperCase())", str))) diff --git a/frontend/src/app/util/svg.cljs b/frontend/src/app/util/svg.cljs index 03d541fa5..21f084d84 100644 --- a/frontend/src/app/util/svg.cljs +++ b/frontend/src/app/util/svg.cljs @@ -820,6 +820,10 @@ (defn line->path [{:keys [attrs] :as node}] (let [tag :path {:keys [x1 y1 x2 y2]} attrs + x1 (or x1 0) + y1 (or y1 0) + x2 (or x2 0) + y2 (or y2 0) attrs (-> attrs (dissoc :x1 :x2 :y1 :y2) (assoc :d (str "M" x1 "," y1 " L" x2 "," y2)))] diff --git a/frontend/src/app/worker.cljs b/frontend/src/app/worker.cljs index b79cc2ea4..49904406c 100644 --- a/frontend/src/app/worker.cljs +++ b/frontend/src/app/worker.cljs @@ -19,9 +19,7 @@ [cljs.spec.alpha :as s] [promesa.core :as p])) -(log/initialize!) -(log/set-level! :root :warn) -(log/set-level! :app :info) +(log/setup! {:app :info}) ;; --- Messages Handling diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 4f514362f..29aa8f002 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -196,7 +196,7 @@ :content blob :is-local true})) (rx/tap #(progress! context :upload-media name)) - (rx/flat-map #(rp/mutation! :upload-file-media-object %)))) + (rx/flat-map #(rp/cmd! :upload-file-media-object %)))) (defn resolve-text-content [node context] (let [resolve (:resolve context)] @@ -513,7 +513,7 @@ :content content :is-local false}))) (rx/tap #(progress! context :upload-media (:name %))) - (rx/merge-map #(rp/mutation! :upload-file-media-object %)) + (rx/merge-map #(rp/cmd! :upload-file-media-object %)) (rx/map (constantly media)) (rx/catch #(do (.error js/console (str "Error uploading media: " (:name media)) ) (rx/empty))))))) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 1d592c7f9..5a4d6563a 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -131,7 +131,10 @@ (or (not full-frame?) (not= :frame (:type shape)) - (gsh/rect-contains-shape? rect shape)))) + (and (d/not-empty? (:shapes shape)) + (gsh/rect-contains-shape? rect shape)) + (and (empty? (:shapes shape)) + (gsh/overlaps? shape rect))))) overlaps? (fn [shape] diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index b663cdc49..c306f5820 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -95,6 +95,9 @@ ;; Show shape name and id :shape-titles + + ;; + :grid-layout }) ;; These events are excluded when we activate the :events flag diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po index 73806e3a4..159d4533a 100644 --- a/frontend/translations/ar.po +++ b/frontend/translations/ar.po @@ -1304,6 +1304,7 @@ msgid "labels.no-invitations" msgstr "لا توجد دعوات." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "اضغط على الزر \"دعوة إلى الفريق\" لدعوة المزيد من الأعضاء إلى هذا الفريق." diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index 23418410d..48f11b611 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -1298,6 +1298,7 @@ msgid "labels.no-invitations" msgstr "No hi ha invitacions." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Feu clic al botó «Convida a l'equip» per convidar més membres a aquest " @@ -3623,19 +3624,19 @@ msgstr "Alinea el centre" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Justifica" +msgstr "Justifica (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Alinea a l'esquerra" +msgstr "Alinea a l'esquerra (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Alinea al centre" +msgstr "Alinea al centre (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Alinea a la dreta" +msgstr "Alinea a la dreta (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3679,7 +3680,7 @@ msgstr "Cap" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Ratllat" +msgstr "Ratllat (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3699,7 +3700,7 @@ msgstr "Inicials en majúscules" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Subratllat" +msgstr "Subratllat (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/cs.po b/frontend/translations/cs.po index 7f9c46aa0..06cb4df2c 100644 --- a/frontend/translations/cs.po +++ b/frontend/translations/cs.po @@ -1197,6 +1197,7 @@ msgid "labels.no-invitations" msgstr "Nejsou žádné pozvánky." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Chcete-li do tohoto týmu pozvat další členy, stiskněte tlačítko „Pozvat do " @@ -3688,19 +3689,19 @@ msgstr "Zarovnat doprostřed" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Zarovnat" +msgstr "Zarovnat (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Zarovnat vlevo" +msgstr "Zarovnat vlevo (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Zarovnat na střed" +msgstr "Zarovnat na střed (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Zarovnat vpravo" +msgstr "Zarovnat vpravo (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3744,7 +3745,7 @@ msgstr "Žádné" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Přeškrtnutí" +msgstr "Přeškrtnutí (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3764,7 +3765,7 @@ msgstr "První písmeno velké" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Podtrhnout" +msgstr "Podtrhnout (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/de.po b/frontend/translations/de.po index 812d01987..8d1288458 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -1337,6 +1337,7 @@ msgid "labels.no-invitations" msgstr "Es gibt keine Einladungen." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Drücken Sie die Schaltfläche \"Zum Team einladen\", um weitere Mitglieder " @@ -3907,19 +3908,19 @@ msgstr "Zentrieren" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Ausrichtung in der Breite" +msgstr "Ausrichtung in der Breite (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Linksbündig ausrichten" +msgstr "Linksbündig ausrichten (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "An Mitte ausrichten" +msgstr "An Mitte ausrichten (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Rechtsbündig ausrichten" +msgstr "Rechtsbündig ausrichten (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3964,7 +3965,7 @@ msgstr "Keine" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Durchgestrichen" +msgstr "Durchgestrichen (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3984,7 +3985,7 @@ msgstr "Kapitälchen" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Unterstrichen" +msgstr "Unterstrichen (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/el.po b/frontend/translations/el.po index 4b30892c2..f437e1a38 100644 --- a/frontend/translations/el.po +++ b/frontend/translations/el.po @@ -1797,19 +1797,19 @@ msgstr "Ευθυγράμμιση κέντρο" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Δικαιολόγηση" +msgstr "Δικαιολόγηση (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Στοίχιση αριστερά" +msgstr "Στοίχιση αριστερά (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Στοίχιση στο κέντρο" +msgstr "Στοίχιση στο κέντρο (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Για ευθυγράμμιση προς τα δεξιά" +msgstr "Για ευθυγράμμιση προς τα δεξιά (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -1849,7 +1849,7 @@ msgstr "Κανένας" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Διαγράμμιση" +msgstr "Διαγράμμιση (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -1869,7 +1869,7 @@ msgstr "Τίτλος υπόθεση" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "υπογράμμιση" +msgstr "υπογράμμιση (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 42c5dca40..6bfeeeb1d 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -313,11 +313,11 @@ msgid "dashboard.export-binary-multi" msgstr "Download %s Penpot files (.penpot)" msgid "dashboard.export-frames" -msgstr "Export boards to PDF" +msgstr "Export boards as PDF" #: src/app/main/ui/export.cljs msgid "dashboard.export-frames.title" -msgstr "Export to PDF" +msgstr "Export as PDF" msgid "dashboard.export-multi" msgstr "Export Penpot %s files" @@ -443,7 +443,9 @@ msgid "dashboard.import.import-error" msgstr "There was a problem importing the file. The file wasn't imported." msgid "dashboard.import.import-message" -msgstr "%s files have been imported successfully." +msgid_plural "dashboard.import.import-message" +msgstr[0] "1 file has been imported successfully." +msgstr[1] "%s files have been imported successfully." msgid "dashboard.import.import-warning" msgstr "Some files containted invalid objects that have been removed." @@ -710,57 +712,6 @@ msgstr "Your name" msgid "dashboard.your-penpot" msgstr "Your Penpot" -msgid "dashboard.webhooks.description" -msgstr "Webhooks are a simple way to allow other websites and apps to be notified when certain events happen at Penpot. We’ll send a POST request to each of the URLs you provide." - -msgid "dashboard.webhooks.create" -msgstr "Create webhook" - -msgid "dashboard.webhooks.empty.no-webhooks" -msgstr "No webhooks created so far." - -msgid "dashboard.webhooks.empty.add-one" -msgstr "Press the button \"Add webhook\" to add one." - -msgid "dashboard.webhooks.content-type" -msgstr "Content type" - -msgid "dashboard.webhooks.active" -msgstr "Is active" - -msgid "dashboard.webhooks.active.explain" -msgstr "When this hook is triggered event details will be delivered" - -msgid "dashboard.webhooks.update.success" -msgstr "Webhook updated successfully." - -msgid "dashboard.webhooks.create.success" -msgstr "Webhook created successfully." - -msgid "webhooks.last-delivery.success" -msgstr "Last delivery was successfull." - -msgid "errors.webhooks.unexpected" -msgstr "Unexpected error on validating" - -msgid "errors.webhooks.timeout" -msgstr "Timeout" - -msgid "errors.webhooks.connection" -msgstr "Connection error, url not reacheable" - -msgid "errors.webhooks.last-delivery" -msgstr "Last delivery was not successfull." - -msgid "errors.webhooks.ssl-validation" -msgstr "Error on SSL validation." - -msgid "errors.webhooks.invalid-uri" -msgstr "URL does not pass validation." - -msgid "errors.webhooks.unexpected-status" -msgstr "Unexpected status %s" - #: src/app/main/ui/alert.cljs msgid "ds.alert-ok" msgstr "Ok" @@ -1107,7 +1058,7 @@ msgid "inspect.attributes.typography.font-size" msgstr "Font Size" #: src/app/main/ui/inspect/attributes/text.cljs -msgid "inspect.attributes.typography.font-size" +msgid "inspect.attributes.typography.font-weight" msgstr "Font Weight" #: src/app/main/ui/inspect/attributes/text.cljs @@ -1430,6 +1381,7 @@ msgid "labels.no-invitations" msgstr "No pending invitations." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "Click the **Invite people** button to invite people to this team." @@ -1774,21 +1726,22 @@ msgstr[1] "Delete files" msgid "modals.delete-shared-confirm.hint" msgid_plural "modals.delete-shared-confirm.hint" msgstr[0] "" -"If you delete it, those assets will move to the local library of this file. " -"Any unused assets will be lost." +"If you delete it, those assets will no longer be available from other files. " +"Assets that have already been used will remain in this file (no design will be broken!)." + msgstr[1] "" -"If you delete them, those assets will move to the local library of this " -"file. Any unused assets will be lost." +"If you delete them, those assets will no longer be available from other files. " +"Assets that have already been used will remain in this file (no design will be broken!)." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.hint-many" msgid_plural "modals.delete-shared-confirm.hint-many" msgstr[0] "" -"If you delete it, those assets will move to the local libraries of these " -"files. Any unused assets will be lost." +"If you delete it, those assets will no longer be available from other files. " +"Assets that have already been used will remain in these files (no design will be broken!)." msgstr[1] "" -"If you delete them, those assets will move to the local libraries of these " -"files. Any unused assets will be lost." +"If you delete them, those assets will no longer be available from other files. " +"Assets that have already been used will remain in these file (no design will be broken!)." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" @@ -1872,6 +1825,9 @@ msgstr "Send invitation" msgid "modals.invite-member.emails" msgstr "Emails, comma separated" +msgid "modals.invite-member.repeated-invitation" +msgstr "Some emails are from current team members. Their invitations will not be sent." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Invite members to the team" @@ -1973,21 +1929,21 @@ msgstr[1] "Unpublish" msgid "modals.unpublish-shared-confirm.hint" msgid_plural "modals.unpublish-shared-confirm.hint" msgstr[0] "" -"If you unpublish it, those assets will move to the local library of this " -"file." +"If you unpublish it, those assets will no longer be available from other files. " +"Assets that have already been used will remain in this file (no design will be broken!)." msgstr[1] "" -"If you unpublish them, those assets will move to the local library of this " -"file." +"If you unpublish them, those assets will no longer be available from other files. " +"Assets that have already been used will remain in this file (no design will be broken!)." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.hint-many" msgid_plural "modals.unpublish-shared-confirm.hint-many" msgstr[0] "" -"If you unpublish it, those assets will move to the local libraries of these " -"files." +"If you unpublish it, those assets will no longer be available from other files. " +"Assets that have already been used will remain in these files (no design will be broken!)." msgstr[1] "" -"If you unpublish them, those assets will move to the local libraries of " -"these files." +"If you unpublish them, those assets will no longer be available from other files. " +"Assets that have already been used will remain in these file (no design will be broken!)." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" @@ -2509,6 +2465,12 @@ msgstr "Go to viewer comment section" msgid "shortcuts.open-dashboard" msgstr "Go to dashboard" +msgid "shortcuts.select-next" +msgstr "Select next layer" + +msgid "shortcuts.select-prev" +msgstr "Select previous layer" + msgid "shortcuts.open-inspect" msgstr "Go to viewer inspect section" @@ -3091,6 +3053,18 @@ msgstr "Show rulers" msgid "workspace.header.menu.show-textpalette" msgstr "Show fonts palette" +msgid "workspace.header.menu.enable-scale-content" +msgstr "Enable proportional scale" + +msgid "workspace.header.menu.disable-scale-content" +msgstr "Disable proportional scale" + +msgid "workspace.header.menu.undo" +msgstr "Undo" + +msgid "workspace.header.menu.redo" +msgstr "Redo" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Reset" @@ -3582,7 +3556,7 @@ msgstr "Open overlay: %s" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-open-url" -msgstr "Open url" +msgstr "Open URL" #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-out" @@ -3880,7 +3854,7 @@ msgstr "All corners" #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs msgid "workspace.options.radius.single-corners" -msgstr "Individual corners" +msgstr "Independent corners" msgid "workspace.options.recent-fonts" msgstr "Recent" @@ -4046,19 +4020,19 @@ msgstr "Align center" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Justify" +msgstr "Justify (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Align left" +msgstr "Align left (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Align middle" +msgstr "Align middle (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Align right" +msgstr "Align right (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -4102,7 +4076,7 @@ msgstr "None" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Strikethrough" +msgstr "Strikethrough (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -4118,11 +4092,11 @@ msgstr "Selection text" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.titlecase" -msgstr "Title Case" +msgstr "Title case" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Underline" +msgstr "Underline (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 8b50b06cf..002975c9b 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -450,7 +450,9 @@ msgid "dashboard.import.import-error" msgstr "Hubo un problema importando el fichero. No ha podido ser importado." msgid "dashboard.import.import-message" -msgstr "%s files have been imported succesfully." +msgid_plural "dashboard.import.import-message" +msgstr[0] "1 fichero se ha importado correctamente." +msgstr[1] "%s ficheros se han importado correctamente." msgid "dashboard.import.import-warning" msgstr "Algunos ficheros contenían objetos erroneos que no han sido importados." @@ -1450,6 +1452,7 @@ msgid "labels.no-invitations" msgstr "No hay invitaciones." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "Pulsa el botón 'Invitar al equipo' para añadir más integrantes al equipo." @@ -1806,21 +1809,21 @@ msgstr[1] "Borrar archivos" msgid "modals.delete-shared-confirm.hint" msgid_plural "modals.delete-shared-confirm.hint" msgstr[0] "" -"Si lo borras, esos elementos pasarán a formar parte de la biblioteca local " -"de este archivo. Cualquier elemento en desuso se perderá." +"Si lo borras, sus elementos no estarán disponibles para otros archivos. " +"Los elementos que hayan sido utilizados permanecerán en el archivo (¡ningún diseño se romperá!)." msgstr[1] "" -"Si los borras, esos elementos pasarán a formar parte de la biblioteca local " -"de este archivo. Cualquier elemento en desuso se perderá." +"Si los borras, sus elementos no estarán disponibles para otros archivos. " +"Los elementos que hayan sido utilizados permanecerán en el archivo (¡ningún diseño se romperá!)." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.hint-many" msgid_plural "modals.delete-shared-confirm.hint-many" msgstr[0] "" -"Si lo borras, esos elementos pasarán a formar parte de las bibliotecas " -"locales de estos archivos. Cualquier elemento en desuso se perderá." +"Si lo borras, sus elementos no estarán disponibles para otros archivos. " +"Los elementos que hayan sido utilizados permanecerán en los archivo (¡ningún diseño se romperá!)." msgstr[1] "" -"Si los borras, esos elementos pasarán a formar parte de las bibliotecas " -"locales de estos archivos. Cualquier elemento en desuso se perderá." +"Si los borras, sus elementos no estarán disponibles para otros archivos. " +"Los elementos que hayan sido utilizados permanecerán en los archivo (¡ningún diseño se romperá!)." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.delete-shared-confirm.message" @@ -1904,6 +1907,9 @@ msgstr "Enviar invitacion" msgid "modals.invite-member.emails" msgstr "Emails, separados por coma" +msgid "modals.invite-member.repeated-invitation" +msgstr "Algunas direcciones de correo ya se encuentran entre los miembros. Estas invitaciones no serán enviadas." + #: src/app/main/ui/dashboard/team.cljs msgid "modals.invite-team-member.title" msgstr "Invitar a miembros al equipo" @@ -2008,21 +2014,22 @@ msgstr[1] "Despublicar" msgid "modals.unpublish-shared-confirm.hint" msgid_plural "modals.unpublish-shared-confirm.hint" msgstr[0] "" -"Si la despublicas, esos elementos pasarán a formar parte de la biblioteca " -"local de este archivo." +"Si la despublicas, sus elementos no estarán disponibles para otros archivos. " +"Los elementos que hayan sido utilizados permanecerán en el archivo (¡ningún diseño se romperá!)." + msgstr[1] "" -"Si las despublicas, esos elementos pasarán a formar parte de la biblioteca " -"local de este archivo." +"Si las despublicas, sus elementos no estarán disponibles para otros archivos. " +"Los elementos que hayan sido utilizados permanecerán en el archivo (¡ningún diseño se romperá!)." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.hint-many" msgid_plural "modals.unpublish-shared-confirm.hint-many" msgstr[0] "" -"Si la despublicas, esos elementos pasarán a formar parte de las bibliotecas " -"locales de estos archivos." +"Si la despublicas, sus elementos no estarán disponibles para otros archivos. " +"Los elementos que hayan sido utilizados permanecerán en los archivo (¡ningún diseño se romperá!)." msgstr[1] "" -"Si las despublicas, esos elementos pasarán a formar parte de las " -"bibliotecas locales de estos archivos." +"Si las despublicas, sus elementos no estarán disponibles para otros archivos. " +"Los elementos que hayan sido utilizados permanecerán en los archivo (¡ningún diseño se romperá!)." #: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs msgid "modals.unpublish-shared-confirm.message" @@ -2603,6 +2610,12 @@ msgstr "Comentarios" msgid "shortcuts.open-dashboard" msgstr "Ir al dashboard" +msgid "shortcuts.select-next" +msgstr "Seleccionar capa siguiente" + +msgid "shortcuts.select-prev" +msgstr "Seleccionar capa anterior" + msgid "shortcuts.open-inspect" msgstr "Ir al inspector" @@ -3203,6 +3216,18 @@ msgstr "Mostrar reglas" msgid "workspace.header.menu.show-textpalette" msgstr "Mostrar paleta de textos" +msgid "workspace.header.menu.enable-scale-content" +msgstr "Activar escala proporcional" + +msgid "workspace.header.menu.disable-scale-content" +msgstr "Desactivar escala proporcional" + +msgid "workspace.header.menu.undo" +msgstr "Deshacer" + +msgid "workspace.header.menu.redo" +msgstr "Rehacer" + #: src/app/main/ui/workspace/header.cljs msgid "workspace.header.reset-zoom" msgstr "Restablecer" @@ -4187,19 +4212,19 @@ msgstr "Aliniear al centro" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Justificar" +msgstr "Justificar (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Alinear a la izquierda" +msgstr "Alinear a la izquierda (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Alinear al centro" +msgstr "Alinear al centro (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Alinear a la derecha" +msgstr "Alinear a la derecha (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -4244,7 +4269,7 @@ msgstr "Nada" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Tachado" +msgstr "Tachado (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -4264,7 +4289,7 @@ msgstr "Título" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Subrayado" +msgstr "Subrayado (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/eu.po b/frontend/translations/eu.po index 4275a680a..14619ece5 100644 --- a/frontend/translations/eu.po +++ b/frontend/translations/eu.po @@ -1294,6 +1294,7 @@ msgid "labels.no-invitations" msgstr "Ez dago gonbidapenik." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "Sakatu 'Taldera gonbdiatu' taldekide gehiago izateko." @@ -3800,19 +3801,19 @@ msgstr "Lerrokatu erdian" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Justifikatu" +msgstr "Justifikatu (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Lerrokatu ezkerrean" +msgstr "Lerrokatu ezkerrean (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Lerrokatu erdian" +msgstr "Lerrokatu erdian (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Lerrokatu eskuman" +msgstr "Lerrokatu eskuman (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3856,7 +3857,7 @@ msgstr "Bat ere ez" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Gaineko marra" +msgstr "Gaineko marra (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3876,7 +3877,7 @@ msgstr "Izenburuaren mota" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Azpimarra" +msgstr "Azpimarra (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/fa.po b/frontend/translations/fa.po index d32e737e8..099813523 100644 --- a/frontend/translations/fa.po +++ b/frontend/translations/fa.po @@ -1289,6 +1289,7 @@ msgid "labels.no-invitations" msgstr "هیچ دعوتنامه‌ای وجود ندارد." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "دکمه \"دعوت به تیم\" را فشار دهید تا اعضای بیشتری را به این تیم دعوت کنید." @@ -2579,15 +2580,15 @@ msgstr "تراز در مرکز" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "تراز چپ" +msgstr "تراز چپ (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "تراز وسط" +msgstr "تراز وسط (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "تراز راست" +msgstr "تراز راست (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -2644,7 +2645,7 @@ msgstr "متن انتخابی" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "خط‌زیر" +msgstr "خط‌زیر (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 34e7f93c0..8293f224b 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -1339,6 +1339,7 @@ msgid "labels.no-invitations" msgstr "Il n'y a pas d'invitations." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Appuyez sur le bouton \"Inviter à l'équipe\" pour inviter d'autres membres " @@ -3647,19 +3648,19 @@ msgstr "Aligner au centre" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Justifier" +msgstr "Justifier (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Aligner à gauche" +msgstr "Aligner à gauche (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Aligner verticalement au milieu" +msgstr "Aligner verticalement au milieu (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Aligner à droite" +msgstr "Aligner à droite (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3704,7 +3705,7 @@ msgstr "Aucune" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Barré" +msgstr "Barré (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3724,7 +3725,7 @@ msgstr "Premières Lettres en Capitales" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Soulignage" +msgstr "Soulignage (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/he.po b/frontend/translations/he.po index 0f9258a4a..6e1122b18 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -1301,6 +1301,7 @@ msgid "labels.no-invitations" msgstr "אין הזמנות." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "לחיצה על הכפתור „הזמנה לצוות” תאפשר להזמין חברים נוספים לצוות הזה." @@ -3863,19 +3864,19 @@ msgstr "יישור למרכז" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "יישור לשני הצדדים" +msgstr "יישור לשני הצדדים (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "יישור שמאלה" +msgstr "יישור שמאלה (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "יישור לאמצע" +msgstr "יישור לאמצע (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "יישור ימינה" +msgstr "יישור ימינה (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3920,7 +3921,7 @@ msgstr "ללא" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "קו חוצה" +msgstr "קו חוצה (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3940,7 +3941,7 @@ msgstr "רישיות כותרת" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "קו תחתי" +msgstr "קו תחתי (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/hr.po b/frontend/translations/hr.po index 06f15fb74..05a2b3b16 100644 --- a/frontend/translations/hr.po +++ b/frontend/translations/hr.po @@ -1289,6 +1289,7 @@ msgid "labels.no-invitations" msgstr "Nema pozivnica." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "Pritisni gumb \"Pozovi u tim\" da pozoveš više članova u ovaj tim." @@ -3812,19 +3813,19 @@ msgstr "Poravnaj sredinu" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs #, fuzzy msgid "workspace.options.text-options.align-justify" -msgstr "Složi u blok" +msgstr "Složi u blok (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Poravnaj lijevo" +msgstr "Poravnaj lijevo (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Poravnaj sredinu" +msgstr "Poravnaj sredinu (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Poravnaj desno" +msgstr "Poravnaj desno (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3869,7 +3870,7 @@ msgstr "Nijedan" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Precrtanko" +msgstr "Precrtanko (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3889,7 +3890,7 @@ msgstr "Velika i mala slova" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Podcrtano" +msgstr "Podcrtano (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/id.po b/frontend/translations/id.po index b73c52e4f..3abd0c93c 100644 --- a/frontend/translations/id.po +++ b/frontend/translations/id.po @@ -1195,6 +1195,7 @@ msgid "labels.no-invitations" msgstr "Tidak ada undangan." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Tekan tombol \"Undang ke tim\" untuk mengundang lebih banyak anggota ke tim " @@ -3673,19 +3674,19 @@ msgstr "Paskan ke tengah" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Justify" +msgstr "Justify (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Paskan ke kiri" +msgstr "Paskan ke kiri (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Paskan ke tengah" +msgstr "Paskan ke tengah (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Paskan ke kanan" +msgstr "Paskan ke kanan (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3729,7 +3730,7 @@ msgstr "Tidak ada" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Coret" +msgstr "Coret (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3749,7 +3750,7 @@ msgstr "Huruf Judul" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Garis bawah" +msgstr "Garis bawah (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/it.po b/frontend/translations/it.po index 586ad4354..74568c656 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -1294,6 +1294,7 @@ msgid "labels.no-invitations" msgstr "Non ci sono inviti." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Premi il pulsante \"Invita nel team\" per invitare altri membri in questo " diff --git a/frontend/translations/pl.po b/frontend/translations/pl.po index b9de0e236..22f5cb6a6 100644 --- a/frontend/translations/pl.po +++ b/frontend/translations/pl.po @@ -1280,6 +1280,7 @@ msgid "labels.no-invitations" msgstr "Brak zaproszeń." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Naciśnij przycisk „Zaproś do zespołu”, aby zaprosić więcej członków do tego " @@ -3572,19 +3573,19 @@ msgstr "Wyrównaj do środka" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Wyjustuj" +msgstr "Wyjustuj (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Wyrównaj do lewej" +msgstr "Wyrównaj do lewej (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Wyrównaj do środka" +msgstr "Wyrównaj do środka (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Wyrównaj do prawej" +msgstr "Wyrównaj do prawej (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3628,7 +3629,7 @@ msgstr "Brak" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Przekreślenie" +msgstr "Przekreślenie (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3648,7 +3649,7 @@ msgstr "Nazwy Własne" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Podkreślenie" +msgstr "Podkreślenie (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po index b72c5bf18..69eb0dd08 100644 --- a/frontend/translations/pt_BR.po +++ b/frontend/translations/pt_BR.po @@ -1295,6 +1295,7 @@ msgid "labels.no-invitations" msgstr "Não há convites." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Pressione o botão \"Convidar para equipe\" para convidar mais membros para " @@ -3794,19 +3795,19 @@ msgstr "Alinhar ao centro" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Justificar" +msgstr "Justificar (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Alinhar à esquerda" +msgstr "Alinhar à esquerda (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Alinhar no meio" +msgstr "Alinhar no meio (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Alinhar à direita" +msgstr "Alinhar à direita (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3850,7 +3851,7 @@ msgstr "Nenhum" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Tachado" +msgstr "Tachado (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3870,7 +3871,7 @@ msgstr "Capitalização de Título" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Sublinhado" +msgstr "Sublinhado (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/pt_PT.po b/frontend/translations/pt_PT.po index bbd280519..7664dc936 100644 --- a/frontend/translations/pt_PT.po +++ b/frontend/translations/pt_PT.po @@ -1298,6 +1298,7 @@ msgid "labels.no-invitations" msgstr "Não há convites." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Clica no botão \"Convidar para a equipa\" para convidar mais membros para " @@ -3797,19 +3798,19 @@ msgstr "Alinhar ao centro" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Justificar" +msgstr "Justificar (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Alinhar à esquerda" +msgstr "Alinhar à esquerda (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Alinhar ao meio" +msgstr "Alinhar ao meio (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Alinhar à direita" +msgstr "Alinhar à direita (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3853,7 +3854,7 @@ msgstr "Nenhum" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Rasurado" +msgstr "Rasurado (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3873,7 +3874,7 @@ msgstr "Capitalizar iniciais" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Sublinhado" +msgstr "Sublinhado (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index f484c60ff..f988d98d4 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -1291,6 +1291,7 @@ msgid "labels.no-invitations" msgstr "Nu există invitații." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Apăsați butonul „Invitați în echipă” pentru a invita mai mulți membri în " @@ -3766,19 +3767,19 @@ msgstr "Aliniază centru" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Justifică" +msgstr "Justifică (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Aliniază la stânga" +msgstr "Aliniază la stânga (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Aliniază la mijloc" +msgstr "Aliniază la mijloc (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Aliniază la dreapta" +msgstr "Aliniază la dreapta (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3822,7 +3823,7 @@ msgstr "Nici unul" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Barat" +msgstr "Barat (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3842,7 +3843,7 @@ msgstr "Încadrare Titlu" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Subliniază" +msgstr "Subliniază (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index d17d78225..8f1d36d01 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -1278,6 +1278,7 @@ msgid "labels.no-invitations" msgstr "Приглашений нет." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Нажмите кнопку «Пригласить в команду», чтобы пригласить в эту команду " @@ -2688,19 +2689,19 @@ msgstr "Выравнивание по центру" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "Выравнивание по ширине" +msgstr "Выравнивание по ширине (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Выравнивание по левому краю" +msgstr "Выравнивание по левому краю (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Выравнивание по центру" +msgstr "Выравнивание по центру (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Выравнивание по правому краю" +msgstr "Выравнивание по правому краю (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -2744,7 +2745,7 @@ msgstr "Не задано" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Перечеркнутый" +msgstr "Перечеркнутый (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -2764,7 +2765,7 @@ msgstr "Каждое слово с заглавной буквы" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Подчеркнутый" +msgstr "Подчеркнутый (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 81d91ff2c..8edfa5b08 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -1324,6 +1324,7 @@ msgid "labels.no-invitations" msgstr "Davet yok." #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "" "Bu takıma daha fazla üye davet etmek için \"Takıma davet et\" düğmesine " @@ -3893,19 +3894,19 @@ msgstr "Ortaya hizala" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "İki yana yasla" +msgstr "İki yana yasla (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "Sola hizala" +msgstr "Sola hizala (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "Merkeze hizala" +msgstr "Merkeze hizala (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "Sağa hizala" +msgstr "Sağa hizala (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3950,7 +3951,7 @@ msgstr "Hiçbiri" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "Üstü çizili" +msgstr "Üstü çizili (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3970,7 +3971,7 @@ msgstr "İlk Harfi Büyük" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "Altı Çizili" +msgstr "Altı Çizili (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index 34fbdefea..7165e9b2a 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -1252,6 +1252,7 @@ msgid "labels.no-invitations" msgstr "没有邀请。" #: src/app/main/ui/dashboard/team.cljs +#, markdown msgid "labels.no-invitations-hint" msgstr "点击\"邀请加入团队\",邀请更多成员加入这个团队。" @@ -3677,19 +3678,19 @@ msgstr "居中对齐" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-justify" -msgstr "整理" +msgstr "整理 (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-left" -msgstr "靠左对齐" +msgstr "靠左对齐 (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-middle" -msgstr "中间对齐" +msgstr "中间对齐 (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-right" -msgstr "靠右对齐" +msgstr "靠右对齐 (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-top" @@ -3733,7 +3734,7 @@ msgstr "无" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.strikethrough" -msgstr "删除线" +msgstr "删除线 (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.title" @@ -3753,7 +3754,7 @@ msgstr "首字母大写" #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.underline" -msgstr "下划线" +msgstr "下划线 (%s)" #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.options.text-options.uppercase" diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 4a2d2502a..122df5776 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1281,6 +1281,11 @@ css@^3.0.0: source-map "^0.6.1" source-map-resolve "^0.6.0" +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + cssmin@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/cssmin/-/cssmin-0.4.3.tgz#c9194077e0ebdacd691d5f59015b9d819f38d015" @@ -2113,6 +2118,13 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +generic-names@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" + integrity sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A== + dependencies: + loader-utils "^3.2.0" + get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" @@ -2572,6 +2584,11 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -3235,6 +3252,11 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +loader-utils@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" + integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -3242,6 +3264,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -4121,7 +4148,57 @@ postcss-load-config@^3.0.0: lilconfig "^2.0.4" yaml "^1.10.2" -postcss-value-parser@^4.2.0: +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" + integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-modules@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-6.0.0.tgz#cac283dbabbbdc2558c45391cbd0e2df9ec50118" + integrity sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ== + dependencies: + generic-names "^4.0.0" + icss-utils "^5.1.0" + lodash.camelcase "^4.3.0" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + string-hash "^1.1.1" + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: + version "6.0.11" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" + integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -5024,6 +5101,11 @@ stream-to-array@^2.3.0: dependencies: any-promise "^1.1.0" +string-hash@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" + integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -5530,7 +5612,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@^1.0.2, 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= diff --git a/manage.sh b/manage.sh index eae0538fd..79d1e568f 100755 --- a/manage.sh +++ b/manage.sh @@ -17,6 +17,7 @@ function print-current-version { } function build-devenv { + set +e; echo "Building development image $DEVENV_IMGNAME:latest..." pushd docker/devenv; diff --git a/version.txt b/version.txt index 06fb41b63..84cc52946 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.17.2 +1.18.0