Compare commits

...

28 Commits

Author SHA1 Message Date
Andrey Antukh
dea090e7d3 📚 Update version.txt file. 2021-12-29 11:17:55 +01:00
Andrey Antukh
ba5e345677 Merge branch 'staging' 2021-12-29 11:17:06 +01:00
Andrey Antukh
13ae7b0976 📚 Update changelog. 2021-12-29 11:16:04 +01:00
elhombretecla
e9c654f30d Minor enhacements on onboarding modal. 2021-12-28 11:34:21 +01:00
Andrey Antukh
c240b69b5a 📎 Minor changes on error report template. 2021-12-28 11:19:38 +01:00
Andrey Antukh
d8f4176487 📎 Minor fixes on versions. 2021-12-27 11:41:13 +01:00
Andrey Antukh
220ab22115 🐛 Fix error reporting hook. 2021-12-27 11:30:22 +01:00
Andrey Antukh
67776c46d6 🐛 Fix NPE on email complains checking. 2021-12-27 11:13:08 +01:00
Andrey Antukh
4bc2d7444d 📎 Minor changes on dev tools. 2021-12-27 09:32:20 +01:00
Andrey Antukh
5c6d72b353 Improve logging performance and format. 2021-12-24 12:40:44 +01:00
Andrey Antukh
1839397ebc Minor enhacements on log processing. 2021-12-23 18:36:58 +01:00
Andrey Antukh
c6054f7ab2 💄 Improve json namespace API (and fix linter). 2021-12-23 00:04:37 +01:00
Andrey Antukh
98d5789b1b :lisptick: Cosmetic changes. 2021-12-22 19:04:03 +01:00
Andrey Antukh
31c07274cd 📎 Increase default session expiration to 15 days. 2021-12-22 18:44:49 +01:00
Andrey Antukh
37a736339e 🔥 Remove ALPHA and BETA batges. 2021-12-22 18:41:06 +01:00
Andrey Antukh
869abcc835 🐛 Fix incorrect grid calculation when size is 1. 2021-12-22 18:38:22 +01:00
Andrey Antukh
a6f05ea8c2 💄 Minor syntax cosmetic changes. 2021-12-22 18:37:29 +01:00
Andrey Antukh
6812099900 Simplify frames selection mechanism. 2021-12-22 18:37:08 +01:00
Andrey Antukh
53e6d7ef2a 🐛 Fix numeric-input component. 2021-12-22 17:06:59 +01:00
Andrey Antukh
c2f604cd01 Properly use take-until on shape movement streams. 2021-12-22 17:06:16 +01:00
Andrey Antukh
d06cfed50e 🐛 Add missing import. 2021-12-22 15:01:46 +01:00
Andrey Antukh
e06d063946 📎 Remove ALPHA label from feedback button. 2021-12-22 14:59:39 +01:00
Andrey Antukh
634ec1b113 Ensure valid messages on zmq listener. 2021-12-22 14:28:09 +01:00
Andrey Antukh
0bf883d5b2 📎 More updates to logging deps. 2021-12-22 14:09:23 +01:00
Andrey Antukh
c6d0e0124f ⬆️ Update log4j2 dependency to 2.17.0 2021-12-22 11:34:07 +01:00
Andrey Antukh
ce115c53e2 📎 Minor fixes on repl script. 2021-12-22 11:33:53 +01:00
Andrey Antukh
7014bc7a3c 🐛 Fix issue when typography name is empty. 2021-12-22 11:03:11 +01:00
Andrey Antukh
eb1bcfba83 🎉 Backport questions form integration.
Among other related that need to be ported.
2021-12-20 16:16:29 +01:00
87 changed files with 1461 additions and 1012 deletions

View File

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

View File

@@ -74,5 +74,3 @@
;; (prn "==============" rtype (into {} ?meta))
;; (prn (api/sexpr result))
{:node result}))

View File

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

View File

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

View File

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

View File

