mirror of
https://github.com/penpot/penpot.git
synced 2025-05-18 21:06:11 +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
|
### :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)
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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)))))
|
||||||
|
|
|
@ -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)))))
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
[:*
|
[:*
|
||||||
|
|
|
@ -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}]
|
||||||
|
|
||||||
|
|
|
@ -230,6 +230,7 @@
|
||||||
(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-invitations (st/emitf (dd/go-to-team-invitations))
|
||||||
go-settings (st/emitf (dd/go-to-team-settings))
|
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)
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
go-invitations (st/emitf (dd/go-to-team-invitations))
|
||||||
invite-member (st/emitf (modal/show {:type ::invite-member :team team}))
|
invite-member (st/emitf (modal/show {:type ::invite-member :team team}))
|
||||||
members-section? (= section :dashboard-team-members)
|
members-section? (= section :dashboard-team-members)
|
||||||
settings-section? (= section :dashboard-team-settings)
|
settings-section? (= section :dashboard-team-settings)
|
||||||
|
invitations-section? (= section :dashboard-team-invitations)
|
||||||
permissions (:permissions team)]
|
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)
|
|
||||||
(fn []
|
|
||||||
(dom/set-html-title
|
(dom/set-html-title
|
||||||
(tr "title.team-members"
|
(tr "title.team-members"
|
||||||
(if (:is-default team)
|
(if (:is-default team)
|
||||||
(tr "dashboard.your-penpot")
|
(tr "dashboard.your-penpot")
|
||||||
(:name team))))))
|
(: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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue