mirror of
https://github.com/penpot/penpot.git
synced 2025-05-31 04:46:11 +02:00
642 lines
24 KiB
Clojure
642 lines
24 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns app.main.ui.dashboard.team
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.spec :as us]
|
|
[app.config :as cfg]
|
|
[app.main.data.dashboard :as dd]
|
|
[app.main.data.events :as ev]
|
|
[app.main.data.messages :as msg]
|
|
[app.main.data.modal :as modal]
|
|
[app.main.data.users :as du]
|
|
[app.main.refs :as refs]
|
|
[app.main.store :as st]
|
|
[app.main.ui.components.dropdown :refer [dropdown]]
|
|
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
|
[app.main.ui.components.forms :as fm]
|
|
[app.main.ui.dashboard.change-owner]
|
|
[app.main.ui.dashboard.team-form]
|
|
[app.main.ui.icons :as i]
|
|
[app.util.dom :as dom]
|
|
[app.util.i18n :as i18n :refer [tr]]
|
|
[beicon.core :as rx]
|
|
[cljs.spec.alpha :as s]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(mf/defc header
|
|
{::mf/wrap [mf/memo]}
|
|
[{:keys [section team] :as props}]
|
|
(let [go-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
|
|
go-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
|
|
go-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
|
|
invite-member (mf/use-fn
|
|
(mf/deps team)
|
|
#(st/emit! (modal/show {:type :invite-members :team team :origin :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.team
|
|
[:div.dashboard-title
|
|
[:h1 (cond
|
|
members-section? (tr "labels.members")
|
|
settings-section? (tr "labels.settings")
|
|
invitations-section? (tr "labels.invitations")
|
|
:else nil)]]
|
|
[: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")]]]]
|
|
[: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])]]))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; INVITATIONS MODAL
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn get-available-roles
|
|
[permissions]
|
|
(->> [{:value "editor" :label (tr "labels.editor")}
|
|
(when (:is-admin permissions)
|
|
{:value "admin" :label (tr "labels.admin")})
|
|
;; Temporarily disabled viewer roles
|
|
;; https://tree.taiga.io/project/uxboxproject/issue/1083
|
|
;; {:value "viewer" :label (tr "labels.viewer")}
|
|
]
|
|
(filterv identity)))
|
|
|
|
(s/def ::emails (s/and ::us/set-of-valid-emails d/not-empty?))
|
|
(s/def ::role ::us/keyword)
|
|
(s/def ::team-id ::us/uuid)
|
|
|
|
(s/def ::invite-member-form
|
|
(s/keys :req-un [::role ::emails ::team-id]))
|
|
|
|
(mf/defc invite-members-modal
|
|
{::mf/register modal/components
|
|
::mf/register-as :invite-members}
|
|
[{:keys [team origin]}]
|
|
(let [perms (:permissions team)
|
|
roles (mf/use-memo (mf/deps perms) #(get-available-roles perms))
|
|
initial (mf/use-memo (constantly {:role "editor" :team-id (:id team)}))
|
|
form (fm/use-form :spec ::invite-member-form
|
|
:initial initial)
|
|
error-text (mf/use-state "")
|
|
|
|
on-success
|
|
(fn []
|
|
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
|
|
(modal/hide)
|
|
(dd/fetch-team-invitations)))
|
|
|
|
on-error
|
|
(fn [{:keys [type code] :as error}]
|
|
(cond
|
|
(and (= :validation type)
|
|
(= :profile-is-muted code))
|
|
(st/emit! (msg/error (tr "errors.profile-is-muted"))
|
|
(modal/hide))
|
|
|
|
(and (= :validation type)
|
|
(or (= :member-is-muted code)
|
|
(= :email-has-permanent-bounces code)))
|
|
(swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error)))
|
|
|
|
:else
|
|
(st/emit! (msg/error (tr "errors.generic"))
|
|
(modal/hide))))
|
|
|
|
on-submit
|
|
(fn [form]
|
|
(let [params (:clean-data @form)
|
|
mdata {:on-success (partial on-success form)
|
|
:on-error (partial on-error form)}]
|
|
(st/emit! (-> (dd/invite-team-members (with-meta params mdata))
|
|
(with-meta {::ev/origin origin}))
|
|
(dd/fetch-team-invitations))))]
|
|
|
|
[:div.modal.dashboard-invite-modal.form-container
|
|
{:class (dom/classnames
|
|
:hero (= origin :hero))}
|
|
[:& fm/form {:on-submit on-submit :form form}
|
|
[:div.title
|
|
[:span.text (tr "modals.invite-team-member.title")]]
|
|
|
|
(when-not (= "" @error-text)
|
|
[:div.error
|
|
[:span.icon i/msg-error]
|
|
[:span.text @error-text]])
|
|
[:div.form-row
|
|
[:p.label (tr "onboarding.choice.team-up.roles")]
|
|
[:& fm/select {:name :role :options roles}]]
|
|
[:div.form-row
|
|
|
|
|
|
[:& fm/multi-input {:type "email"
|
|
:name :emails
|
|
:auto-focus? true
|
|
:trim true
|
|
:valid-item-fn us/parse-email
|
|
:label (tr "modals.invite-member.emails")
|
|
:on-submit on-submit}]]
|
|
|
|
[:div.action-buttons
|
|
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; MEMBERS SECTION
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(mf/defc member-info [{:keys [member profile] :as props}]
|
|
(let [is-you? (= (:id profile) (:id member))]
|
|
[:*
|
|
[:div.member-image
|
|
[:img {:src (cfg/resolve-profile-photo-url member)}]]
|
|
[:div.member-info
|
|
[:div.member-name (:name member)
|
|
(when is-you?
|
|
[:span.you (tr "labels.you")])]
|
|
[:div.member-email (:email member)]]]))
|
|
|
|
(mf/defc rol-info [{:keys [member team set-admin set-editor set-owner profile] :as props}]
|
|
(let [member-is-owner? (:is-owner member)
|
|
member-is-admin? (and (:is-admin member) (not member-is-owner?))
|
|
member-is-editor? (and (:can-edit member) (and (not member-is-admin?) (not member-is-owner?)))
|
|
show? (mf/use-state false)
|
|
you-owner? (get-in team [:permissions :is-owner])
|
|
you-admin? (get-in team [:permissions :is-admin])
|
|
can-change-rol? (or you-owner? you-admin?)
|
|
not-superior? (or you-owner? (and can-change-rol? (or member-is-admin? member-is-editor?)))
|
|
role (cond
|
|
member-is-owner? "labels.owner"
|
|
member-is-admin? "labels.admin"
|
|
member-is-editor? "labels.editor"
|
|
:else "labels.viewer")
|
|
is-you? (= (:id profile) (:id member))]
|
|
[:*
|
|
(if (and can-change-rol? not-superior? (not (and is-you? you-owner?)))
|
|
[:div.rol-selector.has-priv {:on-click #(reset! show? true)}
|
|
[:span.rol-label (tr role)]
|
|
[:span.icon i/arrow-down]]
|
|
[:div.rol-selector
|
|
[:span.rol-label (tr role)]])
|
|
|
|
[:& dropdown {:show @show?
|
|
:on-close #(reset! show? false)}
|
|
[:ul.dropdown.options-dropdown
|
|
[:li {:on-click set-admin} (tr "labels.admin")]
|
|
[:li {:on-click set-editor} (tr "labels.editor")]
|
|
;; Temporarily disabled viewer role
|
|
;; https://tree.taiga.io/project/uxboxproject/issue/1083
|
|
;; [:li {:on-click set-viewer} (tr "labels.viewer")]
|
|
(when you-owner?
|
|
[:li {:on-click (partial set-owner member)} (tr "labels.owner")])]]]))
|
|
|
|
(mf/defc member-actions [{:keys [member team delete leave profile] :as props}]
|
|
(let [is-owner? (:is-owner member)
|
|
owner? (get-in team [:permissions :is-owner])
|
|
admin? (get-in team [:permissions :is-admin])
|
|
show? (mf/use-state false)
|
|
is-you? (= (:id profile) (:id member))
|
|
can-delete? (or owner? admin?)]
|
|
[:*
|
|
(when (or is-you? (and can-delete? (not (and is-owner? (not owner?)))))
|
|
[:span.icon {:on-click #(reset! show? true)} [i/actions]])
|
|
[:& dropdown {:show @show?
|
|
:on-close #(reset! show? false)}
|
|
[:ul.dropdown.actions-dropdown
|
|
(when is-you?
|
|
[:li {:on-click leave} (tr "dashboard.leave-team")])
|
|
(when (and can-delete? (not is-you?) (not (and is-owner? (not owner?))))
|
|
[:li {:on-click delete} (tr "labels.remove-member")])]]]))
|
|
|
|
(mf/defc team-member
|
|
{::mf/wrap [mf/memo]}
|
|
[{:keys [team member members profile] :as props}]
|
|
|
|
(let [owner? (dm/get-in team [:permissions :is-owner])
|
|
set-role
|
|
(mf/use-fn
|
|
(mf/deps member)
|
|
(fn [role]
|
|
(let [params {:member-id (:id member) :role role}]
|
|
(st/emit! (dd/update-team-member-role params)))))
|
|
|
|
|
|
set-owner-fn (mf/use-fn (mf/deps set-role) (partial set-role :owner))
|
|
set-admin (mf/use-fn (mf/deps set-role) (partial set-role :admin))
|
|
set-editor (mf/use-fn (mf/deps set-role) (partial set-role :editor))
|
|
;; set-viewer (partial set-role :viewer)
|
|
|
|
set-owner
|
|
(mf/use-fn
|
|
(mf/deps set-owner-fn member)
|
|
(fn [member]
|
|
(st/emit! (modal/show
|
|
{:type :confirm
|
|
:title (tr "modals.promote-owner-confirm.title")
|
|
:message (tr "modals.promote-owner-confirm.message" (:name member))
|
|
:scd-message (tr "modals.promote-owner-confirm.hint")
|
|
:accept-label (tr "modals.promote-owner-confirm.accept")
|
|
:on-accept set-owner-fn
|
|
:accept-style :primary}))))
|
|
|
|
delete-member-fn
|
|
(mf/use-fn
|
|
(mf/deps member)
|
|
(fn [] (st/emit! (dd/delete-team-member {:member-id (:id member)}))))
|
|
|
|
on-success
|
|
(mf/use-fn
|
|
(mf/deps profile)
|
|
(fn []
|
|
(st/emit! (dd/go-to-projects (:default-team-id profile))
|
|
(modal/hide)
|
|
(du/fetch-teams))))
|
|
|
|
on-error
|
|
(mf/use-fn
|
|
(fn [{:keys [code] :as error}]
|
|
(condp = code
|
|
|
|
:no-enough-members-for-leave
|
|
(rx/of (msg/error (tr "errors.team-leave.insufficient-members")))
|
|
|
|
:member-does-not-exist
|
|
(rx/of (msg/error (tr "errors.team-leave.member-does-not-exists")))
|
|
|
|
:owner-cant-leave-team
|
|
(rx/of (msg/error (tr "errors.team-leave.owner-cant-leave")))
|
|
|
|
(rx/throw error))))
|
|
|
|
delete-fn
|
|
(mf/use-fn
|
|
(mf/deps team on-success on-error)
|
|
(fn []
|
|
(st/emit! (dd/delete-team (with-meta team {:on-success on-success
|
|
:on-error on-error})))))
|
|
|
|
leave-fn
|
|
(mf/use-fn
|
|
(mf/deps on-success on-error)
|
|
(fn [member-id]
|
|
(let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))]
|
|
(st/emit! (dd/leave-team (with-meta params
|
|
{:on-success on-success
|
|
:on-error on-error}))))))
|
|
|
|
leave-and-close
|
|
(mf/use-fn
|
|
(mf/deps delete-fn)
|
|
(fn []
|
|
(st/emit! (modal/show
|
|
{:type :confirm
|
|
:title (tr "modals.leave-confirm.title")
|
|
:message (tr "modals.leave-and-close-confirm.message" (:name team))
|
|
:scd-message (tr "modals.leave-and-close-confirm.hint")
|
|
:accept-label (tr "modals.leave-confirm.accept")
|
|
:on-accept delete-fn}))))
|
|
|
|
change-owner-and-leave
|
|
(mf/use-fn
|
|
(mf/deps profile team leave-fn)
|
|
(fn []
|
|
(st/emit! (dd/fetch-team-members)
|
|
(modal/show
|
|
{:type :leave-and-reassign
|
|
:profile profile
|
|
:team team
|
|
:accept leave-fn}))))
|
|
|
|
leave
|
|
(mf/use-fn
|
|
(mf/deps leave-fn)
|
|
(fn []
|
|
(st/emit! (modal/show
|
|
{:type :confirm
|
|
:title (tr "modals.leave-confirm.title")
|
|
:message (tr "modals.leave-confirm.message")
|
|
:accept-label (tr "modals.leave-confirm.accept")
|
|
:on-accept leave-fn}))))
|
|
|
|
preset-leave (cond (= 1 (count members)) leave-and-close
|
|
(= true owner?) change-owner-and-leave
|
|
:else leave)
|
|
|
|
delete
|
|
(mf/use-fn
|
|
(mf/deps delete-member-fn)
|
|
(fn []
|
|
(st/emit! (modal/show
|
|
{:type :confirm
|
|
:title (tr "modals.delete-team-member-confirm.title")
|
|
:message (tr "modals.delete-team-member-confirm.message")
|
|
:accept-label (tr "modals.delete-team-member-confirm.accept")
|
|
:on-accept delete-member-fn}))))]
|
|
|
|
[:div.table-row
|
|
[:div.table-field.name
|
|
[:& member-info {:member member :profile profile}]]
|
|
[:div.table-field.roles
|
|
[:& rol-info {:member member
|
|
:team team
|
|
:set-admin set-admin
|
|
:set-editor set-editor
|
|
:set-owner set-owner
|
|
:profile profile}]]
|
|
[:div.table-field.actions
|
|
[:& member-actions {:member member
|
|
:profile profile
|
|
:team team
|
|
:delete delete
|
|
:leave preset-leave}]]]))
|
|
|
|
(mf/defc team-members
|
|
[{:keys [members-map team profile] :as props}]
|
|
(let [members (->> (vals members-map)
|
|
(sort-by :created-at)
|
|
(remove :is-owner))
|
|
owner (->> (vals members-map)
|
|
(d/seek :is-owner))]
|
|
[:div.dashboard-table.team-members
|
|
[:div.table-header
|
|
[:div.table-field.name (tr "labels.member")]
|
|
[:div.table-field.role (tr "labels.role")]]
|
|
[:div.table-rows
|
|
[:& team-member {:member owner :team team :profile profile :members members-map}]
|
|
(for [item members]
|
|
[:& team-member {:member item :team team :profile profile :key (:id item) :members members-map}])]]))
|
|
|
|
(mf/defc team-members-page
|
|
[{:keys [team profile] :as props}]
|
|
(let [members-map (mf/deref refs/dashboard-team-members)]
|
|
|
|
(mf/with-effect [team]
|
|
(dom/set-html-title
|
|
(tr "title.team-members"
|
|
(if (:is-default team)
|
|
(tr "dashboard.your-penpot")
|
|
(:name team)))))
|
|
|
|
(mf/with-effect
|
|
(st/emit! (dd/fetch-team-members)))
|
|
|
|
[:*
|
|
[:& header {:section :dashboard-team-members
|
|
:team team}]
|
|
[:section.dashboard-container.dashboard-team-members
|
|
[:& team-members {:profile profile
|
|
: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! (msg/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))
|
|
(msg/error (tr "errors.profile-is-muted"))
|
|
|
|
(and (= :validation type)
|
|
(= :member-is-muted code))
|
|
(msg/error (tr "errors.member-is-muted"))
|
|
|
|
(and (= :validation type)
|
|
(= :email-has-permanent-bounces code))
|
|
(msg/error (tr "errors.email-has-permanent-bounces" email))
|
|
|
|
:else
|
|
(msg/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 {:emails [email]
|
|
:team-id (:id team)
|
|
:resend? true
|
|
:role invitation-role}
|
|
mdata {:on-success on-success
|
|
:on-error (partial on-error email)}]
|
|
(st/emit! (-> (dd/invite-team-members (with-meta params mdata))
|
|
(with-meta {::ev/origin :team}))
|
|
(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)
|
|
|
|
members-map (mf/deref refs/dashboard-team-members)
|
|
owner (->> (vals members-map)
|
|
(d/seek :is-owner))
|
|
|
|
stats (mf/deref refs/dashboard-team-stats)
|
|
|
|
on-image-click
|
|
(mf/use-callback #(dom/click (mf/ref-val finput)))
|
|
|
|
on-file-selected
|
|
(fn [file]
|
|
(st/emit! (dd/update-team-photo {:file file})))]
|
|
|
|
|
|
(mf/use-effect
|
|
(mf/deps team)
|
|
(fn []
|
|
(dom/set-html-title (tr "title.team-settings"
|
|
(if (:is-default team)
|
|
(tr "dashboard.your-penpot")
|
|
(:name team))))))
|
|
|
|
|
|
(mf/use-effect
|
|
#(st/emit! (dd/fetch-team-members)
|
|
(dd/fetch-team-stats)))
|
|
|
|
[:*
|
|
[:& header {:section :dashboard-team-settings
|
|
:team team}]
|
|
[:section.dashboard-container.dashboard-team-settings
|
|
[:div.team-settings
|
|
[:div.horizontal-blocks
|
|
[:div.block.info-block
|
|
[:div.label (tr "dashboard.team-info")]
|
|
[:div.name (:name team)]
|
|
[:div.icon
|
|
[:span.update-overlay {:on-click on-image-click} i/image]
|
|
[:img {:src (cfg/resolve-team-photo-url team)}]
|
|
[:& file-uploader {:accept "image/jpeg,image/png"
|
|
:multi false
|
|
:ref finput
|
|
:on-selected on-file-selected}]]]
|
|
|
|
[:div.block.owner-block
|
|
[: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") ")")]]
|
|
[:div.summary
|
|
[:span.icon i/user]
|
|
[:span.text (tr "dashboard.num-of-members" (count members-map))]]]
|
|
|
|
[:div.block.stats-block
|
|
[:div.label (tr "dashboard.team-projects")]
|
|
[:div.projects
|
|
[:span.icon i/folder]
|
|
[:span.text (tr "labels.num-of-projects" (i18n/c (dec (:projects stats))))]]
|
|
[:div.files
|
|
[:span.icon i/file-html]
|
|
[:span.text (tr "labels.num-of-files" (i18n/c (:files stats)))]]]]]]]))
|