0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-23 23:18:48 -05:00

📎 Add more events instrumentation

This commit is contained in:
Andrey Antukh 2022-03-21 15:00:50 +01:00 committed by Andrés Moya
parent bf6211903c
commit 8acc9af1f5
10 changed files with 216 additions and 189 deletions

View file

@ -180,17 +180,6 @@
;; --- HTTP HANDLERS
(defn extract-utm-props
"Extracts additional data from user params."
[params]
(reduce-kv (fn [params k v]
(let [sk (name k)]
(cond-> params
(str/starts-with? sk "utm_")
(assoc (->> sk str/kebab (keyword "penpot")) v))))
{}
params))
(defn- retrieve-profile
[{:keys [pool executor] :as cfg} info]
(px/with-dispatch executor
@ -252,7 +241,7 @@
(defn- auth-handler
[{:keys [tokens] :as cfg} {:keys [params] :as request} respond raise]
(try
(let [props (extract-utm-props params)
(let [props (audit/extract-utm-params params)
state (tokens :generate
{:iss :oauth
:invitation-token (:invitation-token params)

View file

@ -34,6 +34,20 @@
(yrq/get-header request "x-real-ip")
(yrq/remote-addr request)))
(defn extract-utm-params
"Extracts additional data from params and namespace them under
`penpot` ns."
[params]
(letfn [(process-param [params k v]
(let [sk (d/name k)]
(cond-> params
(str/starts-with? sk "utm_")
(assoc (->> sk str/kebab (keyword "penpot")) v)
(str/starts-with? sk "mtm_")
(assoc (->> sk str/kebab (keyword "penpot")) v))))]
(reduce-kv process-param {} params)))
(defn profile->props
[profile]
(-> profile

View file

@ -12,7 +12,6 @@
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.http.oauth :refer [extract-utm-props]]
[app.loggers.audit :as audit]
[app.media :as media]
[app.rpc.mutations.teams :as teams]
@ -223,7 +222,7 @@
[conn params]
(let [id (or (:id params) (uuid/next))
props (-> (extract-utm-props params)
props (-> (audit/extract-utm-params params)
(merge (:props params))
(db/tjson))

View file

@ -13,6 +13,7 @@
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.loggers.audit :as audit]
[app.media :as media]
[app.rpc.mutations.projects :as projects]
[app.rpc.permissions :as perms]
@ -357,14 +358,14 @@
:opt-un [::email ::emails]))
(sv/defmethod ::invite-team-member
"A rpc call that allow to send a single or multiple invitations to
join the team."
[{:keys [pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)
emails (or emails #{})
emails (if email (conj emails email) emails)
]
emails (cond-> (or emails #{}) (string? email) (conj email))]
(when-not (:is-admin perms)
(ex/raise :type :validation
@ -385,7 +386,9 @@
:profile profile
:role role))
)
nil)))
(with-meta {}
{::audit/props {:invitations (count emails)}}))))
(def sql:upsert-team-invitation
"insert into team_invitation(team_id, email_to, role, valid_until)
@ -395,19 +398,19 @@
(defn- create-team-invitation
[{:keys [conn tokens team profile role email] :as cfg}]
(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 "48h")
itoken (tokens :generate
{:iss :team-invitation
:exp token-exp
:profile-id (:id profile)
:role role
:team-id (:id team)
:member-email (:email member email)
:member-id (:id member)})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
itoken (tokens :generate
{:iss :team-invitation
:exp token-exp
:profile-id (:id profile)
:role role
:team-id (:id team)
:member-email (:email member email)
:member-id (:id member)})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
@ -443,21 +446,14 @@
(s/and ::create-team (s/keys :req-un [::emails ::role])))
(sv/defmethod ::create-team-and-invite-members
[{:keys [pool audit] :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]
(let [team (create-team conn params)
profile (db/get-by-id conn :profile profile-id)]
(let [team (create-team conn params)
audit-fn (:audit cfg)
profile (db/get-by-id conn :profile profile-id)]
;; Create invitations for all provided emails.
(doseq [email emails]
(audit :cmd :submit
:type "mutation"
:name "create-team-invitation"
:profile-id profile-id
:props {:email email
:role role
:profile-id profile-id})
(create-team-invitation
(assoc cfg
:conn conn
@ -465,8 +461,17 @@
:profile profile
:email email
:role role)))
team)))
(with-meta team
{:before-complete
#(audit-fn :cmd :submit
:type "mutation"
:name "invite-team-member"
:profile-id profile-id
:props {:emails emails
:role role
:profile-id profile-id
:invitations (count emails)})}))))
;; --- Mutation: Update invitation role

View file

@ -44,16 +44,15 @@
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= {} (:result out)))
(t/is (= 1 (:call-count (deref mock))))
(t/is (= 1 (:num invitation))))
;; invite internal user without complaints
(th/reset-mock! mock)
(let [data (assoc data :email (:email profile2))
out (th/mutation! data)]
(t/is (nil? (:result out)))
(t/is (= {} (:result out)))
(t/is (= 1 (:call-count (deref mock)))))
;; invite user with complaint
@ -61,7 +60,7 @@
(th/reset-mock! mock)
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)]
(t/is (nil? (:result out)))
(t/is (= {} (:result out)))
(t/is (= 1 (:call-count (deref mock)))))
;; invite user with bounce