@@ -130,10 +130,10 @@
</div>
{% endif %}
{% if error %}
{% if hint %}
<div class="table-row">
<div class="table-key">HINT: </div>
<div class="table-val">{{error.message}}</div>
<div class="table-val">{{hint}}</div>
</div>
{% endif %}
@@ -144,15 +144,9 @@
</div>
{% endif %}
{% if explain %}
<div>(<a href="#explain">go to explain</a>)</div>
{% endif %}
{% if data %}
<div>(<a href="#edata">go to edata</a>)</div>
{% endif %}
{% if error %}
<div>(<a href="#trace">go to trace</a>)</div>
{% endif %}
<div>(<a href="#explain">go to explain</a>)</div>
<div>(<a href="#edata">go to edata</a>)</div>
<div>(<a href="#trace">go to trace</a>)</div>
{% if params %}
<div id="params" class="table-row multiline">
@@ -163,25 +157,39 @@
</div>
{% endif %}
{% if explain %}
<div id="explain" class="table-row multiline">
<div class="table-key">EXPLAIN: </div>
<div class="table-val">
<pre>{{explain}}</pre>
</div>
</div>
{% endif %}
{% if data %}
<div id="edata" class="table-row multiline">
<div class="table-key">EDATA: </div>
<div class="table-key">ERROR DATA: </div>
<div class="table-val">
<pre>{{data}}</pre>
</div>
</div>
{% endif %}
{% if error %}
{% if spec-problems %}
<div id="edata" class="table-row multiline">
<div class="table-key">SPEC PROBLEMS: </div>
<div class="table-val">
<pre>{{spec-problems}}</pre>
</div>
</div>
{% endif %}
{% if cause %}
<div id="trace" class="table-row multiline">
<div class="table-key">TRACE:</div>
<div class="table-val">
<pre>{{cause}}</pre>
</div>
</div>
{% elif trace %}
<div id="trace" class="table-row multiline">
<div class="table-key">TRACE:</div>
<div class="table-val">
<pre>{{trace}}</pre>
</div>
</div>
{% elif error %}
<div id="trace" class="table-row multiline">
<div class="table-key">TRACE:</div>
<div class="table-val">

View File

@@ -2,7 +2,7 @@
<Configuration status="info" monitorInterval="30">
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] [%t] %level{length=1} %logger{36} - %msg%n"/>
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"/>
</Console>
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
"</pre>\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")

View File

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

View File

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

View File

@@ -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/<! output)]

View File

@@ -68,7 +68,7 @@
:timeout 6000
:method :post
:headers {"content-type" "application/json"}
:body (json/encode payload)})]
:body (json/write payload)})]
(cond
(= (:status response) 204)
true

View File

@@ -31,7 +31,7 @@
rsp (http/send! {:uri uri
:method :post
:headers {"content-type" "application/json"}
:body (json/encode-str {:text text})})]
:body (json/write-str {:text text})})]
(when (not= (:status rsp) 200)
(l/error :hint "error on sending data to mattermost"
:response (pr-str rsp))))
@@ -62,7 +62,8 @@
(when uri
(l/info :msg "initializing mattermost error reporter" :uri uri)
(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/<! output)]

View File

@@ -7,6 +7,7 @@
(ns app.loggers.zmq
"A generic ZMQ listener."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.util.json :as json]
@@ -33,7 +34,7 @@
(l/info :msg "intializing ZMQ receiver" :bind endpoint)
(let [buffer (a/chan 1)
output (a/chan 1 (comp (filter map?)
(map prepare)))
(keep prepare)))
mult (a/mult output)]
(when endpoint
(a/thread (start-rcv-loop {:out buffer :endpoint endpoint})))
@@ -52,6 +53,11 @@
[_ f]
(a/close! (::buffer (meta f))))
(def ^:private json-mapper
(json/mapper
{:encode-key-fn str/camel
:decode-key-fn (comp keyword str/kebab)}))
(defn- start-rcv-loop
([] (start-rcv-loop nil))
([{:keys [out endpoint] :or {endpoint "tcp://localhost:5556"}}]
@@ -63,7 +69,7 @@
(.. socket (setReceiveTimeOut 5000))
(loop []
(let [msg (.recv ^ZMQ$Socket socket)
msg (json/decode msg)
msg (ex/ignoring (json/read msg json-mapper))
msg (if (nil? msg) :empty msg)]
(if (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)))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
(ns cljs.user)
(defn hello
[]
(js/console.log "hello"))

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -89,3 +89,4 @@
@import "main/partials/handoff";
@import "main/partials/exception-page";
@import "main/partials/share-link";
@import "main/partials/af-signup-questions";

View File

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

View File

@@ -12,6 +12,7 @@
display: flex;
align-items: center;
padding: 32px;
z-index: 1000;
cursor: pointer;

View File

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

View File

@@ -4,7 +4,6 @@
:jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"]
:dev-http {8888 "classpath:public"}
:builds
{:main
{:target :browser

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -166,7 +166,7 @@
(defn append-child!
[el child]
(.appendChild el child))
(.appendChild ^js el child))
(defn get-first-child
[el]

View File

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

View File

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

View File

@@ -88,6 +88,7 @@
:credentials credentials
:referrerPolicy "no-referrer"
:signal signal}]
(-> (js/fetch (str uri) params)
(p/then (fn [response]
(vreset! abortable? false)

View File

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

View File

@@ -3245,4 +3245,16 @@ msgid "workspace.updates.update"
msgstr "Update"
msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the path"
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."

View File

@@ -1 +1 @@
1.10.1-beta
1.10.3-beta