diff --git a/CHANGES.md b/CHANGES.md index a9a180abe..56250b1a8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index 773ebcde2..6ae7754d7 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -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))) diff --git a/backend/src/app/rpc/queries/teams.clj b/backend/src/app/rpc/queries/teams.clj index 75b568cfb..2eca88bf2 100644 --- a/backend/src/app/rpc/queries/teams.clj +++ b/backend/src/app/rpc/queries/teams.clj @@ -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))))) diff --git a/backend/test/app/services_teams_test.clj b/backend/test/app/services_teams_test.clj index ef7f34d4d..fa233ce71 100644 --- a/backend/test/app/services_teams_test.clj +++ b/backend/test/app/services_teams_test.clj @@ -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))))) diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss index 251cbe1c9..4027e6de3 100644 --- a/frontend/resources/styles/main/partials/dashboard-header.scss +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -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; } diff --git a/frontend/resources/styles/main/partials/dashboard-team.scss b/frontend/resources/styles/main/partials/dashboard-team.scss index d84b5f8c0..a9d1d5872 100644 --- a/frontend/resources/styles/main/partials/dashboard-team.scss +++ b/frontend/resources/styles/main/partials/dashboard-team.scss @@ -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 { diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index 0ffabb0b1..338385b79 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -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; } diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 6e34b0f5d..4adcd3dcc 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -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 diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index b94e0af0e..09dfad73e 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -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])) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 05c58144e..2fa0102c0 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -71,6 +71,7 @@ :dashboard-fonts :dashboard-font-providers :dashboard-team-members + :dashboard-team-invitations :dashboard-team-settings) [:* diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index a377a70df..bcf746f50 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -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}] diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index ae3c5c696..afe5e95e1 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 5dd0b65ee..bc53a9840 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -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))]]] diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index 28d402c85..efc66c02c 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -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] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 4844fe661..f27f39d9c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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" \ No newline at end of file +msgstr "Click to close the path" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index f456a5f0d..09327ba22 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -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" \ No newline at end of file +msgstr "Pulsar para cerrar la ruta"