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 %} -
(go to explain)
- {% endif %} - {% if data %} -
(go to edata)
- {% endif %} - {% if error %} -
(go to trace)
- {% endif %} +
(go to explain)
+
(go to edata)
+
(go to trace)
{% if params %}
@@ -163,25 +157,39 @@
{% endif %} - {% if explain %} -
-
EXPLAIN:
-
-
{{explain}}
-
-
- {% endif %} - {% if data %}
-
EDATA:
+
ERROR DATA:
{{data}}
{% endif %} - {% if error %} + {% if spec-problems %} +
+
SPEC PROBLEMS:
+
+
{{spec-problems}}
+
+
+ {% endif %} + + {% if cause %} +
+
TRACE:
+
+
{{cause}}
+
+
+ {% elif trace %} +
+
TRACE:
+
+
{{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."