View file

@ -426,21 +426,21 @@
(rx/tap #(tm/schedule on-success))
(rx/catch on-error))))))
(defn invite-team-member
[{:keys [emails role] :as params}]
(defn invite-team-members
[{:keys [emails role team-id resend?] :as params}]
(us/assert ::us/set-of-emails emails)
(us/assert ::us/keyword role)
(ptk/reify ::invite-team-member
(us/assert ::us/uuid team-id)
(ptk/reify ::invite-team-members
IDeref
(-deref [_] {:role role})
(-deref [_] {:role role :team-id team-id :resend? resend?})
ptk/WatchEvent
(watch [_ state _]
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
team-id (:current-team-id state)
params (assoc params :team-id team-id)]
params (dissoc params :resend?)]
(->> (rp/mutation! :invite-team-member params)
(rx/tap on-success)
(rx/catch on-error))))))

View file

@ -85,7 +85,7 @@
(derive :app.main.data.dashboard/delete-team-member ::generic-action)
(derive :app.main.data.dashboard/duplicate-project ::generic-action)
(derive :app.main.data.dashboard/file-created ::generic-action)
(derive :app.main.data.dashboard/invite-team-member ::generic-action)
(derive :app.main.data.dashboard/invite-team-members ::generic-action)
(derive :app.main.data.dashboard/leave-team ::generic-action)
(derive :app.main.data.dashboard/move-files ::generic-action)
(derive :app.main.data.dashboard/move-project ::generic-action)
@ -113,6 +113,7 @@
(derive :app.main.data.workspace.persistence/attach-library ::generic-action)
(derive :app.main.data.workspace.persistence/detach-library ::generic-action)
(derive :app.main.data.workspace.persistence/set-file-shard ::generic-action)
(derive :app.main.data.workspace.selection/toggle-focus-mode ::generic-action)
(derive :app.main.data.workspace/create-page ::generic-action)
(derive :app.main.data.workspace/set-workspace-layout ::generic-action)
(derive :app.main.data.workspace/toggle-layout-flag ::generic-action)

View file

