🎉 Add invitation section to dashboard

This commit is contained in:
Migara 2022-02-17 11:57:25 +01:00 committed by Andrey Antukh
parent 486d89c5d0
commit 9d04dc7d9a
16 changed files with 480 additions and 64 deletions

View file

@ -18,6 +18,7 @@
### :sparkles: New features ### :sparkles: New features
. Add new invitations section [Taiga #2797](https://tree.taiga.io/project/penpot/us/2797)
- Ability to add multiple fills to a shape [Taiga #1394](https://tree.taiga.io/project/penpot/us/1394) - Ability to add multiple fills to a shape [Taiga #1394](https://tree.taiga.io/project/penpot/us/1394)
- Team members redesign [Taiga #2283](https://tree.taiga.io/project/penpot/us/2283) - Team members redesign [Taiga #2283](https://tree.taiga.io/project/penpot/us/2283)
- Rotation to snap to 15º intervals with shift [Taiga #2437](https://tree.taiga.io/project/penpot/issue/2437) - Rotation to snap to 15º intervals with shift [Taiga #2437](https://tree.taiga.io/project/penpot/issue/2437)

View file

@ -447,8 +447,7 @@
(sv/defmethod ::update-team-invitation-role (sv/defmethod ::update-team-invitation-role
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}] [{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id team-id) (let [perms (teams/get-permissions conn profile-id team-id)]
team (db/get-by-id conn :team team-id)]
(when-not (:is-admin perms) (when-not (:is-admin perms)
(ex/raise :type :validation (ex/raise :type :validation
@ -456,5 +455,23 @@
(db/update! conn :team-invitation (db/update! conn :team-invitation
{:role (name role) :updated-at (dt/now)} {:role (name role) :updated-at (dt/now)}
{:team-id (:id team) :email-to (str/lower email)}) {:team-id team-id :email-to (str/lower email)})
nil)))
;; --- Mutation: Delete invitation
(s/def ::delete-team-invitation
(s/keys :req-un [::profile-id ::team-id ::email]))
(sv/defmethod ::delete-team-invitation
[{:keys [pool] :as cfg} {:keys [profile-id team-id email] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(db/delete! conn :team-invitation
{:team-id team-id :email-to (str/lower email)})
nil))) nil)))

View file

@ -238,11 +238,12 @@
(s/keys :req-un [::profile-id ::team-id])) (s/keys :req-un [::profile-id ::team-id]))
(def sql:team-invitations (def sql:team-invitations
"select email_to as email, role, (valid_until < now()) as expired from team_invitation where team_id = ?") "select email_to as email, role, (valid_until < now()) as expired
from team_invitation where team_id = ? order by valid_until desc")
(sv/defmethod ::team-invitations (sv/defmethod ::team-invitations
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}] [{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(db/exec! conn [sql:team-invitations team-id]))) (->> (db/exec! conn [sql:team-invitations team-id])
(mapv #(update % :role keyword)))))

View file

@ -224,3 +224,28 @@
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out))) (t/is (nil? (:result out)))
(t/is (= "admin" (:role result)))))) (t/is (= "admin" (:role result))))))
(t/deftest delete-team-invitation
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
data {::th/type :delete-team-invitation
:profile-id (:id prof)
:team-id (:id team)
:email "TEST1@mail.com"}]
;;insert an entry on the database with an invitation
(db/insert! th/*pool* :team-invitation
{:team-id (:team-id data)
:email-to "test1@mail.com"
:role "editor"
:valid-until (dt/in-future "48h")})
(let [out (th/mutation! data)
;;retrieve the value from the database and check its content
result (db/get-by-params th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"}
{:check-not-found false})]
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))
(t/is (nil? result)))))

View file

@ -5,14 +5,18 @@
// Copyright (c) UXBOX Labs SL // Copyright (c) UXBOX Labs SL
.dashboard-header { .dashboard-header {
align-items: center;
background-color: $color-white;
display: flex; display: flex;
align-items: center;
justify-content: space-between;
background-color: $color-white;
height: 63px; height: 63px;
padding: $size-1 $size-4 $size-1 $size-2; padding: $size-1 $size-4 $size-1 $size-2;
position: relative; position: relative;
z-index: 10; z-index: 10;
justify-content: space-between; &.team {
display: grid;
grid-template-columns: 20% 1fr 20%;
}
.element-name { .element-name {
margin-right: $size-2; margin-right: $size-2;
@ -33,27 +37,26 @@
nav { nav {
display: flex; display: flex;
width: 300px; align-items: flex-end;
justify-content: center; justify-content: center;
z-index: 1; z-index: 1;
margin-top: 39px;
ul { ul {
display: flex; display: flex;
align-items: center;
font-size: $fs14; font-size: $fs14;
justify-content: center; justify-content: center;
margin: 0;
} }
li { li {
a { a {
display: flex;
align-items: center; align-items: center;
flex-basis: 140px;
border-bottom: 3px solid transparent; border-bottom: 3px solid transparent;
color: $color-gray-30; color: $color-gray-30;
display: flex;
height: 40px; height: 40px;
padding: $size-1 $size-5; padding: $size-1 $size-5;
flex-basis: 140px;
&:hover { &:hover {
color: $color-black; color: $color-black;
@ -71,6 +74,8 @@
.dashboard-title { .dashboard-title {
display: flex; display: flex;
align-items: center;
h1 { h1 {
color: $color-black; color: $color-black;
display: flex; display: flex;
@ -103,6 +108,12 @@
} }
} }
.dashboard-buttons {
display: flex;
justify-content: end;
align-items: center;
}
.dashboard-header-actions { .dashboard-header-actions {
display: flex; display: flex;
} }

View file

@ -4,7 +4,7 @@
padding: 14px; padding: 14px;
box-shadow: 0px 4px 8px rgba($color-black, 0.25); box-shadow: 0px 4px 8px rgba($color-black, 0.25);
border-radius: 8px; border-radius: 8px;
width: 414px; width: 500px;
position: fixed; position: fixed;
form { form {
@ -18,12 +18,12 @@
} }
.custom-input { .custom-input {
width: 272px; width: 314px;
margin-right: 10px; margin-right: 10px;
} }
.custom-select { .custom-select {
width: 103px; width: 160px;
overflow: hidden; overflow: hidden;
} }
@ -40,7 +40,20 @@
} }
} }
.dashboard-team-members { .dashboard-team-members,
.dashboard-team-invitations {
.empty-invitations {
height: 156px;
max-width: 1040px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border: 1px dashed $color-gray-20;
margin-top: 16px;
}
.table-row { .table-row {
background-color: $color-white; background-color: $color-white;
height: 63px; height: 63px;
@ -88,13 +101,16 @@
.rol-selector { .rol-selector {
&.has-priv { &.has-priv {
border: 1px solid $color-gray-20; border: 1px solid $color-gray-20;
cursor: pointer;
} }
min-width: 160px; min-width: 160px;
height: 32px; height: 32px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
border-radius: 2px; border-radius: 2px;
padding: 3px 8px; padding: 3px 8px;
font-size: $fs14;
} }
} }
@ -105,6 +121,30 @@
min-width: 180px; min-width: 180px;
} }
} }
&.status {
.status-badge {
color: $color-white;
border-radius: 12px;
min-width: 74px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
&.pending {
background-color: $color-warning;
}
&.expired {
background-color: $color-gray-20;
}
.status-label {
font-size: $fs12;
}
}
}
} }
.dropdown { .dropdown {

View file

@ -90,14 +90,25 @@
margin-top: 20px; margin-top: 20px;
font-size: $fs16; font-size: $fs16;
&.team-members {
margin-bottom: 52px;
}
&.invitations {
.table-row {
display: grid;
grid-template-columns: 43% 1fr 109px 12px;
}
}
.table-header { .table-header {
display: grid;
grid-template-columns: 43% 1fr 109px 12px;
max-width: 1040px; max-width: 1040px;
display: flex;
background-color: $color-white; background-color: $color-white;
color: $color-gray-30; color: $color-gray-30;
width: 100%; width: 100%;
height: 40px; height: 40px;
align-items: center;
padding: 0px 16px; padding: 0px 16px;
} }

View file

@ -130,6 +130,24 @@
(->> (rp/query :team-stats {:team-id team-id}) (->> (rp/query :team-stats {:team-id team-id})
(rx/map team-stats-fetched)))))) (rx/map team-stats-fetched))))))
;; --- EVENT: fetch-team-invitations
(defn team-invitations-fetched
[invitations]
(ptk/reify ::team-invitations-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :dashboard-team-invitations invitations))))
(defn fetch-team-invitations
[]
(ptk/reify ::fetch-team-invitations
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/query :team-invitations {:team-id team-id})
(rx/map team-invitations-fetched))))))
;; --- EVENT: fetch-projects ;; --- EVENT: fetch-projects
(defn projects-fetched (defn projects-fetched
@ -427,6 +445,38 @@
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))
(defn update-team-invitation-role
[{:keys [email team-id role] :as params}]
(us/assert ::us/email email)
(us/assert ::us/uuid team-id)
(us/assert ::us/keyword role)
(ptk/reify ::update-team-invitation-role
IDeref
(-deref [_] {:role role})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/mutation! :update-team-invitation-role params)
(rx/tap on-success)
(rx/catch on-error))))))
(defn delete-team-invitation
[{:keys [email team-id] :as params}]
(us/assert ::us/email email)
(us/assert ::us/uuid team-id)
(ptk/reify ::delete-team-invitation
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/mutation! :delete-team-invitation params)
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: delete-team ;; --- EVENT: delete-team
(defn delete-team (defn delete-team
@ -785,6 +835,14 @@
(let [team-id (:current-team-id state)] (let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-team-members {:team-id team-id})))))) (rx/of (rt/nav :dashboard-team-members {:team-id team-id}))))))
(defn go-to-team-invitations
[]
(ptk/reify ::go-to-team-invitations
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of (rt/nav :dashboard-team-invitations {:team-id team-id}))))))
(defn go-to-team-settings (defn go-to-team-settings
[] []
(ptk/reify ::go-to-team-settings (ptk/reify ::go-to-team-settings

View file

@ -67,6 +67,9 @@
(def dashboard-team-members (def dashboard-team-members
(l/derived :dashboard-team-members st/state)) (l/derived :dashboard-team-members st/state))
(def dashboard-team-invitations
(l/derived :dashboard-team-invitations st/state))
(def dashboard-selected-project (def dashboard-selected-project
(l/derived (fn [state] (l/derived (fn [state]
(get-in state [:dashboard-local :selected-project])) (get-in state [:dashboard-local :selected-project]))

View file

@ -71,6 +71,7 @@
:dashboard-fonts :dashboard-fonts
:dashboard-font-providers :dashboard-font-providers
:dashboard-team-members :dashboard-team-members
:dashboard-team-invitations
:dashboard-team-settings) :dashboard-team-settings)
[:* [:*

View file

@ -20,7 +20,7 @@
[app.main.ui.dashboard.projects :refer [projects-section]] [app.main.ui.dashboard.projects :refer [projects-section]]
[app.main.ui.dashboard.search :refer [search-page]] [app.main.ui.dashboard.search :refer [search-page]]
[app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]] [app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page]]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[goog.events :as events] [goog.events :as events]
@ -73,6 +73,9 @@
:dashboard-team-members :dashboard-team-members
[:& team-members-page {:team team :profile profile}] [:& team-members-page {:team team :profile profile}]
:dashboard-team-invitations
[:& team-invitations-page {:team team}]
:dashboard-team-settings :dashboard-team-settings
[:& team-settings-page {:team team :profile profile}] [:& team-settings-page {:team team :profile profile}]

View file

@ -229,12 +229,13 @@
(mf/defc team-options-dropdown (mf/defc team-options-dropdown
[{:keys [team profile] :as props}] [{:keys [team profile] :as props}]
(let [go-members (st/emitf (dd/go-to-team-members)) (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))
go-settings (st/emitf (dd/go-to-team-settings))
members-map (mf/deref refs/dashboard-team-members) members-map (mf/deref refs/dashboard-team-members)
members (vals members-map) members (vals members-map)
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin])) can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
on-success on-success
(fn [] (fn []
@ -307,6 +308,7 @@
[:ul.dropdown.options-dropdown [:ul.dropdown.options-dropdown
[:li {:on-click go-members :data-test "team-members"} (tr "labels.members")] [:li {:on-click go-members :data-test "team-members"} (tr "labels.members")]
[:li {:on-click go-invitations :data-test "team-invitations"} (tr "labels.invitations")]
[:li {:on-click go-settings :data-test "team-settings"} (tr "labels.settings")] [:li {:on-click go-settings :data-test "team-settings"} (tr "labels.settings")]
[:hr] [:hr]
(when can-rename? (when can-rename?
@ -466,7 +468,7 @@
[:div.profile {:on-click #(reset! show true) [:div.profile {:on-click #(reset! show true)
:data-test "profile-btn"} :data-test "profile-btn"}
[:img {:src photo}] [:img {:src photo}]
[:span (:fullname profile)] [:span (:fullname profile)]]
[:& dropdown {:on-close #(reset! show false) [:& dropdown {:on-close #(reset! show false)
:show @show} :show @show}
@ -498,7 +500,7 @@
[:li.separator {:on-click #(on-click (du/logout) %) [:li.separator {:on-click #(on-click (du/logout) %)
:data-test "logout-profile-opt"} :data-test "logout-profile-opt"}
[:span.icon i/exit] [:span.icon i/exit]
[:span.text (tr "labels.logout")]]]]] [:span.text (tr "labels.logout")]]]]
(when (and team profile) (when (and team profile)
[:& comments-section {:profile profile [:& comments-section {:profile profile

View file

@ -27,33 +27,40 @@
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
;; TEAM SECTION
(mf/defc header (mf/defc header
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [section team] :as props}] [{:keys [section team] :as props}]
(let [go-members (st/emitf (dd/go-to-team-members)) (let [go-members (st/emitf (dd/go-to-team-members))
go-settings (st/emitf (dd/go-to-team-settings)) go-settings (st/emitf (dd/go-to-team-settings))
invite-member (st/emitf (modal/show {:type ::invite-member :team team})) go-invitations (st/emitf (dd/go-to-team-invitations))
members-section? (= section :dashboard-team-members) invite-member (st/emitf (modal/show {:type ::invite-member :team team}))
settings-section? (= section :dashboard-team-settings) members-section? (= section :dashboard-team-members)
permissions (:permissions team)] settings-section? (= section :dashboard-team-settings)
invitations-section? (= section :dashboard-team-invitations)
permissions (:permissions team)]
[:header.dashboard-header [:header.dashboard-header.team
[:div.dashboard-title [:div.dashboard-title
[:h1 (cond [:h1 (cond
members-section? (tr "labels.members") members-section? (tr "labels.members")
settings-section? (tr "labels.settings") settings-section? (tr "labels.settings")
invitations-section? (tr "labels.invitations")
:else nil)]] :else nil)]]
[:nav [:nav.dashboard-header-menu
[:ul [:ul.dashboard-header-options
[:li {:class (when members-section? "active")} [:li {:class (when members-section? "active")}
[:a {:on-click go-members} (tr "labels.members")]] [:a {:on-click go-members} (tr "labels.members")]]
[:li {:class (when invitations-section? "active")}
[:a {:on-click go-invitations} (tr "labels.invitations")]]
[:li {:class (when settings-section? "active")} [:li {:class (when settings-section? "active")}
[:a {:on-click go-settings} (tr "labels.settings")]]]] [:a {:on-click go-settings} (tr "labels.settings")]]]]
[:div.dashboard-buttons
(if (and members-section? (:is-admin permissions)) (if (and (or invitations-section? members-section?) (:is-admin permissions))
[:a.btn-secondary.btn-small {:on-click invite-member :data-test "invite-member"} [:a.btn-secondary.btn-small {:on-click invite-member :data-test "invite-member"}
(tr "dashboard.invite-profile")] (tr "dashboard.invite-profile")]
[:div])])) [:div.blank-space])]]))
(defn get-available-roles (defn get-available-roles
[permissions] [permissions]
@ -82,7 +89,8 @@
:initial initial) :initial initial)
on-success on-success
(st/emitf (dm/success (tr "notifications.invitation-email-sent")) (st/emitf (dm/success (tr "notifications.invitation-email-sent"))
(modal/hide)) (modal/hide)
(dd/fetch-team-invitations))
on-error on-error
(fn [form {:keys [type code] :as error}] (fn [form {:keys [type code] :as error}]
@ -108,7 +116,8 @@
(let [params (:clean-data @form) (let [params (:clean-data @form)
mdata {:on-success (partial on-success form) mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}] :on-error (partial on-error form)}]
(st/emit! (dd/invite-team-member (with-meta params mdata)))))] (st/emit! (dd/invite-team-member (with-meta params mdata))
(dd/fetch-team-invitations))))]
[:div.modal.dashboard-invite-modal.form-container [:div.modal.dashboard-invite-modal.form-container
[:& fm/form {:on-submit on-submit :form form} [:& fm/form {:on-submit on-submit :form form}
@ -124,6 +133,8 @@
[:div.action-buttons [:div.action-buttons
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]])) [:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))
;; TEAM MEMBERS SECTION
(mf/defc member-info [{:keys [member profile] :as props}] (mf/defc member-info [{:keys [member profile] :as props}]
(let [is-you? (= (:id profile) (:id member))] (let [is-you? (= (:id profile) (:id member))]
[:* [:*
@ -149,14 +160,13 @@
member-is-admin? "labels.admin" member-is-admin? "labels.admin"
member-is-editor? "labels.editor" member-is-editor? "labels.editor"
:else "labels.viewer") :else "labels.viewer")
is-you? (= (:id profile) (:id member)) is-you? (= (:id profile) (:id member))]
]
[:* [:*
(if (and can-change-rol? not-superior? (not (and is-you? you-owner?))) (if (and can-change-rol? not-superior? (not (and is-you? you-owner?)))
[:div.rol-selector.has-priv [:div.rol-selector.has-priv {:on-click #(reset! show? true)}
[:span.rol-label (tr role)] [:span.rol-label (tr role)]
[:span.icon {:on-click #(reset! show? true)} i/arrow-down]] [:span.icon i/arrow-down]]
[::div.rol-selector [:div.rol-selector
[:span.rol-label (tr role)]]) [:span.rol-label (tr role)]])
[:& dropdown {:show @show? [:& dropdown {:show @show?
@ -312,10 +322,10 @@
(remove :is-owner)) (remove :is-owner))
owner (->> (vals members-map) owner (->> (vals members-map)
(d/seek :is-owner))] (d/seek :is-owner))]
[:div.dashboard-table [:div.dashboard-table.team-members
[:div.table-header [:div.table-header
[:div.table-field.name (tr "labels.member")] [:div.table-field.name (tr "labels.member")]
[:div.table-field.permissions (tr "labels.role")]] [:div.table-field.role (tr "labels.role")]]
[:div.table-rows [:div.table-rows
[:& team-member {:member owner :team team :profile profile :members members-map}] [:& team-member {:member owner :team team :profile profile :members members-map}]
(for [item members] (for [item members]
@ -325,17 +335,15 @@
[{:keys [team profile] :as props}] [{:keys [team profile] :as props}]
(let [members-map (mf/deref refs/dashboard-team-members)] (let [members-map (mf/deref refs/dashboard-team-members)]
(mf/use-effect (mf/with-effect [team]
(mf/deps team) (dom/set-html-title
(fn [] (tr "title.team-members"
(dom/set-html-title (if (:is-default team)
(tr "title.team-members" (tr "dashboard.your-penpot")
(if (:is-default team) (:name team)))))
(tr "dashboard.your-penpot")
(:name team))))))
(mf/use-effect (mf/with-effect
(st/emitf (dd/fetch-team-members))) (st/emit! (dd/fetch-team-members)))
[:* [:*
[:& header {:section :dashboard-team-members [:& header {:section :dashboard-team-members
@ -345,6 +353,160 @@
:team team :team team
:members-map members-map}]]])) :members-map members-map}]]]))
;; INVITATIONS SECTION
(mf/defc invitation-role-selector
[{:keys [can-invite? role status change-to-admin change-to-editor] :as props}]
(let [show? (mf/use-state false)
role-label (cond
(= role :owner) "labels.owner"
(= role :admin) "labels.admin"
(= role :editor) "labels.editor"
:else "labels.viewer")]
[:*
(if (and can-invite? (= status :pending))
[:div.rol-selector.has-priv {:on-click #(reset! show? true)}
[:span.rol-label (tr role-label)]
[:span.icon i/arrow-down]]
[:div.rol-selector
[:span.rol-label (tr role-label)]])
[:& dropdown {:show @show?
:on-close #(reset! show? false)}
[:ul.dropdown.options-dropdown
[:li {:on-click change-to-admin} (tr "labels.admin")]
[:li {:on-click change-to-editor} (tr "labels.editor")]]]]))
(mf/defc invitation-status-badge
[{:keys [status] :as props}]
(let [status-label (if (= status :expired)
(tr "labels.expired-invitation")
(tr "labels.pending-invitation"))]
[:div.status-badge {:class (dom/classnames
:expired (= status :expired)
:pending (= status :pending))}
[:span.status-label (tr status-label)]]))
(mf/defc invitation-actions [{:keys [can-modify? delete resend] :as props}]
(let [show? (mf/use-state false)]
(when can-modify?
[:*
[:span.icon {:on-click #(reset! show? true)} [i/actions]]
[:& dropdown {:show @show?
:on-close #(reset! show? false)}
[:ul.dropdown.actions-dropdown
[:li {:on-click resend} (tr "labels.resend-invitation")]
[:li {:on-click delete} (tr "labels.delete-invitation")]]]])))
(mf/defc invitation-row
{::mf/wrap [mf/memo]}
[{:keys [invitation can-invite? team] :as props}]
(let [expired? (:expired invitation)
email (:email invitation)
invitation-role (:role invitation)
status (if expired?
:expired
:pending)
on-success
#(st/emit! (dm/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))
(dm/error (tr "errors.profile-is-muted"))
(and (= :validation type)
(= :member-is-muted code))
(dm/error (tr "errors.member-is-muted"))
(and (= :validation type)
(= :email-has-permanent-bounces code))
(dm/error (tr "errors.email-has-permanent-bounces" email))
:else
(dm/error (tr "errors.generic"))))
change-rol
(fn [role]
(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 {:email email :team-id (:id team) :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))))]
[: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}]]]))
(mf/defc empty-invitation-table [can-invite?]
[:div.empty-invitations
[:span (tr "labels.no-invitations")]
(when (:can-invite? can-invite?) [:span (tr "labels.no-invitations-hint")])])
(mf/defc invitation-section
[{:keys [team invitations] :as props}]
(let [owner? (get-in team [:permissions :is-owner])
admin? (get-in team [:permissions :is-admin])
can-invite? (or owner? admin?)]
[:div.dashboard-table.invitations
[:div.table-header
[:div.table-field.name (tr "labels.invitations")]
[:div.table-field.role (tr "labels.role")]
[:div.table-field.status (tr "labels.status")]]
(if (empty? invitations)
[:& empty-invitation-table {:can-invite? can-invite?}]
[:div.table-rows
(for [invitation invitations]
[:& invitation-row {:key (:email invitation) :invitation invitation :can-invite? can-invite? :team team}])])]))
(mf/defc team-invitations-page
[{:keys [team] :as props}]
(let [invitations (mf/deref refs/dashboard-team-invitations)]
(mf/with-effect [team]
(dom/set-html-title
(tr "title.team-invitations"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect
(st/emit! (dd/fetch-team-invitations)))
[:*
[:& header {:section :dashboard-team-invitations
:team team}]
[:section.dashboard-container.dashboard-team-invitations
[:& invitation-section {:team team
:invitations invitations}]]]))
;; SETTINGS SECTION
(mf/defc team-settings-page (mf/defc team-settings-page
[{:keys [team] :as props}] [{:keys [team] :as props}]
(let [finput (mf/use-ref) (let [finput (mf/use-ref)
@ -397,7 +559,7 @@
[:div.label (tr "dashboard.team-members")] [:div.label (tr "dashboard.team-members")]
[:div.owner [:div.owner
[:span.icon [:img {:src (cfg/resolve-profile-photo-url owner)}]] [:span.icon [:img {:src (cfg/resolve-profile-photo-url owner)}]]
[:span.text (str (:name owner) " (" (tr "labels.owner") ")") ]] [:span.text (str (:name owner) " (" (tr "labels.owner") ")")]]
[:div.summary [:div.summary
[:span.icon i/user] [:span.icon i/user]
[:span.text (tr "dashboard.num-of-members" (count members-map))]]] [:span.text (tr "dashboard.num-of-members" (count members-map))]]]

View file

@ -66,6 +66,7 @@
["/dashboard/team/:team-id" ["/dashboard/team/:team-id"
["/members" :dashboard-team-members] ["/members" :dashboard-team-members]
["/invitations" :dashboard-team-invitations]
["/settings" :dashboard-team-settings] ["/settings" :dashboard-team-settings]
["/projects" :dashboard-projects] ["/projects" :dashboard-projects]
["/search" :dashboard-search] ["/search" :dashboard-search]

View file

@ -1164,6 +1164,14 @@ msgstr "Members"
msgid "labels.member" msgid "labels.member"
msgstr "Member" msgstr "Member"
#: src/app/main/ui/dashboard/team.cljs
msgid "labels.invitations"
msgstr "Invitations"
#: src/app/main/ui/dashboard/team.cljs
msgid "labels.status"
msgstr "Status"
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
msgid "labels.you" msgid "labels.you"
msgstr "(you)" msgstr "(you)"
@ -1300,6 +1308,34 @@ msgstr "Service Unavailable"
msgid "labels.settings" msgid "labels.settings"
msgstr "Settings" msgstr "Settings"
#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
msgid "labels.invitations"
msgstr "Invitations"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.pending-invitation"
msgstr "Pending"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.expired-invitation"
msgstr "Expired"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.resend-invitation"
msgstr "Resend invitation"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.delete-invitation"
msgstr "Delete invitation"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.no-invitations"
msgstr "There are no invitations."
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.no-invitations-hint"
msgstr "Press the button \"Invite to team\" to invite more members to this team."
msgid "labels.share-prototype" msgid "labels.share-prototype"
msgstr "Share prototype" msgstr "Share prototype"
@ -1831,6 +1867,10 @@ msgstr "Profile - Penpot"
msgid "title.team-members" msgid "title.team-members"
msgstr "Members - %s - Penpot" msgstr "Members - %s - Penpot"
#: src/app/main/ui/dashboard/team.cljs
msgid "title.team-invitations"
msgstr "Invitations - %s - Penpot"
#: src/app/main/ui/dashboard/team.cljs #: src/app/main/ui/dashboard/team.cljs
msgid "title.team-settings" msgid "title.team-settings"
msgstr "Settings - %s - Penpot" msgstr "Settings - %s - Penpot"
@ -3502,4 +3542,4 @@ msgid "workspace.updates.update"
msgstr "Update" msgstr "Update"
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the path" msgstr "Click to close the path"

View file

@ -1165,6 +1165,14 @@ msgstr "Integrantes"
msgid "labels.member" msgid "labels.member"
msgstr "Integrante" msgstr "Integrante"
#: src/app/main/ui/dashboard/team.cljs
msgid "labels.invitations"
msgstr "Invitaciones"
#: src/app/main/ui/dashboard/team.cljs
msgid "labels.status"
msgstr "Status"
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs #: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
msgid "labels.you" msgid "labels.you"
msgstr "(tú)" msgstr "(tú)"
@ -1301,6 +1309,34 @@ msgstr "El servicio no está disponible"
msgid "labels.settings" msgid "labels.settings"
msgstr "Configuración" msgstr "Configuración"
#: src/app/main/ui/settings/sidebar.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/sidebar.cljs
msgid "labels.invitations"
msgstr "Invitaciones"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.pending-invitation"
msgstr "Pendiente"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.expired-invitation"
msgstr "Expirada"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.resend-invitation"
msgstr "Reenviar invitacion"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.delete-invitation"
msgstr "Eliminar invitation"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.no-invitations"
msgstr "No hay invitaciones"
#:src/app/main/ui/dashboard/team.cljs
msgid "labels.no-invitations-hint"
msgstr "Pulsa el botón 'Invitar al equipo' para añadir más integrantes al equipo."
#: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs #: src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs, src/app/main/ui/viewer/header.cljs
msgid "labels.share-prototype" msgid "labels.share-prototype"
msgstr "Compartir prototipo" msgstr "Compartir prototipo"
@ -1849,6 +1885,10 @@ msgstr "Perfil - Penpot"
msgid "title.team-members" msgid "title.team-members"
msgstr "Integrantes - %s - Penpot" msgstr "Integrantes - %s - Penpot"
#: src/app/main/ui/dashboard/team.cljs
msgid "title.team-invitations"
msgstr "Invitaciones - %s - Penpot"
#: src/app/main/ui/dashboard/team.cljs #: src/app/main/ui/dashboard/team.cljs
msgid "title.team-settings" msgid "title.team-settings"
msgstr "Configuración - %s - Penpot" msgstr "Configuración - %s - Penpot"
@ -3516,4 +3556,4 @@ msgid "workspace.updates.update"
msgstr "Actualizar" msgstr "Actualizar"
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta" msgstr "Pulsar para cerrar la ruta"