penpot/frontend/src/app/main/ui/dashboard/team.cljs
Andrey Antukh c70bc5baff ♻️ Refactor dashboard state management.
Mainly for performance, also affects backend endpoints.
2021-05-10 14:53:47 +02:00

307 lines
10 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) UXBOX Labs SL
(ns app.main.ui.dashboard.team
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cfg]
[app.main.constants :as c]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[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.team-form]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.time :as dt]
[cljs.spec.alpha :as s]
[okulary.core :as l]
[rumext.alpha :as mf]))
(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}))
members-section? (= section :dashboard-team-members)
settings-section? (= section :dashboard-team-settings)]
[:header.dashboard-header
[:div.dashboard-title
[:h1 (cond
members-section? (tr "labels.members")
settings-section? (tr "labels.settings")
nil)]]
[:nav
[:ul
[:li {:class (when members-section? "active")}
[:a {:on-click go-members} (tr "labels.members")]]
[:li {:class (when settings-section? "active")}
[:a {:on-click go-settings} (tr "labels.settings")]]]]
(if members-section?
[:a.btn-secondary.btn-small {:on-click invite-member}
(tr "dashboard.invite-profile")]
[:div])]))
(defn get-available-roles
[]
[{:value "" :label (tr "labels.role")}
{:value "admin" :label (tr "labels.admin")}
{:value "editor" :label (tr "labels.editor")}
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/uxboxproject/issue/1083
;; {:value "viewer" :label (tr "labels.viewer")}
])
(s/def ::email ::us/email)
(s/def ::role ::us/keyword)
(s/def ::invite-member-form
(s/keys :req-un [::role ::email]))
(mf/defc invite-member-modal
{::mf/register modal/components
::mf/register-as ::invite-member}
[]
(let [roles (mf/use-memo get-available-roles)
initial (mf/use-memo (constantly {:role "editor"}))
form (fm/use-form :spec ::invite-member-form
:initial initial)
on-success
(st/emitf (dm/success (tr "notifications.invitation-email-sent"))
(modal/hide))
on-error
(fn [form {:keys [type code] :as error}]
(let [email (get @form [:data :email])]
(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))
(dm/error (tr "errors.email-has-permanent-bounces" email))
:else
(dm/error (tr "errors.generic")))))
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-member (with-meta params mdata)))))]
[:div.modal.dashboard-invite-modal.form-container
[:& fm/form {:on-submit on-submit :form form}
[:div.title
[:span.text (tr "modals.invite-member.title")]]
[:div.form-row
[:& fm/input {:name :email
:label (tr "labels.email")}]
[:& fm/select {:name :role
:options roles}]]
[:div.action-buttons
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))
(mf/defc team-member
{::mf/wrap [mf/memo]}
[{:keys [team member profile] :as props}]
(let [show? (mf/use-state false)
set-role
(fn [role]
(let [params {:member-id (:id member) :role role}]
(st/emit! (dd/update-team-member-role params))))
set-owner-fn (partial set-role :owner)
set-admin (partial set-role :admin)
set-editor (partial set-role :editor)
set-viewer (partial set-role :viewer)
set-owner
(st/emitf (modal/show
{:type :confirm
:title (tr "modals.promote-owner-confirm.title")
:message (tr "modals.promote-owner-confirm.message")
:accept-label (tr "modals.promote-owner-confirm.accept")
:on-accept set-owner-fn}))
delete-fn
(st/emitf (dd/delete-team-member {:member-id (:id member)}))
delete
(st/emitf (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-fn}))]
[:div.table-row
[:div.table-field.name (:name member)]
[:div.table-field.email (:email member)]
[:div.table-field.permissions
[:*
(cond
(:is-owner member)
[:span.label (tr "labels.owner")]
(:is-admin member)
[:span.label (tr "labels.admin")]
(:can-edit member)
[:span.label (tr "labels.editor")]
:else
[:span.label (tr "labels.viewer")])
(when (and (not (:is-owner member))
(or (:is-admin team)
(:is-owner team)))
[:span.icon {:on-click #(reset! show? true)} i/arrow-down])]
[:& 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 (:is-owner team)
[:*
[:hr]
[:li {:on-click set-owner} (tr "dashboard.promote-to-owner")]])
[:hr]
(when (and (or (:is-owner team)
(:is-admin team))
(not= (:id profile)
(:id member)))
[:li {:on-click delete} (tr "labels.remove")])]]]]))
(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
[:div.table-header
[:div.table-field.name (tr "labels.name")]
[:div.table-field.email (tr "labels.email")]
[:div.table-field.permissions (tr "labels.permissions")]]
[:div.table-rows
[:& team-member {:member owner :team team :profile profile}]
(for [item members]
[:& team-member {:member item :team team :profile profile :key (:id item)}])]]))
(mf/defc team-members-page
[{: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/use-effect
(st/emitf (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}]]]))
(mf/defc team-settings-page
[{:keys [team profile] :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/emitf (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/exit]
[:img {:src (cfg/resolve-team-photo-url team)}]
[:& file-uploader {:accept "image/jpeg,image/png"
:multi false
:input-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)))]]]]]]]))