mirror of
https://github.com/penpot/penpot.git
synced 2025-05-18 02:36:13 +02:00
🎉 Add invitation section to dashboard
This commit is contained in:
parent
486d89c5d0
commit
9d04dc7d9a
16 changed files with 480 additions and 64 deletions
|
@ -18,6 +18,7 @@
|
|||
|
||||
### :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)
|
||||
- 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)
|
||||
|
|
|
@ -447,8 +447,7 @@
|
|||
(sv/defmethod ::update-team-invitation-role
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)
|
||||
team (db/get-by-id conn :team team-id)]
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
|
@ -456,5 +455,23 @@
|
|||
|
||||
(db/update! conn :team-invitation
|
||||
{: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)))
|
||||
|
|
|
@ -238,11 +238,12 @@
|
|||
(s/keys :req-un [::profile-id ::team-id]))
|
||||
|
||||
(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
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(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)))))
|
||||
|
|
|
@ -224,3 +224,28 @@
|
|||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:result out)))
|
||||
(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)))))
|
||||
|
|
|
@ -5,14 +5,18 @@
|
|||
// Copyright (c) UXBOX Labs SL
|
||||
|
||||
.dashboard-header {
|
||||
align-items: center;
|
||||
background-color: $color-white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: $color-white;
|
||||
height: 63px;
|
||||
padding: $size-1 $size-4 $size-1 $size-2;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
justify-content: space-between;
|
||||
&.team {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr 20%;
|
||||
}
|
||||
|
||||
.element-name {
|
||||
margin-right: $size-2;
|
||||
|
@ -33,27 +37,26 @@
|
|||
|
||||
nav {
|
||||
display: flex;
|
||||
width: 300px;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
margin-top: 39px;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: $fs14;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-basis: 140px;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: $color-gray-30;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
padding: $size-1 $size-5;
|
||||
flex-basis: 140px;
|
||||
|
||||
&:hover {
|
||||
color: $color-black;
|
||||
|
@ -71,6 +74,8 @@
|
|||
|
||||
.dashboard-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
color: $color-black;
|
||||
display: flex;
|
||||
|
@ -103,6 +108,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dashboard-buttons {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-header-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
padding: 14px;
|
||||
box-shadow: 0px 4px 8px rgba($color-black, 0.25);
|
||||
border-radius: 8px;
|
||||
width: 414px;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
|
||||
form {
|
||||
|
@ -18,12 +18,12 @@
|
|||
}
|
||||
|
||||
.custom-input {
|
||||
width: 272px;
|
||||
width: 314px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
width: 103px;
|
||||
width: 160px;
|
||||
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 {
|
||||
background-color: $color-white;
|
||||
height: 63px;
|
||||
|
@ -88,13 +101,16 @@
|
|||
.rol-selector {
|
||||
&.has-priv {
|
||||
border: 1px solid $color-gray-20;
|
||||
cursor: pointer;
|
||||
}
|
||||
min-width: 160px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
padding: 3px 8px;
|
||||
font-size: $fs14;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,6 +121,30 @@
|
|||
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 {
|
||||
|
|
|
@ -90,14 +90,25 @@
|
|||
margin-top: 20px;
|
||||
font-size: $fs16;
|
||||
|
||||
&.team-members {
|
||||
margin-bottom: 52px;
|
||||
}
|
||||
|
||||
&.invitations {
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 43% 1fr 109px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 43% 1fr 109px 12px;
|
||||
max-width: 1040px;
|
||||
display: flex;
|
||||
background-color: $color-white;
|
||||
color: $color-gray-30;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
padding: 0px 16px;
|
||||
}
|
||||
|
||||
|
|
|
@ -130,6 +130,24 @@
|
|||
(->> (rp/query :team-stats {:team-id team-id})
|
||||
(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
|
||||
|
||||
(defn projects-fetched
|
||||
|
@ -427,6 +445,38 @@
|
|||
(rx/tap on-success)
|
||||
(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
|
||||
|
||||
(defn delete-team
|
||||
|
@ -785,6 +835,14 @@
|
|||
(let [team-id (:current-team-id state)]
|
||||
(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
|
||||
[]
|
||||
(ptk/reify ::go-to-team-settings
|
||||
|
|
|
@ -67,6 +67,9 @@
|
|||
(def dashboard-team-members
|
||||
(l/derived :dashboard-team-members st/state))
|
||||
|
||||
(def dashboard-team-invitations
|
||||
(l/derived :dashboard-team-invitations st/state))
|
||||
|
||||
(def dashboard-selected-project
|
||||
(l/derived (fn [state]
|
||||
(get-in state [:dashboard-local :selected-project]))
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
:dashboard-fonts
|
||||
:dashboard-font-providers
|
||||
:dashboard-team-members
|
||||
:dashboard-team-invitations
|
||||
:dashboard-team-settings)
|
||||
|
||||
[:*
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
[app.main.ui.dashboard.projects :refer [projects-section]]
|
||||
[app.main.ui.dashboard.search :refer [search-page]]
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar]]
|
||||
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
|
||||
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.util.keyboard :as kbd]
|
||||
[goog.events :as events]
|
||||
|
@ -73,6 +73,9 @@
|
|||
:dashboard-team-members
|
||||
[:& team-members-page {:team team :profile profile}]
|
||||
|
||||
:dashboard-team-invitations
|
||||
[:& team-invitations-page {:team team}]
|
||||
|
||||
:dashboard-team-settings
|
||||
[:& team-settings-page {:team team :profile profile}]
|
||||
|
||||
|
|
|
@ -229,12 +229,13 @@
|
|||
|
||||
(mf/defc team-options-dropdown
|
||||
[{:keys [team profile] :as props}]
|
||||
(let [go-members (st/emitf (dd/go-to-team-members))
|
||||
go-settings (st/emitf (dd/go-to-team-settings))
|
||||
(let [go-members (st/emitf (dd/go-to-team-members))
|
||||
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 (vals members-map)
|
||||
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
|
||||
members-map (mf/deref refs/dashboard-team-members)
|
||||
members (vals members-map)
|
||||
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
|
||||
|
||||
on-success
|
||||
(fn []
|
||||
|
@ -307,6 +308,7 @@
|
|||
|
||||
[:ul.dropdown.options-dropdown
|
||||
[: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")]
|
||||
[:hr]
|
||||
(when can-rename?
|
||||
|
@ -466,7 +468,7 @@
|
|||
[:div.profile {:on-click #(reset! show true)
|
||||
:data-test "profile-btn"}
|
||||
[:img {:src photo}]
|
||||
[:span (:fullname profile)]
|
||||
[:span (:fullname profile)]]
|
||||
|
||||
[:& dropdown {:on-close #(reset! show false)
|
||||
:show @show}
|
||||
|
@ -498,7 +500,7 @@
|
|||
[:li.separator {:on-click #(on-click (du/logout) %)
|
||||
:data-test "logout-profile-opt"}
|
||||
[:span.icon i/exit]
|
||||
[:span.text (tr "labels.logout")]]]]]
|
||||
[:span.text (tr "labels.logout")]]]]
|
||||
|
||||
(when (and team profile)
|
||||
[:& comments-section {:profile profile
|
||||
|
|
|
@ -27,33 +27,40 @@
|
|||
[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))
|
||||
invite-member (st/emitf (modal/show {:type ::invite-member :team team}))
|
||||
members-section? (= section :dashboard-team-members)
|
||||
settings-section? (= section :dashboard-team-settings)
|
||||
permissions (:permissions team)]
|
||||
(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}))
|
||||
members-section? (= section :dashboard-team-members)
|
||||
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
|
||||
[:h1 (cond
|
||||
members-section? (tr "labels.members")
|
||||
settings-section? (tr "labels.settings")
|
||||
invitations-section? (tr "labels.invitations")
|
||||
:else nil)]]
|
||||
[:nav
|
||||
[:ul
|
||||
[:nav.dashboard-header-menu
|
||||
[:ul.dashboard-header-options
|
||||
[:li {:class (when members-section? "active")}
|
||||
[: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")}
|
||||
[:a {:on-click go-settings} (tr "labels.settings")]]]]
|
||||
|
||||
(if (and members-section? (:is-admin permissions))
|
||||
[:a.btn-secondary.btn-small {:on-click invite-member :data-test "invite-member"}
|
||||
(tr "dashboard.invite-profile")]
|
||||
[:div])]))
|
||||
[:div.dashboard-buttons
|
||||
(if (and (or invitations-section? members-section?) (:is-admin permissions))
|
||||
[:a.btn-secondary.btn-small {:on-click invite-member :data-test "invite-member"}
|
||||
(tr "dashboard.invite-profile")]
|
||||
[:div.blank-space])]]))
|
||||
|
||||
(defn get-available-roles
|
||||
[permissions]
|
||||
|
@ -82,7 +89,8 @@
|
|||
:initial initial)
|
||||
on-success
|
||||
(st/emitf (dm/success (tr "notifications.invitation-email-sent"))
|
||||
(modal/hide))
|
||||
(modal/hide)
|
||||
(dd/fetch-team-invitations))
|
||||
|
||||
on-error
|
||||
(fn [form {:keys [type code] :as error}]
|
||||
|
@ -108,7 +116,8 @@
|
|||
(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-member (with-meta params mdata))
|
||||
(dd/fetch-team-invitations))))]
|
||||
|
||||
[:div.modal.dashboard-invite-modal.form-container
|
||||
[:& fm/form {:on-submit on-submit :form form}
|
||||
|
@ -124,6 +133,8 @@
|
|||
[:div.action-buttons
|
||||
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))
|
||||
|
||||
;; TEAM MEMBERS SECTION
|
||||
|
||||
(mf/defc member-info [{:keys [member profile] :as props}]
|
||||
(let [is-you? (= (:id profile) (:id member))]
|
||||
[:*
|
||||
|
@ -149,14 +160,13 @@
|
|||
member-is-admin? "labels.admin"
|
||||
member-is-editor? "labels.editor"
|
||||
: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?)))
|
||||
[:div.rol-selector.has-priv
|
||||
[:div.rol-selector.has-priv {:on-click #(reset! show? true)}
|
||||
[:span.rol-label (tr role)]
|
||||
[:span.icon {:on-click #(reset! show? true)} i/arrow-down]]
|
||||
[::div.rol-selector
|
||||
[:span.icon i/arrow-down]]
|
||||
[:div.rol-selector
|
||||
[:span.rol-label (tr role)]])
|
||||
|
||||
[:& dropdown {:show @show?
|
||||
|
@ -312,10 +322,10 @@
|
|||
(remove :is-owner))
|
||||
owner (->> (vals members-map)
|
||||
(d/seek :is-owner))]
|
||||
[:div.dashboard-table
|
||||
[:div.dashboard-table.team-members
|
||||
[:div.table-header
|
||||
[:div.table-field.name (tr "labels.member")]
|
||||
[:div.table-field.permissions (tr "labels.role")]]
|
||||
[:div.table-field.role (tr "labels.role")]]
|
||||
[:div.table-rows
|
||||
[:& team-member {:member owner :team team :profile profile :members members-map}]
|
||||
(for [item members]
|
||||
|
@ -325,17 +335,15 @@
|
|||
[{:keys [team profile] :as props}]
|
||||
(let [members-map (mf/deref refs/dashboard-team-members)]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(dom/set-html-title
|
||||
(tr "title.team-members"
|
||||
(if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team))))))
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title
|
||||
(tr "title.team-members"
|
||||
(if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team)))))
|
||||
|
||||
(mf/use-effect
|
||||
(st/emitf (dd/fetch-team-members)))
|
||||
(mf/with-effect
|
||||
(st/emit! (dd/fetch-team-members)))
|
||||
|
||||
[:*
|
||||
[:& header {:section :dashboard-team-members
|
||||
|
@ -345,6 +353,160 @@
|
|||
: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}]
|
||||
(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
|
||||
[{:keys [team] :as props}]
|
||||
(let [finput (mf/use-ref)
|
||||
|
@ -397,7 +559,7 @@
|
|||
[:div.label (tr "dashboard.team-members")]
|
||||
[:div.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
|
||||
[:span.icon i/user]
|
||||
[:span.text (tr "dashboard.num-of-members" (count members-map))]]]
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
|
||||
["/dashboard/team/:team-id"
|
||||
["/members" :dashboard-team-members]
|
||||
["/invitations" :dashboard-team-invitations]
|
||||
["/settings" :dashboard-team-settings]
|
||||
["/projects" :dashboard-projects]
|
||||
["/search" :dashboard-search]
|
||||
|
|
|
@ -1164,6 +1164,14 @@ msgstr "Members"
|
|||
msgid "labels.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
|
||||
msgid "labels.you"
|
||||
msgstr "(you)"
|
||||
|
@ -1300,6 +1308,34 @@ msgstr "Service Unavailable"
|
|||
msgid "labels.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"
|
||||
msgstr "Share prototype"
|
||||
|
||||
|
@ -1831,6 +1867,10 @@ msgstr "Profile - Penpot"
|
|||
msgid "title.team-members"
|
||||
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
|
||||
msgid "title.team-settings"
|
||||
msgstr "Settings - %s - Penpot"
|
||||
|
@ -3502,4 +3542,4 @@ msgid "workspace.updates.update"
|
|||
msgstr "Update"
|
||||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Click to close the path"
|
||||
msgstr "Click to close the path"
|
||||
|
|
|
@ -1165,6 +1165,14 @@ msgstr "Integrantes"
|
|||
msgid "labels.member"
|
||||
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
|
||||
msgid "labels.you"
|
||||
msgstr "(tú)"
|
||||
|
@ -1301,6 +1309,34 @@ msgstr "El servicio no está disponible"
|
|||
msgid "labels.settings"
|
||||
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
|
||||
msgid "labels.share-prototype"
|
||||
msgstr "Compartir prototipo"
|
||||
|
@ -1849,6 +1885,10 @@ msgstr "Perfil - Penpot"
|
|||
msgid "title.team-members"
|
||||
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
|
||||
msgid "title.team-settings"
|
||||
msgstr "Configuración - %s - Penpot"
|
||||
|
@ -3516,4 +3556,4 @@ msgid "workspace.updates.update"
|
|||
msgstr "Actualizar"
|
||||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Pulsar para cerrar la ruta"
|
||||
msgstr "Pulsar para cerrar la ruta"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue