0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-14 07:51:35 -05:00

Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2023-07-06 13:52:23 +02:00
commit 4561a87450
70 changed files with 655 additions and 416 deletions

View file

@ -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

View file

@ -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"]

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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]

View file

@ -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))

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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))))
)))

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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"

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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)

View file

@ -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

View file

@ -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"}

View file

@ -246,7 +246,7 @@
pointer-events: auto;
.thread-groups {
height: 100%;
height: calc(100% - 34px);
overflow-y: scroll;
hr {
border: 0;

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -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 {

View file

@ -182,8 +182,8 @@
right: 0;
top: 50px;
width: 256px;
height: 100%;
z-index: 10;
height: calc(100vh - 48px);
z-index: 9;
}
.empty-state {

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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))))))

View file

@ -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

View file

@ -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))

View file

@ -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}

View file

@ -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)

View file

@ -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

View file

@ -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}))))

View file

@ -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)

View file

@ -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

View file

@ -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")])]))

View file

@ -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

View file

@ -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

View file

@ -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 _]

View file

@ -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)

View file

@ -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"}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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}]]))

View file

@ -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}]]))

View file

@ -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)}))

View file

@ -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

View file

@ -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")]]]]])]]]))

View file

@ -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

View file

@ -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}]]]))
:selected-full selected-full}]]]))

View file

@ -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 []

View file

@ -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)

View file

@ -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])]]

View file

@ -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))

View file

@ -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])))

View file

@ -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

View file

@ -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}]))))

View file

@ -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]

View file

@ -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"

View file

@ -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"