mirror of
https://github.com/penpot/penpot.git
synced 2025-06-07 01:41:38 +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
|
@ -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]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue