mirror of
https://github.com/penpot/penpot.git
synced 2025-06-05 09:51:38 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
5cf64c1440
21 changed files with 561 additions and 244 deletions
|
@ -446,3 +446,11 @@
|
||||||
{:email email :type "bounce"}
|
{:email email :type "bounce"}
|
||||||
{:limit 10}))]
|
{:limit 10}))]
|
||||||
(>= (count reports) threshold))))
|
(>= (count reports) threshold))))
|
||||||
|
|
||||||
|
(defn has-reports?
|
||||||
|
([conn email] (has-reports? conn email nil))
|
||||||
|
([conn email {:keys [threshold] :or {threshold 1}}]
|
||||||
|
(let [reports (db/exec! conn (sql/select :global-complaint-report
|
||||||
|
{:email email}
|
||||||
|
{:limit 10}))]
|
||||||
|
(>= (count reports) threshold))))
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
|
[app.common.pprint :as pp]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.sql :as sql]
|
[app.db.sql :as sql]
|
||||||
[app.http.client :as http]
|
[app.http.client :as http]
|
||||||
|
@ -16,10 +17,10 @@
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.worker :as-alias wrk]
|
[app.worker :as-alias wrk]
|
||||||
|
[clojure.data.json :as j]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[jsonista.core :as j]
|
|
||||||
[promesa.exec :as px]
|
[promesa.exec :as px]
|
||||||
[ring.request :as rreq]
|
[ring.request :as rreq]
|
||||||
[ring.response :as-alias rres]))
|
[ring.response :as-alias rres]))
|
||||||
|
@ -136,27 +137,32 @@
|
||||||
|
|
||||||
(defn- parse-json
|
(defn- parse-json
|
||||||
[v]
|
[v]
|
||||||
(ex/ignoring
|
(try
|
||||||
(j/read-value v)))
|
(j/read-str v)
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/wrn :hint "unable to decode request body"
|
||||||
|
:cause cause))))
|
||||||
|
|
||||||
(defn- register-bounce-for-profile
|
(defn- register-bounce-for-profile
|
||||||
[{:keys [::db/pool]} {:keys [type kind profile-id] :as report}]
|
[{:keys [::db/pool]} {:keys [type kind profile-id] :as report}]
|
||||||
(when (= kind "permanent")
|
(when (= kind "permanent")
|
||||||
(db/with-atomic [conn pool]
|
(try
|
||||||
(db/insert! conn :profile-complaint-report
|
(db/insert! pool :profile-complaint-report
|
||||||
{:profile-id profile-id
|
{:profile-id profile-id
|
||||||
:type (name type)
|
:type (name type)
|
||||||
:content (db/tjson report)})
|
:content (db/tjson report)})
|
||||||
|
|
||||||
;; TODO: maybe also try to find profiles by mail and if exists
|
(catch Throwable cause
|
||||||
;; register profile reports for them?
|
(l/warn :hint "unable to persist profile complaint"
|
||||||
|
:cause cause)))
|
||||||
|
|
||||||
(doseq [recipient (:recipients report)]
|
(doseq [recipient (:recipients report)]
|
||||||
(db/insert! conn :global-complaint-report
|
(db/insert! pool :global-complaint-report
|
||||||
{:email (:email recipient)
|
{:email (:email recipient)
|
||||||
:type (name type)
|
:type (name type)
|
||||||
:content (db/tjson report)}))
|
:content (db/tjson report)}))
|
||||||
|
|
||||||
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
|
(let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))]
|
||||||
(when (some #(= (:email profile) (:email %)) (:recipients report))
|
(when (some #(= (:email profile) (:email %)) (:recipients report))
|
||||||
;; If the report matches the profile email, this means that
|
;; If the report matches the profile email, this means that
|
||||||
;; the report is for itself, can be caused when a user
|
;; the report is for itself, can be caused when a user
|
||||||
|
@ -164,55 +170,77 @@
|
||||||
;; permanently rejecting receiving the email. In this case we
|
;; permanently rejecting receiving the email. In this case we
|
||||||
;; have no option to mark the user as muted (and in this case
|
;; have no option to mark the user as muted (and in this case
|
||||||
;; the profile will be also inactive.
|
;; the profile will be also inactive.
|
||||||
(db/update! conn :profile
|
|
||||||
|
(l/inf :hint "mark profile: muted"
|
||||||
|
:profile-id (str (:id profile))
|
||||||
|
:email (:email profile)
|
||||||
|
:reason "bounce report"
|
||||||
|
:report-id (:feedback-id report))
|
||||||
|
|
||||||
|
(db/update! pool :profile
|
||||||
{:is-muted true}
|
{:is-muted true}
|
||||||
{:id profile-id}))))))
|
{:id profile-id}
|
||||||
|
{::db/return-keys false})))))
|
||||||
|
|
||||||
(defn- register-complaint-for-profile
|
(defn- register-complaint-for-profile
|
||||||
[{:keys [::db/pool]} {:keys [type profile-id] :as report}]
|
[{:keys [::db/pool]} {:keys [type profile-id] :as report}]
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(db/insert! conn :profile-complaint-report
|
(try
|
||||||
|
(db/insert! pool :profile-complaint-report
|
||||||
{:profile-id profile-id
|
{:profile-id profile-id
|
||||||
:type (name type)
|
:type (name type)
|
||||||
:content (db/tjson report)})
|
:content (db/tjson report)})
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/warn :hint "unable to persist profile complaint"
|
||||||
|
:cause cause)))
|
||||||
|
|
||||||
;; TODO: maybe also try to find profiles by email and if exists
|
;; TODO: maybe also try to find profiles by email and if exists
|
||||||
;; register profile reports for them?
|
;; register profile reports for them?
|
||||||
(doseq [email (:recipients report)]
|
(doseq [email (:recipients report)]
|
||||||
(db/insert! conn :global-complaint-report
|
(db/insert! pool :global-complaint-report
|
||||||
{:email email
|
{:email email
|
||||||
:type (name type)
|
:type (name type)
|
||||||
:content (db/tjson report)}))
|
:content (db/tjson report)}))
|
||||||
|
|
||||||
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
|
(let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))]
|
||||||
(when (some #(= % (:email profile)) (:recipients report))
|
(when (some #(= % (:email profile)) (:recipients report))
|
||||||
;; If the report matches the profile email, this means that
|
;; If the report matches the profile email, this means that
|
||||||
;; the report is for itself, rare case but can happen; In this
|
;; the report is for itself, rare case but can happen; In this
|
||||||
;; case just mark profile as muted (very rare case).
|
;; case just mark profile as muted (very rare case).
|
||||||
(db/update! conn :profile
|
(l/inf :hint "mark profile: muted"
|
||||||
|
:profile-id (str (:id profile))
|
||||||
|
:email (:email profile)
|
||||||
|
:reason "complaint report"
|
||||||
|
:report-id (:feedback-id report))
|
||||||
|
|
||||||
|
(db/update! pool :profile
|
||||||
{:is-muted true}
|
{:is-muted true}
|
||||||
{:id profile-id})))))
|
{:id profile-id}
|
||||||
|
{::db/return-keys false}))))
|
||||||
|
|
||||||
(defn- process-report
|
(defn- process-report
|
||||||
[cfg {:keys [type profile-id] :as report}]
|
[cfg {:keys [type profile-id] :as report}]
|
||||||
(l/trace :action "processing report" :report (pr-str report))
|
|
||||||
(cond
|
(cond
|
||||||
;; In this case we receive a bounce/complaint notification without
|
;; In this case we receive a bounce/complaint notification without
|
||||||
;; confirmed identity, we just emit a warning but do nothing about
|
;; confirmed identity, we just emit a warning but do nothing about
|
||||||
;; it because this is not a normal case. All notifications should
|
;; it because this is not a normal case. All notifications should
|
||||||
;; come with profile identity.
|
;; come with profile identity.
|
||||||
(nil? profile-id)
|
(nil? profile-id)
|
||||||
(l/warn :msg "a notification without identity received from AWS"
|
(l/wrn :hint "not-identified report"
|
||||||
:report (pr-str report))
|
::l/body (pp/pprint-str report {:length 40 :level 6}))
|
||||||
|
|
||||||
(= "bounce" type)
|
(= "bounce" type)
|
||||||
(register-bounce-for-profile cfg report)
|
(do
|
||||||
|
(l/trc :hint "bounce report"
|
||||||
|
::l/body (pp/pprint-str report {:length 40 :level 6}))
|
||||||
|
(register-bounce-for-profile cfg report))
|
||||||
|
|
||||||
(= "complaint" type)
|
(= "complaint" type)
|
||||||
(register-complaint-for-profile cfg report)
|
(do
|
||||||
|
(l/trc :hint "complaint report"
|
||||||
|
::l/body (pp/pprint-str report {:length 40 :level 6}))
|
||||||
|
(register-complaint-for-profile cfg report))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(l/warn :msg "unrecognized report received from AWS"
|
(l/wrn :hint "unrecognized report"
|
||||||
:report (pr-str report))))
|
::l/body (pp/pprint-str report {:length 20 :level 4}))))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -209,7 +209,19 @@
|
||||||
(str/lower (:password params)))
|
(str/lower (:password params)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :email-as-password
|
:code :email-as-password
|
||||||
:hint "you can't use your email as password")))
|
:hint "you can't use your email as password"))
|
||||||
|
|
||||||
|
(when (eml/has-bounce-reports? cfg (:email params))
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:email (:email params)
|
||||||
|
:hint "looks like the email has bounce reports"))
|
||||||
|
|
||||||
|
(when (eml/has-complaint-reports? cfg (:email params))
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :email-has-complaints
|
||||||
|
:email (:email params)
|
||||||
|
:hint "looks like the email has complaint reports")))
|
||||||
|
|
||||||
(defn prepare-register
|
(defn prepare-register
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
|
||||||
|
@ -286,14 +298,17 @@
|
||||||
(try
|
(try
|
||||||
(-> (db/insert! conn :profile params)
|
(-> (db/insert! conn :profile params)
|
||||||
(profile/decode-row))
|
(profile/decode-row))
|
||||||
(catch org.postgresql.util.PSQLException e
|
(catch org.postgresql.util.PSQLException cause
|
||||||
(let [state (.getSQLState e)]
|
(let [state (.getSQLState cause)]
|
||||||
(if (not= state "23505")
|
(if (not= state "23505")
|
||||||
(throw e)
|
(throw cause)
|
||||||
|
|
||||||
|
(do
|
||||||
|
(l/error :hint "not an error" :cause cause)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :email-already-exists
|
:code :email-already-exists
|
||||||
:hint "email already exists"
|
:hint "email already exists"
|
||||||
:cause e)))))))
|
:cause cause))))))))
|
||||||
|
|
||||||
(defn create-profile-rels!
|
(defn create-profile-rels!
|
||||||
[conn {:keys [id] :as profile}]
|
[conn {:keys [id] :as profile}]
|
||||||
|
@ -398,7 +413,9 @@
|
||||||
::audit/profile-id (:id profile)}))
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
(do
|
(do
|
||||||
(send-email-verification! cfg profile)
|
(when-not (eml/has-reports? conn (:email profile))
|
||||||
|
(send-email-verification! cfg profile))
|
||||||
|
|
||||||
(rph/with-meta {:email (:email profile)}
|
(rph/with-meta {:email (:email profile)}
|
||||||
{::audit/replace-props props
|
{::audit/replace-props props
|
||||||
::audit/context {:action "email-verification"}
|
::audit/context {:action "email-verification"}
|
||||||
|
@ -406,9 +423,9 @@
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [elapsed? (elapsed-verify-threshold? profile)
|
(let [elapsed? (elapsed-verify-threshold? profile)
|
||||||
bounce? (eml/has-bounce-reports? conn (:email profile))
|
complaints? (eml/has-reports? conn (:email profile))
|
||||||
action (if bounce?
|
action (if complaints?
|
||||||
"ignore-because-bounce"
|
"ignore-because-complaints"
|
||||||
(if elapsed?
|
(if elapsed?
|
||||||
"resend-email-verification"
|
"resend-email-verification"
|
||||||
"ignore"))]
|
"ignore"))]
|
||||||
|
@ -446,7 +463,7 @@
|
||||||
;; ---- COMMAND: Request Profile Recovery
|
;; ---- COMMAND: Request Profile Recovery
|
||||||
|
|
||||||
(defn- request-profile-recovery
|
(defn- request-profile-recovery
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [email] :as params}]
|
||||||
(letfn [(create-recovery-token [{:keys [id] :as profile}]
|
(letfn [(create-recovery-token [{:keys [id] :as profile}]
|
||||||
(let [token (tokens/generate (::setup/props cfg)
|
(let [token (tokens/generate (::setup/props cfg)
|
||||||
{:iss :password-recovery
|
{:iss :password-recovery
|
||||||
|
@ -468,7 +485,6 @@
|
||||||
:extra-data ptoken})
|
:extra-data ptoken})
|
||||||
nil))]
|
nil))]
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [profile (->> (profile/clean-email email)
|
(let [profile (->> (profile/clean-email email)
|
||||||
(profile/get-profile-by-email conn))]
|
(profile/get-profile-by-email conn))]
|
||||||
|
|
||||||
|
@ -487,12 +503,16 @@
|
||||||
:profile-id (str (:id profile))
|
:profile-id (str (:id profile))
|
||||||
:profile-email (:email profile))
|
:profile-email (:email profile))
|
||||||
|
|
||||||
|
(eml/has-complaint-reports? conn (:email profile))
|
||||||
|
(l/wrn :hint "attempt of profile recovery: email has complaints"
|
||||||
|
:profile-id (str (:id profile))
|
||||||
|
:profile-email (:email profile))
|
||||||
|
|
||||||
(not (elapsed-verify-threshold? profile))
|
(not (elapsed-verify-threshold? profile))
|
||||||
(l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed"
|
(l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed"
|
||||||
:profile-id (str (:id profile))
|
:profile-id (str (:id profile))
|
||||||
:profile-email (:email profile))
|
:profile-email (:email profile))
|
||||||
|
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do
|
(do
|
||||||
(db/update! conn :profile
|
(db/update! conn :profile
|
||||||
|
@ -500,7 +520,7 @@
|
||||||
{:id (:id profile)})
|
{:id (:id profile)})
|
||||||
(->> profile
|
(->> profile
|
||||||
(create-recovery-token)
|
(create-recovery-token)
|
||||||
(send-email-notification conn))))))))
|
(send-email-notification conn)))))))
|
||||||
|
|
||||||
|
|
||||||
(def schema:request-profile-recovery
|
(def schema:request-profile-recovery
|
||||||
|
@ -512,6 +532,6 @@
|
||||||
::doc/added "1.15"
|
::doc/added "1.15"
|
||||||
::sm/params schema:request-profile-recovery}
|
::sm/params schema:request-profile-recovery}
|
||||||
[cfg params]
|
[cfg params]
|
||||||
(request-profile-recovery cfg params))
|
(db/tx-run! cfg request-profile-recovery params))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -276,19 +276,19 @@
|
||||||
(sv/defmethod ::request-email-change
|
(sv/defmethod ::request-email-change
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
::sm/params schema:request-email-change}
|
::sm/params schema:request-email-change}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id email] :as params}]
|
[cfg {:keys [::rpc/profile-id email] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/tx-run! cfg
|
||||||
(let [profile (db/get-by-id conn :profile profile-id)
|
(fn [cfg]
|
||||||
cfg (assoc cfg ::conn conn)
|
(let [profile (db/get-by-id cfg :profile profile-id)
|
||||||
params (assoc params
|
params (assoc params
|
||||||
:profile profile
|
:profile profile
|
||||||
:email (clean-email email))]
|
:email (clean-email email))]
|
||||||
(if (contains? cf/flags :smtp)
|
(if (contains? cf/flags :smtp)
|
||||||
(request-email-change! cfg params)
|
(request-email-change! cfg params)
|
||||||
(change-email-immediately! cfg params)))))
|
(change-email-immediately! cfg params))))))
|
||||||
|
|
||||||
(defn- change-email-immediately!
|
(defn- change-email-immediately!
|
||||||
[{:keys [::conn]} {:keys [profile email] :as params}]
|
[{:keys [::db/conn]} {:keys [profile email] :as params}]
|
||||||
(when (not= email (:email profile))
|
(when (not= email (:email profile))
|
||||||
(check-profile-existence! conn params))
|
(check-profile-existence! conn params))
|
||||||
|
|
||||||
|
@ -299,7 +299,7 @@
|
||||||
{:changed true})
|
{:changed true})
|
||||||
|
|
||||||
(defn- request-email-change!
|
(defn- request-email-change!
|
||||||
[{:keys [::conn] :as cfg} {:keys [profile email] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [profile email] :as params}]
|
||||||
(let [token (tokens/generate (::setup/props cfg)
|
(let [token (tokens/generate (::setup/props cfg)
|
||||||
{:iss :change-email
|
{:iss :change-email
|
||||||
:exp (dt/in-future "15m")
|
:exp (dt/in-future "15m")
|
||||||
|
@ -319,9 +319,28 @@
|
||||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
|
||||||
|
|
||||||
(when (eml/has-bounce-reports? conn email)
|
(when (eml/has-bounce-reports? conn email)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :restriction
|
||||||
:code :email-has-permanent-bounces
|
:code :email-has-permanent-bounces
|
||||||
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
:email email
|
||||||
|
:hint "looks like the email has bounce reports"))
|
||||||
|
|
||||||
|
(when (eml/has-complaint-reports? conn email)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :email-has-complaints
|
||||||
|
:email email
|
||||||
|
:hint "looks like the email has spam complaint reports"))
|
||||||
|
|
||||||
|
(when (eml/has-bounce-reports? conn (:email profile))
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:email (:email profile)
|
||||||
|
:hint "looks like the email has bounce reports"))
|
||||||
|
|
||||||
|
(when (eml/has-complaint-reports? conn (:email profile))
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :email-has-complaints
|
||||||
|
:email (:email profile)
|
||||||
|
:hint "looks like the email has spam complaint reports"))
|
||||||
|
|
||||||
(eml/send! {::eml/conn conn
|
(eml/send! {::eml/conn conn
|
||||||
::eml/factory eml/change-email
|
::eml/factory eml/change-email
|
||||||
|
|
|
@ -734,12 +734,19 @@
|
||||||
:email email
|
:email email
|
||||||
:hint "the profile has reported repeatedly as spam or has bounces"))
|
:hint "the profile has reported repeatedly as spam or has bounces"))
|
||||||
|
|
||||||
;; Secondly check if the invited member email is part of the global spam/bounce report.
|
;; Secondly check if the invited member email is part of the global bounce report.
|
||||||
(when (eml/has-bounce-reports? conn email)
|
(when (eml/has-bounce-reports? conn email)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :restriction
|
||||||
:code :email-has-permanent-bounces
|
:code :email-has-permanent-bounces
|
||||||
:email email
|
:email email
|
||||||
:hint "the email you invite has been repeatedly reported as spam or bounce"))
|
:hint "the email you invite has been repeatedly reported as bounce"))
|
||||||
|
|
||||||
|
;; Secondly check if the invited member email is part of the global complain report.
|
||||||
|
(when (eml/has-complaint-reports? conn email)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :email-has-complaints
|
||||||
|
:email email
|
||||||
|
:hint "the email you invite has been repeatedly reported as spam"))
|
||||||
|
|
||||||
;; When we have email verification disabled and invitation user is
|
;; When we have email verification disabled and invitation user is
|
||||||
;; already present in the database, we proceed to add it to the
|
;; already present in the database, we proceed to add it to the
|
||||||
|
|
|
@ -590,9 +590,10 @@
|
||||||
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
||||||
|
|
||||||
(let [out (th/command! data)]
|
(let [out (th/command! data)]
|
||||||
(t/is (th/success? out))
|
(t/is (not (th/success? out)))
|
||||||
(let [result (:result out)]
|
(let [edata (-> out :error ex-data)]
|
||||||
(t/is (contains? result :token))))))
|
(t/is (= :restriction (:type edata)))
|
||||||
|
(t/is (= :email-has-permanent-bounces (:code edata)))))))
|
||||||
|
|
||||||
(t/deftest register-profile-with-complained-email
|
(t/deftest register-profile-with-complained-email
|
||||||
(let [pool (:app.db/pool th/*system*)
|
(let [pool (:app.db/pool th/*system*)
|
||||||
|
@ -603,9 +604,11 @@
|
||||||
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
|
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
|
||||||
|
|
||||||
(let [out (th/command! data)]
|
(let [out (th/command! data)]
|
||||||
(t/is (th/success? out))
|
(t/is (not (th/success? out)))
|
||||||
(let [result (:result out)]
|
|
||||||
(t/is (contains? result :token))))))
|
(let [edata (-> out :error ex-data)]
|
||||||
|
(t/is (= :restriction (:type edata)))
|
||||||
|
(t/is (= :email-has-complaints (:code edata)))))))
|
||||||
|
|
||||||
(t/deftest register-profile-with-email-as-password
|
(t/deftest register-profile-with-email-as-password
|
||||||
(let [data {::th/type :prepare-register-profile
|
(let [data {::th/type :prepare-register-profile
|
||||||
|
@ -639,17 +642,23 @@
|
||||||
(let [out (th/command! data)]
|
(let [out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:result out)))
|
(t/is (nil? (:result out)))
|
||||||
(t/is (= 2 (:call-count @mock))))
|
|
||||||
|
(let [edata (-> out :error ex-data)]
|
||||||
|
(t/is (= :restriction (:type edata)))
|
||||||
|
(t/is (= :email-has-complaints (:code edata))))
|
||||||
|
|
||||||
|
(t/is (= 1 (:call-count @mock))))
|
||||||
|
|
||||||
;; with bounces
|
;; with bounces
|
||||||
(th/create-global-complaint-for pool {:type :bounce :email (:email data)})
|
(th/create-global-complaint-for pool {:type :bounce :email (:email data)})
|
||||||
(let [out (th/command! data)
|
(let [out (th/command! data)]
|
||||||
error (:error out)]
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (th/ex-info? error))
|
|
||||||
(t/is (th/ex-of-type? error :validation))
|
(let [edata (-> out :error ex-data)]
|
||||||
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
|
(t/is (= :restriction (:type edata)))
|
||||||
(t/is (= 2 (:call-count @mock)))))))
|
(t/is (= :email-has-permanent-bounces (:code edata))))
|
||||||
|
|
||||||
|
(t/is (= 1 (:call-count @mock)))))))
|
||||||
|
|
||||||
|
|
||||||
(t/deftest email-change-request-without-smtp
|
(t/deftest email-change-request-without-smtp
|
||||||
|
@ -714,7 +723,7 @@
|
||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:result out)))
|
(t/is (nil? (:result out)))
|
||||||
(t/is (= 2 (:call-count @mock))))
|
(t/is (= 1 (:call-count @mock))))
|
||||||
|
|
||||||
;; with valid email and active user with global bounce
|
;; with valid email and active user with global bounce
|
||||||
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
|
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
|
||||||
|
@ -723,7 +732,7 @@
|
||||||
(t/is (nil? (:result out)))
|
(t/is (nil? (:result out)))
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (= 2 (:call-count @mock))))))))
|
(t/is (= 1 (:call-count @mock))))))))
|
||||||
|
|
||||||
|
|
||||||
(t/deftest update-profile-password
|
(t/deftest update-profile-password
|
||||||
|
|
|
@ -62,8 +62,8 @@
|
||||||
(th/reset-mock! mock)
|
(th/reset-mock! mock)
|
||||||
(let [data (assoc data :emails ["foo@bar.com"])
|
(let [data (assoc data :emails ["foo@bar.com"])
|
||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
(t/is (th/success? out))
|
(t/is (not (th/success? out)))
|
||||||
(t/is (= 1 (:call-count (deref mock)))))
|
(t/is (= 0 (:call-count (deref mock)))))
|
||||||
|
|
||||||
;; get invitation token
|
;; get invitation token
|
||||||
(let [params {::th/type :get-team-invitation-token
|
(let [params {::th/type :get-team-invitation-token
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
(t/is (= 0 (:call-count @mock)))
|
(t/is (= 0 (:call-count @mock)))
|
||||||
|
|
||||||
(let [edata (-> out :error ex-data)]
|
(let [edata (-> out :error ex-data)]
|
||||||
(t/is (= :validation (:type edata)))
|
(t/is (= :restriction (:type edata)))
|
||||||
(t/is (= :email-has-permanent-bounces (:code edata)))))
|
(t/is (= :email-has-permanent-bounces (:code edata)))))
|
||||||
|
|
||||||
;; invite internal user that is muted
|
;; invite internal user that is muted
|
||||||
|
|
|
@ -711,52 +711,14 @@
|
||||||
(ctyl/delete-typography data id))
|
(ctyl/delete-typography data id))
|
||||||
|
|
||||||
;; === Operations
|
;; === Operations
|
||||||
|
|
||||||
(defmethod process-operation :set
|
(defmethod process-operation :set
|
||||||
[on-changed shape op]
|
[on-changed shape op]
|
||||||
(let [attr (:attr op)
|
(ctn/set-shape-attr shape
|
||||||
group (get ctk/sync-attrs attr)
|
(:attr op)
|
||||||
val (:val op)
|
(:val op)
|
||||||
shape-val (get shape attr)
|
:on-changed on-changed
|
||||||
ignore (or (:ignore-touched op) (= attr :position-data)) ;; position-data is a derived attribute and
|
:ignore-touched (:ignore-touched op)
|
||||||
ignore-geometry (:ignore-geometry op) ;; never triggers touched by itself
|
:ignore-geometry (:ignore-geometry op)))
|
||||||
is-geometry? (and (or (= group :geometry-group)
|
|
||||||
(and (= group :content-group) (= (:type shape) :path)))
|
|
||||||
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
|
|
||||||
;; TODO: the check of :width and :height probably may be removed
|
|
||||||
;; after the check added in data/workspace/modifiers/check-delta
|
|
||||||
;; function. Better check it and test toroughly when activating
|
|
||||||
;; components-v2 mode.
|
|
||||||
in-copy? (ctk/in-component-copy? shape)
|
|
||||||
|
|
||||||
;; For geometric attributes, there are cases in that the value changes
|
|
||||||
;; slightly (e.g. when rounding to pixel, or when recalculating text
|
|
||||||
;; positions in different zoom levels). To take this into account, we
|
|
||||||
;; ignore geometric changes smaller than 1 pixel.
|
|
||||||
equal? (if is-geometry?
|
|
||||||
(gsh/close-attrs? attr val shape-val 1)
|
|
||||||
(gsh/close-attrs? attr val shape-val))]
|
|
||||||
|
|
||||||
;; Notify when value has changed, except when it has not moved relative to the
|
|
||||||
;; component head.
|
|
||||||
(when (and group (not equal?) (not (and ignore-geometry is-geometry?)))
|
|
||||||
(on-changed shape))
|
|
||||||
|
|
||||||
(cond-> shape
|
|
||||||
;; Depending on the origin of the attribute change, we need or not to
|
|
||||||
;; set the "touched" flag for the group the attribute belongs to.
|
|
||||||
;; In some cases we need to ignore touched only if the attribute is
|
|
||||||
;; geometric (position, width or transformation).
|
|
||||||
(and in-copy? group (not ignore) (not equal?)
|
|
||||||
(not (and ignore-geometry is-geometry?)))
|
|
||||||
(-> (update :touched cfh/set-touched-group group)
|
|
||||||
(dissoc :remote-synced))
|
|
||||||
|
|
||||||
(nil? val)
|
|
||||||
(dissoc attr)
|
|
||||||
|
|
||||||
(some? val)
|
|
||||||
(assoc attr val))))
|
|
||||||
|
|
||||||
(defmethod process-operation :set-touched
|
(defmethod process-operation :set-touched
|
||||||
[_ shape op]
|
[_ shape op]
|
||||||
|
|
|
@ -357,15 +357,6 @@
|
||||||
;; COMPONENTS HELPERS
|
;; COMPONENTS HELPERS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn set-touched-group
|
|
||||||
[touched group]
|
|
||||||
(when group
|
|
||||||
(conj (or touched #{}) group)))
|
|
||||||
|
|
||||||
(defn touched-group?
|
|
||||||
[shape group]
|
|
||||||
((or (:touched shape) #{}) group))
|
|
||||||
|
|
||||||
(defn make-container
|
(defn make-container
|
||||||
[page-or-component type]
|
[page-or-component type]
|
||||||
(assoc page-or-component :type type))
|
(assoc page-or-component :type type))
|
||||||
|
|
|
@ -153,14 +153,29 @@
|
||||||
(defn build-message
|
(defn build-message
|
||||||
[props]
|
[props]
|
||||||
(loop [props (seq props)
|
(loop [props (seq props)
|
||||||
result []]
|
result []
|
||||||
|
body nil]
|
||||||
(if-let [[k v] (first props)]
|
(if-let [[k v] (first props)]
|
||||||
(if (simple-ident? k)
|
(cond
|
||||||
|
(simple-ident? k)
|
||||||
(recur (next props)
|
(recur (next props)
|
||||||
(conj result (str (name k) "=" (pr-str v))))
|
(conj result (str (name k) "=" (pr-str v)))
|
||||||
|
body)
|
||||||
|
|
||||||
|
(= ::body k)
|
||||||
(recur (next props)
|
(recur (next props)
|
||||||
result))
|
result
|
||||||
(str/join ", " result))))
|
v)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(recur (next props)
|
||||||
|
result
|
||||||
|
body))
|
||||||
|
|
||||||
|
(let [message (str/join ", " result)]
|
||||||
|
(if (string? body)
|
||||||
|
(str message "\n" body)
|
||||||
|
message)))))
|
||||||
|
|
||||||
(defn build-stack-trace
|
(defn build-stack-trace
|
||||||
[cause]
|
[cause]
|
||||||
|
|
|
@ -288,13 +288,23 @@
|
||||||
(some? (:shape-ref ref-shape))
|
(some? (:shape-ref ref-shape))
|
||||||
(pcb/update-shapes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape)))
|
(pcb/update-shapes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape)))
|
||||||
|
|
||||||
;; When advancing level, if the referenced shape has a swap slot, it must be
|
;; When advancing level, the normal touched groups (not swap slots) of the
|
||||||
;; copied to the current shape, because the shape-ref now will not be pointing
|
;; ref-shape must be merged into the current shape, because they refer to
|
||||||
;; to a near main (except for first level subcopies).
|
;; the new referenced shape.
|
||||||
|
(some? ref-shape)
|
||||||
|
(pcb/update-shapes
|
||||||
|
[(:id shape)]
|
||||||
|
#(assoc % :touched
|
||||||
|
(clojure.set/union (:touched shape)
|
||||||
|
(ctk/normal-touched-groups ref-shape))))
|
||||||
|
|
||||||
|
;; Swap slot must also be copied if the current shape has not any,
|
||||||
|
;; except if this is the first level subcopy.
|
||||||
(and (some? (ctk/get-swap-slot ref-shape))
|
(and (some? (ctk/get-swap-slot ref-shape))
|
||||||
(nil? (ctk/get-swap-slot shape))
|
(nil? (ctk/get-swap-slot shape))
|
||||||
(not= (:id shape) shape-id))
|
(not= (:id shape) shape-id))
|
||||||
(pcb/update-shapes [(:id shape)] #(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape))))))]
|
(pcb/update-shapes [(:id shape)] #(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape))))))]
|
||||||
|
|
||||||
(reduce skip-near changes children)))
|
(reduce skip-near changes children)))
|
||||||
|
|
||||||
(defn prepare-restore-component
|
(defn prepare-restore-component
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
[app.common.test-helpers.ids-map :as thi]
|
[app.common.test-helpers.ids-map :as thi]
|
||||||
[app.common.types.color :as ctc]
|
[app.common.types.color :as ctc]
|
||||||
[app.common.types.colors-list :as ctcl]
|
[app.common.types.colors-list :as ctcl]
|
||||||
|
[app.common.types.container :as ctn]
|
||||||
[app.common.types.file :as ctf]
|
[app.common.types.file :as ctf]
|
||||||
[app.common.types.pages-list :as ctpl]
|
[app.common.types.pages-list :as ctpl]
|
||||||
[app.common.types.shape :as cts]
|
[app.common.types.shape :as cts]
|
||||||
|
@ -69,6 +70,19 @@
|
||||||
(thf/current-page file))]
|
(thf/current-page file))]
|
||||||
(ctst/get-shape page id)))
|
(ctst/get-shape page id)))
|
||||||
|
|
||||||
|
(defn update-shape
|
||||||
|
[file shape-label attr val & {:keys [page-label]}]
|
||||||
|
(let [page (if page-label
|
||||||
|
(thf/get-page file page-label)
|
||||||
|
(thf/current-page file))
|
||||||
|
shape (ctst/get-shape page (thi/id shape-label))]
|
||||||
|
(ctf/update-file-data
|
||||||
|
file
|
||||||
|
(fn [file-data]
|
||||||
|
(ctpl/update-page file-data
|
||||||
|
(:id page)
|
||||||
|
#(ctst/set-shape % (ctn/set-shape-attr shape attr val)))))))
|
||||||
|
|
||||||
(defn sample-color
|
(defn sample-color
|
||||||
[label & {:keys [] :as params}]
|
[label & {:keys [] :as params}]
|
||||||
(ctc/make-color (assoc params :id (thi/new-id! label))))
|
(ctc/make-color (assoc params :id (thi/new-id! label))))
|
||||||
|
|
|
@ -202,6 +202,11 @@
|
||||||
[group]
|
[group]
|
||||||
(str/starts-with? (name group) "swap-slot-"))
|
(str/starts-with? (name group) "swap-slot-"))
|
||||||
|
|
||||||
|
(defn normal-touched-groups
|
||||||
|
"Gets all touched groups that are not swap slots."
|
||||||
|
[shape]
|
||||||
|
(into #{} (remove swap-slot? (:touched shape))))
|
||||||
|
|
||||||
(defn group->swap-slot
|
(defn group->swap-slot
|
||||||
[group]
|
[group]
|
||||||
(uuid/uuid (subs (name group) 10)))
|
(uuid/uuid (subs (name group) 10)))
|
||||||
|
|
|
@ -537,3 +537,48 @@
|
||||||
(if (or no-changes? (not (invalid-structure-for-component? objects parent children pasting? libraries)))
|
(if (or no-changes? (not (invalid-structure-for-component? objects parent children pasting? libraries)))
|
||||||
[parent-id (get-frame parent-id)]
|
[parent-id (get-frame parent-id)]
|
||||||
(recur (:parent-id parent) objects children pasting? libraries))))))
|
(recur (:parent-id parent) objects children pasting? libraries))))))
|
||||||
|
|
||||||
|
;; --- SHAPE UPDATE
|
||||||
|
|
||||||
|
(defn set-shape-attr
|
||||||
|
[shape attr val & {:keys [on-changed ignore-touched ignore-geometry]}]
|
||||||
|
(let [group (get ctk/sync-attrs attr)
|
||||||
|
shape-val (get shape attr)
|
||||||
|
ignore (or ignore-touched (= attr :position-data)) ;; position-data is a derived attribute and
|
||||||
|
is-geometry? (and (or (= group :geometry-group) ;; never triggers touched by itself
|
||||||
|
(and (= group :content-group) (= (:type shape) :path)))
|
||||||
|
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
|
||||||
|
;; TODO: the check of :width and :height probably may be removed
|
||||||
|
;; after the check added in data/workspace/modifiers/check-delta
|
||||||
|
;; function. Better check it and test toroughly when activating
|
||||||
|
;; components-v2 mode.
|
||||||
|
in-copy? (ctk/in-component-copy? shape)
|
||||||
|
|
||||||
|
;; For geometric attributes, there are cases in that the value changes
|
||||||
|
;; slightly (e.g. when rounding to pixel, or when recalculating text
|
||||||
|
;; positions in different zoom levels). To take this into account, we
|
||||||
|
;; ignore geometric changes smaller than 1 pixel.
|
||||||
|
equal? (if is-geometry?
|
||||||
|
(gsh/close-attrs? attr val shape-val 1)
|
||||||
|
(gsh/close-attrs? attr val shape-val))]
|
||||||
|
|
||||||
|
;; Notify when value has changed, except when it has not moved relative to the
|
||||||
|
;; component head.
|
||||||
|
(when (and on-changed group (not equal?) (not (and ignore-geometry is-geometry?)))
|
||||||
|
(on-changed shape))
|
||||||
|
|
||||||
|
(cond-> shape
|
||||||
|
;; Depending on the origin of the attribute change, we need or not to
|
||||||
|
;; set the "touched" flag for the group the attribute belongs to.
|
||||||
|
;; In some cases we need to ignore touched only if the attribute is
|
||||||
|
;; geometric (position, width or transformation).
|
||||||
|
(and in-copy? group (not ignore) (not equal?)
|
||||||
|
(not (and ignore-geometry is-geometry?)))
|
||||||
|
(-> (update :touched ctk/set-touched-group group)
|
||||||
|
(dissoc :remote-synced))
|
||||||
|
|
||||||
|
(nil? val)
|
||||||
|
(dissoc attr)
|
||||||
|
|
||||||
|
(some? val)
|
||||||
|
(assoc attr val))))
|
||||||
|
|
|
@ -504,8 +504,8 @@
|
||||||
(assoc :proportion-lock true)))
|
(assoc :proportion-lock true)))
|
||||||
|
|
||||||
(defn setup-shape
|
(defn setup-shape
|
||||||
"A function that initializes the geometric data of
|
"A function that initializes the geometric data of the shape. The props must
|
||||||
the shape. The props must have :x :y :width :height."
|
contain at least :x :y :width :height."
|
||||||
[{:keys [type] :as props}]
|
[{:keys [type] :as props}]
|
||||||
(let [shape (make-minimal-shape type)
|
(let [shape (make-minimal-shape type)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns common-tests.logic.comp-detach-with-swap-test
|
(ns common-tests.logic.comp-detach-with-nested-test
|
||||||
(:require
|
(:require
|
||||||
[app.common.files.changes-builder :as pcb]
|
[app.common.files.changes-builder :as pcb]
|
||||||
[app.common.logic.libraries :as cll]
|
[app.common.logic.libraries :as cll]
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
(t/use-fixtures :each thi/test-fixture)
|
(t/use-fixtures :each thi/test-fixture)
|
||||||
|
|
||||||
;; Related .penpot file: common/test/cases/detach-with-swap.penpot
|
;; Related .penpot file: common/test/cases/detach-with-nested.penpot
|
||||||
(defn- setup-file
|
(defn- setup-file
|
||||||
[]
|
[]
|
||||||
;; {:r-ellipse} [:name Ellipse, :type :frame] # [Component :c-ellipse]
|
;; {:r-ellipse} [:name Ellipse, :type :frame] # [Component :c-ellipse]
|
||||||
|
@ -195,3 +195,177 @@
|
||||||
(t/is (= (:shape-ref copy-nested-rectangle) (thi/id :rectangle)))
|
(t/is (= (:shape-ref copy-nested-rectangle) (thi/id :rectangle)))
|
||||||
(t/is (nil? (ctk/get-swap-slot copy-nested-rectangle)))))
|
(t/is (nil? (ctk/get-swap-slot copy-nested-rectangle)))))
|
||||||
|
|
||||||
|
(t/deftest test-propagate-touched
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (-> (setup-file)
|
||||||
|
(ths/update-shape :nested2-ellipse :fills (ths/sample-fills-color :fill-color "#fabada"))
|
||||||
|
(thc/instantiate-component :c-big-board
|
||||||
|
:copy-big-board
|
||||||
|
:children-labels [:copy-h-board-with-ellipse
|
||||||
|
:copy-nested2-h-ellipse
|
||||||
|
:copy-nested2-ellipse]))
|
||||||
|
|
||||||
|
page (thf/current-page file)
|
||||||
|
nested2-ellipse (ths/get-shape file :nested2-ellipse)
|
||||||
|
copy-nested2-ellipse (ths/get-shape file :copy-nested2-ellipse)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
|
||||||
|
(pcb/with-page page)
|
||||||
|
(pcb/with-objects (:objects page)))
|
||||||
|
page
|
||||||
|
{(:id file) file}
|
||||||
|
(thi/id :copy-big-board))
|
||||||
|
file' (thf/apply-changes file changes)
|
||||||
|
|
||||||
|
;; ==== Get
|
||||||
|
nested2-ellipse' (ths/get-shape file' :nested2-ellipse)
|
||||||
|
copy-nested2-ellipse' (ths/get-shape file' :copy-nested2-ellipse)
|
||||||
|
fills' (:fills copy-nested2-ellipse')
|
||||||
|
fill' (first fills')]
|
||||||
|
|
||||||
|
;; ==== Check
|
||||||
|
|
||||||
|
;; The touched group must be propagated to the copy, because now this copy
|
||||||
|
;; has the original ellipse component as near main, but its attributes have
|
||||||
|
;; been inherited from the ellipse inside big-board.
|
||||||
|
(t/is (= (:touched nested2-ellipse) #{:fill-group}))
|
||||||
|
(t/is (= (:touched copy-nested2-ellipse) nil))
|
||||||
|
(t/is (= (:touched nested2-ellipse') #{:fill-group}))
|
||||||
|
(t/is (= (:touched copy-nested2-ellipse') #{:fill-group}))
|
||||||
|
(t/is (= (count fills') 1))
|
||||||
|
(t/is (= (:fill-color fill') "#fabada"))
|
||||||
|
(t/is (= (:fill-opacity fill') 1))))
|
||||||
|
|
||||||
|
(t/deftest test-merge-touched
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (-> (setup-file)
|
||||||
|
(ths/update-shape :nested2-ellipse :fills (ths/sample-fills-color :fill-color "#fabada"))
|
||||||
|
(thc/instantiate-component :c-big-board
|
||||||
|
:copy-big-board
|
||||||
|
:children-labels [:copy-h-board-with-ellipse
|
||||||
|
:copy-nested2-h-ellipse
|
||||||
|
:copy-nested2-ellipse])
|
||||||
|
(ths/update-shape :copy-nested2-ellipse :name "Modified name")
|
||||||
|
(ths/update-shape :copy-nested2-ellipse :fills (ths/sample-fills-color :fill-color "#abcdef")))
|
||||||
|
|
||||||
|
page (thf/current-page file)
|
||||||
|
nested2-ellipse (ths/get-shape file :nested2-ellipse)
|
||||||
|
copy-nested2-ellipse (ths/get-shape file :copy-nested2-ellipse)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
|
||||||
|
(pcb/with-page page)
|
||||||
|
(pcb/with-objects (:objects page)))
|
||||||
|
page
|
||||||
|
{(:id file) file}
|
||||||
|
(thi/id :copy-big-board))
|
||||||
|
file' (thf/apply-changes file changes)
|
||||||
|
|
||||||
|
;; ==== Get
|
||||||
|
nested2-ellipse' (ths/get-shape file' :nested2-ellipse)
|
||||||
|
copy-nested2-ellipse' (ths/get-shape file' :copy-nested2-ellipse)
|
||||||
|
fills' (:fills copy-nested2-ellipse')
|
||||||
|
fill' (first fills')]
|
||||||
|
|
||||||
|
;; ==== Check
|
||||||
|
|
||||||
|
;; If the copy have been already touched, merge the groups and preserve the modifications.
|
||||||
|
(t/is (= (:touched nested2-ellipse) #{:fill-group}))
|
||||||
|
(t/is (= (:touched copy-nested2-ellipse) #{:name-group :fill-group}))
|
||||||
|
(t/is (= (:touched nested2-ellipse') #{:fill-group}))
|
||||||
|
(t/is (= (:touched copy-nested2-ellipse') #{:name-group :fill-group}))
|
||||||
|
(t/is (= (count fills') 1))
|
||||||
|
(t/is (= (:fill-color fill') "#abcdef"))
|
||||||
|
(t/is (= (:fill-opacity fill') 1))))
|
||||||
|
|
||||||
|
(t/deftest test-dont-propagete-touched-when-swapped-copy
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (-> (setup-file)
|
||||||
|
(ths/update-shape :nested-rectangle :fills (ths/sample-fills-color :fill-color "#fabada"))
|
||||||
|
(thc/instantiate-component :c-big-board
|
||||||
|
:copy-big-board
|
||||||
|
:children-labels [:copy-h-board-with-ellipse
|
||||||
|
:copy-nested2-h-ellipse
|
||||||
|
:copy-nested2-ellipse])
|
||||||
|
(thc/component-swap :copy-h-board-with-ellipse
|
||||||
|
:c-board-with-rectangle
|
||||||
|
:copy-h-board-with-rectangle
|
||||||
|
:children-labels [:copy-nested2-h-rectangle
|
||||||
|
:copy-nested2-rectangle]))
|
||||||
|
|
||||||
|
page (thf/current-page file)
|
||||||
|
nested2-rectangle (ths/get-shape file :nested2-rectangle)
|
||||||
|
copy-nested2-rectangle (ths/get-shape file :copy-nested2-rectangle)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
|
||||||
|
(pcb/with-page page)
|
||||||
|
(pcb/with-objects (:objects page)))
|
||||||
|
page
|
||||||
|
{(:id file) file}
|
||||||
|
(thi/id :copy-big-board))
|
||||||
|
file' (thf/apply-changes file changes)
|
||||||
|
|
||||||
|
;; ==== Get
|
||||||
|
nested2-rectangle' (ths/get-shape file' :nested2-rectangle)
|
||||||
|
copy-nested2-rectangle' (ths/get-shape file' :copy-nested2-rectangle)
|
||||||
|
fills' (:fills copy-nested2-rectangle')
|
||||||
|
fill' (first fills')]
|
||||||
|
|
||||||
|
;; ==== Check
|
||||||
|
|
||||||
|
;; If the copy has been swapped, there is nothing to propagate since it's already
|
||||||
|
;; pointing to the swapped near main.
|
||||||
|
(t/is (= (:touched nested2-rectangle) nil))
|
||||||
|
(t/is (= (:touched copy-nested2-rectangle) nil))
|
||||||
|
(t/is (= (:touched nested2-rectangle') nil))
|
||||||
|
(t/is (= (:touched copy-nested2-rectangle') nil))
|
||||||
|
(t/is (= (count fills') 1))
|
||||||
|
(t/is (= (:fill-color fill') "#fabada"))
|
||||||
|
(t/is (= (:fill-opacity fill') 1))))
|
||||||
|
|
||||||
|
(t/deftest test-propagate-touched-when-swapped-main
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (-> (setup-file)
|
||||||
|
(thc/component-swap :nested2-h-ellipse
|
||||||
|
:c-rectangle
|
||||||
|
:nested2-h-rectangle
|
||||||
|
:children-labels [:nested2-rectangle])
|
||||||
|
(ths/update-shape :nested2-rectangle :fills (ths/sample-fills-color :fill-color "#fabada"))
|
||||||
|
(thc/instantiate-component :c-big-board
|
||||||
|
:copy-big-board
|
||||||
|
:children-labels [:copy-h-board-with-ellipse
|
||||||
|
:copy-nested2-h-rectangle
|
||||||
|
:copy-nested2-rectangle]))
|
||||||
|
|
||||||
|
page (thf/current-page file)
|
||||||
|
nested2-rectangle (ths/get-shape file :nested2-rectangle)
|
||||||
|
copy-nested2-rectangle (ths/get-shape file :copy-nested2-rectangle)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
changes (cll/generate-detach-instance (-> (pcb/empty-changes nil)
|
||||||
|
(pcb/with-page page)
|
||||||
|
(pcb/with-objects (:objects page)))
|
||||||
|
page
|
||||||
|
{(:id file) file}
|
||||||
|
(thi/id :copy-big-board))
|
||||||
|
file' (thf/apply-changes file changes)
|
||||||
|
|
||||||
|
;; ==== Get
|
||||||
|
nested2-rectangle' (ths/get-shape file' :nested2-rectangle)
|
||||||
|
copy-nested2-rectangle' (ths/get-shape file' :copy-nested2-rectangle)
|
||||||
|
fills' (:fills copy-nested2-rectangle')
|
||||||
|
fill' (first fills')]
|
||||||
|
|
||||||
|
;; ==== Check
|
||||||
|
|
||||||
|
;; If the main has been swapped, there is no difference. It propagates the same as
|
||||||
|
;; if it were the original component.
|
||||||
|
(t/is (= (:touched nested2-rectangle) #{:fill-group}))
|
||||||
|
(t/is (= (:touched copy-nested2-rectangle) nil))
|
||||||
|
(t/is (= (:touched nested2-rectangle') #{:fill-group}))
|
||||||
|
(t/is (= (:touched copy-nested2-rectangle') #{:fill-group}))
|
||||||
|
(t/is (= (count fills') 1))
|
||||||
|
(t/is (= (:fill-color fill') "#fabada"))
|
||||||
|
(t/is (= (:fill-opacity fill') 1))))
|
|
@ -52,7 +52,8 @@
|
||||||
:profile-is-muted
|
:profile-is-muted
|
||||||
(rx/of (msg/error (tr "errors.profile-is-muted")))
|
(rx/of (msg/error (tr "errors.profile-is-muted")))
|
||||||
|
|
||||||
:email-has-permanent-bounces
|
(:email-has-permanent-bounces
|
||||||
|
:email-has-complaints)
|
||||||
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data))))
|
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data))))
|
||||||
|
|
||||||
(rx/throw cause)))))
|
(rx/throw cause)))))
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
on-error
|
on-error
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [form cause]
|
(fn [form cause]
|
||||||
(let [{:keys [type code]} (ex-data cause)]
|
(let [{:keys [type code] :as edata} (ex-data cause)]
|
||||||
(condp = [type code]
|
(condp = [type code]
|
||||||
[:restriction :registration-disabled]
|
[:restriction :registration-disabled]
|
||||||
(st/emit! (msg/error (tr "errors.registration-disabled")))
|
(st/emit! (msg/error (tr "errors.registration-disabled")))
|
||||||
|
@ -51,6 +51,12 @@
|
||||||
[:restriction :email-domain-is-not-allowed]
|
[:restriction :email-domain-is-not-allowed]
|
||||||
(st/emit! (msg/error (tr "errors.email-domain-not-allowed")))
|
(st/emit! (msg/error (tr "errors.email-domain-not-allowed")))
|
||||||
|
|
||||||
|
[:restriction :email-has-permanent-bounces]
|
||||||
|
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" (:email edata))))
|
||||||
|
|
||||||
|
[:restriction :email-has-complaints]
|
||||||
|
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" (:email edata))))
|
||||||
|
|
||||||
[:validation :email-as-password]
|
[:validation :email-as-password]
|
||||||
(swap! form assoc-in [:errors :password]
|
(swap! form assoc-in [:errors :password]
|
||||||
{:code "errors.email-as-password"})
|
{:code "errors.email-as-password"})
|
||||||
|
|
|
@ -168,21 +168,22 @@
|
||||||
(dd/fetch-team-invitations)))
|
(dd/fetch-team-invitations)))
|
||||||
|
|
||||||
on-error
|
on-error
|
||||||
(fn [{:keys [type code] :as error}]
|
(fn [_form cause]
|
||||||
|
(let [{:keys [type code] :as error} (ex-data cause)]
|
||||||
(cond
|
(cond
|
||||||
(and (= :validation type)
|
(and (= :validation type)
|
||||||
(= :profile-is-muted code))
|
(= :profile-is-muted code))
|
||||||
(st/emit! (msg/error (tr "errors.profile-is-muted"))
|
(st/emit! (msg/error (tr "errors.profile-is-muted"))
|
||||||
(modal/hide))
|
(modal/hide))
|
||||||
|
|
||||||
(and (= :validation type)
|
|
||||||
(or (= :member-is-muted code)
|
(or (= :member-is-muted code)
|
||||||
(= :email-has-permanent-bounces code)))
|
(= :email-has-permanent-bounces code)
|
||||||
|
(= :email-has-complaints code))
|
||||||
(swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error)))
|
(swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error)))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(st/emit! (msg/error (tr "errors.generic"))
|
(st/emit! (msg/error (tr "errors.generic"))
|
||||||
(modal/hide))))
|
(modal/hide)))))
|
||||||
|
|
||||||
on-submit
|
on-submit
|
||||||
(fn [form]
|
(fn [form]
|
||||||
|
@ -574,7 +575,8 @@
|
||||||
on-error
|
on-error
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps email)
|
(mf/deps email)
|
||||||
(fn [{:keys [type code] :as error}]
|
(fn [cause]
|
||||||
|
(let [{:keys [type code] :as error} (ex-data cause)]
|
||||||
(cond
|
(cond
|
||||||
(and (= :validation type)
|
(and (= :validation type)
|
||||||
(= :profile-is-muted code))
|
(= :profile-is-muted code))
|
||||||
|
@ -584,12 +586,13 @@
|
||||||
(= :member-is-muted code))
|
(= :member-is-muted code))
|
||||||
(rx/of (msg/error (tr "errors.member-is-muted")))
|
(rx/of (msg/error (tr "errors.member-is-muted")))
|
||||||
|
|
||||||
(and (= :validation type)
|
(and (= :restriction type)
|
||||||
(= :email-has-permanent-bounces code))
|
(or (= :email-has-permanent-bounces code)
|
||||||
|
(= :email-has-complaints code)))
|
||||||
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email)))
|
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email)))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(rx/throw error))))
|
(rx/throw cause)))))
|
||||||
|
|
||||||
on-delete
|
on-delete
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
@ -599,7 +602,6 @@
|
||||||
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
||||||
(st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
|
(st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
|
||||||
|
|
||||||
|
|
||||||
on-resend-success
|
on-resend-success
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn []
|
(fn []
|
||||||
|
|
|
@ -21,8 +21,9 @@
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(defn- on-error
|
(defn- on-error
|
||||||
[form error]
|
[form cause]
|
||||||
(case (:code (ex-data error))
|
(let [{:keys [code] :as error} (ex-data cause)]
|
||||||
|
(case code
|
||||||
:email-already-exists
|
:email-already-exists
|
||||||
(swap! form (fn [data]
|
(swap! form (fn [data]
|
||||||
(let [error {:message (tr "errors.email-already-exists")}]
|
(let [error {:message (tr "errors.email-already-exists")}]
|
||||||
|
@ -31,11 +32,11 @@
|
||||||
:profile-is-muted
|
:profile-is-muted
|
||||||
(rx/of (msg/error (tr "errors.profile-is-muted")))
|
(rx/of (msg/error (tr "errors.profile-is-muted")))
|
||||||
|
|
||||||
:email-has-permanent-bounces
|
(:email-has-permanent-bounces
|
||||||
(let [email (get @form [:data :email-1])]
|
:email-has-complaints)
|
||||||
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email))))
|
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email error))))
|
||||||
|
|
||||||
(rx/throw error)))
|
(rx/throw cause))))
|
||||||
|
|
||||||
(defn- on-success
|
(defn- on-success
|
||||||
[profile data]
|
[profile data]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue