mirror of
https://github.com/penpot/penpot.git
synced 2025-02-02 12:28:54 -05:00
🎉 Add the ability to copy team invitation link
This commit is contained in:
parent
7d2e3a0864
commit
842463ed1b
9 changed files with 262 additions and 135 deletions
|
@ -322,7 +322,7 @@
|
||||||
::http.client/client (ig/ref ::http.client/client)
|
::http.client/client (ig/ref ::http.client/client)
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
|
::props (ig/ref :app.setup/props)
|
||||||
:pool (ig/ref ::db/pool)
|
:pool (ig/ref ::db/pool)
|
||||||
:session (ig/ref :app.http.session/manager)
|
:session (ig/ref :app.http.session/manager)
|
||||||
:sprops (ig/ref :app.setup/props)
|
:sprops (ig/ref :app.setup/props)
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.emails :as eml]
|
[app.emails :as eml]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
|
[app.main :as-alias main]
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc.climit :as climit]
|
[app.rpc.climit :as climit]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
|
@ -260,7 +261,7 @@
|
||||||
|
|
||||||
(def sql:team-invitations
|
(def sql:team-invitations
|
||||||
"select email_to as email, role, (valid_until < now()) as expired
|
"select email_to as email, role, (valid_until < now()) as expired
|
||||||
from team_invitation where team_id = ? order by valid_until desc")
|
from team_invitation where team_id = ? order by valid_until desc, created_at desc")
|
||||||
|
|
||||||
(defn get-team-invitations
|
(defn get-team-invitations
|
||||||
[conn team-id]
|
[conn team-id]
|
||||||
|
@ -628,25 +629,37 @@
|
||||||
"insert into team_invitation(team_id, email_to, role, valid_until)
|
"insert into team_invitation(team_id, email_to, role, valid_until)
|
||||||
values (?, ?, ?, ?)
|
values (?, ?, ?, ?)
|
||||||
on conflict(team_id, email_to) do
|
on conflict(team_id, email_to) do
|
||||||
update set role = ?, valid_until = ?, updated_at = now();")
|
update set role = ?, updated_at = now();")
|
||||||
|
|
||||||
|
(defn- create-invitation-token
|
||||||
|
[cfg {:keys [expire profile-id team-id member-id member-email role]}]
|
||||||
|
(tokens/generate (::main/props cfg)
|
||||||
|
{:iss :team-invitation
|
||||||
|
:exp expire
|
||||||
|
:profile-id profile-id
|
||||||
|
:role role
|
||||||
|
:team-id team-id
|
||||||
|
:member-email member-email
|
||||||
|
:member-id member-id}))
|
||||||
|
|
||||||
|
(defn- create-profile-identity-token
|
||||||
|
[cfg profile]
|
||||||
|
(tokens/generate (::main/props cfg)
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:exp (dt/in-future {:days 30})}))
|
||||||
|
|
||||||
(defn- create-invitation
|
(defn- create-invitation
|
||||||
[{:keys [conn sprops team profile role email] :as cfg}]
|
[{:keys [::conn] :as cfg} {:keys [team profile role email] :as params}]
|
||||||
(let [member (profile/retrieve-profile-data-by-email conn email)
|
(let [member (profile/retrieve-profile-data-by-email conn email)
|
||||||
token-exp (dt/in-future "168h") ;; 7 days
|
expire (dt/in-future "168h") ;; 7 days
|
||||||
email (str/lower email)
|
itoken (create-invitation-token cfg {:profile-id (:id profile)
|
||||||
itoken (tokens/generate sprops
|
:expire expire
|
||||||
{:iss :team-invitation
|
:team-id (:id team)
|
||||||
:exp token-exp
|
:member-email (or (:email member) email)
|
||||||
:profile-id (:id profile)
|
:member-id (:id member)
|
||||||
:role role
|
:role role})
|
||||||
:team-id (:id team)
|
ptoken (create-profile-identity-token cfg profile)]
|
||||||
:member-email (:email member email)
|
|
||||||
:member-id (:id member)})
|
|
||||||
ptoken (tokens/generate sprops
|
|
||||||
{:iss :profile-identity
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:exp (dt/in-future {:days 30})})]
|
|
||||||
|
|
||||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
|
@ -687,11 +700,10 @@
|
||||||
{:id (:id member)})))
|
{:id (:id member)})))
|
||||||
(do
|
(do
|
||||||
(db/exec-one! conn [sql:upsert-team-invitation
|
(db/exec-one! conn [sql:upsert-team-invitation
|
||||||
(:id team) (str/lower email) (name role)
|
(:id team) (str/lower email) (name role) expire (name role)])
|
||||||
token-exp (name role) token-exp])
|
|
||||||
(eml/send! {::eml/conn conn
|
(eml/send! {::eml/conn conn
|
||||||
::eml/factory eml/invite-to-team
|
::eml/factory eml/invite-to-team
|
||||||
:public-uri (:public-uri cfg)
|
:public-uri (cf/get :public-uri)
|
||||||
:to email
|
:to email
|
||||||
:invited-by (:fullname profile)
|
:invited-by (:fullname profile)
|
||||||
:team (:name team)
|
:team (:name team)
|
||||||
|
@ -727,15 +739,14 @@
|
||||||
:code :profile-is-muted
|
:code :profile-is-muted
|
||||||
: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"))
|
||||||
|
|
||||||
(let [invitations (->> emails
|
(let [cfg (assoc cfg ::conn conn)
|
||||||
|
invitations (->> emails
|
||||||
(map (fn [email]
|
(map (fn [email]
|
||||||
(assoc cfg
|
{:email (str/lower email)
|
||||||
:email email
|
:team team
|
||||||
:conn conn
|
:profile profile
|
||||||
:team team
|
:role role}))
|
||||||
:profile profile
|
(map (partial create-invitation cfg)))]
|
||||||
:role role)))
|
|
||||||
(map create-invitation))]
|
|
||||||
(with-meta (vec invitations)
|
(with-meta (vec invitations)
|
||||||
{::audit/props {:invitations (count invitations)}})))))
|
{::audit/props {:invitations (count invitations)}})))))
|
||||||
|
|
||||||
|
@ -743,26 +754,26 @@
|
||||||
;; --- Mutation: Create Team & Invite Members
|
;; --- Mutation: Create Team & Invite Members
|
||||||
|
|
||||||
(s/def ::emails ::us/set-of-valid-emails)
|
(s/def ::emails ::us/set-of-valid-emails)
|
||||||
(s/def ::create-team-and-invitations
|
(s/def ::create-team-with-invitations
|
||||||
(s/merge ::create-team
|
(s/merge ::create-team
|
||||||
(s/keys :req-un [::emails ::role])))
|
(s/keys :req-un [::emails ::role])))
|
||||||
|
|
||||||
(sv/defmethod ::create-team-and-invitations
|
(sv/defmethod ::create-team-with-invitations
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [team (create-team conn params)
|
(let [team (create-team conn params)
|
||||||
profile (db/get-by-id conn :profile profile-id)]
|
profile (db/get-by-id conn :profile profile-id)
|
||||||
|
cfg (assoc cfg ::conn conn)]
|
||||||
|
|
||||||
;; Create invitations for all provided emails.
|
;; Create invitations for all provided emails.
|
||||||
(doseq [email emails]
|
(->> emails
|
||||||
(create-invitation
|
(map (fn [email]
|
||||||
(assoc cfg
|
{:team team
|
||||||
:conn conn
|
:profile profile
|
||||||
:team team
|
:email (str/lower email)
|
||||||
:profile profile
|
:role role}))
|
||||||
:email email
|
(run! (partial create-invitation cfg)))
|
||||||
:role role)))
|
|
||||||
|
|
||||||
(-> team
|
(-> team
|
||||||
(vary-meta assoc ::audit/props {:invitations (count emails)})
|
(vary-meta assoc ::audit/props {:invitations (count emails)})
|
||||||
|
@ -777,6 +788,28 @@
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:invitations (count emails)}})))))))
|
:invitations (count emails)}})))))))
|
||||||
|
|
||||||
|
;; --- Query: get-team-invitation-token
|
||||||
|
|
||||||
|
(s/def ::get-team-invitation-token
|
||||||
|
(s/keys :req-un [::profile-id ::team-id ::email]))
|
||||||
|
|
||||||
|
(sv/defmethod ::get-team-invitation-token
|
||||||
|
{::doc/added "1.17"}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email] :as params}]
|
||||||
|
(check-read-permissions! pool profile-id team-id)
|
||||||
|
(let [invit (-> (db/get pool :team-invitation
|
||||||
|
{:team-id team-id
|
||||||
|
:email-to (str/lower email)})
|
||||||
|
(update :role keyword))
|
||||||
|
member (profile/retrieve-profile-data-by-email pool (:email invit))
|
||||||
|
token (create-invitation-token cfg {:team-id (:team-id invit)
|
||||||
|
:profile-id profile-id
|
||||||
|
:expire (:expire invit)
|
||||||
|
:role (:role invit)
|
||||||
|
:member-id (:id member)
|
||||||
|
:member-email (or (:email member) (:email-to invit))})]
|
||||||
|
{:token token}))
|
||||||
|
|
||||||
;; --- Mutation: Update invitation role
|
;; --- Mutation: Update invitation role
|
||||||
|
|
||||||
(s/def ::update-team-invitation-role
|
(s/def ::update-team-invitation-role
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
[{:keys [conn session] :as cfg} {:keys [profile-id token]}
|
[{:keys [conn session] :as cfg} {:keys [profile-id token]}
|
||||||
{:keys [member-id team-id member-email] :as claims}]
|
{:keys [member-id team-id member-email] :as claims}]
|
||||||
|
|
||||||
(us/assert ::team-invitation-claims claims)
|
(us/verify! ::team-invitation-claims claims)
|
||||||
|
|
||||||
(let [invitation (db/get* conn :team-invitation
|
(let [invitation (db/get* conn :team-invitation
|
||||||
{:team-id team-id :email-to member-email})
|
{:team-id team-id :email-to member-email})
|
||||||
|
|
|
@ -153,21 +153,20 @@
|
||||||
:code :profile-is-muted
|
:code :profile-is-muted
|
||||||
: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"))
|
||||||
|
|
||||||
(let [invitations (->> emails
|
(let [cfg (assoc cfg ::cmd.teams/conn conn)
|
||||||
|
invitations (->> emails
|
||||||
(map (fn [email]
|
(map (fn [email]
|
||||||
(assoc cfg
|
{:email (str/lower email)
|
||||||
:email email
|
:team team
|
||||||
:conn conn
|
:profile profile
|
||||||
:team team
|
:role role}))
|
||||||
:profile profile
|
(map (partial #'cmd.teams/create-invitation cfg)))]
|
||||||
:role role)))
|
|
||||||
(map #'cmd.teams/create-invitation))]
|
|
||||||
(with-meta (vec invitations)
|
(with-meta (vec invitations)
|
||||||
{::audit/props {:invitations (count invitations)}})))))
|
{::audit/props {:invitations (count invitations)}})))))
|
||||||
|
|
||||||
;; --- Mutation: Create Team & Invite Members
|
;; --- Mutation: Create Team & Invite Members
|
||||||
|
|
||||||
(s/def ::create-team-and-invite-members ::cmd.teams/create-team-and-invitations)
|
(s/def ::create-team-and-invite-members ::cmd.teams/create-team-with-invitations)
|
||||||
|
|
||||||
(sv/defmethod ::create-team-and-invite-members
|
(sv/defmethod ::create-team-and-invite-members
|
||||||
{::doc/added "1.0"
|
{::doc/added "1.0"
|
||||||
|
@ -175,17 +174,17 @@
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id emails role] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id emails role] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [team (cmd.teams/create-team conn params)
|
(let [team (cmd.teams/create-team conn params)
|
||||||
profile (db/get-by-id conn :profile profile-id)]
|
profile (db/get-by-id conn :profile profile-id)
|
||||||
|
cfg (assoc cfg ::cmd.teams/conn conn)]
|
||||||
|
|
||||||
;; Create invitations for all provided emails.
|
;; Create invitations for all provided emails.
|
||||||
(doseq [email emails]
|
(->> emails
|
||||||
(#'cmd.teams/create-invitation
|
(map (fn [email]
|
||||||
(assoc cfg
|
{:team team
|
||||||
:conn conn
|
:profile profile
|
||||||
:team team
|
:email (str/lower email)
|
||||||
:profile profile
|
:role role}))
|
||||||
:email email
|
(run! (partial #'cmd.teams/create-invitation cfg)))
|
||||||
:role role)))
|
|
||||||
|
|
||||||
(-> team
|
(-> team
|
||||||
(vary-meta assoc ::audit/props {:invitations (count emails)})
|
(vary-meta assoc ::audit/props {:invitations (count emails)})
|
||||||
|
|
|
@ -63,6 +63,16 @@
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(t/is (= 1 (:call-count (deref mock)))))
|
(t/is (= 1 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
;; get invitation token
|
||||||
|
(let [params {::th/type :get-team-invitation-token
|
||||||
|
:profile-id (:id profile1)
|
||||||
|
:team-id (:id team)
|
||||||
|
:email "foo@bar.com"}
|
||||||
|
out (th/command! params)]
|
||||||
|
(t/is (th/success? out))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(contains? result :token)))
|
||||||
|
|
||||||
;; invite user with bounce
|
;; invite user with bounce
|
||||||
(th/reset-mock! mock)
|
(th/reset-mock! mock)
|
||||||
|
|
||||||
|
@ -235,8 +245,6 @@
|
||||||
|
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(t/deftest invite-team-member-with-email-verification-disabled
|
(t/deftest invite-team-member-with-email-verification-disabled
|
||||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.data.events :as ev]
|
[app.main.data.events :as ev]
|
||||||
[app.main.data.fonts :as df]
|
[app.main.data.fonts :as df]
|
||||||
[app.main.data.media :as di]
|
[app.main.data.media :as di]
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
[app.util.timers :as tm]
|
[app.util.timers :as tm]
|
||||||
|
[app.util.webapi :as wapi]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
[potok.core :as ptk]))
|
[potok.core :as ptk]))
|
||||||
|
@ -403,7 +405,7 @@
|
||||||
params {:name name
|
params {:name name
|
||||||
:emails #{emails}
|
:emails #{emails}
|
||||||
:role role}]
|
:role role}]
|
||||||
(->> (rp/cmd! :create-team-and-invitations params)
|
(->> (rp/cmd! :create-team-with-invitations params)
|
||||||
(rx/tap on-success)
|
(rx/tap on-success)
|
||||||
(rx/map team-created)
|
(rx/map team-created)
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
@ -509,6 +511,36 @@
|
||||||
(rx/tap on-success)
|
(rx/tap on-success)
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn copy-invitation-link
|
||||||
|
[{:keys [email team-id] :as params}]
|
||||||
|
(us/assert! ::us/email email)
|
||||||
|
(us/assert! ::us/uuid team-id)
|
||||||
|
|
||||||
|
(ptk/reify ::copy-invitation-link
|
||||||
|
IDeref
|
||||||
|
(-deref [_] {:email email :team-id team-id})
|
||||||
|
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state _]
|
||||||
|
(let [{:keys [on-success on-error]
|
||||||
|
:or {on-success identity
|
||||||
|
on-error rx/throw}} (meta params)
|
||||||
|
router (:router state)]
|
||||||
|
|
||||||
|
(->> (rp/cmd! :get-team-invitation-token params)
|
||||||
|
(rx/map (fn [params]
|
||||||
|
(rt/resolve router :auth-verify-token {} params)))
|
||||||
|
(rx/map (fn [fragment]
|
||||||
|
(assoc @cf/public-uri :fragment fragment)))
|
||||||
|
(rx/tap (fn [uri]
|
||||||
|
(wapi/write-to-clipboard (str uri))))
|
||||||
|
(rx/tap on-success)
|
||||||
|
(rx/ignore)
|
||||||
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
|
||||||
(defn update-team-invitation-role
|
(defn update-team-invitation-role
|
||||||
[{:keys [email team-id role] :as params}]
|
[{:keys [email team-id role] :as params}]
|
||||||
(us/assert! ::us/email email)
|
(us/assert! ::us/email email)
|
||||||
|
|
|
@ -448,81 +448,113 @@
|
||||||
:pending (= status :pending))}
|
:pending (= status :pending))}
|
||||||
[:span.status-label (tr status-label)]]))
|
[:span.status-label (tr status-label)]]))
|
||||||
|
|
||||||
(mf/defc invitation-actions [{:keys [can-modify? delete resend] :as props}]
|
(mf/defc invitation-actions
|
||||||
(let [show? (mf/use-state false)]
|
[{:keys [invitation team] :as props}]
|
||||||
(when can-modify?
|
(let [show? (mf/use-state false)
|
||||||
[:*
|
|
||||||
[:span.icon {:on-click #(reset! show? true)} [i/actions]]
|
team-id (:id team)
|
||||||
[:& dropdown {:show @show?
|
email (:email invitation)
|
||||||
:on-close #(reset! show? false)}
|
role (:role invitation)
|
||||||
[:ul.dropdown.actions-dropdown
|
|
||||||
[:li {:on-click resend} (tr "labels.resend-invitation")]
|
on-resend-success
|
||||||
[:li {:on-click delete} (tr "labels.delete-invitation")]]]])))
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
|
||||||
|
(modal/hide))))
|
||||||
|
|
||||||
|
on-copy-success
|
||||||
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(st/emit! (msg/success (tr "notifications.invitation-link-copied"))
|
||||||
|
(modal/hide))))
|
||||||
|
|
||||||
|
on-error
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps email)
|
||||||
|
(fn [{:keys [type code] :as error}]
|
||||||
|
(cond
|
||||||
|
(and (= :validation type)
|
||||||
|
(= :profile-is-muted code))
|
||||||
|
(rx/of (msg/error (tr "errors.profile-is-muted")))
|
||||||
|
|
||||||
|
(and (= :validation type)
|
||||||
|
(= :member-is-muted code))
|
||||||
|
(rx/of (msg/error (tr "errors.member-is-muted")))
|
||||||
|
|
||||||
|
(and (= :validation type)
|
||||||
|
(= :email-has-permanent-bounces code))
|
||||||
|
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email)))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(rx/throw error))))
|
||||||
|
|
||||||
|
delete-fn
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps email team-id)
|
||||||
|
(fn []
|
||||||
|
(let [params {:email email :team-id team-id}
|
||||||
|
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
||||||
|
(st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
|
||||||
|
|
||||||
|
resend-fn
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps email team-id)
|
||||||
|
(fn []
|
||||||
|
(let [params (with-meta {:emails [email]
|
||||||
|
:team-id team-id
|
||||||
|
:resend? true
|
||||||
|
:role role}
|
||||||
|
{:on-success on-resend-success
|
||||||
|
:on-error on-error})]
|
||||||
|
(st/emit!
|
||||||
|
(-> (dd/invite-team-members params)
|
||||||
|
(with-meta {::ev/origin :team}))))))
|
||||||
|
|
||||||
|
copy-fn
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps email team-id)
|
||||||
|
(fn []
|
||||||
|
(let [params (with-meta {:email email :team-id team-id}
|
||||||
|
{:on-success on-copy-success
|
||||||
|
:on-error on-error})]
|
||||||
|
(prn "KKK1")
|
||||||
|
(st/emit!
|
||||||
|
(-> (dd/copy-invitation-link params)
|
||||||
|
(with-meta {::ev/origin :team}))))))]
|
||||||
|
|
||||||
|
|
||||||
|
[:*
|
||||||
|
[:span.icon {:on-click #(reset! show? true)} [i/actions]]
|
||||||
|
[:& dropdown {:show @show?
|
||||||
|
:on-close #(reset! show? false)}
|
||||||
|
[:ul.dropdown.actions-dropdown
|
||||||
|
[:li {:on-click copy-fn} (tr "labels.copy-invitation-link")]
|
||||||
|
[:li {:on-click resend-fn} (tr "labels.resend-invitation")]
|
||||||
|
[:li {:on-click delete-fn} (tr "labels.delete-invitation")]]]]))
|
||||||
|
|
||||||
(mf/defc invitation-row
|
(mf/defc invitation-row
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
[{:keys [invitation can-invite? team] :as props}]
|
[{:keys [invitation can-invite? team] :as props}]
|
||||||
|
|
||||||
(let [expired? (:expired invitation)
|
(let [expired? (:expired invitation)
|
||||||
email (:email invitation)
|
email (:email invitation)
|
||||||
invitation-role (:role invitation)
|
role (:role invitation)
|
||||||
status (if expired?
|
status (if expired? :expired :pending)
|
||||||
:expired
|
|
||||||
:pending)
|
|
||||||
|
|
||||||
on-success
|
|
||||||
#(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
|
|
||||||
(modal/hide)
|
|
||||||
(dd/fetch-team-invitations))
|
|
||||||
|
|
||||||
|
|
||||||
on-error
|
|
||||||
(fn [email {:keys [type code] :as error}]
|
|
||||||
(cond
|
|
||||||
(and (= :validation type)
|
|
||||||
(= :profile-is-muted code))
|
|
||||||
(msg/error (tr "errors.profile-is-muted"))
|
|
||||||
|
|
||||||
(and (= :validation type)
|
|
||||||
(= :member-is-muted code))
|
|
||||||
(msg/error (tr "errors.member-is-muted"))
|
|
||||||
|
|
||||||
(and (= :validation type)
|
|
||||||
(= :email-has-permanent-bounces code))
|
|
||||||
(msg/error (tr "errors.email-has-permanent-bounces" email))
|
|
||||||
|
|
||||||
:else
|
|
||||||
(msg/error (tr "errors.generic"))))
|
|
||||||
|
|
||||||
change-rol
|
change-rol
|
||||||
(fn [role]
|
(mf/use-fn
|
||||||
(let [params {:email email :team-id (:id team) :role role}
|
(mf/deps team email)
|
||||||
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
(fn [role]
|
||||||
(st/emit! (dd/update-team-invitation-role (with-meta params mdata)))))
|
(let [params {:email email :team-id (:id team) :role role}
|
||||||
|
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
||||||
|
(st/emit! (dd/update-team-invitation-role (with-meta params mdata))))))]
|
||||||
|
|
||||||
delete-invitation
|
|
||||||
(fn []
|
|
||||||
(let [params {:email email :team-id (:id team)}
|
|
||||||
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
|
||||||
(st/emit! (dd/delete-team-invitation (with-meta params mdata)))))
|
|
||||||
|
|
||||||
resend-invitation
|
|
||||||
(fn []
|
|
||||||
(let [params {:emails [email]
|
|
||||||
:team-id (:id team)
|
|
||||||
:resend? true
|
|
||||||
:role invitation-role}
|
|
||||||
mdata {:on-success on-success
|
|
||||||
:on-error (partial on-error email)}]
|
|
||||||
(st/emit! (-> (dd/invite-team-members (with-meta params mdata))
|
|
||||||
(with-meta {::ev/origin :team}))
|
|
||||||
(dd/fetch-team-invitations))))]
|
|
||||||
[:div.table-row
|
[:div.table-row
|
||||||
[:div.table-field.mail email]
|
[:div.table-field.mail email]
|
||||||
[:div.table-field.roles
|
[:div.table-field.roles
|
||||||
[:& invitation-role-selector
|
[:& invitation-role-selector
|
||||||
{:can-invite? can-invite?
|
{:can-invite? can-invite?
|
||||||
:role invitation-role
|
:role role
|
||||||
:status status
|
:status status
|
||||||
:change-to-editor (partial change-rol :editor)
|
:change-to-editor (partial change-rol :editor)
|
||||||
:change-to-admin (partial change-rol :admin)}]]
|
:change-to-admin (partial change-rol :admin)}]]
|
||||||
|
@ -530,20 +562,22 @@
|
||||||
[:div.table-field.status
|
[:div.table-field.status
|
||||||
[:& invitation-status-badge {:status status}]]
|
[:& invitation-status-badge {:status status}]]
|
||||||
[:div.table-field.actions
|
[:div.table-field.actions
|
||||||
[:& invitation-actions
|
(when can-invite?
|
||||||
{:can-modify? can-invite?
|
[:& invitation-actions
|
||||||
:delete delete-invitation
|
{:invitation invitation
|
||||||
:resend resend-invitation}]]]))
|
:team team}])]]))
|
||||||
|
|
||||||
(mf/defc empty-invitation-table [can-invite?]
|
(mf/defc empty-invitation-table
|
||||||
|
[{:keys [can-invite?] :as props}]
|
||||||
[:div.empty-invitations
|
[:div.empty-invitations
|
||||||
[:span (tr "labels.no-invitations")]
|
[:span (tr "labels.no-invitations")]
|
||||||
(when (:can-invite? can-invite?) [:span (tr "labels.no-invitations-hint")])])
|
(when can-invite?
|
||||||
|
[:span (tr "labels.no-invitations-hint")])])
|
||||||
|
|
||||||
(mf/defc invitation-section
|
(mf/defc invitation-section
|
||||||
[{:keys [team invitations] :as props}]
|
[{:keys [team invitations] :as props}]
|
||||||
(let [owner? (get-in team [:permissions :is-owner])
|
(let [owner? (dm/get-in team [:permissions :is-owner])
|
||||||
admin? (get-in team [:permissions :is-admin])
|
admin? (dm/get-in team [:permissions :is-admin])
|
||||||
can-invite? (or owner? admin?)]
|
can-invite? (or owner? admin?)]
|
||||||
|
|
||||||
[:div.dashboard-table.invitations
|
[:div.dashboard-table.invitations
|
||||||
|
@ -555,7 +589,11 @@
|
||||||
[:& empty-invitation-table {:can-invite? can-invite?}]
|
[:& empty-invitation-table {:can-invite? can-invite?}]
|
||||||
[:div.table-rows
|
[:div.table-rows
|
||||||
(for [invitation invitations]
|
(for [invitation invitations]
|
||||||
[:& invitation-row {:key (:email invitation) :invitation invitation :can-invite? can-invite? :team team}])])]))
|
[:& invitation-row
|
||||||
|
{:key (:email invitation)
|
||||||
|
:invitation invitation
|
||||||
|
:can-invite? can-invite?
|
||||||
|
:team team}])])]))
|
||||||
|
|
||||||
(mf/defc team-invitations-page
|
(mf/defc team-invitations-page
|
||||||
[{:keys [team] :as props}]
|
[{:keys [team] :as props}]
|
||||||
|
@ -568,7 +606,7 @@
|
||||||
(tr "dashboard.your-penpot")
|
(tr "dashboard.your-penpot")
|
||||||
(:name team)))))
|
(:name team)))))
|
||||||
|
|
||||||
(mf/with-effect
|
(mf/with-effect []
|
||||||
(st/emit! (dd/fetch-team-invitations)))
|
(st/emit! (dd/fetch-team-invitations)))
|
||||||
|
|
||||||
[:*
|
[:*
|
||||||
|
|
|
@ -1436,6 +1436,10 @@ msgstr "Rename team"
|
||||||
msgid "labels.resend-invitation"
|
msgid "labels.resend-invitation"
|
||||||
msgstr "Resend invitation"
|
msgstr "Resend invitation"
|
||||||
|
|
||||||
|
#: src/app/main/ui/dashboard/team.cljs
|
||||||
|
msgid "labels.copy-invitation-link"
|
||||||
|
msgstr "Copy invitation link"
|
||||||
|
|
||||||
#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
|
#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
|
||||||
msgid "labels.retry"
|
msgid "labels.retry"
|
||||||
msgstr "Retry"
|
msgstr "Retry"
|
||||||
|
@ -1957,6 +1961,10 @@ msgstr "Update a component in a shared library"
|
||||||
msgid "notifications.invitation-email-sent"
|
msgid "notifications.invitation-email-sent"
|
||||||
msgstr "Invitation sent successfully"
|
msgstr "Invitation sent successfully"
|
||||||
|
|
||||||
|
#: src/app/main/ui/dashboard/team.cljs
|
||||||
|
msgid "notifications.invitation-link-copied"
|
||||||
|
msgstr "Invitation link copied"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/delete_account.cljs
|
#: src/app/main/ui/settings/delete_account.cljs
|
||||||
msgid "notifications.profile-deletion-not-allowed"
|
msgid "notifications.profile-deletion-not-allowed"
|
||||||
msgstr "You can't delete you profile. Reassign your teams before proceed."
|
msgstr "You can't delete you profile. Reassign your teams before proceed."
|
||||||
|
|
|
@ -1606,6 +1606,10 @@ msgstr "Renombra el equipo"
|
||||||
msgid "labels.resend-invitation"
|
msgid "labels.resend-invitation"
|
||||||
msgstr "Reenviar invitacion"
|
msgstr "Reenviar invitacion"
|
||||||
|
|
||||||
|
#: src/app/main/ui/dashboard/team.cljs
|
||||||
|
msgid "labels.copy-invitation-link"
|
||||||
|
msgstr "Copiar link de invitación"
|
||||||
|
|
||||||
#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
|
#: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
|
||||||
msgid "labels.retry"
|
msgid "labels.retry"
|
||||||
msgstr "Reintentar"
|
msgstr "Reintentar"
|
||||||
|
@ -2172,6 +2176,11 @@ msgstr "Actualizar un componente en biblioteca"
|
||||||
msgid "notifications.invitation-email-sent"
|
msgid "notifications.invitation-email-sent"
|
||||||
msgstr "Invitación enviada con éxito"
|
msgstr "Invitación enviada con éxito"
|
||||||
|
|
||||||
|
#: src/app/main/ui/dashboard/team.cljs
|
||||||
|
msgid "notifications.invitation-link-copied"
|
||||||
|
msgstr "Enlace de invitacion copiado"
|
||||||
|
|
||||||
|
|
||||||
#: src/app/main/ui/settings/delete_account.cljs
|
#: src/app/main/ui/settings/delete_account.cljs
|
||||||
msgid "notifications.profile-deletion-not-allowed"
|
msgid "notifications.profile-deletion-not-allowed"
|
||||||
msgstr "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir."
|
msgstr "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir."
|
||||||
|
|
Loading…
Add table
Reference in a new issue