@ -293,29 +293,6 @@
(rx/catch (constantly (rx/of 1)))
(rx/map #(logged-out params)))))))
;; --- EVENT: register
(s/def ::register
(s/keys :req-un [::fullname ::password ::email]))
(defn register
"Create a register event instance."
[data]
(s/assert ::register data)
(ptk/reify ::register
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :register-profile data)
(rx/tap on-success)
(rx/catch on-error))))
ptk/EffectEvent
(effect [_ _ _]
(swap! storage dissoc ::redirect-to))))
;; --- Update Profile
(defn update-profile

View file

@ -68,8 +68,8 @@
(st/emit! (dm/error (tr "errors.generic")))))
(defn- handle-prepare-register-success
[_form {:keys [token] :as result}]
(st/emit! (rt/nav :auth-register-validate {} {:token token})))
[_ params]
(st/emit! (rt/nav :auth-register-validate {} params)))
(mf/defc register-form
[{:keys [params] :as props}]
@ -83,8 +83,9 @@
(mf/use-callback
(fn [form _event]
(reset! submitted? true)
(let [params (:clean-data @form)]
(->> (rp/mutation :prepare-register-profile params)
(let [cdata (:clean-data @form)]
(->> (rp/mutation :prepare-register-profile cdata)
(rx/map #(merge % params))
(rx/finalize #(reset! submitted? false))
(rx/subs (partial handle-prepare-register-success form)
(partial handle-prepare-register-error form))))))
@ -160,13 +161,6 @@
(defn- handle-register-error
[form error]
(case (:code error)
:registration-disabled
(st/emit! (dm/error (tr "errors.registration-disabled")))
:email-has-permanent-bounces
(let [email (get @form [:data :email])]
(st/emit! (dm/error (tr "errors.email-has-permanent-bounces" email))))
:email-already-exists
(swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"})

View file

@ -7,10 +7,11 @@
(ns app.main.ui.dashboard.team
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.spec :as us]
[app.config :as cfg]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.refs :as refs]
@ -27,15 +28,14 @@
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
;; TEAM SECTION
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [section team] :as props}]
(let [go-members (st/emitf (dd/go-to-team-members))
go-settings (st/emitf (dd/go-to-team-settings))
go-invitations (st/emitf (dd/go-to-team-invitations))
invite-member (st/emitf (modal/show {:type ::invite-member :team team}))
(let [go-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
go-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
go-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
invite-member (mf/use-fn #(st/emit! (modal/show {:type :invite-members :team team})))
members-section? (= section :dashboard-team-members)
settings-section? (= section :dashboard-team-settings)
invitations-section? (= section :dashboard-team-invitations)
@ -62,12 +62,16 @@
(tr "dashboard.invite-profile")]
[:div.blank-space])]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITATIONS MODAL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-available-roles
[permissions]
(->> [{:value "editor" :label (tr "labels.editor")}
(when (:is-admin permissions)
{:value "admin" :label (tr "labels.admin")})
;; Temporarily disabled viewer role
;; Temporarily disabled viewer roles
;; https://tree.taiga.io/project/uxboxproject/issue/1083
;; {:value "viewer" :label (tr "labels.viewer")}
]
@ -75,31 +79,34 @@
(s/def ::emails (s/and ::us/set-of-emails d/not-empty?))
(s/def ::role ::us/keyword)
(s/def ::invite-member-form
(s/keys :req-un [::role ::emails]))
(s/def ::team-id ::us/uuid)
(mf/defc invite-member-modal
(s/def ::invite-member-form
(s/keys :req-un [::role ::emails ::team-id]))
(mf/defc invite-members-modal
{::mf/register modal/components
::mf/register-as ::invite-member}
::mf/register-as :invite-members}
[{:keys [team]}]
(let [perms (:permissions team)
roles (mf/use-memo (mf/deps perms) #(get-available-roles perms))
initial (mf/use-memo (constantly {:role "editor"}))
initial (mf/use-memo (constantly {:role "editor" :team-id (:id team)}))
form (fm/use-form :spec ::invite-member-form
:initial initial)
error-text (mf/use-state "")
on-success
(st/emitf (dm/success (tr "notifications.invitation-email-sent"))
(modal/hide)
(dd/fetch-team-invitations))
(fn []
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
(modal/hide)
(dd/fetch-team-invitations)))
on-error
(fn [{:keys [type code] :as error}]
(cond
(and (= :validation type)
(= :profile-is-muted code))
(st/emit! (dm/error (tr "errors.profile-is-muted"))
(st/emit! (msg/error (tr "errors.profile-is-muted"))
(modal/hide))
(and (= :validation type)
@ -108,7 +115,7 @@
(swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error)))
:else
(st/emit! (dm/error (tr "errors.generic"))
(st/emit! (msg/error (tr "errors.generic"))
(modal/hide))))
on-submit
@ -116,9 +123,9 @@
(let [params (:clean-data @form)
mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}]
(st/emit! (dd/invite-team-member (with-meta params mdata))
(st/emit! (dd/invite-team-members (with-meta params mdata))
(dd/fetch-team-invitations))))]
[:div.modal.dashboard-invite-modal.form-container
[:& fm/form {:on-submit on-submit :form form}
[:div.title
@ -141,7 +148,9 @@
[:div.action-buttons
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))
;; TEAM MEMBERS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MEMBERS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc member-info [{:keys [member profile] :as props}]
(let [is-you? (= (:id profile) (:id member))]
@ -210,101 +219,126 @@
{::mf/wrap [mf/memo]}
[{:keys [team member members profile] :as props}]
(let [set-role
(fn [role]
(let [params {:member-id (:id member) :role role}]
(st/emit! (dd/update-team-member-role params))))
owner? (get-in team [:permissions :is-owner])
(let [owner? (dm/get-in team [:permissions :is-owner])
set-role
(mf/use-fn
(mf/deps member)
(fn [role]
(let [params {:member-id (:id member) :role role}]
(st/emit! (dd/update-team-member-role params)))))
set-owner-fn (partial set-role :owner)
set-admin (partial set-role :admin)
set-editor (partial set-role :editor)
set-owner-fn (mf/use-fn (mf/deps set-role) (partial set-role :owner))
set-admin (mf/use-fn (mf/deps set-role) (partial set-role :admin))
set-editor (mf/use-fn (mf/deps set-role) (partial set-role :editor))
;; set-viewer (partial set-role :viewer)
set-owner
(fn [member]
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.promote-owner-confirm.title")
:message (tr "modals.promote-owner-confirm.message" (:name member))
:scd-message (tr "modals.promote-owner-confirm.hint")
:accept-label (tr "modals.promote-owner-confirm.accept")
:on-accept set-owner-fn
:accept-style :primary})))
(mf/use-fn
(mf/deps set-owner-fn member)
(fn [member]
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.promote-owner-confirm.title")
:message (tr "modals.promote-owner-confirm.message" (:name member))
:scd-message (tr "modals.promote-owner-confirm.hint")
:accept-label (tr "modals.promote-owner-confirm.accept")
:on-accept set-owner-fn
:accept-style :primary}))))
delete-member-fn
(st/emitf (dd/delete-team-member {:member-id (:id member)}))
(mf/use-fn
(mf/deps member)
(fn [] (st/emit! (dd/delete-team-member {:member-id (:id member)}))))
on-success
(fn []
(st/emit! (dd/go-to-projects (:default-team-id profile))
(modal/hide)
(du/fetch-teams)))
(mf/use-fn
(mf/deps profile)
(fn []
(st/emit! (dd/go-to-projects (:default-team-id profile))
(modal/hide)
(du/fetch-teams))))
on-error
(fn [{:keys [code] :as error}]
(condp = code
(mf/use-fn
(fn [{:keys [code] :as error}]
(condp = code
:no-enough-members-for-leave
(rx/of (dm/error (tr "errors.team-leave.insufficient-members")))
:no-enough-members-for-leave
(rx/of (msg/error (tr "errors.team-leave.insufficient-members")))
:member-does-not-exist
(rx/of (dm/error (tr "errors.team-leave.member-does-not-exists")))
:member-does-not-exist
(rx/of (msg/error (tr "errors.team-leave.member-does-not-exists")))
:owner-cant-leave-team
(rx/of (dm/error (tr "errors.team-leave.owner-cant-leave")))
:owner-cant-leave-team
(rx/of (msg/error (tr "errors.team-leave.owner-cant-leave")))
(rx/throw error)))
(rx/throw error))))
delete-fn
(fn []
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
:on-error on-error}))))
(mf/use-fn
(mf/deps team on-success on-error)
(fn []
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
:on-error on-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})))))
(mf/use-fn
(mf/deps on-success on-error)
(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}))))))
leave-and-close
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.leave-confirm.title")
:message (tr "modals.leave-and-close-confirm.message" (:name team))
:scd-message (tr "modals.leave-and-close-confirm.hint")
:accept-label (tr "modals.leave-confirm.accept")
:on-accept delete-fn}))
(mf/use-fn
(mf/deps delete-fn)
(fn []
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.leave-confirm.title")
:message (tr "modals.leave-and-close-confirm.message" (:name team))
:scd-message (tr "modals.leave-and-close-confirm.hint")
:accept-label (tr "modals.leave-confirm.accept")
:on-accept delete-fn}))))
change-owner-and-leave
(fn []
(st/emit! (dd/fetch-team-members)
(modal/show
{:type :leave-and-reassign
:profile profile
:team team
:accept leave-fn})))
(mf/use-fn
(mf/deps profile team leave-fn)
(fn []
(st/emit! (dd/fetch-team-members)
(modal/show
{:type :leave-and-reassign
:profile profile
:team team
:accept leave-fn}))))
leave
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.leave-confirm.title")
:message (tr "modals.leave-confirm.message")
:accept-label (tr "modals.leave-confirm.accept")
:on-accept leave-fn}))
(mf/use-fn
(mf/deps leave-fn)
(fn []
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.leave-confirm.title")
:message (tr "modals.leave-confirm.message")
:accept-label (tr "modals.leave-confirm.accept")
:on-accept leave-fn}))))
preset-leave (cond (= 1 (count members)) leave-and-close
(= true owner?) change-owner-and-leave
:else leave)
delete
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.delete-team-member-confirm.title")
:message (tr "modals.delete-team-member-confirm.message")
:accept-label (tr "modals.delete-team-member-confirm.accept")
:on-accept delete-member-fn}))]
(mf/use-fn
(mf/deps delete-member-fn)
(fn []
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-team-member-confirm.title")
:message (tr "modals.delete-team-member-confirm.message")
:accept-label (tr "modals.delete-team-member-confirm.accept")
:on-accept delete-member-fn}))))]
[:div.table-row
[:div.table-field.name
@ -361,7 +395,9 @@
:team team
:members-map members-map}]]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITATIONS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc invitation-role-selector
[{:keys [can-invite? role status change-to-admin change-to-editor] :as props}]
@ -418,7 +454,7 @@
:pending)
on-success
#(st/emit! (dm/success (tr "notifications.invitation-email-sent"))
#(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
(modal/hide)
(dd/fetch-team-invitations))
@ -428,18 +464,18 @@
(cond
(and (= :validation type)
(= :profile-is-muted code))
(dm/error (tr "errors.profile-is-muted"))
(msg/error (tr "errors.profile-is-muted"))
(and (= :validation type)
(= :member-is-muted code))
(dm/error (tr "errors.member-is-muted"))
(msg/error (tr "errors.member-is-muted"))
(and (= :validation type)
(= :email-has-permanent-bounces code))
(dm/error (tr "errors.email-has-permanent-bounces" email))
(msg/error (tr "errors.email-has-permanent-bounces" email))
:else
(dm/error (tr "errors.generic"))))
(msg/error (tr "errors.generic"))))
change-rol
(fn [role]
@ -455,20 +491,31 @@
resend-invitation
(fn []
(let [params {:email email :team-id (:id team) :role invitation-role}
(let [params {:email 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-member (with-meta params mdata)))
(st/emit! (dd/fetch-team-invitations))))]
(st/emit! (dd/invite-team-members (with-meta params mdata))
(dd/fetch-team-invitations))))]
[:div.table-row
[:div.table-field.mail email]
[:div.table-field.roles [:& invitation-role-selector {:can-invite? can-invite?
:role invitation-role
:status status
:change-to-editor (partial change-rol :editor)
:change-to-admin (partial change-rol :admin)}]]
[:div.table-field.status [:& invitation-status-badge {:status status}]]
[:div.table-field.actions [:& invitation-actions {:can-modify? can-invite? :delete delete-invitation :resend resend-invitation}]]]))
[:div.table-field.roles
[:& invitation-role-selector
{:can-invite? can-invite?
:role invitation-role
:status status
:change-to-editor (partial change-rol :editor)
:change-to-admin (partial change-rol :admin)}]]
[:div.table-field.status
[:& invitation-status-badge {:status status}]]
[:div.table-field.actions
[:& invitation-actions
{:can-modify? can-invite?
:delete delete-invitation
:resend resend-invitation}]]]))
(mf/defc empty-invitation-table [can-invite?]
[:div.empty-invitations
@ -513,7 +560,9 @@
[:& invitation-section {:team team
:invitations invitations}]]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SETTINGS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc team-settings-page
[{:keys [team] :as props}]