diff --git a/CHANGES.md b/CHANGES.md index d7992b563d..b6c7dae0fb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,14 +28,32 @@ - Add the ability to disable google fonts provider with the `disable-google-fonts-provider` flag - Add the ability to disable dashboard templates section with the `disable-dashboard-templates-section` flag - Add the ability to use the registration whitelist with OICD [Github #3348](https://github.com/penpot/penpot/issues/3348) -- Add support for local caching of google fonts (this avoids exposing - the final user IP to goolge and reduces the amount of request sent - to google) +- Add support for local caching of google fonts (this avoids exposing the final user IP to + goolge and reduces the amount of request sent to google) ### :bug: Bugs fixed - Fix files can be opened from multiple urls [Taiga #5310](https://tree.taiga.io/project/penpot/issue/5310) - Fix asset color item was created from the selected layer [Taiga #5180](https://tree.taiga.io/project/penpot/issue/5180) +- Fix unpublish more than one library at the same time [Taiga #5532](https://tree.taiga.io/project/penpot/issue/5532) +- Fix drag projects on dahsboard [Taiga #5531](https://tree.taiga.io/project/penpot/issue/5531) +- Fix allow team name to be all blank [Taiga #5527](https://tree.taiga.io/project/penpot/issue/5527) +- Fix search font visualitation [Taiga #5523](https://tree.taiga.io/project/penpot/issue/5523) +- Fix create and account only with spaces [Taiga #5518](https://tree.taiga.io/project/penpot/issue/5518) +- Fix context menu outside screen [Taiga #5524](https://tree.taiga.io/project/penpot/issue/5524) +- Fix graphic item rename on assets pannel [Taiga #5556](https://tree.taiga.io/project/penpot/issue/5556) +- Fix component and media name validation on assets panel [Taiga #5555](https://tree.taiga.io/project/penpot/issue/5555) +- Fix problem with selection shortcuts [Taiga #5492](https://tree.taiga.io/project/penpot/issue/5492) +- Fix issue with paths line to curve and concurrent editing [Taiga #5191](https://tree.taiga.io/project/penpot/issue/5191) +- Fix problems with locked layers [Taiga #5139](https://tree.taiga.io/project/penpot/issue/5139) +- Fix export from shared prototype [Taiga #5565](https://tree.taiga.io/project/penpot/issue/5565) +- Fix email change: validation error displaying even after both fields are identical [Taiga #5514](https://tree.taiga.io/project/penpot/issue/5514) +- Fix scroll on viewer comment list [Taiga #5563](https://tree.taiga.io/project/penpot/issue/5563) +- Fix context menu z-index [Taiga #5561](https://tree.taiga.io/project/penpot/issue/5561) +- Fix select all checkbox on shared link config [Taiga #5566](https://tree.taiga.io/project/penpot/issue/5566) +- Fix validation on full name input on account creation [Taiga #5516](https://tree.taiga.io/project/penpot/issue/5516) +- Fix validation on team name input [Taiga #5510](https://tree.taiga.io/project/penpot/issue/5510) + ### :arrow_up: Deps updates diff --git a/backend/deps.edn b/backend/deps.edn index 742a29217a..0b4e5180a4 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -6,7 +6,7 @@ org.clojure/clojure {:mvn/version "1.11.1"} org.clojure/core.async {:mvn/version "1.6.673"} - com.github.luben/zstd-jni {:mvn/version "1.5.2-5"} + com.github.luben/zstd-jni {:mvn/version "1.5.5-4"} io.prometheus/simpleclient {:mvn/version "0.16.0"} io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} @@ -17,7 +17,7 @@ io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"} - io.lettuce/lettuce-core {:mvn/version "6.2.2.RELEASE"} + io.lettuce/lettuce-core {:mvn/version "6.2.4.RELEASE"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/yetti @@ -26,8 +26,8 @@ :git/url "https://github.com/funcool/yetti.git" :exclusions [org.slf4j/slf4j-api]} - com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"} - metosin/reitit-core {:mvn/version "0.5.18"} + com.github.seancorfield/next.jdbc {:mvn/version "1.3.883"} + metosin/reitit-core {:mvn/version "0.6.0"} org.postgresql/postgresql {:mvn/version "42.6.0"} @@ -35,12 +35,12 @@ io.whitfin/siphash {:mvn/version "2.0.0"} - buddy/buddy-hashers {:mvn/version "1.8.158"} - buddy/buddy-sign {:mvn/version "3.4.333"} + buddy/buddy-hashers {:mvn/version "2.0.167"} + buddy/buddy-sign {:mvn/version "3.5.351"} - com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.5"} + com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.6"} - org.jsoup/jsoup {:mvn/version "1.15.3"} + org.jsoup/jsoup {:mvn/version "1.16.1"} org.im4java/im4java {:git/tag "1.4.0-penpot-2" :git/sha "e2b3e16" @@ -49,14 +49,14 @@ org.lz4/lz4-java {:mvn/version "1.8.0"} org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} - integrant/integrant {:mvn/version "0.8.0"} + integrant/integrant {:mvn/version "0.8.1"} dawran6/emoji {:mvn/version "0.1.5"} markdown-clj/markdown-clj {:mvn/version "1.11.4"} ;; Pretty Print specs pretty-spec/pretty-spec {:mvn/version "0.1.4"} - software.amazon.awssdk/s3 {:mvn/version "2.19.29"} + software.amazon.awssdk/s3 {:mvn/version "2.20.96"} } :paths ["src" "resources" "target/classes"] diff --git a/backend/src/app/auth.clj b/backend/src/app/auth.clj index 500f07916f..0037e69001 100644 --- a/backend/src/app/auth.clj +++ b/backend/src/app/auth.clj @@ -13,8 +13,8 @@ (def default-params {:alg :argon2id - :memory (* 32768 2) - :iterations 5 + :memory (* 32768 2) ;; 64 MiB + :iterations 7 :parallelism (px/get-available-processors)}) (defn derive-password diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index 36cf0afe9d..5330efc098 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -25,8 +25,7 @@ [app.tokens :as tokens] [app.util.json :as json] [app.util.time :as dt] - [buddy.core.keys :as keys] - [buddy.sign.jws :as jws] + [buddy.sign.jwk :as jwk] [buddy.sign.jwt :as jwt] [clojure.set :as set] [clojure.spec.alpha :as s] @@ -109,7 +108,7 @@ (defn- process-oidc-jwks [keys] (reduce (fn [result {:keys [kid] :as kdata}] - (let [pkey (ex/try! (keys/jwk->public-key kdata))] + (let [pkey (ex/try! (jwk/public-key kdata))] (if (ex/exception? pkey) (do (l/warn :hint "unable to create public key" @@ -392,7 +391,7 @@ (defn- get-user-info [{:keys [provider]} tdata] (try - (let [{:keys [kid alg] :as theader} (jws/decode-header (:token/id tdata))] + (let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))] (when-let [key (if (str/starts-with? (name alg) "hs") (:client-secret provider) (get-in provider [:jwks kid]))] @@ -425,8 +424,12 @@ code (get params :code) state (tokens/verify props {:token state :iss :oauth}) tdata (fetch-access-token cfg code) - info (or (get-user-info cfg tdata) - (fetch-user-info cfg tdata)) + info (case (cf/get :oidc-user-info-source) + :token (get-user-info cfg tdata) + :userinfo (fetch-user-info cfg tdata) + (or (get-user-info cfg tdata) + (fetch-user-info cfg tdata))) + info (process-user-info provider tdata info)] (l/trace :hint "user info" :info info) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index d2cfc2cfb8..ed98060d2b 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -146,6 +146,7 @@ (s/def ::google-client-id ::us/string) (s/def ::google-client-secret ::us/string) (s/def ::oidc-client-id ::us/string) +(s/def ::oidc-user-info-source ::us/keyword) (s/def ::oidc-client-secret ::us/string) (s/def ::oidc-base-uri ::us/string) (s/def ::oidc-token-uri ::us/string) @@ -242,6 +243,7 @@ ::google-client-secret ::oidc-client-id ::oidc-client-secret + ::oidc-user-info-source ::oidc-base-uri ::oidc-token-uri ::oidc-auth-uri diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index 4578a74b3c..9475a5a041 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -9,7 +9,7 @@ (:require [app.common.data :as d] [app.common.logging :as l] - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -19,9 +19,7 @@ [app.rpc.climit :as-alias climit] [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])) + [app.util.services :as sv])) (defn- event->row [event] [(uuid/next) @@ -52,25 +50,23 @@ (when (seq events) (db/insert-multi! pool :audit-log event-columns events)))) -(s/def ::name ::us/string) -(s/def ::type ::us/string) -(s/def ::props (s/map-of ::us/keyword any?)) -(s/def ::timestamp dt/instant?) -(s/def ::context (s/map-of ::us/keyword any?)) +(def schema:event + [:map {:title "Event"} + [:name [:string {:max 250}]] + [:type [:string {:max 250}]] + [:props + [:map-of :keyword :any]] + [:context {:optional true} + [:map-of :keyword :any]]]) -(s/def ::event - (s/keys :req-un [::type ::name ::props ::timestamp] - :opt-un [::context])) - -(s/def ::events (s/every ::event)) - -(s/def ::push-audit-events - (s/keys :req [::rpc/profile-id] - :req-un [::events])) +(def schema:push-audit-events + [:map {:title "push-audit-events"} + [:events [:vector schema:event]]]) (sv/defmethod ::push-audit-events {::climit/id :submit-audit-events-by-profile ::climit/key-fn ::rpc/profile-id + ::sm/params schema:push-audit-events ::audit/skip true ::doc/added "1.17"} [{:keys [::db/pool] :as cfg} params] diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index cbd8394a56..7281cfed25 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -8,9 +8,10 @@ (:require [app.auth :as auth] [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.schema :as sm] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -26,18 +27,13 @@ [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s] [cuerdas.core :as str])) -(s/def ::email ::us/email) -(s/def ::fullname ::us/not-empty-string) -(s/def ::lang ::us/string) -(s/def ::path ::us/string) -(s/def ::password ::us/not-empty-string) -(s/def ::old-password ::us/not-empty-string) -(s/def ::theme ::us/string) -(s/def ::invitation-token ::us/not-empty-string) -(s/def ::token ::us/not-empty-string) +(def schema:password + [::sm/word-string {:max 500}]) + +(def schema:token + [::sm/word-string {:max 1000}]) ;; ---- COMMAND: login with password @@ -101,22 +97,22 @@ (rph/with-meta {::audit/props (audit/profile->props profile) ::audit/profile-id (:id profile)})))))) -(s/def ::login-with-password - (s/keys :req-un [::email ::password] - :opt-un [::invitation-token])) +(def schema:login-with-password + [:map {:title "login-with-password"} + [:email ::sm/email] + [:password schema:password] + [:invitation-token {:optional true} schema:token]]) (sv/defmethod ::login-with-password "Performs authentication using penpot password." {::rpc/auth false - ::doc/added "1.15"} + ::doc/added "1.15" + ::sm/params schema:login-with-password} [cfg params] (login-with-password cfg params)) ;; ---- COMMAND: Logout -(s/def ::logout - (s/keys :opt [::rpc/profile-id])) - (sv/defmethod ::logout "Clears the authentication cookie and logout the current session." {::rpc/auth false @@ -141,13 +137,15 @@ (update-password conn)) nil))) -(s/def ::token ::us/not-empty-string) -(s/def ::recover-profile - (s/keys :req-un [::token ::password])) +(def schema:recover-profile + [:map {:title "recover-profile"} + [:token schema:token] + [:password schema:password]]) (sv/defmethod ::recover-profile {::rpc/auth false - ::doc/added "1.15"} + ::doc/added "1.15" + ::sm/params schema:recover-profile} [cfg params] (recover-profile cfg params)) @@ -228,13 +226,16 @@ (with-meta {:token token} {::audit/profile-id uuid/zero}))) -(s/def ::prepare-register-profile - (s/keys :req-un [::email ::password] - :opt-un [::invitation-token])) +(def schema:prepare-register-profile + [:map {:title "prepare-register-profile"} + [:email ::sm/email] + [:password schema:password] + [:invitation-token {:optional true} schema:token]]) (sv/defmethod ::prepare-register-profile {::rpc/auth false - ::doc/added "1.15"} + ::doc/added "1.15" + ::sm/params schema:prepare-register-profile} [cfg params] (prepare-register cfg params)) @@ -244,7 +245,7 @@ "Create the profile entry on the database with limited set of input attrs (all the other attrs are filled with default values)." [conn {:keys [email] :as params}] - (us/assert! ::us/email email) + (dm/assert! ::sm/email email) (let [id (or (:id params) (uuid/next)) props (-> (audit/extract-utm-params params) (merge (:props params)) @@ -391,12 +392,16 @@ {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)}))))) -(s/def ::register-profile - (s/keys :req-un [::token ::fullname])) + +(def schema:register-profile + [:map {:title "register-profile"} + [:token schema:token] + [:fullname [::sm/word-string {:max 100}]]]) (sv/defmethod ::register-profile {::rpc/auth false - ::doc/added "1.15"} + ::doc/added "1.15" + ::sm/params schema:register-profile} [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] (-> (assoc cfg ::db/conn conn) @@ -448,12 +453,15 @@ (create-recovery-token) (send-email-notification conn)))))) -(s/def ::request-profile-recovery - (s/keys :req-un [::email])) + +(def schema:request-profile-recovery + [:map {:title "request-profile-recovery"} + [:email ::sm/email]]) (sv/defmethod ::request-profile-recovery {::rpc/auth false - ::doc/added "1.15"} + ::doc/added "1.15" + ::sm/params schema:request-profile-recovery} [cfg params] (request-profile-recovery cfg params)) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index e3b88a8bcb..c44c13aa46 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -512,6 +512,7 @@ [:map {:title "GetPage"} [:file-id ::sm/uuid] [:page-id {:optional true} ::sm/uuid] + [:share-id {:optional true} ::sm/uuid] [:object-id {:optional true} ::sm/uuid] [:features {:optional true} ::features]]) @@ -527,14 +528,12 @@ Mainly used for rendering purposes." {::doc/added "1.17" ::sm/params ::get-page} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] (dm/with-open [conn (db/open pool)] - (check-read-permissions! conn profile-id file-id) - - (binding [pmap/*load-fn* (partial load-pointer conn file-id)] - (get-page conn params)))) - - + (let [perms (get-permissions conn profile-id file-id share-id)] + (check-read-permissions! perms) + (binding [pmap/*load-fn* (partial load-pointer conn file-id)] + (get-page conn params))))) ;; --- COMMAND QUERY: get-team-shared-files diff --git a/backend/src/app/rpc/commands/fonts.clj b/backend/src/app/rpc/commands/fonts.clj index 5aab17fde3..256132e847 100644 --- a/backend/src/app/rpc/commands/fonts.clj +++ b/backend/src/app/rpc/commands/fonts.clj @@ -36,6 +36,7 @@ (s/def ::id ::us/uuid) (s/def ::name ::us/not-empty-string) (s/def ::project-id ::us/uuid) +(s/def ::share-id ::us/uuid) (s/def ::style valid-style) (s/def ::team-id ::us/uuid) (s/def ::weight valid-weight) @@ -47,7 +48,8 @@ (s/keys :req [::rpc/profile-id] :opt-un [::team-id ::file-id - ::project-id]) + ::project-id + ::share-id]) (fn [o] (or (contains? o :team-id) (contains? o :file-id) @@ -55,7 +57,7 @@ (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}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id share-id] :as params}] (dm/with-open [conn (db/open pool)] (cond (uuid? team-id) @@ -74,11 +76,12 @@ (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) + project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]}) + perms (files/get-permissions conn profile-id file-id share-id)] + (files/check-read-permissions! perms) (db/query conn :team-font-variant - {:team-id (:team-id project) - :deleted-at nil}))))) + {:team-id (:team-id project) + :deleted-at nil}))))) (declare create-font-variant) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 0a44998ed4..56b352ffcd 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -27,7 +27,6 @@ [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s] [cuerdas.core :as str])) (declare check-profile-existence!) @@ -41,7 +40,7 @@ (def schema:profile [:map {:title "Profile"} [:id ::sm/uuid] - [:fullname :string] + [:fullname [::sm/word-string {:max 250}]] [:email ::sm/email] [:is-active {:optional true} :boolean] [:is-blocked {:optional true} :boolean] @@ -82,12 +81,15 @@ ;; --- MUTATION: Update Profile (own) +(def schema:update-profile + [:map {:title "update-profile"} + [:fullname [::sm/word-string {:max 250}]] + [:lang {:optional true} [:string {:max 5}]] + [:theme {:optional true} [:string {:max 250}]]]) + (sv/defmethod ::update-profile {::doc/added "1.0" - ::sm/params [:map {:title "UpdateProfileParams"} - [:fullname {:min 1} :string] - [:lang {:optional true} :string] - [:theme {:optional true} :string]] + ::sm/params schema:update-profile ::sm/result schema:profile} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}] @@ -128,11 +130,14 @@ (declare update-profile-password!) (declare invalidate-profile-session!) +(def schema:update-profile-password + [:map {:title "update-profile-password"} + [:password [::sm/word-string {:max 500}]] + [:old-password [::sm/word-string {:max 500}]]]) + (sv/defmethod ::update-profile-password {:doc/added "1.0" - ::sm/params [:map {:title "UpdateProfilePasswordParams"} - [:password :string] - [:old-password :string]] + ::sm/params schema:update-profile-password ::sm/result :nil} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}] @@ -178,10 +183,13 @@ (declare upload-photo) (declare update-profile-photo) +(def schema:update-profile-photo + [:map {:title "update-profile-photo"} + [:file ::media/upload]]) + (sv/defmethod ::update-profile-photo {:doc/added "1.1" - ::sm/params [:map {:title "UpdateProfilePhotoParams"} - [:file ::media/upload]] + ::sm/params schema:update-profile-photo ::sm/result :nil} [cfg {:keys [::rpc/profile-id file] :as params}] ;; Validate incoming mime type @@ -239,11 +247,13 @@ (declare ^:private request-email-change!) (declare ^:private change-email-immediately!) -(s/def ::request-email-change - (s/keys :req [::rpc/profile-id] - :req-un [::email])) +(def schema:request-email-change + [:map {:title "request-email-change"} + [:email ::sm/email]]) (sv/defmethod ::request-email-change + {::doc/added "1.0" + ::sm/params schema: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) @@ -304,12 +314,13 @@ ;; --- MUTATION: Update Profile Props -(s/def ::props map?) -(s/def ::update-profile-props - (s/keys :req [::rpc/profile-id] - :req-un [::props])) +(def schema:update-profile-props + [:map {:title "update-profile-props"} + [:props [:map-of :keyword :any]]]) (sv/defmethod ::update-profile-props + {::doc/added "1.0" + ::sm/params schema: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) @@ -329,15 +340,12 @@ (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 + {::doc/added "1.0"} [{: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) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index ac8b0ce45e..d2edaeec98 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -14,6 +14,7 @@ [app.common.pages :as cp] [app.common.pprint :as pp] [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -414,6 +415,14 @@ (println (us/pretty-explain data)) + (= :params-validation (:code data)) + (app.common.pprint/pprint + (sm/humanize-data (::sm/explain data))) + + (= :data-validation (:code data)) + (app.common.pprint/pprint + (sm/humanize-data (::sm/explain data))) + (= :service-error (:type data)) (print-error! (.getCause ^Throwable error)) diff --git a/backend/test/backend_tests/rpc_audit_test.clj b/backend/test/backend_tests/rpc_audit_test.clj index df860ca037..233728dac3 100644 --- a/backend/test/backend_tests/rpc_audit_test.clj +++ b/backend/test/backend_tests/rpc_audit_test.clj @@ -39,8 +39,8 @@ params {::th/type :push-audit-events ::rpc/profile-id (:id prof) :events [{:name "navigate" - :props {:project-id proj-id - :team-id team-id + :props {:project-id (str proj-id) + :team-id (str team-id) :route "dashboard-files"} :context {:engine "blink"} :profile-id (:id prof) @@ -71,8 +71,8 @@ params {::th/type :push-audit-events ::rpc/profile-id (:id prof) :events [{:name "navigate" - :props {:project-id proj-id - :team-id team-id + :props {:project-id (str proj-id) + :team-id (str team-id) :route "dashboard-files"} :context {:engine "blink"} :profile-id uuid/zero @@ -91,6 +91,8 @@ (t/is (= 1 (count rows))) (t/is (= (:id prof) (:profile-id row))) (t/is (= "navigate" (:name row))) - (t/is (= "frontend" (:source row))))))) + (t/is (= "frontend" (:source row)))) + + ))) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 743bbdb406..d39fd2d393 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -252,6 +252,7 @@ :components-v2 true :changes changes} out (th/command! params)] + ;; (th/print-result! out) (t/is (nil? (:error out))) (:result out)))] @@ -278,7 +279,7 @@ [{:type :add-obj :page-id page-id :id shid - :parent-id uuid/zero + :parent-id uuid/zero :frame-id uuid/zero :components-v2 true :obj {:id shid @@ -286,7 +287,7 @@ :frame-id uuid/zero :parent-id uuid/zero :type :image - :metadata {:id (:id fmo1)}}}]) + :metadata {:id (:id fmo1) :width 200 :height 200 :mtype "image/jpeg"}}}]) ;; Check that reference storage objects on filemediaobjects ;; are the same because of deduplication feature. diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 846c5e1e93..a28f186c81 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -278,7 +278,7 @@ (let [error (:error out)] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) - (t/is (th/ex-of-code? error :spec-validation)))) + (t/is (th/ex-of-code? error :params-validation)))) ;; try correct register (let [data {::th/type :register-profile diff --git a/common/deps.edn b/common/deps.edn index 5ff183c57a..28c4b5b9c6 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -1,40 +1,37 @@ {:deps {org.clojure/clojure {:mvn/version "1.11.1"} org.clojure/data.json {:mvn/version "2.4.0"} - org.clojure/tools.cli {:mvn/version "1.0.214"} + org.clojure/tools.cli {:mvn/version "1.0.219"} org.clojure/clojurescript {:mvn/version "1.11.60"} org.clojure/test.check {:mvn/version "1.1.1"} org.clojure/data.fressian {:mvn/version "1.0.0"} ;; Logging - org.apache.logging.log4j/log4j-api {:mvn/version "2.19.0"} - 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-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"} + org.apache.logging.log4j/log4j-api {:mvn/version "2.20.0"} + org.apache.logging.log4j/log4j-core {:mvn/version "2.20.0"} + org.apache.logging.log4j/log4j-web {:mvn/version "2.20.0"} + org.apache.logging.log4j/log4j-jul {:mvn/version "2.20.0"} + org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.20.0"} + org.slf4j/slf4j-api {:mvn/version "2.0.7"} + pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.30"} - selmer/selmer {:mvn/version "1.12.55"} + selmer/selmer {:mvn/version "1.12.58"} criterium/criterium {:mvn/version "0.4.6"} metosin/jsonista {:mvn/version "0.3.7"} metosin/malli {:mvn/version "0.11.0"} expound/expound {:mvn/version "0.9.0"} - com.cognitect/transit-clj {:mvn/version "1.0.329"} + com.cognitect/transit-clj {:mvn/version "1.0.333"} com.cognitect/transit-cljs {:mvn/version "0.8.280"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/cuerdas {:mvn/version "2022.06.16-403"} - funcool/promesa - {:git/tag "11.0-alpha13" - :git/sha "f6cab38" - :git/url "https://github.com/funcool/promesa.git"} + funcool/promesa {:mvn/version "11.0.671"} funcool/datoteka {:mvn/version "3.0.66" :exclusions [funcool/promesa]} - lambdaisland/uri {:mvn/version "1.13.95" + lambdaisland/uri {:mvn/version "1.15.125" :exclusions [org.clojure/data.json]} frankiesardo/linked {:mvn/version "1.3.0"} @@ -44,7 +41,7 @@ ;; exception printing fipp/fipp {:mvn/version "0.6.26"} - io.aviso/pretty {:mvn/version "1.3"} + io.aviso/pretty {:mvn/version "1.4.4"} environ/environ {:mvn/version "1.2.0"}} :paths ["src" "target/classes"] :aliases diff --git a/common/src/app/common/path/commands.cljc b/common/src/app/common/path/commands.cljc index 7a02f6ec4d..a434cb09e4 100644 --- a/common/src/app/common/path/commands.cljc +++ b/common/src/app/common/path/commands.cljc @@ -50,12 +50,15 @@ (defn update-curve-to [command h1 h2] - (-> command - (assoc :command :curve-to) - (assoc-in [:params :c1x] (:x h1)) - (assoc-in [:params :c1y] (:y h1)) - (assoc-in [:params :c2x] (:x h2)) - (assoc-in [:params :c2y] (:y h2)))) + (let [params {:x (-> command :params :x) + :y (-> command :params :y) + :c1x (:x h1) + :c1y (:y h1) + :c2x (:x h2) + :c2y (:y h2)}] + (-> command + (assoc :command :curve-to) + (assoc :params params)))) (defn make-curve-to [to h1 h2] diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc index 8a62947dfb..19e3046f88 100644 --- a/common/src/app/common/schema.cljc +++ b/common/src/app/common/schema.cljc @@ -282,7 +282,9 @@ (def! ::email {:type ::email :pred (fn [s] - (and (string? s) (re-seq email-re s))) + (and (string? s) + (< (count s) 250) + (re-seq email-re s))) :type-properties {:title "email" :description "string with valid email address" @@ -464,6 +466,7 @@ (def! ::word-string {:type ::word-string :pred #(and (string? %) (not (str/blank? %))) + :property-pred (m/-min-max-pred count) :type-properties {:title "string" :description "string" diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index 731f31de27..f84db3e4d2 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -260,7 +260,11 @@ (let [frame-ids (cond->> (all-frames-by-position objects position) (some? excluded) - (remove excluded)) + (remove excluded) + + :always + (remove #(or (dm/get-in objects [% :hidden]) + (dm/get-in objects [% :blocked])))) frame-set (set frame-ids)] @@ -276,7 +280,10 @@ "Search the top nested frame in a list of ids" [objects ids] - (let [frame-ids (->> ids (filter #(cph/frame-shape? objects %))) + (let [frame-ids (->> ids + (filter #(cph/frame-shape? objects %)) + (remove #(or (dm/get-in objects [% :hidden]) + (dm/get-in objects [% :blocked])))) frame-set (set frame-ids)] (loop [current-id (first frame-ids)] (let [current-shape (get objects current-id) diff --git a/exporter/src/app/handlers/export_shapes.cljs b/exporter/src/app/handlers/export_shapes.cljs index 4a8e53dab9..a8e108ae46 100644 --- a/exporter/src/app/handlers/export_shapes.cljs +++ b/exporter/src/app/handlers/export_shapes.cljs @@ -28,6 +28,7 @@ (s/def ::name ::us/string) (s/def ::object-id ::us/uuid) (s/def ::page-id ::us/uuid) +(s/def ::share-id ::us/uuid) (s/def ::profile-id ::us/uuid) (s/def ::scale ::us/number) (s/def ::suffix ::us/string) @@ -35,7 +36,8 @@ (s/def ::wait ::us/boolean) (s/def ::export - (s/keys :req-un [::page-id ::file-id ::object-id ::type ::suffix ::scale ::name])) + (s/keys :req-un [::page-id ::file-id ::object-id ::type ::suffix ::scale ::name] + :opt-un [::share-id])) (s/def ::exports (s/coll-of ::export :kind vector? :min-count 1)) @@ -89,7 +91,6 @@ proc (-> (rd/render export on-progress) (p/then (constantly resource)) (p/catch on-error))] - (if wait (p/then proc #(assoc exchange :response/body (dissoc % :path))) (assoc exchange :response/body (dissoc resource :path))))) @@ -188,6 +189,7 @@ (process-partition [[part1 :as part]] {:file-id (:file-id part1) :page-id (:page-id part1) + :share-id (:share-id part1) :name (:name part1) :token token :type (:type part1) diff --git a/exporter/src/app/renderer.cljs b/exporter/src/app/renderer.cljs index 25d462a6cd..b4abaca4bc 100644 --- a/exporter/src/app/renderer.cljs +++ b/exporter/src/app/renderer.cljs @@ -18,12 +18,14 @@ (s/def ::type #{:jpeg :png :pdf :svg}) (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) +(s/def ::share-id ::us/uuid) (s/def ::scale ::us/number) (s/def ::token ::us/string) (s/def ::filename ::us/string) (s/def ::object - (s/keys :req-un [::id ::name ::suffix ::filename])) + (s/keys :req-un [::id ::name ::suffix ::filename] + :opt-un [::share-id])) (s/def ::objects (s/coll-of ::object :min-count 1)) diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index 074a826a70..38022db739 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -17,7 +17,7 @@ [promesa.core :as p])) (defn render - [{:keys [file-id page-id token scale type objects] :as params} on-object] + [{:keys [file-id page-id share-id token scale type objects] :as params} on-object] (letfn [(prepare-options [uri] #js {:screen #js {:width bw/default-viewport-width :height bw/default-viewport-height} @@ -48,9 +48,9 @@ ;; take the screnshot of requested objects, one by one (p/run! (partial render-object page) objects) nil))] - (p/let [params {:file-id file-id :page-id page-id + :share-id share-id :object-id (mapv :id objects) :route "objects"} uri (-> (cf/get :public-uri) diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index a4f442c9f5..1464f62f3c 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -17,7 +17,7 @@ [promesa.core :as p])) (defn render - [{:keys [file-id page-id token scale type objects] :as params} on-object] + [{:keys [file-id page-id share-id token scale type objects] :as params} on-object] (letfn [(prepare-options [uri] #js {:screen #js {:width bw/default-viewport-width :height bw/default-viewport-height} @@ -31,6 +31,7 @@ (prepare-uri [base-uri object-id] (let [params {:file-id file-id :page-id page-id + :share-id share-id :object-id object-id :route "objects"}] (-> base-uri diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 277dac18d1..ba3287a644 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -108,7 +108,7 @@ :height height})) (defn render - [{:keys [page-id file-id objects token scale type]} on-object] + [{:keys [page-id file-id share-id objects token scale type]} on-object] (letfn [(convert-to-ppm [pngpath] (let [ppmpath (str/concat pngpath "origin.ppm")] (l/trace :fn :convert-to-ppm :path ppmpath) @@ -338,9 +338,9 @@ ;; take the screnshot of requested objects, one by one (p/run! (partial render-object page) objects) nil))] - (p/let [params {:file-id file-id :page-id page-id + :share-id share-id :render-embed true :object-id (mapv :id objects) :route "objects"} diff --git a/frontend/resources/styles/main/partials/comments.scss b/frontend/resources/styles/main/partials/comments.scss index b2d89ce3f9..7c2d4bf002 100644 --- a/frontend/resources/styles/main/partials/comments.scss +++ b/frontend/resources/styles/main/partials/comments.scss @@ -246,7 +246,7 @@ pointer-events: auto; .thread-groups { - height: 100%; + height: calc(100% - 34px); overflow-y: scroll; hr { border: 0; diff --git a/frontend/resources/styles/main/partials/dashboard-fonts.scss b/frontend/resources/styles/main/partials/dashboard-fonts.scss index bd93dca5f3..544e50341e 100644 --- a/frontend/resources/styles/main/partials/dashboard-fonts.scss +++ b/frontend/resources/styles/main/partials/dashboard-fonts.scss @@ -60,7 +60,7 @@ font-size: $fs14; background-color: $color-white; display: flex; - min-width: 1000px; + max-width: 1000px; width: 100%; min-height: 97px; align-items: center; diff --git a/frontend/resources/styles/main/partials/dashboard-team.scss b/frontend/resources/styles/main/partials/dashboard-team.scss index 384b748edc..45ee9d6ecb 100644 --- a/frontend/resources/styles/main/partials/dashboard-team.scss +++ b/frontend/resources/styles/main/partials/dashboard-team.scss @@ -324,7 +324,7 @@ box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); z-index: 12; top: 30px; - left: 6px; + left: -151px; width: 155px; hr { diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index 5efa8efb3b..f48e89e57d 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -466,20 +466,27 @@ .dashboard-templates-section { position: absolute; + display: flex; + flex-direction: column; + justify-content: flex-end; bottom: 0; width: 100%; - height: 285px; + height: 228px; transition: bottom 300ms; - + pointer-events: none; &.collapsed { bottom: -228px; transition: bottom 300ms; } .title { - width: 100%; + pointer-events: all; + width: fit-content; + top: -56px; + right: -28px; text-align: right; height: 56px; + position: absolute; button { border: none; cursor: pointer; @@ -529,6 +536,7 @@ display: flex; align-items: center; justify-content: center; + pointer-events: all; svg { width: 12px; height: 12px; @@ -550,6 +558,7 @@ } .content { + pointer-events: all; background-color: $color-white; width: 200%; height: 229px; diff --git a/frontend/resources/styles/main/partials/sidebar-layers.scss b/frontend/resources/styles/main/partials/sidebar-layers.scss index 79ad3b4aff..e9d3256b5c 100644 --- a/frontend/resources/styles/main/partials/sidebar-layers.scss +++ b/frontend/resources/styles/main/partials/sidebar-layers.scss @@ -355,10 +355,12 @@ span.element-name { .pages-tool-bar { display: flex; justify-content: space-between; - height: 32px; - margin-top: 8px; + height: 40px; + padding: 0; &.search { + margin-top: 8px; + padding: 8px; .search-box { border: 1px solid $color-gray-20; border-radius: $br4; @@ -403,6 +405,67 @@ span.element-name { margin: 0 2px 0 5px; cursor: pointer; } + .page-name { + padding: 8px; + margin-top: 8px; + } + .icon-search { + margin-top: 8px; + } + .focus-title { + width: 100%; + height: 100%; + display: grid; + align-items: center; + grid-template-columns: 16px 1fr auto; + grid-column-gap: 8px; + cursor: pointer; + + .back-button { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + background: none; + border: none; + padding: 0; + margin: 0; + + svg { + width: 12px; + height: 12px; + fill: $color-gray-20; + transform: rotate(180deg); + } + + &:hover { + svg { + fill: $color-primary; + } + } + } + + .focus-name { + height: 100%; + display: flex; + justify-content: flex-start; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .focus-mode { + color: $color-primary; + border: 1px solid $color-primary; + border-radius: $br3; + font-size: $fs10; + text-transform: uppercase; + padding: 0px 4px; + display: flex; + align-items: center; + } + } } } .active-filters { diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index 09ac24e249..d8498fa7b3 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -40,6 +40,7 @@ flex-shrink: 0; padding: $size-2; overflow: hidden; + margin: 0; svg { fill: $color-gray-20; @@ -136,52 +137,6 @@ padding: 0.25rem; } } - - & .focus-title { - width: 100%; - height: 100%; - display: grid; - align-items: center; - grid-template-columns: auto 1fr auto; - grid-column-gap: 8px; - cursor: pointer; - - & .back-button { - background: none; - border: none; - padding: 0; - margin: 0; - - & svg { - fill: $color-white; - transform: rotate(180deg); - margin-top: 3px; - } - - &:hover { - svg { - fill: $color-primary; - } - } - } - - & .focus-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - & .focus-mode { - color: $color-primary; - border: 1px solid $color-primary; - border-radius: $br3; - font-size: $fs10; - text-transform: uppercase; - padding: 0px 4px; - display: flex; - align-items: center; - } - } } .assets-bar .tool-window { diff --git a/frontend/resources/styles/main/partials/viewer.scss b/frontend/resources/styles/main/partials/viewer.scss index 1ed55ff1c4..3a52326a7a 100644 --- a/frontend/resources/styles/main/partials/viewer.scss +++ b/frontend/resources/styles/main/partials/viewer.scss @@ -182,8 +182,8 @@ right: 0; top: 50px; width: 256px; - height: 100%; - z-index: 10; + height: calc(100vh - 48px); + z-index: 9; } .empty-state { diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs index 1ad280babb..729d5d74f7 100644 --- a/frontend/src/app/main/data/exports.cljs +++ b/frontend/src/app/main/data/exports.cljs @@ -75,7 +75,7 @@ {:exports (vec exports)}))))))) (defn show-viewer-export-dialog - [{:keys [shapes page-id file-id exports]}] + [{:keys [shapes page-id file-id share-id exports]}] (ptk/reify ::show-viewer-export-dialog ptk/WatchEvent (watch [_ _ _] @@ -87,8 +87,9 @@ (assoc :file-id file-id) (assoc :object-id (:id shape)) (assoc :shape (dissoc shape :exports)) - (assoc :name (:name shape))))] - (rx/of (modal/show :export-shapes {:exports (vec exports)})))))) + (assoc :name (:name shape)) + (cond-> share-id (assoc :share-id share-id))))] + (rx/of (modal/show :export-shapes {:exports (vec exports)})))))) #_TODO (defn show-workspace-export-frames-dialog [frames] diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 966af4fdd2..7043327671 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -938,7 +938,10 @@ (rx/of (dwe/start-edition-mode id)) (:group :bool :frame) - (rx/of (dws/select-shapes (into (d/ordered-set) shapes))) + (let [shapes-ids (into (d/ordered-set) + (remove #(dm/get-in objects [% :hidden])) + shapes)] + (rx/of (dws/select-shapes shapes-ids))) :svg-raw nil diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index 796fe86203..ed9790e248 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -63,20 +63,22 @@ ;; Add & select the created shape to the workspace (rx/concat - (if (= :text (:type shape)) + (if (or (= :text (:type shape)) (= :frame (:type shape))) (rx/of (dwu/start-undo-transaction (:id shape))) (rx/empty)) (rx/of (dwsh/add-shape shape {:no-select? (= tool :curve)})) (if (= :frame (:type shape)) - (->> (uw/ask! {:cmd :selection/query - :page-id page-id - :rect (:selrect shape) - :include-frames? true - :full-frame? true}) - (rx/map #(cph/clean-loops objects %)) - (rx/map #(dwsh/move-shapes-into-frame (:id shape) %))) + (rx/concat + (->> (uw/ask! {:cmd :selection/query + :page-id page-id + :rect (:selrect shape) + :include-frames? true + :full-frame? true}) + (rx/map #(cph/clean-loops objects %)) + (rx/map #(dwsh/move-shapes-into-frame (:id shape) %))) + (rx/of (dwu/commit-undo-transaction (:id shape)))) (rx/empty))))) ;; Delay so the mouse event can read the drawing state diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index c6901212a1..d27d5794f1 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -40,6 +40,7 @@ [app.util.router :as rt] [app.util.time :as dt] [beicon.core :as rx] + [cuerdas.core :as str] [potok.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default @@ -184,21 +185,22 @@ (defn rename-media [id new-name] - (dm/assert! (uuid? id)) - (dm/assert! (string? new-name)) + (dm/verify! (uuid? id)) + (dm/verify! (string? new-name)) (ptk/reify ::rename-media ptk/WatchEvent (watch [it state _] - (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))))))) - + (let [new-name (str/trim new-name)] + (if (str/empty? new-name) + (rx/empty) + (let [[path name] (cph/parse-path-name new-name) + data (get state :workspace-data) + 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 [{:keys [id] :as params}] @@ -340,24 +342,20 @@ (defn rename-component "Rename the component with the given id, in the current file library." [id new-name] - (dm/assert! (uuid? id)) - (dm/assert! (string? new-name)) + (dm/verify! (uuid? id)) + (dm/verify! (string? new-name)) (ptk/reify ::rename-component ptk/WatchEvent (watch [it state _] - (when (and (some? new-name) (not= "" new-name)) - (let [data (get state :workspace-data) - [path name] (cph/parse-path-name new-name) - components-v2 (features/active-feature? state :components-v2) + (let [new-name (str/trim new-name)] + (if (str/empty? new-name) + (rx/empty) + (let [data (get state :workspace-data) + [path name] (cph/parse-path-name new-name) + components-v2 (features/active-feature? state :components-v2) - update-fn - (fn [component] - ;; NOTE: we need to ensure the component exists, - ;; because there are small possibilities of race - ;; conditions with component deletion. - ;; - ;; FIXME: this race conditon should be handled in pcb/update-component - (when component + update-fn + (fn [component] (cond-> component :always (assoc :path path @@ -366,13 +364,13 @@ (not components-v2) (update :objects ;; Give the same name to the root shape - #(assoc-in % [id :name] name))))) + #(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 rename-component-and-main-instance [component-id name] diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index db4dfee10a..9bab204bf7 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -249,7 +249,7 @@ (lookup uuid/zero)) toselect (->> (cph/get-immediate-children objects (:id parent)) - (into (d/ordered-set) (comp (remove :blocked) (map :id))))] + (into (d/ordered-set) (comp (remove :hidden) (remove :blocked) (map :id))))] (rx/of (select-shapes toselect)))))) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 295e880a9a..0ef0f39a28 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -143,12 +143,14 @@ (cond-> (ctl/grid-layout? objects frame-id) (pcb/update-shapes [frame-id] ctl/assign-cells)))))) -(defn move-shapes-into-frame [frame-id shapes] +(defn move-shapes-into-frame + [frame-id shapes] (ptk/reify ::move-shapes-into-frame ptk/WatchEvent (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) + shapes (->> shapes (remove #(dm/get-in objects [% :blocked]))) changes (-> (pcb/empty-changes it page-id) (pcb/with-objects objects)) changes (prepare-move-shapes-into-frame changes diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 7e9d99d670..53fbbbafb1 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -196,7 +196,7 @@ (-> (update-in [:svg-attrs :style] dissoc :mix-blend-mode) (assoc :blend-mode (-> (get-in shape [:svg-attrs :style :mix-blend-mode]) assert-valid-blend-mode))))) -(defn create-raw-svg [name frame-id svg-data {:keys [tag attrs] :as data}] +(defn create-raw-svg [name frame-id svg-data {:keys [attrs] :as data}] (let [{:keys [x y width height offset-x offset-y]} svg-data] (-> {:id (uuid/next) :type :svg-raw @@ -206,7 +206,6 @@ :height height :x x :y y - :hidden (= tag :defs) :content (cond-> data (map? data) (update :attrs usvg/clean-attrs))} (assoc :svg-attrs attrs) @@ -437,6 +436,7 @@ children (cond->> (:content element-data) (contains? usvg/parent-tags tag) (mapv #(usvg/inherit-attributes attrs %)))] + [shape children])))))) (defn create-svg-children @@ -537,7 +537,8 @@ 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 + ;; 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) @@ -588,6 +589,7 @@ [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)) diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs index 3f752f7f62..397edc4d9a 100644 --- a/frontend/src/app/main/ui/auth/recovery.cljs +++ b/frontend/src/app/main/ui/auth/recovery.cljs @@ -56,7 +56,9 @@ (mf/defc recovery-form [{:keys [params] :as props}] (let [form (fm/use-form :spec ::recovery-form - :validators [password-equality] + :validators [password-equality + (fm/validate-not-empty :password-1 (tr "auth.password-not-empty")) + (fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))] :initial params)] [:& fm/form {:on-submit on-submit :form form} diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 76ce3a1ac7..01986eccbd 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -87,7 +87,8 @@ [{:keys [params on-success-callback] :as props}] (let [initial (mf/use-memo (mf/deps params) (constantly params)) form (fm/use-form :spec ::register-form - :validators [validate] + :validators [validate + (fm/validate-not-empty :password (tr "auth.password-not-empty"))] :initial initial) submitted? (mf/use-state false) @@ -219,6 +220,8 @@ (mf/defc register-validate-form [{:keys [params on-success-callback] :as props}] (let [form (fm/use-form :spec ::register-validate-form + :validators [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space")) + (fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))] :initial params) submitted? (mf/use-state false) diff --git a/frontend/src/app/main/ui/components/dropdown_menu.cljs b/frontend/src/app/main/ui/components/dropdown_menu.cljs index c397b5c26f..75e32b60b1 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 "key") + key (gobj/get props "unique-key") data-test (gobj/get props "data-test")] [:li {:id id :class klass diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index f753038175..deb8f6a46f 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -24,7 +24,7 @@ (def use-form fm/use-form) (mf/defc input - [{:keys [label help-icon disabled form hint trim children data-test] :as props}] + [{:keys [label help-icon disabled form hint trim children data-test on-change-value] :as props}] (let [input-type (get props :type "text") input-name (get props :name) more-classes (get props :class) @@ -57,6 +57,8 @@ :else help-icon) + on-change-value (or on-change-value (constantly nil)) + klass (str more-classes " " (dom/classnames :focus @focus? @@ -80,7 +82,8 @@ on-change (fn [event] (let [value (-> event dom/get-target dom/get-input-value)] (swap! form assoc-in [:touched input-name] true) - (fm/on-input-change form input-name value trim))) + (fm/on-input-change form input-name value trim) + (on-change-value name value))) on-blur (fn [_] @@ -135,7 +138,6 @@ (string? hint) [:span.hint hint])]])) - (mf/defc textarea [{:keys [label disabled form hint trim] :as props}] (let [input-name (get props :name) @@ -222,7 +224,7 @@ (for [item options] [:> :option (clj->js (cond-> {:key (:value item) :value (:value item)} (:disabled item) (assoc :disabled "disabled") - (:hidden item) (assoc :style {:display "none"}))) + (:hidden item) (assoc :style {:display "none"}))) (:label item)])] [:div.input-container {:class (dom/classnames :disabled disabled :focus @focus?)} @@ -237,7 +239,7 @@ [{:keys [name options form trim on-change-value] :as props}] (let [form (or form (mf/use-ctx form-ctx)) value (get-in @form [:data name] "") - on-change-value (or on-change-value (constantly nil)) + on-change-value (or on-change-value (constantly nil)) on-change (fn [event] (let [value (-> event dom/get-target dom/get-value)] (swap! form assoc-in [:touched name] true) @@ -409,3 +411,31 @@ "caution" (:caution item))} [:span.text (:text item)] [:span.icon {:on-click #(remove-item! item)} i/cross]]])])])) + +;; --- Validators + +(defn all-spaces? + [value] + (let [trimmed (str/trim value)] + (str/empty? trimmed))) + +(def max-length-allowed 250) +(def max-uri-length-allowed 2048) + +(defn max-length? + [value length] + (> (count value) length)) + +(defn validate-length + [field length errors-msg ] + (fn [errors data] + (cond-> errors + (max-length? (get data field) length) + (assoc field {:message errors-msg})))) + +(defn validate-not-empty + [field error-msg] + (fn [errors data] + (cond-> errors + (all-spaces? (get data field)) + (assoc field {:message error-msg})))) diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index cea4baf067..23726875ea 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -142,7 +142,11 @@ #(st/emit! (dd/set-file-shared (assoc file :is-shared true))) del-shared - #(st/emit! (dd/set-file-shared (assoc file :is-shared false))) + (mf/use-fn + (mf/deps files) + (fn [_] + (run! #(st/emit! (dd/set-file-shared (assoc % :is-shared false))) files))) + on-add-shared (fn [event] @@ -216,23 +220,23 @@ (when current-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))})})}])) + {: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? [{:option-name (tr "dashboard.duplicate-multi" file-count) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 05d852b304..10e3d2646a 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -495,17 +495,20 @@ (mf/use-fn (mf/deps selected-project) (fn [e] - (when (dnd/has-type? e "penpot/files") - (dom/prevent-default e) - (when-not (or (dnd/from-child? e) + (cond + (dnd/has-type? e "penpot/files") + (do + (dom/prevent-default e) + (when-not (or (dnd/from-child? e) (dnd/broken-event? e)) - (when (not= selected-project project-id) - (reset! dragging? true)))) + (when (not= selected-project project-id) + (reset! dragging? true)))) - (when (or (dnd/has-type? e "Files") - (dnd/has-type? e "application/x-moz-file")) - (dom/prevent-default e) - (reset! dragging? true)))) + (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file")) + (do + (dom/prevent-default e) + (reset! dragging? true))))) on-drag-over (mf/use-fn @@ -531,19 +534,22 @@ (mf/use-fn (mf/deps files selected-files) (fn [e] - (when (or (dnd/has-type? e "Files") - (dnd/has-type? e "application/x-moz-file")) - (dom/prevent-default e) - (reset! dragging? false) - (import-files (.-files (.-dataTransfer e)))) + (cond + (dnd/has-type? e "penpot/files") + (do + (reset! dragging? false) + (when (not= selected-project project-id) + (let [data {:ids (into #{} (keys selected-files)) + :project-id project-id} + mdata {:on-success on-drop-success}] + (st/emit! (dd/move-files (with-meta data mdata)))))) - (when (dnd/has-type? e "penpot/files") - (reset! dragging? false) - (when (not= selected-project project-id) - (let [data {:ids (into #{} (keys selected-files)) - :project-id project-id} - mdata {:on-success on-drop-success}] - (st/emit! (dd/move-files (with-meta data mdata))))))))] + (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file")) + (do + (dom/prevent-default e) + (reset! dragging? false) + (import-files (.-files (.-dataTransfer e)))))))] [:div.dashboard-grid {:on-drag-enter on-drag-enter :on-drag-over on-drag-over diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index a494b1b36f..571b97fca8 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -243,6 +243,7 @@ (when (kbd/enter? event) (team-selected (:default-team-id profile) event))) :id "teams-selector-default-team" + :unique-key "default-team" :klass "team-name"} [:span.team-icon i/logo-icon] [:span.team-text (tr "dashboard.your-penpot")] @@ -256,7 +257,7 @@ (team-selected (:id team-item) event))) :id (str "teams-selector-" (:id team-item)) :klass "team-name" - :key (dm/str (:id team-item))} + :unique-key (dm/str (:id team-item))} [:span.team-icon [:img {:src (cf/resolve-team-photo-url team-item) :alt (:name team-item)}]] @@ -270,7 +271,7 @@ (on-create-clicked event))) :id "teams-selector-create-team" :klass "team-name action" - :key "teams-selector-create-team"} + :unique-key "teams-selector-create-team"} [:span.team-icon.new-team i/close] [:span.team-text (tr "dashboard.create-new-team")]]])) @@ -364,16 +365,16 @@ (when (kbd/enter? event) (go-members))) :id "teams-options-members" - :key "teams-options-members" - :data-test "team-members"} + :unique-key "teams-options-members" + :data-test "team-members"} (tr "labels.members")] [:& dropdown-menu-item {:on-click go-invitations :on-key-down (fn [event] (when (kbd/enter? event) (go-invitations))) :id "teams-options-invitations" - :key "teams-options-invitations" - :data-test "team-invitations"} + :unique-key "teams-options-invitations" + :data-test "team-invitations"} (tr "labels.invitations")] (when (contains? cf/flags :webhooks) @@ -382,7 +383,7 @@ (when (kbd/enter? event) (go-webhooks))) :id "teams-options-webhooks" - :key "teams-options-webhooks"} + :unique-key "teams-options-webhooks"} (tr "labels.webhooks")]) [:& dropdown-menu-item {:on-click go-settings @@ -390,8 +391,8 @@ (when (kbd/enter? event) (go-settings))) :id "teams-options-settings" - :key "teams-options-settings" - :data-test "team-settings"} + :unique-key "teams-options-settings" + :data-test "team-settings"} (tr "labels.settings")] [:hr] @@ -401,8 +402,8 @@ (when (kbd/enter? event) (on-rename-clicked))) :id "teams-options-rename" - :key "teams-options-rename" - :data-test "rename-team"} + :unique-key "teams-options-rename" + :data-test "rename-team"} (tr "labels.rename")]) (cond @@ -412,7 +413,7 @@ (when (kbd/enter? event) (leave-and-close))) :id "teams-options-leave-team" - :key "teams-options-leave-team"} + :unique-key "teams-options-leave-team"} (tr "dashboard.leave-team")] @@ -422,8 +423,8 @@ (when (kbd/enter? event) (on-leave-as-owner-clicked))) :id "teams-options-leave-team" - :key "teams-options-leave-team" - :data-test "leave-team"} + :unique-key "teams-options-leave-team" + :data-test "leave-team"} (tr "dashboard.leave-team")] (> (count members) 1) @@ -432,7 +433,7 @@ (when (kbd/enter? event) (on-leave-clicked))) :id "teams-options-leave-team" - :key "teams-options-leave-team"} + :unique-key "teams-options-leave-team"} (tr "dashboard.leave-team")]) @@ -442,9 +443,9 @@ (when (kbd/enter? event) (on-delete-clicked))) :id "teams-options-delete-team" - :key "teams-options-delete-team" + :unique-key "teams-options-delete-team" :klass "warning" - :data-test "delete-team"} + :data-test "delete-team"} (tr "dashboard.delete-team")])])) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index dcd4ef8675..f2a6625c4d 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -658,7 +658,8 @@ (let [initial (mf/use-memo (fn [] (or (some-> webhook (update :uri str)) {:is-active false :mtype "application/json"}))) form (fm/use-form :spec ::webhook-form - :initial initial) + :initial initial + :validators [(fm/validate-length :uri fm/max-uri-length-allowed (tr "team.webhooks.max-length"))]) on-success (mf/use-fn (fn [_] @@ -750,8 +751,7 @@ [:& fm/input {:type "checkbox" :form form :name :is-active - :label (tr "dashboard.webhooks.active")}] - ] + :label (tr "dashboard.webhooks.active")}]] [:div.explain (tr "dashboard.webhooks.active.explain")]]] [:div.modal-footer diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs index 673343fcf9..e09501b60b 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -66,10 +66,12 @@ (on-create-submit form)))) (mf/defc team-form-modal {::mf/register modal/components - ::mf/register-as :team-form} + ::mf/register-as :team-form} [{:keys [team] :as props}] (let [initial (mf/use-memo (fn [] (or team {}))) form (fm/use-form :spec ::team-form + :validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space")) + (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))] :initial initial)] [:div.modal-overlay [:div.modal-container.team-form-modal diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index f34fab29b8..2dd7ee3de8 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -54,7 +54,9 @@ ::mf/register-as :onboarding-team} [] (let [form (fm/use-form :spec ::team-form - :initial {}) + :initial {} + :validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space")) + (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))]) on-submit (mf/use-callback (fn [form _] diff --git a/frontend/src/app/main/ui/settings/access_tokens.cljs b/frontend/src/app/main/ui/settings/access_tokens.cljs index 66606b7531..2444324edf 100644 --- a/frontend/src/app/main/ui/settings/access_tokens.cljs +++ b/frontend/src/app/main/ui/settings/access_tokens.cljs @@ -42,7 +42,6 @@ (str/blank? name) (assoc :name {:message (tr "dashboard.access-tokens.errors-required-name")})))) - (def initial-data {:name "" :expiration-date "never"}) @@ -53,7 +52,9 @@ (let [form (fm/use-form :initial initial-data :spec ::access-token-form - :validators [name-validator]) + :validators [name-validator + (fm/validate-not-empty :name (tr "auth.name.not-all-space")) + (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))]) created (mf/deref token-created-ref) created? (mf/use-state false) locale (mf/deref i18n/locale) diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs index 3bca85224f..26521ed784 100644 --- a/frontend/src/app/main/ui/settings/change_email.cljs +++ b/frontend/src/app/main/ui/settings/change_email.cljs @@ -6,6 +6,8 @@ (ns app.main.ui.settings.change-email (:require + [app.common.data :as d] + [app.common.data.macros :as dma] [app.common.spec :as us] [app.main.data.messages :as dm] [app.main.data.modal :as modal] @@ -29,7 +31,8 @@ email-2 (:email-2 data)] (cond-> errors (and email-1 email-2 (not= email-1 email-2)) - (assoc :email-2 {:message (tr "errors.email-invalid-confirmation")})))) + (assoc :email-2 {:message (tr "errors.email-invalid-confirmation") + :code :different-emails})))) (s/def ::email-change-form (s/keys :req-un [::email-1 ::email-2])) @@ -81,7 +84,17 @@ on-submit (mf/use-callback (mf/deps profile) - (partial on-submit profile))] + (partial on-submit profile)) + + on-email-change + (mf/use-callback + (fn [_ _] + (let [different-emails-error? (= (dma/get-in @form [:errors :email-2 :code]) :different-emails) + email-1 (dma/get-in @form [:clean-data :email-1]) + email-2 (dma/get-in @form [:clean-data :email-2])] + (println "different-emails-error?" (and different-emails-error? (= email-1 email-2))) + (when (and different-emails-error? (= email-1 email-2)) + (swap! form d/dissoc-in [:errors :email-2])))))] [:div.modal-overlay [:div.modal-container.change-email-modal.form-container @@ -105,12 +118,14 @@ [:& fm/input {:type "email" :name :email-1 :label (tr "modals.change-email.new-email") - :trim true}]] + :trim true + :on-change-value on-email-change}]] [:div.fields-row [:& fm/input {:type "email" :name :email-2 :label (tr "modals.change-email.confirm-email") - :trim true}]]]] + :trim true + :on-change-value on-email-change}]]]] [:div.modal-footer [:div.action-buttons {:data-test "change-email-submit"} diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index fd3d9eedbc..d510ef7c5b 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -28,8 +28,9 @@ (mf/defc feedback-form [] (let [profile (mf/deref refs/profile) - form (fm/use-form :spec ::feedback-form) - + form (fm/use-form :spec ::feedback-form + :validators [(fm/validate-length :subject fm/max-length-allowed (tr "auth.name.too-long")) + (fm/validate-not-empty :subject (tr "auth.name.not-all-space"))]) loading (mf/use-state false) on-succes diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs index 9e792d5458..48f15b00ba 100644 --- a/frontend/src/app/main/ui/settings/password.cljs +++ b/frontend/src/app/main/ui/settings/password.cljs @@ -24,7 +24,7 @@ {:message (tr "errors.wrong-old-password")}) :email-as-password (swap! form assoc-in [:errors :password-1] - {:message (tr "errors.email-as-password")}) + {:message (tr "errors.email-as-password")}) (let [msg (tr "generic.error")] (st/emit! (dm/error msg))))) @@ -71,8 +71,10 @@ [{:keys [locale] :as props}] (let [initial (mf/use-memo (constantly {:password-old nil})) form (fm/use-form :spec ::password-form - :validators [password-equality] - :initial initial)] + :validators [(fm/validate-not-empty :password-1 (tr "auth.password-not-empty")) + (fm/validate-not-empty :password-2 (tr "auth.password-not-empty")) + password-equality] + :initial initial)] [:& fm/form {:class "password-form" :on-submit on-submit :form form} @@ -105,7 +107,7 @@ (mf/defc password-page [{:keys [locale]}] (mf/use-effect - #(dom/set-html-title (tr "title.settings.password"))) + #(dom/set-html-title (tr "title.settings.password"))) [:section.dashboard-settings.form-container [:div.form-container diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index 9a13dbaedd..cbb302b709 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -42,7 +42,10 @@ (mf/defc profile-form [] (let [profile (mf/deref refs/profile) - form (fm/use-form :spec ::profile-form :initial profile)] + form (fm/use-form :spec ::profile-form + :initial profile + :validators [(fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long")) + (fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))])] [:& fm/form {:on-submit on-submit :form form diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index a9823e0259..6df9120f19 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -215,7 +215,7 @@ (mf/defc viewer [{:keys [params data]}] - (let [{:keys [page-id section index interactions-mode]} params + (let [{:keys [page-id share-id section index interactions-mode]} params {:keys [file users project permissions]} data allowed (or @@ -519,7 +519,8 @@ :size size :index index :viewer-pagination viewer-pagination - :interactions-mode interactions-mode}] + :interactions-mode interactions-mode + :share-id share-id}] [:& (mf/provider ctx/current-zoom) {:value zoom} diff --git a/frontend/src/app/main/ui/viewer/inspect.cljs b/frontend/src/app/main/ui/viewer/inspect.cljs index f1e901ad00..4faa901099 100644 --- a/frontend/src/app/main/ui/viewer/inspect.cljs +++ b/frontend/src/app/main/ui/viewer/inspect.cljs @@ -35,7 +35,7 @@ (dom/add-class! layout "force-visible")))))) (mf/defc viewport - [{:keys [local file page frame index viewer-pagination size]}] + [{:keys [local file page frame index viewer-pagination size share-id]}] (let [inspect-svg-container-ref (mf/use-ref nil) on-mouse-wheel (fn [event] @@ -76,4 +76,5 @@ [:& right-sidebar {:frame frame :selected (:selected local) :page page - :file file}]])) + :file file + :share-id share-id}]])) diff --git a/frontend/src/app/main/ui/viewer/inspect/attributes.cljs b/frontend/src/app/main/ui/viewer/inspect/attributes.cljs index 997b83a6c0..19fc153112 100644 --- a/frontend/src/app/main/ui/viewer/inspect/attributes.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/attributes.cljs @@ -34,7 +34,7 @@ :text [:layout :text :shadow :blur :stroke :layout-flex-item]}) (mf/defc attributes - [{:keys [page-id file-id shapes frame from libraries]}] + [{:keys [page-id file-id shapes frame from libraries share-id]}] (let [shapes (hooks/use-equal-memo shapes) shapes (mf/with-memo [shapes] (mapv #(gsh/translate-to-frame % frame) shapes)) @@ -66,4 +66,5 @@ {:shapes shapes :type type :page-id page-id - :file-id file-id}]])) + :file-id file-id + :share-id share-id}]])) diff --git a/frontend/src/app/main/ui/viewer/inspect/exports.cljs b/frontend/src/app/main/ui/viewer/inspect/exports.cljs index 55f3a83972..fc8422f816 100644 --- a/frontend/src/app/main/ui/viewer/inspect/exports.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/exports.cljs @@ -17,7 +17,7 @@ (mf/defc exports {::mf/wrap [#(mf/memo % =)]} - [{:keys [shapes page-id file-id type] :as props}] + [{:keys [shapes page-id file-id share-id type] :as props}] (let [exports (mf/use-state []) xstate (mf/deref refs/export) vstate (mf/deref refs/viewer-data) @@ -40,15 +40,17 @@ :exports @exports :filename filename :page-id page-id - :file-id file-id})) + :file-id file-id + :share-id share-id})) ;; In other all cases we only allowed to have a single ;; shape-id because multiple shape-ids are handled ;; separately by the export-modal. - (let [defaults {:page-id page-id - :file-id file-id - :name filename - :object-id (-> shapes first :id)} + (let [defaults (-> {:page-id page-id + :file-id file-id + :name filename + :object-id (-> shapes first :id)} + (cond-> share-id (assoc :share-id share-id))) exports (mapv #(merge % defaults) @exports)] (if (= 1 (count exports)) (st/emit! (de/request-simple-export {:export (first exports)})) diff --git a/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs index c581232842..e3d03bc3f4 100644 --- a/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs +++ b/frontend/src/app/main/ui/viewer/inspect/right_sidebar.cljs @@ -38,7 +38,7 @@ :data local}))))) (mf/defc right-sidebar - [{:keys [frame page file selected shapes page-id file-id from] + [{:keys [frame page file selected shapes page-id file-id share-id from] :or {from :inspect}}] (let [expanded (mf/use-state false) section (mf/use-state :info #_:code) @@ -89,7 +89,8 @@ :frame frame :shapes shapes :from from - :libraries libraries}]] + :libraries libraries + :share-id share-id}]] [:& tabs-element {:id :code :title (tr "inspect.tabs.code")} [:& code {:frame frame diff --git a/frontend/src/app/main/ui/viewer/share_link.cljs b/frontend/src/app/main/ui/viewer/share_link.cljs index 8b0e69685d..3d45606c44 100644 --- a/frontend/src/app/main/ui/viewer/share_link.cljs +++ b/frontend/src/app/main/ui/viewer/share_link.cljs @@ -28,31 +28,36 @@ (defn prepare-params [{:keys [pages who-comment who-inspect]}] - {:pages pages - :who-comment who-comment - :who-inspect who-inspect}) + {:pages pages + :who-comment who-comment + :who-inspect who-inspect}) (mf/defc share-link-dialog {::mf/register modal/components ::mf/register-as :share-link} [{:keys [file page]}] (let [current-page page - slinks (mf/deref refs/share-links) - router (mf/deref refs/router) - route (mf/deref refs/route) - zoom-type (mf/deref refs/viewer-zoom-type) + slinks (mf/deref refs/share-links) + router (mf/deref refs/router) + route (mf/deref refs/route) + zoom-type (mf/deref refs/viewer-zoom-type) - link (mf/use-state nil) - confirm (mf/use-state false) - open-ops (mf/use-state false) + link (mf/use-state nil) + confirm (mf/use-state false) + open-ops (mf/use-state false) - opts (mf/use-state - {:pages-mode "current" - :all-pages false - :pages #{(:id page)} - :who-comment "team" - :who-inspect "team"}) + opts* (mf/use-state + {:pages-mode "current" + :all-pages false + :pages #{(:id page)} + :who-comment "team" + :who-inspect "team"}) + opts (deref opts*) + + selected-pages (:pages opts) + file-pages (->> (get-in file [:data :pages]) + (map #(get-in file [:data :pages-index %]))) close (fn [event] @@ -63,7 +68,7 @@ toggle-all (fn [] (reset! confirm false) - (swap! opts + (swap! opts* (fn [state] (if (= true (:all-pages state)) (-> state @@ -74,23 +79,29 @@ (assoc :pages (into #{} (get-in file [:data :pages])))))))) mark-checked-page - (fn [event id] - (let [target (dom/get-target event) - checked? (.-checked ^js target) - dif-pages? (not= id (first (:pages @opts))) - no-one-page (< 1 (count (:pages @opts))) + (mf/use-fn + (mf/deps selected-pages) + (fn [event id] + (let [target (dom/get-target event) + not-checked? (.-checked ^js target) + dif-pages? (not= id (first selected-pages)) + no-one-page (< 1 (count selected-pages)) should-change (or no-one-page dif-pages?)] (when should-change (reset! confirm false) - (swap! opts update :pages - (fn [pages] - (if checked? - (conj pages id) - (disj pages id))))))) + (swap! opts* + (fn [state] + (let [actual-pages (:pages state) + updated-pages (if not-checked? + (conj actual-pages id) + (disj actual-pages id))] + (-> state + (assoc :pages updated-pages) + (assoc :all-pages (= (count updated-pages) (count file-pages))))))))))) create-link (fn [_] - (let [params (prepare-params @opts) + (let [params (prepare-params opts) params (assoc params :file-id (:id file))] (st/emit! (dc/create-share-link params) (ptk/event ::ev/event {::ev/name "create-shared-link" @@ -111,7 +122,7 @@ delete-link (fn [_] - (let [params (prepare-params @opts) + (let [params (prepare-params opts) slink (d/seek #(= (:flags %) (:flags params)) slinks)] (reset! confirm false) (st/emit! (dc/delete-share-link slink)))) @@ -127,13 +138,13 @@ value (keyword value)] (reset! confirm false) (if (= type :comment) - (swap! opts assoc :who-comment (d/name value)) - (swap! opts assoc :who-inspect (d/name value)))))] + (swap! opts* assoc :who-comment (d/name value)) + (swap! opts* assoc :who-inspect (d/name value)))))] (mf/use-effect - (mf/deps file slinks @opts) + (mf/deps file slinks opts) (fn [] - (let [{:keys [pages who-comment who-inspect] :as params} (prepare-params @opts) + (let [{:keys [pages who-comment who-inspect] :as params} (prepare-params opts) slink (d/seek #(and (= (:who-inspect %) who-inspect) (= (:who-comment %) who-comment) (= (:pages %) pages)) slinks) href (when slink (let [pparams (:path-params route) @@ -204,10 +215,10 @@ [:div.title (tr "common.share-link.manage-ops")]] (when @open-ops [:* - (let [all-selected? (:all-pages @opts) + (let [all-selected? (:all-pages opts) pages (->> (get-in file [:data :pages]) (map #(get-in file [:data :pages-index %]))) - selected (:pages @opts)] + selected selected-pages] [:* [:div.view-mode @@ -253,7 +264,7 @@ (tr "common.share-link.permissions-can-comment")] [:div.items [:select.input-select {:on-change (partial on-who-change :comment) - :value (:who-comment @opts)} + :value (:who-comment opts)} [:option {:value "team"} (tr "common.share-link.team-members")] [:option {:value "all"} (tr "common.share-link.all-users")]]]] [:div.inspect-mode @@ -262,7 +273,7 @@ (tr "common.share-link.permissions-can-inspect")] [:div.items [:select.input-select {:on-change (partial on-who-change :inspect) - :value (:who-inspect @opts)} + :value (:who-inspect opts)} [:option {:value "team"} (tr "common.share-link.team-members")] [:option {:value "all"} (tr "common.share-link.all-users")]]]]])]]])) 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 ae28789f61..64805ef19e 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -133,6 +133,8 @@ (fn [event] (dom/stop-propagation event) (dom/prevent-default event) + (st/emit! ::dwt/finalize-editor-state) + (st/emit! (dwt/initialize-editor-state shape default-decorator)) (reset! blurred true))) on-focus diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs index 216ece7849..677b459dbe 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs @@ -194,7 +194,7 @@ (let [input (mf/ref-val input-ref)] (dom/select-text! input) nil))) - + (if ^boolean new-css-system [:div {:class (dom/classnames (css :asset-list-item) true (css :selected) (contains? selected (:id color)) @@ -503,6 +503,7 @@ (st/emit! (dw/set-assets-section-open file-id :colors true) (ptk/event ::ev/event {::ev/name "add-asset-to-library" :asset-type "color"})) + ;; FIXME: replace interop with dom helpers (modal/show! :colorpicker {:x (.-clientX event) :y (.-clientY event) @@ -592,7 +593,7 @@ [:& cmm/asset-section-block {:role :title-button} (when-not read-only? [:button {:class (dom/classnames (css :assets-btn) true) - :on-click add-color-clicked} + :on-click add-color-clicked} i/add-refactor])]) (when local? @@ -618,4 +619,4 @@ :on-rename-group on-rename-group :on-ungroup on-ungroup :colors colors - :selected-full selected-full}]]])) \ No newline at end of file + :selected-full selected-full}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.cljs index a1ba98b2c5..cadc9d70ab 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/graphics.cljs @@ -390,7 +390,7 @@ (mf/use-fn (fn [] (swap! state (fn [state] - (assoc state :renaming (:component-id state)))))) + (assoc state :renaming (:object-id state)))))) cancel-rename (mf/use-fn (fn [] diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs index 6f41143ac5..e332041808 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs @@ -123,6 +123,8 @@ (mf/deps last-path) (constantly {:asset-name last-path})) form (fm/use-form :spec ::name-group-form + :validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space")) + (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))] :initial initial) create? (empty? path) 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 a3a3883936..1e987b4884 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 @@ -93,13 +93,13 @@ :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) + (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? @@ -275,8 +275,8 @@ {:placeholder "--" :on-change (partial on-change :simple :p1) :on-focus #(do - (dom/select-target %) - (select-paddings true false true false)) + (dom/select-target %) + (select-paddings true false true false)) :value p1}]] [:div.padding-item.tooltip.tooltip-bottom-left @@ -354,7 +354,7 @@ i/auto-gap] [:> numeric-input {:no-validate true :placeholder "--" - :on-focus (fn [event] + :on-focus (fn [event] (select-gap :row-gap) (reset! gap-selected? :row-gap) (dom/select-target event)) @@ -638,12 +638,12 @@ (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 :flex)} i/close])]] 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 af92826ee1..e4885697ad 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 @@ -192,6 +192,8 @@ (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})))) + + is-absolute? (:layout-item-absolute values) is-col? (every? ctl/col? selection-parents) @@ -219,6 +221,8 @@ on-change-position (fn [value] + (when (= value :static) + (st/emit! (dwsl/update-layout-child ids {:layout-item-z-index nil}))) (st/emit! (dwsl/update-layout-child ids {:layout-item-absolute (= value :absolute)}))) on-change-z-index @@ -254,6 +258,8 @@ {:placeholder "--" :on-focus #(dom/select-target %) :on-change #(on-change-z-index %) + :nillable true + :disabled (not is-absolute?) :value (:layout-item-z-index values)}]]]]) (when (not (:layout-item-absolute values)) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index e4787f4ec5..0a12452391 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -203,6 +203,7 @@ ids (into (d/ordered-set) + (remove #(dm/get-in objects [% :blocked])) (ctt/sort-z-index objects ids {:bottom-frames? mod?})) grouped? (fn [id] (contains? #{:group :bool} (get-in objects [id :type]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 74dd73d2fe..7755d080ae 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -149,7 +149,9 @@ text-pos-x (if (:use-for-thumbnail? frame) 15 0)] (when (not (:hidden frame)) - [:g.frame-title {:id (dm/str "frame-title-" (:id frame)) :transform (vwu/title-transform frame zoom)} + [:g.frame-title {:id (dm/str "frame-title-" (:id frame)) + :transform (vwu/title-transform frame zoom) + :pointer-events (when (:blocked frame) "none")} (when (:use-for-thumbnail? frame) [:svg {:x 0 :y -9 diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index 9f3db68afe..20f6e0906d 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -95,16 +95,17 @@ state)) (mf/defc object-svg - [{:keys [page-id file-id object-id render-embed?]}] + [{:keys [page-id file-id share-id object-id render-embed?]}] (let [components-v2 (feat/use-feature :components-v2) fetch-state (mf/use-fn - (mf/deps file-id page-id object-id components-v2) + (mf/deps file-id page-id share-id object-id components-v2) (fn [] (let [features (cond-> #{} components-v2 (conj "components/v2"))] (->> (rx/zip - (repo/cmd! :get-font-variants {:file-id file-id}) + (repo/cmd! :get-font-variants {:file-id file-id :share-id share-id}) (repo/cmd! :get-page {:file-id file-id :page-id page-id + :share-id share-id :object-id object-id :features features})) (rx/tap (fn [[fonts]] @@ -135,16 +136,17 @@ :render-embed? render-embed?}]))) (mf/defc objects-svg - [{:keys [page-id file-id object-ids render-embed?]}] + [{:keys [page-id file-id share-id object-ids render-embed?]}] (let [components-v2 (feat/use-feature :components-v2) fetch-state (mf/use-fn - (mf/deps file-id page-id components-v2) + (mf/deps file-id page-id share-id components-v2) (fn [] (let [features (cond-> #{} components-v2 (conj "components/v2"))] (->> (rx/zip - (repo/cmd! :get-font-variants {:file-id file-id}) + (repo/cmd! :get-font-variants {:file-id file-id :share-id share-id}) (repo/cmd! :get-page {:file-id file-id :page-id page-id + :share-id share-id :features features})) (rx/tap (fn [[fonts]] (when (seq fonts) @@ -164,6 +166,7 @@ (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) +(s/def ::share-id ::us/uuid) (s/def ::object-id (s/or :single ::us/uuid :multiple (s/coll-of ::us/uuid))) @@ -171,24 +174,25 @@ (s/def ::render-objects (s/keys :req-un [::file-id ::page-id ::object-id] - :opt-un [::render-embed])) + :opt-un [::render-embed ::share-id])) (defn- render-objects [params] (let [{:keys [file-id page-id - render-embed] + render-embed + share-id] :as params} (us/conform ::render-objects params) [type object-id] (:object-id params)] - (case type :single (mf/html [:& object-svg {:file-id file-id :page-id page-id + :share-id share-id :object-id object-id :render-embed? render-embed}]) @@ -197,6 +201,7 @@ [:& objects-svg {:file-id file-id :page-id page-id + :share-id share-id :object-ids (into #{} object-id) :render-embed? render-embed}])))) diff --git a/frontend/src/app/util/path/format.cljs b/frontend/src/app/util/path/format.cljs index f7628bc33a..c0d38829db 100644 --- a/frontend/src/app/util/path/format.cljs +++ b/frontend/src/app/util/path/format.cljs @@ -72,7 +72,7 @@ :curve-to (let [{:keys [c1x c1y c2x c2y]} params] - (join-params c1x c1y c2x c2y x y)) + (join-params (or c1x x) (or c1y y) (or c2x x) (or c2y y) x y)) (:smooth-curve-to :quadratic-bezier-curve-to) (let [{:keys [cx cy]} params] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e8dd59406d..bfefc32b13 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -53,6 +53,14 @@ msgstr "Full Name" msgid "auth.login-here" msgstr "Login here" +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "The name must contain at most 250 characters." + +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "The name must contain some character other than space." + #: src/app/main/ui/auth/login.cljs msgid "auth.login-submit" msgstr "Login" @@ -113,6 +121,10 @@ msgstr "Password" msgid "auth.password-length-hint" msgstr "At least 8 characters" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "Password must contain some character other than space." + msgid "auth.privacy-policy" msgstr "Privacy policy" @@ -804,6 +816,10 @@ msgstr "No webhooks created so far." msgid "dashboard.webhooks.update.success" msgstr "Webhook updated successfully." +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "The webhook name must contain at most 2048 characters." + #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" msgstr "Your account" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 42f40e5fcc..dcdd6727b9 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -56,6 +56,14 @@ msgstr "Nombre completo" msgid "auth.login-here" msgstr "Inicia sesión aquí" +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.too-long" +msgstr "El nombre debe contener como máximo 250 caracteres." + +#: src/app/main/ui/settings/team-form.cljs, src/app/main/ui/auth/register.cljs, src/app/main/ui/dashboard/team_form.cljs, src/app/main/ui/onboarding/team_choice.cljs, src/app/main/ui/settings/access_tokens.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/settings/profile.cljs, src/app/main/ui/workspace/sidebar/assets.cljs +msgid "auth.name.not-all-space" +msgstr "El nombre debe contener algún carácter diferente de espacio" + #: src/app/main/ui/auth/login.cljs msgid "auth.login-submit" msgstr "Entrar" @@ -118,6 +126,10 @@ msgstr "Contraseña" msgid "auth.password-length-hint" msgstr "8 caracteres como mínimo" +#: src/app/main/ui/auth/register.cljs +msgid "auth.password-not-empty" +msgstr "La contraseña debe contener algún caracter diferente de espacio" + msgid "auth.privacy-policy" msgstr "Política de privacidad" @@ -820,6 +832,10 @@ msgstr "No hay ningún webhook aún." msgid "dashboard.webhooks.update.success" msgstr "Webhook modificado con éxito." +#: src/app/main/ui/dashboard/team.cljs +msgid "team.webhooks.max-length" +msgstr "El nombre del webhook debe contener como máximo 2048 caracteres." + #: src/app/main/ui/settings.cljs msgid "dashboard.your-account-title" msgstr "Tu cuenta"