🎉 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
. 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)

View file

@ -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)))

View file

@ -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)))))

View file

@ -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)))))

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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

View file

@ -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]))

View file

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

View file

@ -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}]

View file

@ -230,6 +230,7 @@
(mf/defc team-options-dropdown
[{:keys [team profile] :as props}]
(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)
@ -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

View file

@ -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))
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))
[: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])]))
[: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 []
(mf/with-effect [team]
(dom/set-html-title
(tr "title.team-members"
(if (:is-default team)
(tr "dashboard.your-penpot")
(:name team))))))
(: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)

View file

@ -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]

View file

@ -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"

View file

@ -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"