diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn
index d1e91f24bb..4ba5b0442a 100644
--- a/.clj-kondo/config.edn
+++ b/.clj-kondo/config.edn
@@ -3,7 +3,8 @@
rumext.alpha/defc clojure.core/defn
rumext.alpha/fnc clojure.core/fn
app.common.data/export clojure.core/def
- app.db/with-atomic clojure.core/with-open}
+ app.db/with-atomic clojure.core/with-open
+ app.common.logging/with-context clojure.core/do}
:hooks
{:analyze-call
diff --git a/.clj-kondo/hooks/export.clj b/.clj-kondo/hooks/export.clj
index f66d027839..16ab4e76af 100644
--- a/.clj-kondo/hooks/export.clj
+++ b/.clj-kondo/hooks/export.clj
@@ -74,5 +74,3 @@
;; (prn "==============" rtype (into {} ?meta))
;; (prn (api/sexpr result))
{:node result}))
-
-
diff --git a/CHANGES.md b/CHANGES.md
index 6d25633970..6759e02da5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -8,6 +8,30 @@
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
+
+# 1.10.3-beta
+
+### :sparkles: Enhacements
+
+- Make all logging asynchronous, this avoid some overhead on jetty threads at cost of logging latency.
+- Increase default session time to 15 days.
+
+### :bug: Bugs fixed
+
+- Fix unexpected exception on saving pages with default grids [#2409](https://tree.taiga.io/project/penpot/issue/2409)
+- Fix react warnings on setting size 1 on row and column grids.
+- Fix minor issues on ZMQ logging listener (used in error reporting service).
+- Remove "ALPHA" from the code.
+- Fix value and nil handling on numeric-input component. This fixes many issues related to typography, components, etc. renaming.
+- Fix NPE on email complains processing.
+- Fix white page after leaving a team.
+- Fix missing leave team button outside members page.
+
+### :arrow_up: Deps updates
+
+- Update log4j2 dependency.
+
+
# 1.10.2-beta
### :bug: Bugs fixed
diff --git a/backend/deps.edn b/backend/deps.edn
index b7f3295baa..cebf952830 100644
--- a/backend/deps.edn
+++ b/backend/deps.edn
@@ -1,12 +1,6 @@
-{
- ;; :mvn/repos
- ;; {"central" {:url "https://repo1.maven.org/maven2/"}
- ;; "clojars" {:url "https://clojars.org/repo"}
- ;; "jcenter" {:url "https://jcenter.bintray.com/"}
- ;; }
- :deps
- {penpot/common
- {:local/root "../common"}
+{:deps
+ {penpot/common {:local/root "../common"}
+ org.clojure/core.async {:mvn/version "1.5.648"}
;; Logging
org.zeromq/jeromq {:mvn/version "0.5.2"}
@@ -32,7 +26,6 @@
metosin/reitit-ring {:mvn/version "0.5.15"}
org.postgresql/postgresql {:mvn/version "42.2.23"}
com.zaxxer/HikariCP {:mvn/version "5.0.0"}
-
funcool/datoteka {:mvn/version "2.0.0"}
buddy/buddy-core {:mvn/version "1.10.1"}
@@ -49,9 +42,7 @@
io.sentry/sentry {:mvn/version "5.1.2"}
;; Pretty Print specs
- fipp/fipp {:mvn/version "0.6.24"}
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
-
software.amazon.awssdk/s3 {:mvn/version "2.17.40"}}
:paths ["src" "resources"]
diff --git a/backend/dev/user.clj b/backend/dev/user.clj
index 4f47934a8b..d65cd01cd9 100644
--- a/backend/dev/user.clj
+++ b/backend/dev/user.clj
@@ -95,3 +95,10 @@
[{:v1 (alength (blob/encode data {:version 1}))
:v2 (alength (blob/encode data {:version 2}))
:v3 (alength (blob/encode data {:version 3}))}]))
+
+
+(defonce debug-tap
+ (do
+ (add-tap #(locking debug-tap
+ (prn "tap debug:" %)))
+ 1))
diff --git a/backend/resources/error-report.tmpl b/backend/resources/error-report.tmpl
index 452036997e..2ff0abb1f2 100644
--- a/backend/resources/error-report.tmpl
+++ b/backend/resources/error-report.tmpl
@@ -130,10 +130,10 @@
{% endif %}
- {% if error %}
+ {% if hint %}
HINT:
-
{{error.message}}
+
{{hint}}
{% endif %}
@@ -144,15 +144,9 @@
{% endif %}
- {% if explain %}
-
- {% endif %}
- {% if data %}
-
- {% endif %}
- {% if error %}
-
- {% endif %}
+
+
+
{% if params %}
@@ -163,25 +157,39 @@
{% endif %}
- {% if explain %}
-
- {% endif %}
-
{% if data %}
{% endif %}
- {% if error %}
+ {% if spec-problems %}
+
+ {% endif %}
+
+ {% if cause %}
+
+ {% elif trace %}
+
+ {% elif error %}
TRACE:
diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml
index 1b9dba567b..d07a33d7dd 100644
--- a/backend/resources/log4j2-devenv.xml
+++ b/backend/resources/log4j2-devenv.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/backend/scripts/repl b/backend/scripts/repl
index bf63eeb7d3..3ca39aa9c5 100755
--- a/backend/scripts/repl
+++ b/backend/scripts/repl
@@ -2,12 +2,15 @@
export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS"
-export OPTIONS="-A:jmx-remote:dev \
- -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
- -J-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector \
- -J-Dlog4j2.configurationFile=log4j2-devenv.xml \
- -J-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory \
- -J-XX:+UseShenandoahGC -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m";
+
+export OPTIONS="
+ -A:jmx-remote:dev \
+ -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
+ -J-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory \
+ -J-Dlog4j2.configurationFile=log4j2-devenv.xml \
+ -J-XX:+UseShenandoahGC \
+ -J-XX:-OmitStackTraceInFastThrow \
+ -J-Xms50m -J-Xmx512m";
# export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions";
# export OPTIONS="$OPTIONS -J-XX:-TieredCompilation -J-XX:CompileThreshold=10000";
diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj
index 60347cd24a..bcbf372b24 100644
--- a/backend/src/app/db.clj
+++ b/backend/src/app/db.clj
@@ -27,14 +27,16 @@
com.zaxxer.hikari.HikariConfig
com.zaxxer.hikari.HikariDataSource
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
+ java.io.InputStream
+ java.io.OutputStream
java.lang.AutoCloseable
java.sql.Connection
java.sql.Savepoint
org.postgresql.PGConnection
org.postgresql.geometric.PGpoint
+ org.postgresql.jdbc.PgArray
org.postgresql.largeobject.LargeObject
org.postgresql.largeobject.LargeObjectManager
- org.postgresql.jdbc.PgArray
org.postgresql.util.PGInterval
org.postgresql.util.PGobject))
@@ -356,7 +358,7 @@
val (.getValue o)]
(if (or (= typ "json")
(= typ "jsonb"))
- (json/decode-str val)
+ (json/read val)
val)))
(defn decode-transit-pgobject
@@ -392,7 +394,7 @@
[data]
(doto (org.postgresql.util.PGobject.)
(.setType "jsonb")
- (.setValue (json/encode-str data))))
+ (.setValue (json/write-str data))))
;; --- Locks
diff --git a/backend/src/app/emails.clj b/backend/src/app/emails.clj
index 43b6482740..6721c299ca 100644
--- a/backend/src/app/emails.clj
+++ b/backend/src/app/emails.clj
@@ -66,8 +66,8 @@
(:id profile)
(db/interval bounce-max-age)])]
- (and (< complaints complaint-threshold)
- (< bounces bounce-threshold)))))
+ (and (< (or complaints 0) complaint-threshold)
+ (< (or bounces 0) bounce-threshold)))))
(defn has-complaint-reports?
([conn email] (has-complaint-reports? conn email nil))
diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj
index 0dc98852b1..3d8185be19 100644
--- a/backend/src/app/http.clj
+++ b/backend/src/app/http.clj
@@ -90,20 +90,9 @@
(try
(handler request)
(catch Throwable e
- (try
- (let [cdata (errors/get-error-context request e)]
- (l/update-thread-context! cdata)
- (l/error :hint "unhandled exception"
- :message (ex-message e)
- :error-id (str (:id cdata))
- :cause e))
- {:status 500 :body "internal server error"}
- (catch Throwable e
- (l/error :hint "unhandled exception"
- :message (ex-message e)
- :cause e)
- {:status 500 :body "internal server error"})))))))
-
+ (l/with-context (errors/get-error-context request e)
+ (l/error :hint (ex-message e) :cause e)
+ {:status 500 :body "internal server error"}))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Http Main Handler (Router)
diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj
index ec489054e8..fac7094471 100644
--- a/backend/src/app/http/errors.clj
+++ b/backend/src/app/http/errors.clj
@@ -7,11 +7,11 @@
(ns app.http.errors
"A errors handling for the http server."
(:require
- [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.uuid :as uuid]
[clojure.pprint]
+ [clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(defn- parse-client-ip
@@ -20,44 +20,24 @@
(get headers "x-real-ip")
(get request :remote-addr)))
-
-(defn- simple-prune
- ([s] (simple-prune s (* 1024 1024)))
- ([s max-length]
- (if (> (count s) max-length)
- (str (subs s 0 max-length) " [...]")
- s)))
-
-(defn- stringify-data
- [data]
- (binding [clojure.pprint/*print-right-margin* 200]
- (let [result (with-out-str (clojure.pprint/pprint data))]
- (simple-prune result (* 1024 1024)))))
-
(defn get-error-context
[request error]
(let [data (ex-data error)]
- (d/without-nils
- (merge
- {:id (str (uuid/next))
- :path (str (:uri request))
- :method (name (:request-method request))
- :hint (or (:hint data) (ex-message error))
- :params (stringify-data (:params request))
- :data (stringify-data (dissoc data :explain))
- :ip-addr (parse-client-ip request)
- :explain (str/prune (:explain data) (* 1024 1024) "[...]")}
-
- (when-let [id (:profile-id request)]
- {:profile-id id})
+ (merge
+ {:id (uuid/next)
+ :path (:uri request)
+ :method (:request-method request)
+ :hint (or (:hint data) (ex-message error))
+ :params (l/stringify-data (:params request))
+ :spec-problems (some-> data ::s/problems)
+ :ip-addr (parse-client-ip request)
+ :profile-id (:profile-id request)}
(let [headers (:headers request)]
{:user-agent (get headers "user-agent")
:frontend-version (get headers "x-frontend-version" "unknown")})
- (when (map? data)
- {:error-type (:type data)
- :error-code (:code data)})))))
+ (dissoc data ::s/problems))))
(defmulti handle-exception
(fn [err & _rest]
@@ -85,21 +65,17 @@
(:explain edata)
"\n")}
{:status 400
- :body (dissoc edata :data)})))
+ :body (dissoc edata ::s/problems)})))
(defmethod handle-exception :assertion
[error request]
- (let [edata (ex-data error)
- cdata (get-error-context request error)]
- (l/update-thread-context! cdata)
- (l/error :hint "internal error: assertion"
- :error-id (str (:id cdata))
- :cause error)
-
+ (let [edata (ex-data error)]
+ (l/with-context (get-error-context request error)
+ (l/error :hint (ex-message error) :cause error))
{:status 500
:body {:type :server-error
:code :assertion
- :data (dissoc edata :data)}}))
+ :data (dissoc edata ::s/problems)}}))
(defmethod handle-exception :not-found
[err _]
@@ -116,12 +92,10 @@
(if (and (ex/exception? (:rollback edata))
(ex/exception? (:handling edata)))
(handle-exception (:handling edata) request)
- (let [cdata (get-error-context request error)]
- (l/update-thread-context! cdata)
- (l/error :hint "internal error"
- :error-message (ex-message error)
- :error-id (str (:id cdata))
- :cause error)
+ (do
+ (l/with-context (get-error-context request error)
+ (l/error :hint (ex-message error) :cause error))
+
{:status 500
:body {:type :server-error
:code :unexpected
@@ -130,15 +104,13 @@
(defmethod handle-exception org.postgresql.util.PSQLException
[error request]
- (let [cdata (get-error-context request error)
- state (.getSQLState ^java.sql.SQLException error)]
+ (let [state (.getSQLState ^java.sql.SQLException error)]
- (l/update-thread-context! cdata)
- (l/error :hint "psql exception"
- :error-message (ex-message error)
- :error-id (str (:id cdata))
- :sql-state state
- :cause error)
+ (l/with-context (get-error-context request error)
+ (l/error :hint "psql exception"
+ :error-message (ex-message error)
+ :state state
+ :cause error))
(cond
(= state "57014")
diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj
index 8c8bf11514..de3343dbca 100644
--- a/backend/src/app/http/middleware.clj
+++ b/backend/src/app/http/middleware.clj
@@ -13,7 +13,6 @@
[app.util.json :as json]
[buddy.core.codecs :as bc]
[buddy.core.hash :as bh]
- [clojure.java.io :as io]
[ring.middleware.cookies :refer [wrap-cookies]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
@@ -36,8 +35,7 @@
(t/read! reader)))
(parse-json [body]
- (let [reader (io/reader body)]
- (json/read reader)))
+ (json/read body))
(parse [type body]
(try
diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj
index 462e86c625..2888961402 100644
--- a/backend/src/app/http/session.clj
+++ b/backend/src/app/http/session.clj
@@ -58,7 +58,9 @@
(assoc response :cookies {cookie-name {:path "/"
:http-only true
:value id
- :same-site (if cors? :none :strict)
+ :same-site (cond (not secure?) :lax
+ cors? :none
+ :else :strict)
:secure secure?}})))
(defn- clear-cookies
@@ -71,7 +73,6 @@
(if-let [{:keys [id profile-id] :as session} (retrieve-from-request cfg request)]
(do
(a/>!! (::events-ch cfg) id)
- (l/update-thread-context! {:profile-id profile-id})
(handler (assoc request :profile-id profile-id)))
(handler request))))
@@ -178,7 +179,7 @@
(defmethod ig/prep-key ::gc-task
[_ cfg]
- (merge {:max-age (dt/duration {:days 2})}
+ (merge {:max-age (dt/duration {:days 15})}
(d/without-nils cfg)))
(defmethod ig/init-key ::gc-task
diff --git a/backend/src/app/loggers/database.clj b/backend/src/app/loggers/database.clj
index 2685661d82..ca0fb5d6eb 100644
--- a/backend/src/app/loggers/database.clj
+++ b/backend/src/app/loggers/database.clj
@@ -36,7 +36,7 @@
(db/insert! conn :server-error-report
{:id id :content (db/tjson event)})))
-(defn- parse-context
+(defn- parse-event-data
[event]
(reduce-kv
(fn [acc k v]
@@ -46,12 +46,11 @@
(str/blank? v) acc
:else (assoc acc k v)))
{}
- (:context event)))
+ event))
(defn parse-event
[event]
- (-> (parse-context event)
- (merge (dissoc event :context))
+ (-> (parse-event-data event)
(assoc :tenant (cf/get :tenant))
(assoc :host (cf/get :host))
(assoc :public-uri (cf/get :public-uri))
@@ -62,6 +61,7 @@
(aa/with-thread executor
(try
(let [event (parse-event event)]
+ (l/debug :hint "registering error on database" :id (:id event))
(persist-on-database! cfg event))
(catch Exception e
(l/warn :hint "unexpected exception on database error logger"
@@ -74,7 +74,8 @@
[_ {:keys [receiver] :as cfg}]
(l/info :msg "initializing database error persistence")
(let [output (a/chan (a/sliding-buffer 128)
- (filter #(= (:level %) "error")))]
+ (filter (fn [event]
+ (= (:logger/level event) "error"))))]
(receiver :sub output)
(a/go-loop []
(let [msg (a/!! out msg)
(recur)
@@ -71,18 +77,30 @@
(.close ^java.lang.AutoCloseable socket)
(.close ^java.lang.AutoCloseable zctx))))))))
+(s/def ::logger-name string?)
+(s/def ::level string?)
+(s/def ::thread string?)
+(s/def ::time-millis integer?)
+(s/def ::message string?)
+(s/def ::context-map map?)
+(s/def ::throw map?)
+
+(s/def ::log4j-event
+ (s/keys :req-un [::logger-name ::level ::thread ::time-millis ::message]
+ :opt-un [::context-map ::thrown]))
+
(defn- prepare
[event]
- (merge
- {:logger (:loggerName event)
- :level (str/lower (:level event))
- :thread (:thread event)
- :created-at (dt/instant (:timeMillis event))
- :message (:message event)}
- (when-let [ctx (:contextMap event)]
- {:context ctx})
- (when-let [thrown (:thrown event)]
- {:error
- {:class (:name thrown)
- :message (:message thrown)
- :trace (:extendedStackTrace thrown)}})))
+ (if (s/valid? ::log4j-event event)
+ (merge {:message (:message event)
+ :created-at (dt/instant (:time-millis event))
+ :logger/name (:logger-name event)
+ :logger/level (str/lower (:level event))}
+
+ (when-let [thrown (:thrown event)]
+ {:trace (:extended-stack-trace thrown)})
+
+ (:context-map event))
+ (do
+ (l/warn :hint "invalid event" :event event)
+ nil)))
diff --git a/backend/src/app/msgbus.clj b/backend/src/app/msgbus.clj
index 146da0c833..dec1335167 100644
--- a/backend/src/app/msgbus.clj
+++ b/backend/src/app/msgbus.clj
@@ -179,18 +179,18 @@
;; Add a unique listener to connection
(.addListener sub-conn
(reify RedisPubSubListener
- (message [it pattern topic message])
- (message [it topic message]
+ (message [_ _pattern _topic _message])
+ (message [_ topic message]
;; There are no back pressure, so we use a slidding
;; buffer for cases when the pubsub broker sends
;; more messages that we can process.
(let [val {:topic topic :message (blob/decode message)}]
(when-not (a/offer! rcv-ch val)
(l/warn :msg "dropping message on subscription loop"))))
- (psubscribed [it pattern count])
- (punsubscribed [it pattern count])
- (subscribed [it topic count])
- (unsubscribed [it topic count])))
+ (psubscribed [_ _pattern _count])
+ (punsubscribed [_ _pattern _count])
+ (subscribed [_ _topic _count])
+ (unsubscribed [_ _topic _count])))
(letfn [(subscribe-to-single-topic [nsubs topic chan]
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/mutations/demo.clj
index 8072b38e28..12786757f5 100644
--- a/backend/src/app/rpc/mutations/demo.clj
+++ b/backend/src/app/rpc/mutations/demo.clj
@@ -36,7 +36,8 @@
:is-active true
:deleted-at (dt/in-future cf/deletion-delay)
:password password
- :props {:onboarding-viewed true}}]
+ :props {}
+ }]
(when-not (contains? cf/flags :demo-users)
(ex/raise :type :validation
diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj
index fb53e9d945..cd621763e3 100644
--- a/backend/src/app/rpc/mutations/profile.clj
+++ b/backend/src/app/rpc/mutations/profile.clj
@@ -335,9 +335,9 @@
;; --- MUTATION: Logout
(s/def ::logout
- (s/keys :req-un [::profile-id]))
+ (s/keys :opt-un [::profile-id]))
-(sv/defmethod ::logout
+(sv/defmethod ::logout {:auth false}
[{:keys [session] :as cfg} _]
(with-meta {}
{:transform-response (:delete session)}))
diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj
index ab71fbcb28..ef65882157 100644
--- a/backend/src/app/rpc/mutations/teams.clj
+++ b/backend/src/app/rpc/mutations/teams.clj
@@ -104,24 +104,53 @@
;; --- Mutation: Leave Team
+(declare role->params)
+
+(s/def ::reassign-to ::us/uuid)
(s/def ::leave-team
- (s/keys :req-un [::profile-id ::id]))
+ (s/keys :req-un [::profile-id ::id]
+ :opt-un [::reassign-to]))
(sv/defmethod ::leave-team
- [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
+ [{:keys [pool] :as cfg} {:keys [id profile-id reassign-to]}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id id)
members (teams/retrieve-team-members conn id)]
- (when (:is-owner perms)
+ (cond
+ ;; we can only proceed if there are more members in the team
+ ;; besides the current profile
+ (<= (count members) 1)
+ (ex/raise :type :validation
+ :code :no-enough-members-for-leave
+ :context {:members (count members)})
+
+ ;; if the `reassign-to` is filled and has a different value
+ ;; than the current profile-id, we proceed to reassing the
+ ;; owner role to profile identified by the `reassign-to`.
+ (and reassign-to (not= reassign-to profile-id))
+ (let [member (d/seek #(= reassign-to (:id %)) members)]
+ (when-not member
+ (ex/raise :type :not-found :code :member-does-not-exist))
+
+ ;; unasign owner role to current profile
+ (db/update! conn :team-profile-rel
+ {:is-owner false}
+ {:team-id id
+ :profile-id profile-id})
+
+ ;; assign owner role to new profile
+ (db/update! conn :team-profile-rel
+ (role->params :owner)
+ {:team-id id :profile-id reassign-to}))
+
+ ;; and finally, if all other conditions does not match and the
+ ;; current profile is owner, we dont allow it because there
+ ;; must always be an owner.
+ (:is-owner perms)
(ex/raise :type :validation
:code :owner-cant-leave-team
- :hint "reasing owner before leave"))
-
- (when-not (> (count members) 1)
- (ex/raise :type :validation
- :code :cant-leave-team
- :context {:members (count members)}))
+ :hint "releasing owner before leave"))
(db/delete! conn :team-profile-rel
{:profile-id profile-id
@@ -129,7 +158,6 @@
nil)))
-
;; --- Mutation: Delete Team
(s/def ::delete-team
@@ -156,7 +184,6 @@
;; --- Mutation: Team Update Role
(declare retrieve-team-member)
-(declare role->params)
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)
diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj
index b68c6a5f17..a3ca758f5e 100644
--- a/backend/src/app/rpc/queries/profile.clj
+++ b/backend/src/app/rpc/queries/profile.clj
@@ -37,10 +37,15 @@
(sv/defmethod ::profile {:auth false}
[{:keys [pool] :as cfg} {:keys [profile-id] :as params}]
- (if profile-id
- (retrieve-profile pool profile-id)
- {:id uuid/zero
- :fullname "Anonymous User"}))
+
+ ;; We need to return the anonymous profile object in two cases, when
+ ;; no profile-id is in session, and when db call raises not found. In all other
+ ;; cases we need to reraise the exception.
+ (or (ex/try*
+ #(some->> profile-id (retrieve-profile pool))
+ #(when (not= :not-found (:type (ex-data %))) (throw %)))
+ {:id uuid/zero
+ :fullname "Anonymous User"}))
(def ^:private sql:default-profile-team
"select t.id, name
diff --git a/backend/src/app/rpc/queries/teams.clj b/backend/src/app/rpc/queries/teams.clj
index 4072372374..49fbd66c15 100644
--- a/backend/src/app/rpc/queries/teams.clj
+++ b/backend/src/app/rpc/queries/teams.clj
@@ -21,8 +21,10 @@
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
+ join team as t on (t.id = tpr.team_id)
where tpr.profile_id = ?
- and tpr.team_id = ?")
+ and tpr.team_id = ?
+ and t.deleted_at is null")
(defn get-permissions
[conn profile-id team-id]
diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj
index 4c3a619009..3c9c6a7d0f 100644
--- a/backend/src/app/storage/impl.clj
+++ b/backend/src/app/storage/impl.clj
@@ -117,11 +117,11 @@
io/IOFactory
(make-reader [_ opts]
(io/make-reader path opts))
- (make-writer [_ opts]
+ (make-writer [_ _]
(throw (UnsupportedOperationException. "not implemented")))
(make-input-stream [_ opts]
(io/make-input-stream path opts))
- (make-output-stream [_ opts]
+ (make-output-stream [_ _]
(throw (UnsupportedOperationException. "not implemented")))
clojure.lang.Counted
(count [_] size)
@@ -138,11 +138,11 @@
io/IOFactory
(make-reader [_ opts]
(io/make-reader bais opts))
- (make-writer [_ opts]
+ (make-writer [_ _]
(throw (UnsupportedOperationException. "not implemented")))
(make-input-stream [_ opts]
(io/make-input-stream bais opts))
- (make-output-stream [_ opts]
+ (make-output-stream [_ _]
(throw (UnsupportedOperationException. "not implemented")))
clojure.lang.Counted
@@ -159,11 +159,11 @@
io/IOFactory
(make-reader [_ opts]
(io/make-reader is opts))
- (make-writer [_ opts]
+ (make-writer [_ _]
(throw (UnsupportedOperationException. "not implemented")))
(make-input-stream [_ opts]
(io/make-input-stream is opts))
- (make-output-stream [_ opts]
+ (make-output-stream [_ _]
(throw (UnsupportedOperationException. "not implemented")))
clojure.lang.Counted
diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj
index 44a232aeb0..f9441be124 100644
--- a/backend/src/app/tasks/telemetry.clj
+++ b/backend/src/app/tasks/telemetry.clj
@@ -59,7 +59,7 @@
response (http/send! {:method :post
:uri (:uri cfg)
:headers {"content-type" "application/json"}
- :body (json/encode-str data)})]
+ :body (json/write-str data)})]
(when (> (:status response) 206)
(ex/raise :type :internal
:code :invalid-response
diff --git a/backend/src/app/util/json.clj b/backend/src/app/util/json.clj
index 0ffd859d1e..edc204c1f5 100644
--- a/backend/src/app/util/json.clj
+++ b/backend/src/app/util/json.clj
@@ -9,22 +9,27 @@
(:require
[jsonista.core :as j]))
-(defn encode-str
- [v]
- (j/write-value-as-string v j/keyword-keys-object-mapper))
+(defn mapper
+ [params]
+ (j/object-mapper params))
+
+(defn write
+ ([v] (j/write-value-as-bytes v j/keyword-keys-object-mapper))
+ ([v mapper] (j/write-value-as-bytes v mapper)))
+
+(defn write-str
+ ([v] (j/write-value-as-string v j/keyword-keys-object-mapper))
+ ([v mapper] (j/write-value-as-string v mapper)))
+
+(defn read
+ ([v] (j/read-value v j/keyword-keys-object-mapper))
+ ([v mapper] (j/read-value v mapper)))
(defn encode
[v]
(j/write-value-as-bytes v j/keyword-keys-object-mapper))
-(defn decode-str
- [v]
- (j/read-value v j/keyword-keys-object-mapper))
-
(defn decode
[v]
(j/read-value v j/keyword-keys-object-mapper))
-(defn read
- [v]
- (j/read-value v j/keyword-keys-object-mapper))
diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj
index 49370e164e..6d979da8cf 100644
--- a/backend/src/app/worker.clj
+++ b/backend/src/app/worker.clj
@@ -266,13 +266,8 @@
(= ::noop (:strategy edata))
(assoc :inc-by 0))
-
- (let [cdata (get-error-context error item)]
- (l/update-thread-context! cdata)
- (l/error :cause error
- :hint "unhandled exception on task"
- :id (:id cdata))
-
+ (l/with-context (get-error-context error item)
+ (l/error :cause error :hint "unhandled exception on task")
(if (>= (:retry-num item) (:max-retries item))
{:status :failed :task item :error error}
{:status :retry :task item :error error})))))
diff --git a/backend/test/app/services_profile_test.clj b/backend/test/app/services_profile_test.clj
index b51bcd8c0f..ba82c0f0e8 100644
--- a/backend/test/app/services_profile_test.clj
+++ b/backend/test/app/services_profile_test.clj
@@ -6,6 +6,7 @@
(ns app.services-profile-test
(:require
+ [app.common.uuid :as uuid]
[app.db :as db]
[app.rpc.mutations.profile :as profile]
[app.test-helpers :as th]
@@ -153,11 +154,8 @@
:profile-id (:id prof)}
out (th/query! params)]
;; (th/print-result! out)
- (let [error (:error out)
- error-data (ex-data error)]
- (t/is (th/ex-info? error))
- (t/is (= (:type error-data) :not-found))))
- ))
+ (let [result (:result out)]
+ (t/is (= uuid/zero (:id result)))))))
(t/deftest registration-domain-whitelist
(let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]
diff --git a/backend/test/app/services_teams_test.clj b/backend/test/app/services_teams_test.clj
index 6e2aaeea7b..4124325b3b 100644
--- a/backend/test/app/services_teams_test.clj
+++ b/backend/test/app/services_teams_test.clj
@@ -33,7 +33,6 @@
:role :editor
:profile-id (:id profile1)}]
-
;; invite external user without complaints
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)]
@@ -136,9 +135,10 @@
:profile-id (:id profile1)}
out (th/query! data)]
;; (th/print-result! out)
- (t/is (nil? (:error out)))
- (let [result (:result out)]
- (t/is (= 0 (count result)))))
+ (let [error (:error out)
+ error-data (ex-data error)]
+ (t/is (th/ex-info? error))
+ (t/is (= (:type error-data) :not-found))))
;; run permanent deletion
(let [result (task {:max-age (dt/duration 0)})]
diff --git a/common/deps.edn b/common/deps.edn
index 048bcb4a12..172c11c5e1 100644
--- a/common/deps.edn
+++ b/common/deps.edn
@@ -1,24 +1,18 @@
{:deps
{org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/data.json {:mvn/version "2.3.1"}
- org.clojure/core.async {:mvn/version "1.3.618"}
org.clojure/tools.cli {:mvn/version "1.0.206"}
metosin/jsonista {:mvn/version "0.3.3"}
org.clojure/clojurescript {:mvn/version "1.10.844"}
;; Logging
- org.clojure/tools.logging {:mvn/version "1.1.0"}
- org.apache.logging.log4j/log4j-api {:mvn/version "2.16.0"}
- org.apache.logging.log4j/log4j-core {:mvn/version "2.16.0"}
- org.apache.logging.log4j/log4j-web {:mvn/version "2.16.0"}
- org.apache.logging.log4j/log4j-jul {:mvn/version "2.16.0"}
- org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.16.0"}
+ org.clojure/tools.logging {:mvn/version "1.2.3"}
+ org.apache.logging.log4j/log4j-api {:mvn/version "2.17.0"}
+ org.apache.logging.log4j/log4j-core {:mvn/version "2.17.0"}
+ org.apache.logging.log4j/log4j-web {:mvn/version "2.17.0"}
+ org.apache.logging.log4j/log4j-jul {:mvn/version "2.17.0"}
+ org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.17.0"}
org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"}
- org.slf4j/jcl-over-slf4j {:mvn/version "2.0.0-alpha1"}
- org.slf4j/log4j-over-slf4j {:mvn/version "2.0.0-alpha1"}
- org.slf4j/osgi-over-slf4j {:mvn/version "2.0.0-alpha1"}
- org.slf4j/jul-to-slf4j {:mvn/version "2.0.0-alpha1"}
- com.lmax/disruptor {:mvn/version "3.4.4"}
selmer/selmer {:mvn/version "1.12.40"}
expound/expound {:mvn/version "0.8.9"}
@@ -38,7 +32,8 @@
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
;; exception printing
- io.aviso/pretty {:mvn/version "0.1.37"}
+ fipp/fipp {:mvn/version "0.6.24"}
+ io.aviso/pretty {:mvn/version "1.1.1"}
environ/environ {:mvn/version "1.2.0"}}
:paths ["src"]
:aliases
diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc
index a39c6c38af..d43d9eb304 100644
--- a/common/src/app/common/data.cljc
+++ b/common/src/app/common/data.cljc
@@ -6,7 +6,7 @@
(ns app.common.data
"Data manipulation and query helper functions."
- (:refer-clojure :exclude [read-string hash-map merge name])
+ (:refer-clojure :exclude [read-string hash-map merge name parse-double])
#?(:cljs
(:require-macros [app.common.data]))
(:require
diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc
index a861bcc27d..f9356e5880 100644
--- a/common/src/app/common/logging.cljc
+++ b/common/src/app/common/logging.cljc
@@ -9,16 +9,23 @@
[app.common.exceptions :as ex]
[clojure.pprint :refer [pprint]]
[cuerdas.core :as str]
+ #?(:clj [io.aviso.exception :as ie])
#?(:cljs [goog.log :as glog]))
- #?(:cljs (:require-macros [app.common.logging]))
- #?(:clj
- (:import
- org.apache.logging.log4j.Level
- org.apache.logging.log4j.LogManager
- org.apache.logging.log4j.Logger
- org.apache.logging.log4j.ThreadContext
- org.apache.logging.log4j.message.MapMessage
- org.apache.logging.log4j.spi.LoggerContext)))
+ #?(:cljs (:require-macros [app.common.logging])
+ :clj (:import
+ org.apache.logging.log4j.Level
+ org.apache.logging.log4j.LogManager
+ org.apache.logging.log4j.Logger
+ org.apache.logging.log4j.ThreadContext
+ org.apache.logging.log4j.CloseableThreadContext
+ org.apache.logging.log4j.message.MapMessage
+ org.apache.logging.log4j.spi.LoggerContext)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; CLJ Specific
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+#?(:clj (set! *warn-on-reflection* true))
#?(:clj
(defn build-map-message
@@ -34,17 +41,69 @@
(def logging-agent
(agent nil :error-mode :continue)))
+(defn- simple-prune
+ ([s] (simple-prune s (* 1024 1024)))
+ ([s max-length]
+ (if (> (count s) max-length)
+ (str (subs s 0 max-length) " [...]")
+ s)))
+
+#?(:clj
+ (defn stringify-data
+ [val]
+ (cond
+ (instance? clojure.lang.Named val)
+ (name val)
+
+ (instance? Throwable val)
+ (binding [ie/*app-frame-names* [#"app.*"]
+ ie/*fonts* nil
+ ie/*traditional* true]
+ (ie/format-exception val nil))
+
+ (string? val)
+ val
+
+ (coll? val)
+ (binding [clojure.pprint/*print-right-margin* 120]
+ (-> (with-out-str (pprint val))
+ (simple-prune (* 1024 1024 3))))
+
+ :else
+ (str val))))
+
+#?(:clj
+ (defn data->context-map
+ ^java.util.Map
+ [data]
+ (into {}
+ (comp (filter second)
+ (map (fn [[key val]]
+ [(stringify-data key)
+ (stringify-data val)])))
+ data)))
+
+#?(:clj
+ (defmacro with-context
+ [data & body]
+ `(let [data# (data->context-map ~data)]
+ (with-open [closeable# (CloseableThreadContext/putAll data#)]
+ ~@body))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Common
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
(defn get-logger
[lname]
#?(:clj (.getLogger ^LoggerContext logger-context ^String lname)
- :cljs
- (glog/getLogger
- (cond
- (string? lname) lname
- (= lname :root) ""
- (simple-ident? lname) (name lname)
- (qualified-ident? lname) (str (namespace lname) "." (name lname))
- :else (str lname)))))
+ :cljs (glog/getLogger
+ (cond
+ (string? lname) lname
+ (= lname :root) ""
+ (simple-ident? lname) (name lname)
+ (qualified-ident? lname) (str (namespace lname) "." (name lname))
+ :else (str lname)))))
(defn get-level
[level]
@@ -87,7 +146,7 @@
:cljs
(when glog/ENABLED
(when-let [l (get-logger logger)]
- (let [level (get-level level)
+ (let [level (get-level level)
record (glog/LogRecord. level message (.getName ^js l))]
(when exception (.setException record exception))
(glog/publishLogRecord l record))))))
@@ -98,7 +157,7 @@
(.isEnabled ^Logger logger ^Level level)))
(defmacro log
- [& {:keys [level cause ::logger ::async ::raw] :as props}]
+ [& {:keys [level cause ::logger ::async ::raw] :or {async true} :as props}]
(if (:ns &env) ; CLJS
`(write-log! ~(or logger (str *ns*))
~level
@@ -112,10 +171,12 @@
~level-sym (get-level ~level)]
(if (enabled? ~logger-sym ~level-sym)
~(if async
- `(send-off logging-agent
- (fn [_#]
- (let [message# (or ~raw (build-map-message ~props))]
- (write-log! ~logger-sym ~level-sym ~cause message#))))
+ `(let [cdata# (ThreadContext/getImmutableContext)]
+ (send-off logging-agent
+ (fn [_#]
+ (with-context (into {:cause ~cause} cdata#)
+ (->> (or ~raw (build-map-message ~props))
+ (write-log! ~logger-sym ~level-sym ~cause))))))
`(let [message# (or ~raw (build-map-message ~props))]
(write-log! ~logger-sym ~level-sym ~cause message#))))))))
@@ -147,24 +208,6 @@
(when (:ns &env)
`(set-level* ~n ~level))))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; CLJ Specific
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-#?(:clj
- (defn update-thread-context!
- [data]
- (run! (fn [[key val]]
- (ThreadContext/put
- (name key)
- (cond
- (coll? val)
- (binding [clojure.pprint/*print-right-margin* 120]
- (with-out-str (pprint val)))
- (instance? clojure.lang.Named val) (name val)
- :else (str val))))
- data)))
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLJS Specific
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -213,7 +256,6 @@
(some-> (get-logger name)
(glog/setLevel (get-level lvl)))))
-
#?(:cljs
(defn set-levels!
[lvls]
diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc
index 6418674750..5bc912c752 100644
--- a/common/src/app/common/pages/helpers.cljc
+++ b/common/src/app/common/pages/helpers.cljc
@@ -256,20 +256,12 @@
(defn select-frames
[objects]
- (let [root (get objects uuid/zero)
- loopfn (fn loopfn [ids]
- (let [id (first ids)
- obj (get objects id)]
- (cond
- (or (nil? id) (nil? obj))
- nil
-
- (= :frame (:type obj))
- (lazy-seq (cons obj (loopfn (rest ids))))
-
- :else
- (lazy-seq (loopfn (rest ids))))))]
- (loopfn (:shapes root))))
+ (let [lookup #(get objects %)
+ frame? #(= :frame (:type %))
+ xform (comp (map lookup)
+ (filter frame?))]
+ (->> (:shapes (lookup uuid/zero))
+ (into [] xform))))
(defn clone-object
"Gets a copy of the object and all its children, with new ids
@@ -436,7 +428,7 @@
[path-name]
(let [path-name-split (split-path path-name)
path (str/join " / " (butlast path-name-split))
- name (last path-name-split)]
+ name (or (last path-name-split) "")]
[path name]))
(defn merge-path-item
diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc
index e7614707ba..5a2f37065d 100644
--- a/common/src/app/common/spec.cljc
+++ b/common/src/app/common/spec.cljc
@@ -208,30 +208,30 @@
;; --- Macros
(defn spec-assert*
- [spec x message context]
- (if (s/valid? spec x)
- x
- (let [data (s/explain-data spec x)
- explain (with-out-str (s/explain-out data))]
+ [spec val hint ctx]
+ (if (s/valid? spec val)
+ val
+ (let [data (s/explain-data spec val)]
(ex/raise :type :assertion
:code :spec-validation
- :hint message
- :data data
- :explain explain
- :context context
- #?@(:cljs [:stack (.-stack (ex-info message {}))])))))
-
+ :hint hint
+ :ctx ctx
+ ::s/problems (::s/problems data)))))
(defmacro assert
"Development only assertion macro."
[spec x]
(when *assert*
(let [nsdata (:ns &env)
- context (when nsdata
+ context (if nsdata
{:ns (str (:name nsdata))
:name (pr-str spec)
:line (:line &env)
- :file (:file (:meta nsdata))})
+ :file (:file (:meta nsdata))}
+ (let [mdata (meta &form)]
+ {:ns (str (ns-name *ns*))
+ :name (pr-str spec)
+ :line (:line mdata)}))
message (str "spec assert: '" (pr-str spec) "'")]
`(spec-assert* ~spec ~x ~message ~context))))
@@ -253,12 +253,9 @@
[spec data]
(let [result (s/conform spec data)]
(when (= result ::s/invalid)
- (let [data (s/explain-data spec data)
- explain (with-out-str
- (s/explain-out data))]
+ (let [data (s/explain-data spec data)]
(throw (ex/error :type :validation
:code :spec-validation
- :explain explain
:data data))))
result))
diff --git a/frontend/deps.edn b/frontend/deps.edn
index 5468b8b552..b2f85c8b1f 100644
--- a/frontend/deps.edn
+++ b/frontend/deps.edn
@@ -22,7 +22,8 @@
:main-opts ["-m" "antq.core"]}
:dev
- {:extra-deps
+ {:extra-paths ["dev"]
+ :extra-deps
{thheller/shadow-cljs {:mvn/version "2.15.12"}
cider/cider-nrepl {:mvn/version "0.26.0"}}}
diff --git a/frontend/dev/bench/core.cljs b/frontend/dev/bench/core.cljs
deleted file mode 100644
index 511f93212c..0000000000
--- a/frontend/dev/bench/core.cljs
+++ /dev/null
@@ -1,112 +0,0 @@
-(ns bench.core
- (:require [kdtree.core :as k]
- [intervaltree.core :as it]
- [cljs.pprint :refer (pprint)]
- [cljs.nodejs :as node]))
-
-(enable-console-print!)
-
-;; --- Index Initialization Bechmark
-
-(defn- bench-init-10000
- []
- (println "1000x1000,10 -> 10000 points")
- (time
- (k/generate 1000 1000 10 10)))
-
-(defn- bench-init-250000
- []
- (time
- (k/generate 5000 5000 10 10)))
-
-(defn bench-init
- []
- (bench-init-10000)
- (bench-init-10000)
- (bench-init-250000)
- (bench-init-250000)
- (bench-init-10000)
- (bench-init-10000)
- (bench-init-250000)
- (bench-init-250000))
-
-;; --- Nearest Search Benchmark
-
-(defn- bench-knn-160000
- []
- (let [tree (k/create)]
- (k/setup tree 4000 4000 10 10)
- (println "KNN Search (160000 points) 1000 times")
- (time
- (dotimes [i 1000]
- (let [pt #js [(rand-int 400)
- (rand-int 400)]]
- (k/nearest tree pt 2))))))
-
-
-(defn- bench-knn-360000
- []
- (let [tree (k/create)]
- (k/initialize tree 6000 6000 10 10)
- (println "KNN Search (360000 points) 1000 times")
- (time
- (dotimes [i 1000]
- (let [pt #js [(rand-int 600)
- (rand-int 600)]]
- (k/nearest tree pt 2))))))
-
-(defn bench-knn
- []
- (bench-knn-160000)
- (bench-knn-360000))
-
-;; --- Accuracity tests
-
-(defn test-accuracity
- []
- (let [tree (k/create)]
- (k/setup tree 4000 4000 20 20)
- (print "[1742 1419]")
- (pprint (js->clj (k/nearest tree #js [1742 1419] 6)))
- (print "[1742 1420]")
- (pprint (js->clj (k/nearest tree #js [1742 1420] 6)))
- ))
-
-(defn test-interval
- []
- (let [tree (it/create)]
- (it/add tree #js [1 5])
- (it/add tree #js [5 7])
- (it/add tree #js [-4 -1])
- (it/add tree #js [-10 -3])
- (it/add tree #js [-20 -10])
- (it/add tree #js [20 30])
- (it/add tree #js [3 9])
- (it/add tree #js [100 200])
- (it/add tree #js [1000 2000])
- (it/add tree #js [6 9])
-
- (js/console.dir tree #js {"depth" nil})
- (js/console.log "contains", 4, (it/contains tree 4))
- (js/console.log "contains", 0, (it/contains tree 0))
- ))
-
-(defn main
- [& [type]]
- (cond
- (= type "kd-init")
- (bench-init)
-
- (= type "kd-search")
- (bench-knn)
-
- (= type "kd-test")
- (test-accuracity)
-
- (= type "interval")
- (test-interval)
-
- :else
- (println "not implemented")))
-
-(set! *main-cli-fn* main)
diff --git a/frontend/dev/cljs/user.cljs b/frontend/dev/cljs/user.cljs
new file mode 100644
index 0000000000..3a927046b9
--- /dev/null
+++ b/frontend/dev/cljs/user.cljs
@@ -0,0 +1,5 @@
+(ns cljs.user)
+
+(defn hello
+ []
+ (js/console.log "hello"))
diff --git a/frontend/resources/images/form/adobe-xd.png b/frontend/resources/images/form/adobe-xd.png
new file mode 100644
index 0000000000..f2946ae396
Binary files /dev/null and b/frontend/resources/images/form/adobe-xd.png differ
diff --git a/frontend/resources/images/form/figma.png b/frontend/resources/images/form/figma.png
new file mode 100644
index 0000000000..5e3bccb1a7
Binary files /dev/null and b/frontend/resources/images/form/figma.png differ
diff --git a/frontend/resources/images/form/invision.png b/frontend/resources/images/form/invision.png
new file mode 100644
index 0000000000..551b8e2c52
Binary files /dev/null and b/frontend/resources/images/form/invision.png differ
diff --git a/frontend/resources/images/form/never-used.png b/frontend/resources/images/form/never-used.png
new file mode 100644
index 0000000000..cda0947ecf
Binary files /dev/null and b/frontend/resources/images/form/never-used.png differ
diff --git a/frontend/resources/images/form/sketch.png b/frontend/resources/images/form/sketch.png
new file mode 100644
index 0000000000..b923c607c8
Binary files /dev/null and b/frontend/resources/images/form/sketch.png differ
diff --git a/frontend/resources/images/form/use-for-1.jpg b/frontend/resources/images/form/use-for-1.jpg
new file mode 100644
index 0000000000..b1fa3da55e
Binary files /dev/null and b/frontend/resources/images/form/use-for-1.jpg differ
diff --git a/frontend/resources/images/form/use-for-2.jpg b/frontend/resources/images/form/use-for-2.jpg
new file mode 100644
index 0000000000..449ab4d9f1
Binary files /dev/null and b/frontend/resources/images/form/use-for-2.jpg differ
diff --git a/frontend/resources/images/form/use-for-3.jpg b/frontend/resources/images/form/use-for-3.jpg
new file mode 100644
index 0000000000..1d8c242d76
Binary files /dev/null and b/frontend/resources/images/form/use-for-3.jpg differ
diff --git a/frontend/resources/images/form/use-for-4.jpg b/frontend/resources/images/form/use-for-4.jpg
new file mode 100644
index 0000000000..140c6539dc
Binary files /dev/null and b/frontend/resources/images/form/use-for-4.jpg differ
diff --git a/frontend/resources/images/form/uxpin.png b/frontend/resources/images/form/uxpin.png
new file mode 100644
index 0000000000..02b5d93314
Binary files /dev/null and b/frontend/resources/images/form/uxpin.png differ
diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss
index 5801d9c0b4..53d0baedf1 100644
--- a/frontend/resources/styles/main-default.scss
+++ b/frontend/resources/styles/main-default.scss
@@ -89,3 +89,4 @@
@import "main/partials/handoff";
@import "main/partials/exception-page";
@import "main/partials/share-link";
+@import "main/partials/af-signup-questions";
diff --git a/frontend/resources/styles/main/partials/af-signup-questions.scss b/frontend/resources/styles/main/partials/af-signup-questions.scss
new file mode 100644
index 0000000000..38c431a2ea
--- /dev/null
+++ b/frontend/resources/styles/main/partials/af-signup-questions.scss
@@ -0,0 +1,257 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) UXBOX Labs SL
+
+.af-form {
+ background-color: $color-white;
+ color: $color-gray-60 !important;
+ max-width: 760px !important;
+ overflow-y: auto;
+ padding: 3rem;
+ width: 100% !important;
+
+ h1, h3 {
+ font-family: 'worksans', sans-serif !important;
+ margin-bottom: .8rem;
+ font-weight: 500 !important;
+ }
+
+ h1 {
+ font-size: $fs38;
+ }
+
+ strong {
+ font-weight: 500;
+ }
+
+ p, label {
+ font-family: 'worksans', sans-serif !important;
+ font-size: $fs14;
+ }
+
+ form {
+ max-width: 760px;
+ width: 100%;
+ }
+
+ button {
+ font-family: 'worksans', sans-serif !important;
+ }
+
+ .af-choice,
+ .af-choice-multiple {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .af-choice-option {
+ max-width: 33%;
+ width: 100%;
+
+ label {
+ font-family: 'worksans', sans-serif !important;
+ font-size: $fs14;
+ padding-left: 0;
+ }
+ }
+
+ .af-choice-multiple {
+ .af-choice-option {
+ max-width: 50%;
+ width: 100%;
+ }
+ }
+
+ .af-divider-block {
+ /* margin-bottom: 2rem; */
+
+ p {
+ &::after,
+ &::before {
+ border-color: transparent;
+ }
+ }
+ }
+
+ .af-dropdown-text,
+ .text {
+ font-family: 'worksans', sans-serif !important;
+ }
+
+ .af-step-next {
+ display: flex;
+ margin-top: 2rem;
+ }
+
+ .af-step-next button {
+ color: $color-black;
+ background-color: $color-primary;
+ max-width: 180px;
+ margin-left: auto;
+ }
+
+ .af-step-previous {
+ margin-top: -40px;
+ }
+
+ .af-step-button {
+ text-align: left;
+ }
+
+ .af-field-input {
+ margin: 0.5rem 0;
+ }
+
+ .af-field-input input[type="text"],
+ .af-choice-option label:before,
+ .af-dropdown {
+ border-color: #c5c6c9 !important;
+ }
+
+ .af-choice-option input:checked+label:before,
+ .af-legal input:checked+label:before {
+ background-color: $color-primary;
+ }
+
+ .af-field-use_of_penpot .af-choice-option input:checked+label,
+ .af-field-previous_design_tool .af-choice-option input:checked+label {
+ &::before {
+ background-color: transparent;
+ border: 2px solid $color-primary !important;
+ }
+ }
+
+ .af-field-use_of_penpot .af-choice-option label {
+ padding-top: 6rem;
+ background-size: 120px;
+ min-height: 150px;
+ }
+
+ .af-field-use_of_penpot .af-choice-option:nth-child(1) label {
+ background-image: url("../images/form/use-for-1.jpg");
+ }
+ .af-field-use_of_penpot .af-choice-option:nth-child(2) label {
+ background-image: url("../images/form/use-for-2.jpg");
+ }
+ .af-field-use_of_penpot .af-choice-option:nth-child(3) label {
+ background-image: url("../images/form/use-for-3.jpg");
+ }
+ .af-field-use_of_penpot .af-choice-option:nth-child(4) label {
+ background-image: url("../images/form/use-for-4.jpg");
+ }
+
+ .af-field-use_of_penpot label,
+ .af-field-previous_design_tool label {
+ display: flex;
+ padding-top: 5rem;
+ justify-content: center;
+ background-size: 50px;
+ background-repeat: no-repeat;
+ background-position: center 1rem;
+ margin: 1rem !important;
+ min-height: 130px;
+ position: relative;
+ text-align: center;
+
+ &:hover {
+ background-color: transparent;
+ box-shadow: 0px 10px 20px rgba(0,0,0,.2);
+ }
+
+ &::before {
+ background-color: transparent;
+ border-radius: 4px;
+ min-width: 100%;
+ min-height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 0;
+ }
+
+ &::after {
+ display: none !important;
+ }
+ }
+
+ .af-field-previous_design_tool .af-choice-option:nth-child(1) label {
+ background-image: url("../images/form/figma.png");
+
+ }
+
+ .af-field-previous_design_tool .af-choice-option:nth-child(2) label {
+ background-image: url("../images/form/sketch.png");
+ }
+
+ .af-field-previous_design_tool .af-choice-option:nth-child(3) label {
+ background-image: url("../images/form/adobe-xd.png");
+ }
+
+ .af-field-previous_design_tool .af-choice-option:nth-child(4) label {
+ background-image: url("../images/form/uxpin.png");
+ }
+
+ .af-field-previous_design_tool .af-choice-option:nth-child(5) label {
+ background-image: url("../images/form/invision.png");
+ }
+
+ .af-field-previous_design_tool .af-choice-option:nth-child(6) label {
+ background-image: url("../images/form/never-used.png");
+ }
+
+ .af-field-previous_design_tool .af-choice-option:nth-child(7) label,
+ .af-field-use_of_penpot .af-choice-option:nth-child(5) label {
+ justify-content: flex-start;
+ min-height: auto;
+ padding-left: 1.4rem;
+ padding-top: 0;
+
+ &:hover {
+ box-shadow: none;
+ }
+
+ &::before {
+ content: "";
+ background-color: #fff;
+ border: 1px solid #e0e6f0;
+ border-width: 1px;
+ border-radius: 50%;
+ min-width: 20px;
+ min-height: 20px;
+ box-sizing: border-box;
+ margin-left: 4px;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ opacity: 0;
+ width: 5px;
+ height: 8px;
+ margin-top: -1px;
+ border: solid #fff;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+ }
+ }
+
+ .af-field-use_of_penpot .af-choice-option:nth-child(5) label {
+ &::before {
+ border-radius: 3px;
+ }
+ }
+
+ .af-field-previous_design_tool .af-choice-option:nth-child(7) input:checked+label,
+ .af-field-use_of_penpot .af-choice-option:nth-child(5) input:checked+label {
+
+ &::before {
+ background-color: $color-primary;
+ }
+
+ &::after {
+ opacity: 1;
+ }
+ }
+}
diff --git a/frontend/resources/styles/main/partials/exception-page.scss b/frontend/resources/styles/main/partials/exception-page.scss
index d46261f89c..f0815818b1 100644
--- a/frontend/resources/styles/main/partials/exception-page.scss
+++ b/frontend/resources/styles/main/partials/exception-page.scss
@@ -12,6 +12,7 @@
display: flex;
align-items: center;
padding: 32px;
+ z-index: 1000;
cursor: pointer;
diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss
index bff16696cb..ec14003a8d 100644
--- a/frontend/resources/styles/main/partials/modal.scss
+++ b/frontend/resources/styles/main/partials/modal.scss
@@ -830,7 +830,7 @@
flex-direction: column;
.modal-top {
- padding-top: 40px;
+ padding: 40px 40px 0 40px;
color: $color-gray-60;
display: flex;
flex-direction: column;
@@ -841,11 +841,13 @@
font-weight: 700;
font-size: 27px;
margin-bottom: $size-3;
+ text-align: center;
}
p {
font-family: 'worksans', sans-serif;
font-weight: 500;
font-size: $fs18;
+ text-align: center;
}
}
@@ -859,23 +861,23 @@
background-position: left top;
background-size: 11%;
}
-
+
.modal-left:hover {
background-image: url("/images/on-solo-hover.svg");
background-size: 15%;
}
-
+
.modal-right {
background-image: url("/images/on-teamup.svg");
background-position: right top;
background-size: 28%;
}
-
+
.modal-right:hover {
background-image: url("/images/on-teamup-hover.svg");
background-size: 32%;
}
-
+
.modal-right,
.modal-left {
background-repeat: no-repeat;
@@ -1001,17 +1003,17 @@
.template-item {
width: 275px;
border: 1px solid $color-gray-10;
-
+
display: flex;
flex-direction: column;
text-align: left;
border-radius: $br-small;
-
+
&:not(:last-child) {
margin-bottom: 22px;
}
}
-
+
.template-item-content {
// height: 144px;
flex-grow: 1;
@@ -1020,7 +1022,7 @@
border-radius: $br-small $br-small 0 0;
}
}
-
+
.template-item-title {
padding: 6px 12px;
height: 64px;
@@ -1135,3 +1137,49 @@
}
}
+
+
+
+.questions-form {
+ .modal-overlay {
+ z-index: 2001;
+ }
+
+ .modal-container {
+ background-image: url("../images/deco-left.png"), url("../images/deco-right.png");
+ background-repeat: no-repeat;
+ background-position: 10% 50px, 90% 50px;
+ background-size: 65px;
+ display: flex;
+ flex-direction: row;
+ height: 100vh;
+ justify-content: center;
+ width: 100vw;
+
+ .af-form {
+ --primary-color: #00C38B;
+ --input-background-color: #ffffff;
+ --label-font-size: $fs16;
+ --field-error-font-color: #E65244;
+ --message-success-font-color: #49D793;
+ --message-fail-font-color: #E65244;
+ --invalid-field-border-color: #E65244;
+ --dropdown-background-color: #ffffff;
+ --primary-font-color: #000;
+ --input-border-color: rgb(224, 230, 240);
+ --input-border-radius: 3px;
+ --button-border-radius: 3px;
+ --message-border-radius: 3px;
+ --checkbox-border-radius: 3px;
+ --dropdown-option-background-color: rgba(0,195,139,1);
+ --dropdown-option-active-background-color: rgba(0,138,98,1);
+ --invalid-field-background-color: rgba(238.51780000000002,205.7178,204.11780000000002,1);
+ --message-fail-background-color: rgba(238.51780000000002,205.7178,204.11780000000002,1);
+ --message-success-background-color: rgba(171,232,197,1);
+ }
+ }
+
+ .modal-overlay {
+ background-color: rgba(0,0,0,0.9);
+ }
+}
diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn
index 942e2c93d1..8d3bc58e09 100644
--- a/frontend/shadow-cljs.edn
+++ b/frontend/shadow-cljs.edn
@@ -4,7 +4,6 @@
:jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"]
:dev-http {8888 "classpath:public"}
-
:builds
{:main
{:target :browser
diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs
index 3dbe3e1277..975688c3af 100644
--- a/frontend/src/app/config.cljs
+++ b/frontend/src/app/config.cljs
@@ -78,6 +78,7 @@
(def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes"))
(def sentry-dsn (obj/get global "penpotSentryDsn"))
+(def onboarding-form-id (obj/get global "penpotOnboardingQuestionsFormId"))
(def flags (atom (parse-flags global)))
(def version (atom (parse-version global)))
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index 982e268f3d..6f728bbe2d 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -16,6 +16,7 @@
[app.main.repo :as rp]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
+ [app.util.timers :as tm]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
@@ -60,6 +61,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare fetch-projects)
+(declare fetch-team-members)
(defn initialize
[{:keys [id] :as params}]
@@ -84,6 +86,7 @@
(rx/merge
(ptk/watch (df/load-team-fonts id) state stream)
(ptk/watch (fetch-projects) state stream)
+ (ptk/watch (fetch-team-members) state stream)
(ptk/watch (du/fetch-teams) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream)))))
@@ -237,13 +240,14 @@
(update :dashboard-files d/merge files))))))
(defn fetch-recent-files
- []
- (ptk/reify ::fetch-recent-files
- ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (->> (rp/query :team-recent-files {:team-id team-id})
- (rx/map recent-files-fetched))))))
+ ([] (fetch-recent-files nil))
+ ([team-id]
+ (ptk/reify ::fetch-recent-files
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [team-id (or team-id (:current-team-id state))]
+ (->> (rp/query :team-recent-files {:team-id team-id})
+ (rx/map recent-files-fetched)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection
@@ -396,16 +400,13 @@
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
- team-id (:current-team-id state)]
- (rx/concat
- (when (uuid? reassign-to)
- (->> (rp/mutation! :update-team-member-role {:team-id team-id
- :role :owner
- :member-id reassign-to})
- (rx/ignore)))
- (->> (rp/mutation! :leave-team {:id team-id})
- (rx/tap on-success)
- (rx/catch on-error)))))))
+ team-id (:current-team-id state)
+ params (cond-> {:id team-id}
+ (uuid? reassign-to)
+ (assoc :reassign-to reassign-to))]
+ (->> (rp/mutation! :leave-team params)
+ (rx/tap #(tm/schedule on-success))
+ (rx/catch on-error))))))
(defn invite-team-member
[{:keys [email role] :as params}]
diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs
index 3356c0ae90..271c133a43 100644
--- a/frontend/src/app/main/data/users.cljs
+++ b/frontend/src/app/main/data/users.cljs
@@ -7,12 +7,12 @@
(ns app.main.data.users
(:require
[app.common.data :as d]
+ [app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.events :as ev]
[app.main.data.media :as di]
- [app.main.data.modal :as modal]
[app.main.repo :as rp]
[app.util.i18n :as i18n]
[app.util.router :as rt]
@@ -93,6 +93,8 @@
;; --- EVENT: fetch-profile
+(declare logout)
+
(def profile-fetched?
(ptk/type? ::profile-fetched))
@@ -105,18 +107,18 @@
ptk/UpdateEvent
(update [_ state]
- (-> state
- (assoc :profile-id id)
- (assoc :profile profile)))
+ (cond-> state
+ (is-authenticated? profile)
+ (-> (assoc :profile-id id)
+ (assoc :profile profile))))
ptk/EffectEvent
(effect [_ state _]
- (let [profile (:profile state)]
- (when (not= uuid/zero (:id profile))
- (swap! storage assoc :profile profile)
- (i18n/set-locale! (:lang profile))
- (some-> (:theme profile)
- (theme/set-current-theme!)))))))
+ (when-let [profile (:profile state)]
+ (swap! storage assoc :profile profile)
+ (i18n/set-locale! (:lang profile))
+ (some-> (:theme profile)
+ (theme/set-current-theme!))))))
(defn fetch-profile
[]
@@ -145,55 +147,84 @@
(rx/mapcat (fn [profile]
(if (= uuid/zero (:id profile))
(rx/empty)
- (rx/of (fetch-teams))))))))))
+ (rx/of (fetch-teams)))))
+ (rx/observe-on :async))))))
;; --- EVENT: login
(defn- logged-in
+ "This is the main event that is executed once we have logged in
+ profile. The profile can proceed from standard login or from
+ accepting invitation, or third party auth signup or singin."
[profile]
- (ptk/reify ::logged-in
- IDeref
- (-deref [_] profile)
+ (letfn [(get-redirect-event []
+ (let [team-id (:default-team-id profile)]
+ (rt/nav' :dashboard-projects {:team-id team-id})))]
- ptk/WatchEvent
- (watch [_ _ _]
- (let [team-id (get-current-team-id profile)]
- (->> (rx/concat
- (rx/of (profile-fetched profile)
- (fetch-teams))
+ (ptk/reify ::logged-in
+ IDeref
+ (-deref [_] profile)
- (->> (rx/of (rt/nav' :dashboard-projects {:team-id team-id}))
- (rx/delay 1000))
-
- (when-not (get-in profile [:props :onboarding-viewed])
- (->> (rx/of (modal/show {:type :onboarding}))
- (rx/delay 1000))))
-
- (rx/observe-on :async))))))
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (when (is-authenticated? profile)
+ (->> (rx/of (profile-fetched profile)
+ (fetch-teams)
+ (get-redirect-event))
+ (rx/observe-on :async)))))))
(s/def ::login-params
(s/keys :req-un [::email ::password]))
+(declare login-from-register)
+
(defn login
[{:keys [email password] :as data}]
(us/verify ::login-params data)
(ptk/reify ::login
ptk/WatchEvent
- (watch [_ _ _]
+ (watch [_ _ stream]
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta data)
params {:email email
:password password
:scope "webapp"}]
- (->> (rx/timer 100)
- (rx/mapcat #(rp/mutation :login params))
- (rx/tap on-success)
- (rx/catch on-error)
- (rx/map (fn [profile]
- (with-meta profile
- {::ev/source "login"})))
- (rx/map logged-in))))))
+
+ ;; NOTE: We can't take the profile value from login because
+ ;; there are cases when login is successfull but the cookie is
+ ;; not set properly (because of possible misconfiguration).
+ ;; So, we proceed to make an additional call to fetch the
+ ;; profile, and ensure that cookie is set correctly. If
+ ;; profile fetch is successful, we mark the user logged in, if
+ ;; the returned profile is an NOT authenticated profile, we
+ ;; proceed to logout and show an error message.
+
+ (rx/merge
+ (->> (rp/mutation :login params)
+ (rx/map fetch-profile)
+ (rx/catch on-error))
+
+ (->> stream
+ (rx/filter profile-fetched?)
+ (rx/take 1)
+ (rx/map deref)
+ (rx/filter (complement is-authenticated?))
+ (rx/tap on-error)
+ (rx/map #(ex/raise :type :authentication))
+ (rx/observe-on :async))
+
+ (->> stream
+ (rx/filter profile-fetched?)
+ (rx/take 1)
+ (rx/map deref)
+ (rx/filter is-authenticated?)
+ (rx/map (fn [profile]
+ (with-meta profile
+ {::ev/source "login"})))
+ (rx/tap on-success)
+ (rx/map logged-in)
+ (rx/observe-on :async)))))))
(defn login-from-token
[{:keys [profile] :as tdata}]
@@ -221,44 +252,46 @@
(rx/map (fn [profile]
(with-meta profile
{::ev/source "register"})))
- (rx/map logged-in))))))
+ (rx/map logged-in)
+ (rx/observe-on :async))))))
;; --- EVENT: logout
(defn logged-out
- []
- (ptk/reify ::logged-out
- ptk/UpdateEvent
- (update [_ state]
- (select-keys state [:route :router :session-id :history]))
+ ([] (logged-out {}))
+ ([_params]
+ (ptk/reify ::logged-out
+ ptk/UpdateEvent
+ (update [_ state]
+ (select-keys state [:route :router :session-id :history]))
- ptk/WatchEvent
- (watch [_ _ _]
- (rx/of (rt/nav :auth-login)))
+ ptk/WatchEvent
+ (watch [_ _ _]
+ ;; NOTE: We need the `effect` of the current event to be
+ ;; executed before the redirect.
+ (->> (rx/of (rt/nav :auth-login))
+ (rx/observe-on :async)))
- ptk/EffectEvent
- (effect [_ _ _]
- (reset! storage {})
- (i18n/reset-locale))))
+ ptk/EffectEvent
+ (effect [_ _ _]
+ (reset! storage {})
+ (i18n/reset-locale)))))
(defn logout
- []
- (ptk/reify ::logout
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/mutation :logout)
- (rx/delay-at-least 300)
- (rx/catch (constantly (rx/of 1)))
- (rx/map logged-out)))))
+ ([] (logout {}))
+ ([params]
+ (ptk/reify ::logout
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/mutation :logout)
+ (rx/delay-at-least 300)
+ (rx/catch (constantly (rx/of 1)))
+ (rx/map #(logged-out params)))))))
;; --- EVENT: register
-;; TODO: remove
-(s/def ::invitation-token ::us/not-empty-string)
-
(s/def ::register
- (s/keys :req-un [::fullname ::password ::email]
- :opt-un [::invitation-token]))
+ (s/keys :req-un [::fullname ::password ::email]))
(defn register
"Create a register event instance."
@@ -347,20 +380,33 @@
(rx/empty)))
(rx/ignore))))))
-
(defn mark-onboarding-as-viewed
([] (mark-onboarding-as-viewed nil))
([{:keys [version]}]
(ptk/reify ::mark-oboarding-as-viewed
ptk/WatchEvent
- (watch [_ state _]
+ (watch [_ _ _]
(let [version (or version (:main @cf/version))
- props (-> (get-in state [:profile :props])
- (assoc :onboarding-viewed true)
- (assoc :release-notes-viewed version))]
+ props {:onboarding-viewed true
+ :release-notes-viewed version}]
(->> (rp/mutation :update-profile-props {:props props})
(rx/map (constantly (fetch-profile)))))))))
+
+(defn mark-questions-as-answered
+ []
+ (ptk/reify ::mark-questions-as-answered
+ ptk/UpdateEvent
+ (update [_ state]
+ (update-in state [:profile :props] assoc :onboarding-questions-answered true))
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [props {:onboarding-questions-answered true}]
+ (->> (rp/mutation :update-profile-props {:props props})
+ (rx/map (constantly (fetch-profile))))))))
+
+
;; --- Update Photo
(defn update-photo
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index e6c50d055b..be4d97e8f7 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -242,11 +242,11 @@
(watch [it state _]
(let [[path name] (cp/parse-path-name (:name typography))
typography (assoc typography :path path :name name)
- prev (get-in state [:workspace-data :typographies (:id typography)])
- rchg {:type :mod-typography
- :typography typography}
- uchg {:type :mod-typography
- :typography prev}]
+ prev (get-in state [:workspace-data :typographies (:id typography)])
+ rchg {:type :mod-typography
+ :typography typography}
+ uchg {:type :mod-typography
+ :typography prev}]
(rx/of (dwu/start-undo-transaction)
(dch/commit-changes {:redo-changes [rchg]
:undo-changes [uchg]
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index 19c667d2c8..2905546d04 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -502,7 +502,6 @@
stopper (rx/filter ms/mouse-up? stream)]
(when-not (empty? selected)
(->> ms/mouse-position
- (rx/take-until stopper)
(rx/map #(gpt/to-vec initial %))
(rx/map #(gpt/length %))
(rx/filter #(> % 1))
@@ -515,7 +514,9 @@
(rx/of (start-move-duplicate initial)
(dws/duplicate-selected false))
;; Otherwise just plain old move
- (rx/of (start-move initial selected)))))))))))
+ (rx/of (start-move initial selected)))))
+ (rx/take-until stopper)))))))
+
(defn- start-move-duplicate
[from-position]
@@ -556,7 +557,6 @@
delta)))
position (->> ms/mouse-position
- (rx/take-until stopper)
(rx/with-latest-from ms/mouse-position-shift)
(rx/map #(fix-axis %)))
@@ -575,7 +575,8 @@
(->> position
(rx/with-latest vector snap-delta)
(rx/map snap/correct-snap-point)
- (rx/map set-local-displacement))
+ (rx/map set-local-displacement)
+ (rx/take-until stopper))
(rx/of (set-modifiers ids)
(apply-modifiers ids)
diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs
index 39350c180e..6514e8332d 100644
--- a/frontend/src/app/main/errors.cljs
+++ b/frontend/src/app/main/errors.cljs
@@ -13,6 +13,7 @@
[app.main.data.users :as du]
[app.main.sentry :as sentry]
[app.main.store :as st]
+ [app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.util.timers :as ts]
[cljs.pprint :refer [pprint]]
@@ -48,7 +49,9 @@
;; here and not in app.main.errors because of circular dependency.
(defmethod ptk/handle-error :authentication
[_]
- (ts/schedule (st/emitf (du/logout))))
+ (let [msg (tr "errors.auth.unable-to-login")]
+ (st/emit! (du/logout {:capture-redirect true}))
+ (ts/schedule 500 (st/emitf (dm/warn msg)))))
;; That are special case server-errors that should be treated
@@ -78,10 +81,7 @@
(js/console.group "Validation Error:")
(ex/ignoring
(js/console.info
- (with-out-str
- (pprint (dissoc error :explain))))
- (when-let [explain (:explain error)]
- (js/console.error explain)))
+ (with-out-str (pprint error))))
(js/console.groupEnd "Validation Error:"))
@@ -135,8 +135,7 @@
(defmethod ptk/handle-error :server-error
[{:keys [data hint] :as error}]
(let [hint (or hint (:hint data) (:message data))
- info (with-out-str (pprint (dissoc data :explain)))
- expl (:explain data)
+ info (with-out-str (pprint data))
msg (str "Internal Server Error: " hint)]
(ts/schedule
@@ -147,7 +146,6 @@
(js/console.group msg)
(js/console.info info)
- (when expl (js/console.error expl))
(js/console.groupEnd msg)))
(defn on-unhandled-error
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index adc3cfaf60..efaaddaa93 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -219,7 +219,7 @@
(l/derived :options workspace-page))
(def workspace-frames
- (l/derived cp/select-frames workspace-page-objects))
+ (l/derived cp/select-frames workspace-page-objects =))
(def workspace-editor
(l/derived :workspace-editor st/state))
diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs
index 515deca043..9c1d79c451 100644
--- a/frontend/src/app/main/ui.cljs
+++ b/frontend/src/app/main/ui.cljs
@@ -6,6 +6,7 @@
(ns app.main.ui
(:require
+ [app.config :as cf]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.auth :refer [auth]]
@@ -17,6 +18,8 @@
[app.main.ui.icons :as i]
[app.main.ui.messages :as msgs]
[app.main.ui.onboarding]
+ [app.main.ui.onboarding.questions]
+ [app.main.ui.releases]
[app.main.ui.render :as render]
[app.main.ui.settings :as settings]
[app.main.ui.static :as static]
@@ -32,7 +35,7 @@
(mf/defc main-page
{::mf/wrap [#(mf/catch % {:fallback on-main-error})]}
- [{:keys [route] :as props}]
+ [{:keys [route profile]}]
(let [{:keys [data params]} route]
[:& (mf/provider ctx/current-route) {:value route}
(case (:name data)
@@ -70,13 +73,32 @@
:dashboard-font-providers
:dashboard-team-members
:dashboard-team-settings)
+
[:*
#_[:div.modal-wrapper
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
- [:& app.main.ui.onboarding/onboarding-modal]
+ #_[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding/onboarding-team-modal]
]
- [:& dashboard {:route route}]]
+ (when-let [props (some-> profile (get :props {}))]
+ (cond
+ (and cf/onboarding-form-id
+ (not (:onboarding-questions-answered props false))
+ (not (:onboarding-viewed props false)))
+
+ [:& app.main.ui.onboarding.questions/questions
+ {:profile profile
+ :form-id cf/onboarding-form-id}]
+
+ (not (:onboarding-viewed props))
+ [:& app.main.ui.onboarding/onboarding-modal {}]
+
+ (and (:onboarding-viewed props)
+ (not= (:release-notes-viewed props) (:main @cf/version))
+ (not= "0.0" (:main @cf/version)))
+ [:& app.main.ui.releases/release-notes-modal {}]))
+
+ [:& dashboard {:route route :profile profile}]]
:viewer
(let [{:keys [query-params path-params]} route
@@ -124,12 +146,14 @@
(mf/defc app
[]
- (let [route (mf/deref refs/route)
- edata (mf/deref refs/exception)]
+ (let [route (mf/deref refs/route)
+ edata (mf/deref refs/exception)
+ profile (mf/deref refs/profile)]
[:& (mf/provider ctx/current-route) {:value route}
- (if edata
- [:& static/exception-page {:data edata}]
- [:*
- [:& msgs/notifications]
- (when route
- [:& main-page {:route route}])])]))
+ [:& (mf/provider ctx/current-profile) {:value profile}
+ (if edata
+ [:& static/exception-page {:data edata}]
+ [:*
+ [:& msgs/notifications]
+ (when route
+ [:& main-page {:route route :profile profile}])])]]))
diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs
index e5a16089f2..31c1eb2302 100644
--- a/frontend/src/app/main/ui/auth/recovery_request.cljs
+++ b/frontend/src/app/main/ui/auth/recovery_request.cljs
@@ -30,8 +30,7 @@
(mf/use-callback
(fn [_ _]
(reset! submitted false)
- (st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
- (rt/nav :auth-login))))
+ (st/emit! (dm/info (tr "auth.notifications.recovery-token-sent")))))
on-error
(mf/use-callback
diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs
index 33e0ef881c..5f5158aaf6 100644
--- a/frontend/src/app/main/ui/components/numeric_input.cljs
+++ b/frontend/src/app/main/ui/components/numeric_input.cljs
@@ -162,5 +162,12 @@
(obj/set! "onKeyDown" handle-key-down)
(obj/set! "onBlur" handle-blur))]
+ (mf/use-effect
+ (mf/deps value-str)
+ (fn []
+ (when-let [input-node (mf/ref-val ref)]
+ (when-not (dom/active? input-node)
+ (dom/set-value! input-node value-str)))))
+
[:> :input props]))
diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs
index e91e4e82af..4fd5d6a1fc 100644
--- a/frontend/src/app/main/ui/context.cljs
+++ b/frontend/src/app/main/ui/context.cljs
@@ -15,8 +15,9 @@
;; for text shapes in the export process
(def text-plain-colors-ctx (mf/create-context false))
-(def current-route (mf/create-context nil))
-(def current-team-id (mf/create-context nil))
+(def current-route (mf/create-context nil))
+(def current-profile (mf/create-context nil))
+(def current-team-id (mf/create-context nil))
(def current-project-id (mf/create-context nil))
-(def current-page-id (mf/create-context nil))
-(def current-file-id (mf/create-context nil))
+(def current-page-id (mf/create-context nil))
+(def current-file-id (mf/create-context nil))
diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs
index 1292c6eafb..500cb83548 100644
--- a/frontend/src/app/main/ui/dashboard.cljs
+++ b/frontend/src/app/main/ui/dashboard.cljs
@@ -7,9 +7,7 @@
(ns app.main.ui.dashboard
(:require
[app.common.spec :as us]
- [app.config :as cf]
[app.main.data.dashboard :as dd]
- [app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
@@ -22,7 +20,6 @@
[app.main.ui.dashboard.search :refer [search-page]]
[app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
- [app.util.timers :as tm]
[rumext.alpha :as mf]))
(defn ^boolean uuid-str?
@@ -77,9 +74,8 @@
nil)])
(mf/defc dashboard
- [{:keys [route] :as props}]
- (let [profile (mf/deref refs/profile)
- section (get-in route [:data :name])
+ [{:keys [route profile] :as props}]
+ (let [section (get-in route [:data :name])
params (parse-params route)
project-id (:project-id params)
@@ -94,18 +90,8 @@
(mf/use-effect
(mf/deps team-id)
- (st/emitf (dd/initialize {:id team-id})))
-
- (mf/use-effect
- (mf/deps)
(fn []
- (let [props (:props profile)
- version (:release-notes-viewed props)]
- (when (and (:onboarding-viewed props)
- (not= version (:main @cf/version))
- (not= "0.0" (:main @cf/version)))
- (tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes
- :version (:main @cf/version)})))))))
+ (st/emit! (dd/initialize {:id team-id}))))
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id}
diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs
index 531df875e4..4ac841f2fd 100644
--- a/frontend/src/app/main/ui/dashboard/file_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs
@@ -115,7 +115,7 @@
(st/emit! (dm/success (tr "dashboard.success-move-file"))))
(if (or navigate? (not= team-id current-team-id))
(st/emit! (dd/go-to-files team-id project-id))
- (st/emit! (dd/fetch-recent-files)
+ (st/emit! (dd/fetch-recent-files team-id)
(dd/clear-selected-files))))
on-move
diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs
index f91e1de5c0..dc181f6e4f 100644
--- a/frontend/src/app/main/ui/dashboard/grid.cljs
+++ b/frontend/src/app/main/ui/dashboard/grid.cljs
@@ -327,8 +327,9 @@
on-finish-import
(mf/use-callback
+ (mf/deps (:id team))
(fn []
- (st/emit! (dd/fetch-recent-files)
+ (st/emit! (dd/fetch-recent-files (:id team))
(dd/clear-selected-files))))
import-files (use-import-file project-id on-finish-import)
@@ -366,7 +367,7 @@
on-drop-success
(fn []
(st/emit! (dm/success (tr "dashboard.success-move-file"))
- (dd/fetch-recent-files)
+ (dd/fetch-recent-files (:id team))
(dd/clear-selected-files)))
on-drop
diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs
index 76da2bc5e7..d25620b048 100644
--- a/frontend/src/app/main/ui/dashboard/projects.cljs
+++ b/frontend/src/app/main/ui/dashboard/projects.cljs
@@ -97,9 +97,10 @@
on-import
(mf/use-callback
+ (mf/deps (:id project) (:id team))
(fn []
(st/emit! (dd/fetch-files {:project-id (:id project)})
- (dd/fetch-recent-files)
+ (dd/fetch-recent-files (:id team))
(dd/clear-selected-files))))]
[:div.dashboard-project-row {:class (when first? "first")}
@@ -163,15 +164,15 @@
(mf/use-effect
(mf/deps team)
(fn []
- (when team
- (let [tname (if (:is-default team)
- (tr "dashboard.your-penpot")
- (:name team))]
- (dom/set-html-title (tr "title.dashboard.projects" tname))))))
+ (let [tname (if (:is-default team)
+ (tr "dashboard.your-penpot")
+ (:name team))]
+ (dom/set-html-title (tr "title.dashboard.projects" tname)))))
(mf/use-effect
+ (mf/deps (:id team))
(fn []
- (st/emit! (dd/fetch-recent-files)
+ (st/emit! (dd/fetch-recent-files (:id team))
(dd/clear-selected-files))))
(when (seq projects)
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs
index fef197689b..a8984d2c6c 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.cljs
+++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs
@@ -28,6 +28,7 @@
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
[app.util.router :as rt]
+ [beicon.core :as rx]
[cljs.spec.alpha :as s]
[goog.functions :as f]
[rumext.alpha :as mf]))
@@ -287,27 +288,39 @@
members-map (mf/deref refs/dashboard-team-members)
members (vals members-map)
- on-rename-clicked
- (st/emitf (modal/show :team-form {:team team}))
-
- on-leaved-success
- (fn []
- (st/emit! (modal/hide)
- (du/fetch-teams)))
-
- leave-fn
- (st/emitf (dd/leave-team (with-meta {} {:on-success on-leaved-success})))
-
- leave-and-reassign-fn
- (fn [member-id]
- (let [params {:reassign-to member-id}]
- (st/emit! (dd/go-to-projects (:default-team-id profile))
- (dd/leave-team (with-meta params {:on-success on-leaved-success})))))
-
- delete-fn
+ on-success
(fn []
(st/emit! (dd/go-to-projects (:default-team-id profile))
- (dd/delete-team (with-meta team {:on-success on-leaved-success}))))
+ (modal/hide)
+ (du/fetch-teams)))
+
+ on-error
+ (fn [{:keys [code] :as error}]
+ (condp = code
+ :no-enough-members-for-leave
+ (rx/of (dm/error (tr "errors.team-leave.insufficient-members")))
+
+ :member-does-not-exist
+ (rx/of (dm/error (tr "errors.team-leave.member-does-not-exists")))
+
+ :owner-cant-leave-team
+ (rx/of (dm/error (tr "errors.team-leave.owner-cant-leave")))
+
+ (rx/throw error)))
+
+ leave-fn
+ (fn [member-id]
+ (let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))]
+ (st/emit! (dd/leave-team (with-meta params
+ {:on-success on-success
+ :on-error on-error})))))
+ delete-fn
+ (fn []
+ (st/emit! (dd/delete-team (with-meta team {:on-success on-success
+ :on-error on-error}))))
+ on-rename-clicked
+ (fn []
+ (st/emit! (modal/show :team-form {:team team})))
on-leave-clicked
(st/emitf (modal/show
@@ -324,7 +337,7 @@
{:type ::leave-and-reassign
:profile profile
:team team
- :accept leave-and-reassign-fn})))
+ :accept leave-fn})))
on-delete-clicked
(st/emitf
@@ -501,7 +514,7 @@
[:li {:on-click (partial on-click :settings-password)}
[:span.icon i/lock]
[:span.text (tr "labels.password")]]
- [:li {:on-click (partial on-click (du/logout))}
+ [:li {:on-click #(on-click (du/logout) %)}
[:span.icon i/exit]
[:span.text (tr "labels.logout")]]
@@ -509,7 +522,7 @@
[:li.feedback {:on-click (partial on-click :settings-feedback)}
[:span.icon i/msg-info]
[:span.text (tr "labels.give-feedback")]
- [:span.primary-badge "ALPHA"]])]]]
+ ])]]]
(when (and team profile)
[:& comments-section {:profile profile
diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs
index d70f633411..0f31f117d3 100644
--- a/frontend/src/app/main/ui/onboarding.cljs
+++ b/frontend/src/app/main/ui/onboarding.cljs
@@ -6,32 +6,16 @@
(ns app.main.ui.onboarding
(:require
- [app.common.spec :as us]
[app.config :as cf]
- [app.main.data.dashboard :as dd]
- [app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
- [app.main.refs :as refs]
[app.main.store :as st]
- [app.main.ui.components.forms :as fm]
- [app.main.ui.icons :as i]
+ [app.main.ui.onboarding.questions]
+ [app.main.ui.onboarding.team-choice]
+ [app.main.ui.onboarding.templates]
[app.main.ui.releases.common :as rc]
- [app.main.ui.releases.v1-10]
- [app.main.ui.releases.v1-4]
- [app.main.ui.releases.v1-5]
- [app.main.ui.releases.v1-6]
- [app.main.ui.releases.v1-7]
- [app.main.ui.releases.v1-8]
- [app.main.ui.releases.v1-9]
- [app.util.dom :as dom]
- [app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
- [app.util.object :as obj]
- [app.util.router :as rt]
[app.util.timers :as tm]
- [beicon.core :as rx]
- [cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
;; --- ONBOARDING LIGHTBOX
@@ -189,297 +173,3 @@
:slide @slide
:navigate navigate
:skip skip)))]]))
-
-(s/def ::name ::us/not-empty-string)
-(s/def ::team-form
- (s/keys :req-un [::name]))
-
-(mf/defc onboarding-choice-modal
- {::mf/register modal/components
- ::mf/register-as :onboarding-choice}
- []
- (let [;; When user choices the option of `fly solo`, we proceed to show
- ;; the onboarding templates modal.
- on-fly-solo
- (fn []
- (tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
-
- ;; When user choices the option of `team up`, we proceed to show
- ;; the team creation modal.
- on-team-up
- (fn []
- (st/emit! (modal/show {:type :onboarding-team})))
- ]
-
- [:div.modal-overlay
- [:div.modal-container.onboarding.final.animated.fadeInUp
- [:div.modal-top
- [:h1 (tr "onboarding.choice.title")]
- [:p (tr "onboarding.choice.desc")]]
- [:div.modal-columns
- [:div.modal-left
- [:div.content-button {:on-click on-fly-solo}
- [:h2 (tr "onboarding.choice.fly-solo")]
- [:p (tr "onboarding.choice.fly-solo-desc")]]]
- [:div.modal-right
- [:div.content-button {:on-click on-team-up}
- [:h2 (tr "onboarding.choice.team-up")]
- [:p (tr "onboarding.choice.team-up-desc")]]]]
- [:img.deco {:src "images/deco-left.png" :border "0"}]
- [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
-
-(mf/defc onboarding-team-modal
- {::mf/register modal/components
- ::mf/register-as :onboarding-team}
- []
- (let [form (fm/use-form :spec ::team-form
- :initial {})
- on-submit
- (mf/use-callback
- (fn [form _]
- (let [tname (get-in @form [:clean-data :name])]
- (st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))]
-
- [:div.modal-overlay
- [:div.modal-container.onboarding-team
- [:div.title
- [:h2 (tr "onboarding.choice.team-up")]
- [:p (tr "onboarding.choice.team-up-desc")]]
-
- [:& fm/form {:form form
- :on-submit on-submit}
-
- [:div.team-row
- [:& fm/input {:type "text"
- :name :name
- :label (tr "onboarding.team-input-placeholder")}]]
-
- [:div.buttons
- [:button.btn-secondary.btn-large
- {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
- (tr "labels.cancel")]
- [:& fm/submit-button
- {:label (tr "labels.next")}]]]
-
- [:img.deco {:src "images/deco-left.png" :border "0"}]
- [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
-
-(defn get-available-roles
- []
- [{:value "editor" :label (tr "labels.editor")}
- {:value "admin" :label (tr "labels.admin")}])
-
-(s/def ::email ::us/email)
-(s/def ::role ::us/keyword)
-(s/def ::invite-form
- (s/keys :req-un [::role ::email]))
-
-;; This is the final step of team creation, consists in provide a
-;; shortcut for invite users.
-
-(mf/defc onboarding-team-invitations-modal
- {::mf/register modal/components
- ::mf/register-as :onboarding-team-invitations}
- [{:keys [name] :as props}]
- (let [initial (mf/use-memo (constantly
- {:role "editor"
- :name name}))
- form (fm/use-form :spec ::invite-form
- :initial initial)
-
- roles (mf/use-memo #(get-available-roles))
-
- on-success
- (mf/use-callback
- (fn [_form response]
- (let [project-id (:default-project-id response)
- team-id (:id response)]
- (st/emit!
- (modal/hide)
- (rt/nav :dashboard-projects {:team-id team-id}))
- (tm/schedule 400 #(st/emit!
- (modal/show {:type :onboarding-templates
- :project-id project-id}))))))
-
- on-error
- (mf/use-callback
- (fn [_form _response]
- (st/emit! (dm/error "Error on creating team."))))
-
- ;; The SKIP branch only creates the team, without invitations
- on-skip
- (mf/use-callback
- (fn [_]
- (let [mdata {:on-success (partial on-success form)
- :on-error (partial on-error form)}
- params {:name name}]
- (st/emit! (dd/create-team (with-meta params mdata))))))
-
- ;; The SUBMIT branch creates the team with the invitations
- on-submit
- (mf/use-callback
- (fn [form _]
- (let [mdata {:on-success (partial on-success form)
- :on-error (partial on-error form)}
- params (:clean-data @form)]
- (st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))]
-
- [:div.modal-overlay
- [:div.modal-container.onboarding-team
- [:div.title
- [:h2 (tr "onboarding.choice.team-up")]
- [:p (tr "onboarding.choice.team-up-desc")]]
-
- [:& fm/form {:form form
- :on-submit on-submit}
-
- [:div.invite-row
- [:& fm/input {:name :email
- :label (tr "labels.email")}]
- [:& fm/select {:name :role
- :options roles}]]
-
- [:div.buttons
- [:button.btn-secondary.btn-large
- {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
- (tr "labels.cancel")]
- [:& fm/submit-button
- {:label (tr "labels.create")}]]
- [:div.skip-action
- {:on-click on-skip}
- [:div.action "Skip and invite later"]]]
- [:img.deco {:src "images/deco-left.png" :border "0"}]
- [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
-
-(mf/defc template-item
- [{:keys [name path image project-id]}]
- (let [downloading? (mf/use-state false)
- link (str (assoc cf/public-uri :path path))
-
- on-finish-import
- (fn []
- (st/emit! (dd/fetch-files {:project-id project-id})
- (dd/fetch-recent-files)
- (dd/clear-selected-files)))
-
- open-import-modal
- (fn [file]
- (st/emit! (modal/show
- {:type :import
- :project-id project-id
- :files [file]
- :on-finish-import on-finish-import})))
- on-click
- (fn []
- (reset! downloading? true)
- (->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
- (rx/subs (fn [{:keys [body] :as response}]
- (open-import-modal {:name name :uri (dom/create-uri body)}))
- (fn [error]
- (js/console.log "error" error))
- (fn []
- (reset! downloading? false)))))
- ]
-
- [:div.template-item
- [:div.template-item-content
- [:img {:src image}]]
- [:div.template-item-title
- [:div.label name]
- (if @downloading?
- [:div.action "Fetching..."]
- [:div.action {:on-click on-click} "+ Add to drafts"])]]))
-
-(mf/defc onboarding-templates-modal
- {::mf/register modal/components
- ::mf/register-as :onboarding-templates}
- ;; NOTE: the project usually comes empty, it only comes fullfilled
- ;; when a user creates a new team just after signup.
- [{:keys [project-id] :as props}]
- (let [close-fn (mf/use-callback #(st/emit! (modal/hide)))
- profile (mf/deref refs/profile)
- project-id (or project-id (:default-project-id profile))]
- [:div.modal-overlay
- [:div.modal-container.onboarding-templates
- [:div.modal-header
- [:div.modal-close-button
- {:on-click close-fn} i/close]]
-
- [:div.modal-content
- [:h3 (tr "onboarding.templates.title")]
- [:p (tr "onboarding.templates.subtitle")]
-
- [:div.templates
- [:& template-item
- {:path "/github/penpot-files/Penpot-Design-system.penpot"
- :image "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
- :name "Penpot Design System"
- :project-id project-id}]
- [:& template-item
- {:path "/github/penpot-files/Material-Design-Kit.penpot"
- :image "https://penpot.app/images/libraries/cover-material.jpg"
- :name "Material Design Kit"
- :project-id project-id}]]]]]))
-
-
-;;; --- RELEASE NOTES MODAL
-
-(mf/defc release-notes
- [{:keys [version] :as props}]
- (let [slide (mf/use-state :start)
- klass (mf/use-state "fadeInDown")
-
- navigate
- (mf/use-callback #(reset! slide %))
-
- next
- (mf/use-callback
- (mf/deps slide)
- (fn []
- (if (= @slide :start)
- (navigate 0)
- (navigate (inc @slide)))))
-
- finish
- (mf/use-callback
- (st/emitf (modal/hide)
- (du/mark-onboarding-as-viewed {:version version})))
- ]
-
- (mf/use-effect
- (mf/deps)
- (fn []
- (st/emitf (du/mark-onboarding-as-viewed {:version version}))))
-
- (mf/use-layout-effect
- (mf/deps @slide)
- (fn []
- (when (not= :start @slide)
- (reset! klass "fadeIn"))
- (let [sem (tm/schedule 300 #(reset! klass nil))]
- (fn []
- (reset! klass nil)
- (tm/dispose! sem)))))
-
- (rc/render-release-notes
- {:next next
- :navigate navigate
- :finish finish
- :klass klass
- :slide slide
- :version version})))
-
-(mf/defc release-notes-modal
- {::mf/wrap-props false
- ::mf/register modal/components
- ::mf/register-as :release-notes}
- [props]
- (let [versions (methods rc/render-release-notes)
- version (obj/get props "version")]
- (when (contains? versions version)
- [:div.relnotes
- [:> release-notes props]])))
-
-(defmethod rc/render-release-notes "0.0"
- [params]
- (rc/render-release-notes (assoc params :version "1.10")))
diff --git a/frontend/src/app/main/ui/onboarding/questions.cljs b/frontend/src/app/main/ui/onboarding/questions.cljs
new file mode 100644
index 0000000000..7a70b8660c
--- /dev/null
+++ b/frontend/src/app/main/ui/onboarding/questions.cljs
@@ -0,0 +1,48 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.ui.onboarding.questions
+ "External form for onboarding questions."
+ (:require
+ [app.main.data.users :as du]
+ [app.main.store :as st]
+ [app.util.dom :as dom]
+ [goog.events :as ev]
+ [promesa.core :as p]
+ [rumext.alpha :as mf]))
+
+(defn load-arengu-sdk
+ [container-ref email form-id]
+ (letfn [(on-init []
+ (when-let [container (mf/ref-val container-ref)]
+ (-> (.embed js/ArenguForms form-id container)
+ (p/then (fn [form]
+ (.setHiddenField ^js form "email" email))))))
+
+ (on-submit-success [_event]
+ (st/emit! (du/mark-questions-as-answered)))
+ ]
+
+ (let [script (dom/create-element "script")
+ head (unchecked-get js/document "head")
+ lkey1 (ev/listen js/document "af-submitForm-success" on-submit-success)]
+
+ (unchecked-set script "src" "https://sdk.arengu.com/forms.js")
+ (unchecked-set script "onload" on-init)
+ (dom/append-child! head script)
+
+ (fn []
+ (ev/unlistenByKey lkey1)))))
+
+(mf/defc questions
+ [{:keys [profile form-id]}]
+ (let [container (mf/use-ref)]
+ (mf/use-effect (partial load-arengu-sdk container (:email profile) form-id))
+ [:div.modal-wrapper.questions-form
+ [:div.modal-overlay
+ [:div.modal-container {:ref container}]]]))
+
+
diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs
new file mode 100644
index 0000000000..6e5961a8fa
--- /dev/null
+++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs
@@ -0,0 +1,181 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.ui.onboarding.team-choice
+ (:require
+ [app.common.spec :as us]
+ [app.main.data.dashboard :as dd]
+ [app.main.data.messages :as dm]
+ [app.main.data.modal :as modal]
+ [app.main.store :as st]
+ [app.main.ui.components.forms :as fm]
+ [app.util.i18n :as i18n :refer [tr]]
+ [app.util.router :as rt]
+ [app.util.timers :as tm]
+ [cljs.spec.alpha :as s]
+ [rumext.alpha :as mf]))
+
+(s/def ::name ::us/not-empty-string)
+(s/def ::team-form
+ (s/keys :req-un [::name]))
+
+(mf/defc onboarding-choice-modal
+ {::mf/register modal/components
+ ::mf/register-as :onboarding-choice}
+ []
+ (let [;; When user choices the option of `fly solo`, we proceed to show
+ ;; the onboarding templates modal.
+ on-fly-solo
+ (fn []
+ (tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
+
+ ;; When user choices the option of `team up`, we proceed to show
+ ;; the team creation modal.
+ on-team-up
+ (fn []
+ (st/emit! (modal/show {:type :onboarding-team})))
+ ]
+
+ [:div.modal-overlay
+ [:div.modal-container.onboarding.final.animated.fadeInUp
+ [:div.modal-top
+ [:h1 (tr "onboarding.welcome.title")]
+ [:p (tr "onboarding.welcome.desc3")]]
+ [:div.modal-columns
+ [:div.modal-left
+ [:div.content-button {:on-click on-fly-solo}
+ [:h2 (tr "onboarding.choice.fly-solo")]
+ [:p (tr "onboarding.choice.fly-solo-desc")]]]
+ [:div.modal-right
+ [:div.content-button {:on-click on-team-up}
+ [:h2 (tr "onboarding.choice.team-up")]
+ [:p (tr "onboarding.choice.team-up-desc")]]]]
+ [:img.deco {:src "images/deco-left.png" :border "0"}]
+ [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
+
+(mf/defc onboarding-team-modal
+ {::mf/register modal/components
+ ::mf/register-as :onboarding-team}
+ []
+ (let [form (fm/use-form :spec ::team-form
+ :initial {})
+ on-submit
+ (mf/use-callback
+ (fn [form _]
+ (let [tname (get-in @form [:clean-data :name])]
+ (st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))]
+
+ [:div.modal-overlay
+ [:div.modal-container.onboarding-team
+ [:div.title
+ [:h2 (tr "onboarding.choice.team-up")]
+ [:p (tr "onboarding.choice.team-up-desc")]]
+
+ [:& fm/form {:form form
+ :on-submit on-submit}
+
+ [:div.team-row
+ [:& fm/input {:type "text"
+ :name :name
+ :label (tr "onboarding.team-input-placeholder")}]]
+
+ [:div.buttons
+ [:button.btn-secondary.btn-large
+ {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
+ (tr "labels.cancel")]
+ [:& fm/submit-button
+ {:label (tr "labels.next")}]]]
+
+ [:img.deco {:src "images/deco-left.png" :border "0"}]
+ [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
+
+(defn get-available-roles
+ []
+ [{:value "editor" :label (tr "labels.editor")}
+ {:value "admin" :label (tr "labels.admin")}])
+
+(s/def ::email ::us/email)
+(s/def ::role ::us/keyword)
+(s/def ::invite-form
+ (s/keys :req-un [::role ::email]))
+
+;; This is the final step of team creation, consists in provide a
+;; shortcut for invite users.
+
+(mf/defc onboarding-team-invitations-modal
+ {::mf/register modal/components
+ ::mf/register-as :onboarding-team-invitations}
+ [{:keys [name] :as props}]
+ (let [initial (mf/use-memo (constantly
+ {:role "editor"
+ :name name}))
+ form (fm/use-form :spec ::invite-form
+ :initial initial)
+
+ roles (mf/use-memo #(get-available-roles))
+
+ on-success
+ (mf/use-callback
+ (fn [_form response]
+ (let [project-id (:default-project-id response)
+ team-id (:id response)]
+ (st/emit!
+ (modal/hide)
+ (rt/nav :dashboard-projects {:team-id team-id}))
+ (tm/schedule 400 #(st/emit!
+ (modal/show {:type :onboarding-templates
+ :project-id project-id}))))))
+
+ on-error
+ (mf/use-callback
+ (fn [_form _response]
+ (st/emit! (dm/error "Error on creating team."))))
+
+ ;; The SKIP branch only creates the team, without invitations
+ on-skip
+ (mf/use-callback
+ (fn [_]
+ (let [mdata {:on-success (partial on-success form)
+ :on-error (partial on-error form)}
+ params {:name name}]
+ (st/emit! (dd/create-team (with-meta params mdata))))))
+
+ ;; The SUBMIT branch creates the team with the invitations
+ on-submit
+ (mf/use-callback
+ (fn [form _]
+ (let [mdata {:on-success (partial on-success form)
+ :on-error (partial on-error form)}
+ params (:clean-data @form)]
+ (st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))]
+
+ [:div.modal-overlay
+ [:div.modal-container.onboarding-team
+ [:div.title
+ [:h2 (tr "onboarding.choice.team-up")]
+ [:p (tr "onboarding.choice.team-up-desc")]]
+
+ [:& fm/form {:form form
+ :on-submit on-submit}
+
+ [:div.invite-row
+ [:& fm/input {:name :email
+ :label (tr "labels.email")}]
+ [:& fm/select {:name :role
+ :options roles}]]
+
+ [:div.buttons
+ [:button.btn-secondary.btn-large
+ {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
+ (tr "labels.cancel")]
+ [:& fm/submit-button
+ {:label (tr "labels.create")}]]
+ [:div.skip-action
+ {:on-click on-skip}
+ [:div.action "Skip and invite later"]]]
+ [:img.deco {:src "images/deco-left.png" :border "0"}]
+ [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
+
diff --git a/frontend/src/app/main/ui/onboarding/templates.cljs b/frontend/src/app/main/ui/onboarding/templates.cljs
new file mode 100644
index 0000000000..91a886d346
--- /dev/null
+++ b/frontend/src/app/main/ui/onboarding/templates.cljs
@@ -0,0 +1,88 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.ui.onboarding.templates
+ (:require
+ [app.config :as cf]
+ [app.main.data.dashboard :as dd]
+ [app.main.data.modal :as modal]
+ [app.main.refs :as refs]
+ [app.main.store :as st]
+ [app.main.ui.icons :as i]
+ [app.util.dom :as dom]
+ [app.util.http :as http]
+ [app.util.i18n :as i18n :refer [tr]]
+ [beicon.core :as rx]
+ [rumext.alpha :as mf]))
+
+(mf/defc template-item
+ [{:keys [name path image project-id]}]
+ (let [downloading? (mf/use-state false)
+ link (str (assoc cf/public-uri :path path))
+
+ on-finish-import
+ (fn []
+ (st/emit! (dd/fetch-recent-files)))
+
+ open-import-modal
+ (fn [file]
+ (st/emit! (modal/show
+ {:type :import
+ :project-id project-id
+ :files [file]
+ :on-finish-import on-finish-import})))
+ on-click
+ (fn []
+ (reset! downloading? true)
+ (->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
+ (rx/subs (fn [{:keys [body] :as response}]
+ (open-import-modal {:name name :uri (dom/create-uri body)}))
+ (fn [error]
+ (js/console.log "error" error))
+ (fn []
+ (reset! downloading? false)))))
+ ]
+
+ [:div.template-item
+ [:div.template-item-content
+ [:img {:src image}]]
+ [:div.template-item-title
+ [:div.label name]
+ (if @downloading?
+ [:div.action "Fetching..."]
+ [:div.action {:on-click on-click} "+ Add to drafts"])]]))
+
+(mf/defc onboarding-templates-modal
+ {::mf/wrap-props false
+ ::mf/register modal/components
+ ::mf/register-as :onboarding-templates}
+ ;; NOTE: the project usually comes empty, it only comes fullfilled
+ ;; when a user creates a new team just after signup.
+ [{:keys [project-id] :as props}]
+ (let [close-fn (mf/use-callback #(st/emit! (modal/hide)))
+ profile (mf/deref refs/profile)
+ project-id (or project-id (:default-project-id profile))]
+ [:div.modal-overlay
+ [:div.modal-container.onboarding-templates
+ [:div.modal-header
+ [:div.modal-close-button
+ {:on-click close-fn} i/close]]
+
+ [:div.modal-content
+ [:h3 (tr "onboarding.templates.title")]
+ [:p (tr "onboarding.templates.subtitle")]
+
+ [:div.templates
+ [:& template-item
+ {:path "/github/penpot-files/Penpot-Design-system.penpot"
+ :image "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
+ :name "Penpot Design System"
+ :project-id project-id}]
+ [:& template-item
+ {:path "/github/penpot-files/Material-Design-Kit.penpot"
+ :image "https://penpot.app/images/libraries/cover-material.jpg"
+ :name "Material Design Kit"
+ :project-id project-id}]]]]]))
diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs
new file mode 100644
index 0000000000..1fee4c1d2c
--- /dev/null
+++ b/frontend/src/app/main/ui/releases.cljs
@@ -0,0 +1,84 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.ui.releases
+ (:require
+ [app.main.data.modal :as modal]
+ [app.main.data.users :as du]
+ [app.main.store :as st]
+ [app.main.ui.releases.common :as rc]
+ [app.main.ui.releases.v1-10]
+ [app.main.ui.releases.v1-4]
+ [app.main.ui.releases.v1-5]
+ [app.main.ui.releases.v1-6]
+ [app.main.ui.releases.v1-7]
+ [app.main.ui.releases.v1-8]
+ [app.main.ui.releases.v1-9]
+ [app.util.object :as obj]
+ [app.util.timers :as tm]
+ [rumext.alpha :as mf]))
+
+;;; --- RELEASE NOTES MODAL
+
+(mf/defc release-notes
+ [{:keys [version] :as props}]
+ (let [slide (mf/use-state :start)
+ klass (mf/use-state "fadeInDown")
+
+ navigate
+ (mf/use-callback #(reset! slide %))
+
+ next
+ (mf/use-callback
+ (mf/deps slide)
+ (fn []
+ (if (= @slide :start)
+ (navigate 0)
+ (navigate (inc @slide)))))
+
+ finish
+ (mf/use-callback
+ (st/emitf (modal/hide)
+ (du/mark-onboarding-as-viewed {:version version})))
+ ]
+
+ (mf/use-effect
+ (mf/deps)
+ (fn []
+ (st/emitf (du/mark-onboarding-as-viewed {:version version}))))
+
+ (mf/use-layout-effect
+ (mf/deps @slide)
+ (fn []
+ (when (not= :start @slide)
+ (reset! klass "fadeIn"))
+ (let [sem (tm/schedule 300 #(reset! klass nil))]
+ (fn []
+ (reset! klass nil)
+ (tm/dispose! sem)))))
+
+ (rc/render-release-notes
+ {:next next
+ :navigate navigate
+ :finish finish
+ :klass klass
+ :slide slide
+ :version version})))
+
+(mf/defc release-notes-modal
+ {::mf/wrap-props false
+ ::mf/register modal/components
+ ::mf/register-as :release-notes}
+ [props]
+ (let [versions (methods rc/render-release-notes)
+ version (obj/get props "version")]
+ (when (contains? versions version)
+ [:div.relnotes
+ [:> release-notes props]])))
+
+(defmethod rc/render-release-notes "0.0"
+ [params]
+ (rc/render-release-notes (assoc params :version "1.10")))
diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs
index fc1fe6e085..e604a4a5b2 100644
--- a/frontend/src/app/main/ui/static.cljs
+++ b/frontend/src/app/main/ui/static.cljs
@@ -6,10 +6,9 @@
(ns app.main.ui.static
(:require
- [app.main.data.users :as du]
- [app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as i]
+ [app.util.globals :as globals]
[app.util.i18n :refer [tr]]
[app.util.object :as obj]
[app.util.router :as rt]
@@ -19,14 +18,7 @@
{::mf/wrap-props false}
[props]
(let [children (obj/get props "children")
- on-click (mf/use-callback
- (fn []
- (let [profile (deref refs/profile)]
- (if (du/is-authenticated? profile)
- (let [team-id (du/get-current-team-id profile)]
- (st/emit! (rt/nav :dashboard-projects {:team-id team-id})))
- (st/emit! (rt/nav :auth-login {}))))))]
-
+ on-click (mf/use-callback #(set! (.-href globals/location) ""))]
[:section.exception-layout
[:div.exception-header
{:on-click on-click}
diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs
index 5b817509f7..828a60913d 100644
--- a/frontend/src/app/main/ui/workspace/header.cljs
+++ b/frontend/src/app/main/ui/workspace/header.cljs
@@ -287,8 +287,8 @@
(when (contains? @cf/flags :user-feedback)
[:li.feedback {:on-click (st/emitf (rt/nav :settings-feedback))}
- [:span (tr "labels.give-feedback")]
- [:span.primary-badge "ALPHA"]])
+ [:span (tr "labels.give-feedback")]])
+
]]]))
;; --- Header Component
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
index b7933d320b..0fa376e891 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.cljs
@@ -64,7 +64,6 @@
(fn [value]
(on-change (assoc-in grid keys-path value))))
- ;; TODO: remove references to :auto
handle-change-size
(mf/use-fn
(mf/deps grid)
@@ -75,7 +74,6 @@
(-> (gg/calculate-default-item-length frame-length margin gutter)
(mth/precision 2))
item-length)]
-
(-> grid
(update :params assoc :size size :item-length item-length)
(on-change)))))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
index 73e29eae9b..fc87a2b3d3 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
@@ -20,13 +20,14 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
-(def measure-attrs [:proportion-lock
- :width :height
- :x :y
- :rotation
- :rx :ry
- :r1 :r2 :r3 :r4
- :selrect])
+(def measure-attrs
+ [:proportion-lock
+ :width :height
+ :x :y
+ :rotation
+ :rx :ry
+ :r1 :r2 :r3 :r4
+ :selrect])
(defn- attr->string [attr values]
(let [value (attr values)]
diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs
index 53d82208af..f4dd216f65 100644
--- a/frontend/src/app/main/ui/workspace/viewport.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport.cljs
@@ -294,7 +294,7 @@
(when show-grids?
[:& frame-grid/frame-grid
- {:zoom zoom}])
+ {:zoom zoom :selected selected :transform transform}])
(when show-pixel-grid?
[:& widgets/pixel-grid
diff --git a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs
index 5cd49cc681..aac177eec2 100644
--- a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs
@@ -11,7 +11,6 @@
[app.common.uuid :as uuid]
[app.main.refs :as refs]
[app.util.geom.grid :as gg]
- [okulary.core :as l]
[rumext.alpha :as mf]))
(mf/defc square-grid [{:keys [frame zoom grid] :as props}]
@@ -42,7 +41,8 @@
:height (:height frame)
:fill (str "url(#" grid-id ")")}]]))
-(mf/defc layout-grid [{:keys [key frame grid]}]
+(mf/defc layout-grid
+ [{:keys [key frame grid]}]
(let [{color-value :color color-opacity :opacity} (-> grid :params :color)
;; Support for old color format
color-value (or color-value (:value (get-in grid [:params :color :value])))
@@ -56,42 +56,37 @@
:strokeOpacity color-opacity
:fill "none"})]
[:g.grid
- (for [{:keys [x y width height]} (gg/grid-areas frame grid)]
- (do
- [:rect {:key (str key "-" x "-" y)
- :x (mth/round x)
- :y (mth/round y)
- :width (- (mth/round (+ x width)) (mth/round x))
- :height (- (mth/round (+ y height)) (mth/round y))
- :style style}]))]))
+ (for [{:keys [x y width height] :as area} (gg/grid-areas frame grid)]
+ [:rect {:key (str key "-" x "-" y)
+ :x (mth/round x)
+ :y (mth/round y)
+ :width (- (mth/round (+ x width)) (mth/round x))
+ :height (- (mth/round (+ y height)) (mth/round y))
+ :style style}])]))
-(mf/defc grid-display-frame [{:keys [frame zoom]}]
- (let [grids (:grids frame)]
- (for [[index {:keys [type display] :as grid}] (map-indexed vector grids)]
- (let [props #js {:key (str (:id frame) "-grid-" index)
- :frame frame
- :zoom zoom
- :grid grid}]
- (when display
- (case type
- :square [:> square-grid props]
- :column [:> layout-grid props]
- :row [:> layout-grid props]))))))
-
-
-(def shapes-moving-ref
- (let [moving-shapes (fn [local]
- (when (= :move (:transform local))
- (:selected local)))]
- (l/derived moving-shapes refs/workspace-local)))
+(mf/defc grid-display-frame
+ [{:keys [frame zoom]}]
+ (for [[index grid] (->> (:grids frame)
+ (filter :display)
+ (map-indexed vector))]
+ (let [props #js {:key (str (:id frame) "-grid-" index)
+ :frame frame
+ :zoom zoom
+ :grid grid}]
+ (case (:type grid)
+ :square [:> square-grid props]
+ :column [:> layout-grid props]
+ :row [:> layout-grid props]))))
(mf/defc frame-grid
{::mf/wrap [mf/memo]}
- [{:keys [zoom]}]
- (let [frames (mf/deref refs/workspace-frames)
- shapes-moving (mf/deref shapes-moving-ref)]
+ [{:keys [zoom transform selected]}]
+ (let [frames (mf/deref refs/workspace-frames)
+ moving (when (= :move transform) selected)
+ is-moving? #(contains? moving (:id %))]
+
[:g.grid-display {:style {:pointer-events "none"}}
- (for [frame (->> frames (remove #(contains? shapes-moving (:id %))))]
+ (for [frame (remove is-moving? frames)]
[:& grid-display-frame {:key (str "grid-" (:id frame))
:zoom zoom
:frame (gsh/transform-shape frame)}])]))
diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs
index 82a94e0ed6..398961057a 100644
--- a/frontend/src/app/util/dom.cljs
+++ b/frontend/src/app/util/dom.cljs
@@ -166,7 +166,7 @@
(defn append-child!
[el child]
- (.appendChild el child))
+ (.appendChild ^js el child))
(defn get-first-child
[el]
diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs
index d2b5d5ce99..c97e384fce 100644
--- a/frontend/src/app/util/forms.cljs
+++ b/frontend/src/app/util/forms.cljs
@@ -37,10 +37,16 @@
[& {:keys [initial] :as opts}]
(let [state (mf/useState 0)
render (aget state 1)
- state-ref (mf/use-ref {:data (if (fn? initial) (initial) initial)
- :errors {}
- :touched {}})
- form (mf/use-memo #(create-form-mutator state-ref render opts))]
+
+ get-state (mf/use-callback
+ (mf/deps initial)
+ (fn []
+ {:data (if (fn? initial) (initial) initial)
+ :errors {}
+ :touched {}}))
+
+ state-ref (mf/use-ref (get-state))
+ form (mf/use-memo (mf/deps initial) #(create-form-mutator state-ref render get-state opts))]
(mf/use-effect
(mf/deps initial)
@@ -72,7 +78,7 @@
(not= cleaned ::s/invalid))))))
(defn- create-form-mutator
- [state-ref render opts]
+ [state-ref render get-state opts]
(reify
IDeref
(-deref [_]
@@ -80,7 +86,9 @@
IReset
(-reset! [it new-value]
- (mf/set-ref-val! state-ref new-value)
+ (if (nil? new-value)
+ (mf/set-ref-val! state-ref (get-state))
+ (mf/set-ref-val! state-ref new-value))
(render inc))
ISwap
diff --git a/frontend/src/app/util/geom/grid.cljs b/frontend/src/app/util/geom/grid.cljs
index 97496d18f6..60cb715e9c 100644
--- a/frontend/src/app/util/geom/grid.cljs
+++ b/frontend/src/app/util/geom/grid.cljs
@@ -23,61 +23,65 @@
frame-length-no-margins (- frame-length (+ margin (- margin gutter)))]
(mth/floor (/ frame-length-no-margins (+ item-length gutter)))))
+(defn- calculate-generic-grid
+ [v width {:keys [size gutter margin item-length type]}]
+ (let [size (if (number? size)
+ size
+ (calculate-size width item-length margin gutter))
+ parts (/ width size)
+
+ width' (min (or item-length ##Inf) (+ parts (- gutter) (/ gutter size) (- (/ (* margin 2) size))))
+
+ offset (case type
+ :right (- width (* width' size) (* gutter (dec size)) margin)
+ :center (/ (- width (* width' size) (* gutter (dec size))) 2)
+ margin)
+
+ gutter (if (= :stretch type)
+ (let [gutter (/ (- width (* width' size) (* margin 2)) (dec size))]
+ (if (mth/finite? gutter) gutter 0))
+ gutter)
+
+ next-v (fn [cur-val]
+ (+ offset v (* (+ width' gutter) cur-val)))]
+
+ [size width' next-v]))
+
(defn- calculate-column-grid
- [{:keys [width height x y] :as frame} {:keys [size gutter margin item-length type] :as params}]
- (let [size (if (number? size) size (calculate-size width item-length margin gutter))
- parts (/ width size)
- item-width (min (or item-length ##Inf) (+ parts (- gutter) (/ gutter size) (- (/ (* margin 2) size))))
- item-height height
- initial-offset (case type
- :right (- width (* item-width size) (* gutter (dec size)) margin)
- :center (/ (- width (* item-width size) (* gutter (dec size))) 2)
- margin)
- gutter (if (= :stretch type) (/ (- width (* item-width size) (* margin 2)) (dec size)) gutter)
- next-x (fn [cur-val] (+ initial-offset x (* (+ item-width gutter) cur-val)))
- next-y (fn [_] y)]
- [size item-width item-height next-x next-y]))
+ [{:keys [width height x y] :as frame} params]
+ (let [[size width next-x] (calculate-generic-grid x width params)]
+ [size width height next-x (constantly y)]))
(defn- calculate-row-grid
- [{:keys [width height x y] :as frame} {:keys [size gutter margin item-length type] :as params}]
- (let [size (if (number? size) size (calculate-size height item-length margin gutter))
- parts (/ height size)
- item-width width
- item-height (min (or item-length ##Inf) (+ parts (- gutter) (/ gutter size) (- (/ (* margin 2) size))))
- initial-offset (case type
- :right (- height (* item-height size) (* gutter (dec size)) margin)
- :center (/ (- height (* item-height size) (* gutter (dec size))) 2)
- margin)
- gutter (if (= :stretch type) (/ (- height (* item-height size) (* margin 2)) (dec size)) gutter)
- next-x (fn [_] x)
- next-y (fn [cur-val] (+ initial-offset y (* (+ item-height gutter) cur-val)))]
- [size item-width item-height next-x next-y]))
+ [{:keys [width height x y] :as frame} params]
+ (let [[size height next-y] (calculate-generic-grid y height params)]
+ [size width height (constantly x) next-y]))
(defn- calculate-square-grid
[{:keys [width height x y] :as frame} {:keys [size] :as params}]
- (let [col-size (quot width size)
- row-size (quot height size)
+ (let [col-size (quot width size)
+ row-size (quot height size)
as-row-col (fn [value] [(quot value col-size) (rem value col-size)])
- next-x (fn [cur-val]
- (let [[_ col] (as-row-col cur-val)] (+ x (* col size))))
- next-y (fn [cur-val]
- (let [[row _] (as-row-col cur-val)] (+ y (* row size))))]
+ next-x (fn [cur-val]
+ (let [[_ col] (as-row-col cur-val)] (+ x (* col size))))
+ next-y (fn [cur-val]
+ (let [[row _] (as-row-col cur-val)] (+ y (* row size))))]
+
[(* col-size row-size) size size next-x next-y]))
(defn grid-areas
"Given a frame and the grid parameters returns the areas defined on the grid"
[frame grid]
(let [grid-fn (case (-> grid :type)
- :column calculate-column-grid
- :row calculate-row-grid
- :square calculate-square-grid)
+ :column calculate-column-grid
+ :row calculate-row-grid
+ :square calculate-square-grid)
[num-items item-width item-height next-x next-y] (grid-fn frame (-> grid :params))]
- (->>
- (range 0 num-items)
- (map #(hash-map :x (next-x %)
- :y (next-y %)
- :width item-width
- :height item-height)))))
+ (->> (range 0 num-items)
+ (map #(hash-map :x (next-x %)
+ :y (next-y %)
+ :width item-width
+ :height item-height)))))
(defn grid-area-points
[{:keys [x y width height]}]
diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs
index 41ac92a38e..15b55b1c5e 100644
--- a/frontend/src/app/util/http.cljs
+++ b/frontend/src/app/util/http.cljs
@@ -88,6 +88,7 @@
:credentials credentials
:referrerPolicy "no-referrer"
:signal signal}]
+
(-> (js/fetch (str uri) params)
(p/then (fn [response]
(vreset! abortable? false)
diff --git a/frontend/src/app/util/router.cljs b/frontend/src/app/util/router.cljs
index 27bfe4374f..f6a74e7233 100644
--- a/frontend/src/app/util/router.cljs
+++ b/frontend/src/app/util/router.cljs
@@ -19,17 +19,16 @@
;; --- Router API
+(defn map->Match
+ [data]
+ (r/map->Match data))
+
(defn resolve
([router id] (resolve router id {} {}))
([router id path-params] (resolve router id path-params {}))
([router id path-params query-params]
(when-let [match (r/match-by-name router id path-params)]
- (if (empty? query-params)
- (r/match->path match)
- (let [query (u/map->query-string query-params)]
- (-> (u/uri (r/match->path match))
- (assoc :query query)
- (str)))))))
+ (r/match->path match query-params))))
(defn create
[routes]
@@ -161,7 +160,3 @@
(e/unlistenByKey key)))))
(rx/take-until stoper)
(rx/subs #(on-change router %)))))))
-
-
-
-
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 87c089241e..09cf0ff064 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -3245,4 +3245,16 @@ msgid "workspace.updates.update"
msgstr "Update"
msgid "workspace.viewport.click-to-close-path"
-msgstr "Click to close the path"
\ No newline at end of file
+msgstr "Click to close the path"
+
+msgid "errors.team-leave.member-does-not-exists"
+msgstr "The member you try to assign does not exist."
+
+msgid "errors.team-leave.owner-cant-leave"
+msgstr "Owner can't leave team, you must reassign the owner role."
+
+msgid "errors.team-leave.insufficient-members"
+msgstr "Insufficient members to leave team, you probably want to delete it."
+
+msgid "errors.auth.unable-to-login"
+msgstr "Looks like you are not authenticated or session expired."