mirror of
https://github.com/penpot/penpot.git
synced 2025-05-19 21:16:10 +02:00
Merge pull request #3405 from penpot/niwinz-bugfixes-2023-w26-2
🐛 Bugfixes & Enhancements
This commit is contained in:
commit
2b37a3c613
4 changed files with 331 additions and 262 deletions
|
@ -57,6 +57,7 @@
|
|||
- Fix focus handling on comments edition [Taiga #5560](https://tree.taiga.io/project/penpot/issue/5560)
|
||||
- Fix incorrect fullname use on registring user after OIDC authentication [Taiga #5517](https://tree.taiga.io/project/penpot/issue/5517)
|
||||
- Fix incorrect modified-at on project after import file [Taiga #5268](https://tree.taiga.io/project/penpot/issue/5268)
|
||||
- Fix incorrect message after sending invitation to already member [Taiga 5599](https://tree.taiga.io/project/penpot/issue/5599)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
|
@ -719,29 +720,22 @@
|
|||
|
||||
itoken))))
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::emails ::us/set-of-valid-emails)
|
||||
(s/def ::create-team-invitations
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id ::role]
|
||||
:opt-un [::email ::emails]))
|
||||
(def ^:private schema:create-team-invitations
|
||||
[:map {:title "create-team-invitations"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:role [::sm/one-of #{:owner :admin :editor}]]
|
||||
[:emails ::sm/set-of-emails]])
|
||||
|
||||
(sv/defmethod ::create-team-invitations
|
||||
"A rpc call that allow to send a single or multiple invitations to
|
||||
join the team."
|
||||
{::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}]
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team-invitations}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (get-permissions conn profile-id team-id)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
team (db/get-by-id conn :team team-id)
|
||||
|
||||
;; Members emails. We don't re-send inviation to already existing members
|
||||
member? (into #{}
|
||||
(map :email)
|
||||
(db/exec! conn [sql:team-members team-id]))
|
||||
|
||||
emails (cond-> (or emails #{}) (string? email) (conj email))]
|
||||
team (db/get-by-id conn :team team-id)]
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/invitations-per-team
|
||||
|
@ -764,9 +758,13 @@
|
|||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||
|
||||
(let [cfg (assoc cfg ::db/conn conn)
|
||||
invitations (into []
|
||||
members (->> (db/exec! conn [sql:team-members team-id])
|
||||
(into #{} (map :email)))
|
||||
|
||||
invitations (into #{}
|
||||
(comp
|
||||
(remove member?)
|
||||
;; We don't re-send inviation to already existing members
|
||||
(remove (partial contains? members))
|
||||
(map (fn [email]
|
||||
{:email (str/lower email)
|
||||
:team team
|
||||
|
@ -774,7 +772,8 @@
|
|||
:role role}))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
(with-meta invitations
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
{::audit/props {:invitations (count invitations)}})))))
|
||||
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
:role :editor}]
|
||||
|
||||
;; invite external user without complaints
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
(let [data (assoc data :emails ["foo@bar.com"])
|
||||
out (th/command! data)
|
||||
;; retrieve the value from the database and check its content
|
||||
invitation (db/exec-one!
|
||||
|
@ -52,7 +52,7 @@
|
|||
|
||||
;; invite internal user without complaints
|
||||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
(let [data (assoc data :emails [(:email profile2)])
|
||||
out (th/command! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count (deref mock)))))
|
||||
|
@ -60,7 +60,7 @@
|
|||
;; invite user with complaint
|
||||
(th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"})
|
||||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
(let [data (assoc data :emails ["foo@bar.com"])
|
||||
out (th/command! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count (deref mock)))))
|
||||
|
@ -79,7 +79,7 @@
|
|||
(th/reset-mock! mock)
|
||||
|
||||
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
(let [data (assoc data :emails ["foo@bar.com"])
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
|
@ -92,7 +92,7 @@
|
|||
;; invite internal user that is muted
|
||||
(th/reset-mock! mock)
|
||||
|
||||
(let [data (assoc data :email (:email profile3))
|
||||
(let [data (assoc data :emails [(:email profile3)])
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
|
@ -118,7 +118,7 @@
|
|||
;; Try to invite a not existing user
|
||||
(let [data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile1)
|
||||
:email "notexisting@example.com"
|
||||
:emails ["notexisting@example.com"]
|
||||
:team-id (:id team)
|
||||
:role :editor}
|
||||
out (th/command! data)]
|
||||
|
@ -126,15 +126,15 @@
|
|||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count @mock)))
|
||||
(t/is (= 1 (-> out :result count)))
|
||||
(t/is (= 1 (-> out :result :total)))
|
||||
|
||||
(let [token (-> out :result first)
|
||||
(let [token (-> out :result :invitations first)
|
||||
claims (tokens/decode sprops token)]
|
||||
(t/is (= :team-invitation (:iss claims)))
|
||||
(t/is (= (:id profile1) (:profile-id claims)))
|
||||
(t/is (= :editor (:role claims)))
|
||||
(t/is (= (:id team) (:team-id claims)))
|
||||
(t/is (= (:email data) (:member-email claims)))
|
||||
(t/is (= (first (:emails data)) (:member-email claims)))
|
||||
(t/is (nil? (:member-id claims)))))
|
||||
|
||||
(th/reset-mock! mock)
|
||||
|
@ -142,7 +142,7 @@
|
|||
;; Try to invite existing user
|
||||
(let [data {::th/type :create-team-invitations
|
||||
::rpc/profile-id (:id profile1)
|
||||
:email (:email profile2)
|
||||
:emails [(:email profile2)]
|
||||
:team-id (:id team)
|
||||
:role :editor}
|
||||
out (th/command! data)]
|
||||
|
@ -150,15 +150,15 @@
|
|||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count @mock)))
|
||||
(t/is (= 1 (-> out :result count)))
|
||||
(t/is (= 1 (-> out :result :total)))
|
||||
|
||||
(let [token (-> out :result first)
|
||||
(let [token (-> out :result :invitations first)
|
||||
claims (tokens/decode sprops token)]
|
||||
(t/is (= :team-invitation (:iss claims)))
|
||||
(t/is (= (:id profile1) (:profile-id claims)))
|
||||
(t/is (= :editor (:role claims)))
|
||||
(t/is (= (:id team) (:team-id claims)))
|
||||
(t/is (= (:email data) (:member-email claims)))
|
||||
(t/is (= (first (:emails data)) (:member-email claims)))
|
||||
(t/is (= (:id profile2) (:member-id claims)))))
|
||||
|
||||
)))
|
||||
|
@ -264,7 +264,7 @@
|
|||
;; invite internal user without complaints
|
||||
(with-redefs [app.config/flags #{}]
|
||||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
(let [data (assoc data :emails [(:email profile2)])
|
||||
out (th/command! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 0 (:call-count (deref mock)))))
|
||||
|
|
|
@ -31,23 +31,27 @@
|
|||
[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)))
|
||||
go-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
|
||||
invite-member (mf/use-fn
|
||||
(mf/deps team)
|
||||
#(st/emit! (modal/show {:type :invite-members
|
||||
:team team
|
||||
:origin :team})))
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
[{:keys [section team]}]
|
||||
(let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
|
||||
on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
|
||||
on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
|
||||
on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
|
||||
|
||||
members-section? (= section :dashboard-team-members)
|
||||
settings-section? (= section :dashboard-team-settings)
|
||||
invitations-section? (= section :dashboard-team-invitations)
|
||||
webhooks-section? (= section :dashboard-team-webhooks)
|
||||
permissions (:permissions team)]
|
||||
permissions (:permissions team)
|
||||
|
||||
on-invite-member
|
||||
(mf/use-fn
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(st/emit! (modal/show {:type :invite-members
|
||||
:team team
|
||||
:origin :team}))))]
|
||||
|
||||
[:header.dashboard-header.team
|
||||
[:div.dashboard-title
|
||||
|
@ -60,17 +64,19 @@
|
|||
[:nav.dashboard-header-menu
|
||||
[:ul.dashboard-header-options
|
||||
[:li {:class (when members-section? "active")}
|
||||
[:a {:on-click go-members} (tr "labels.members")]]
|
||||
[:a {:on-click on-nav-members} (tr "labels.members")]]
|
||||
[:li {:class (when invitations-section? "active")}
|
||||
[:a {:on-click go-invitations} (tr "labels.invitations")]]
|
||||
[:a {:on-click on-nav-invitations} (tr "labels.invitations")]]
|
||||
(when (contains? cfg/flags :webhooks)
|
||||
[:li {:class (when webhooks-section? "active")}
|
||||
[:a {:on-click go-webhooks} (tr "labels.webhooks")]])
|
||||
[:a {:on-click on-nav-webhooks} (tr "labels.webhooks")]])
|
||||
[:li {:class (when settings-section? "active")}
|
||||
[:a {:on-click go-settings} (tr "labels.settings")]]]]
|
||||
[:a {:on-click on-nav-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"}
|
||||
[:a.btn-secondary.btn-small
|
||||
{:on-click on-invite-member
|
||||
:data-test "invite-member"}
|
||||
(tr "dashboard.invite-profile")]
|
||||
[:div.blank-space])]]))
|
||||
|
||||
|
@ -98,10 +104,10 @@
|
|||
|
||||
(mf/defc invite-members-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :invite-members}
|
||||
::mf/register-as :invite-members
|
||||
::mf/wrap-props false}
|
||||
[{:keys [team origin]}]
|
||||
(let [members-map (mf/deref refs/dashboard-team-members)
|
||||
|
||||
perms (:permissions team)
|
||||
|
||||
roles (mf/use-memo (mf/deps perms) #(get-available-roles perms))
|
||||
|
@ -110,15 +116,17 @@
|
|||
: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)))
|
||||
|
||||
current-data-emails (into #{} (dm/get-in @form [:clean-data :emails]))
|
||||
current-members-emails (into #{} (map (comp :email second)) members-map)
|
||||
|
||||
on-success
|
||||
(fn [_form {:keys [total]}]
|
||||
(when (pos? total)
|
||||
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))))
|
||||
|
||||
(st/emit! (modal/hide)
|
||||
(dd/fetch-team-invitations)))
|
||||
|
||||
on-error
|
||||
(fn [{:keys [type code] :as error}]
|
||||
(cond
|
||||
|
@ -185,7 +193,9 @@
|
|||
;; MEMBERS SECTION
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(mf/defc member-info [{:keys [member profile] :as props}]
|
||||
(mf/defc member-info
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [member profile]}]
|
||||
(let [is-you? (= (:id profile) (:id member))]
|
||||
[:*
|
||||
[:div.member-image
|
||||
|
@ -196,93 +206,97 @@
|
|||
[: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}]
|
||||
(mf/defc rol-info
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [member team on-set-admin on-set-editor on-set-owner profile]}]
|
||||
(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])
|
||||
|
||||
you-owner? (dm/get-in team [:permissions :is-owner])
|
||||
you-admin? (dm/get-in team [:permissions :is-admin])
|
||||
is-you? (= (:id profile) (:id member))
|
||||
|
||||
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))]
|
||||
|
||||
on-show (mf/use-fn #(reset! show? true))
|
||||
on-hide (mf/use-fn #(reset! show? false))]
|
||||
[:*
|
||||
(if (and can-change-rol? not-superior? (not (and is-you? you-owner?)))
|
||||
[:div.rol-selector.has-priv {:on-click #(reset! show? true)}
|
||||
[:div.rol-selector.has-priv {:on-click on-show}
|
||||
[: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)}
|
||||
[:& dropdown {:show @show? :on-close on-hide}
|
||||
[:ul.dropdown.options-dropdown
|
||||
[:li {:on-click set-admin} (tr "labels.admin")]
|
||||
[:li {:on-click set-editor} (tr "labels.editor")]
|
||||
[:li {:on-click on-set-admin} (tr "labels.admin")]
|
||||
[:li {:on-click on-set-editor} (tr "labels.editor")]
|
||||
;; Temporarily disabled viewer role
|
||||
;; https://tree.taiga.io/project/penpot/issue/1083
|
||||
;; [:li {:on-click set-viewer} (tr "labels.viewer")]
|
||||
(when you-owner?
|
||||
[:li {:on-click (partial set-owner member)} (tr "labels.owner")])]]]))
|
||||
[:li {:on-click (partial on-set-owner member)} (tr "labels.owner")])]]]))
|
||||
|
||||
(mf/defc member-actions [{:keys [member team delete leave profile] :as props}]
|
||||
(mf/defc member-actions
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [member team on-delete on-leave profile]}]
|
||||
(let [is-owner? (:is-owner member)
|
||||
owner? (get-in team [:permissions :is-owner])
|
||||
admin? (get-in team [:permissions :is-admin])
|
||||
owner? (dm/get-in team [:permissions :is-owner])
|
||||
admin? (dm/get-in team [:permissions :is-admin])
|
||||
show? (mf/use-state false)
|
||||
is-you? (= (:id profile) (:id member))
|
||||
can-delete? (or owner? admin?)]
|
||||
can-delete? (or owner? admin?)
|
||||
|
||||
on-show (mf/use-fn #(reset! show? true))
|
||||
on-hide (mf/use-fn #(reset! show? false))]
|
||||
|
||||
[:*
|
||||
(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)}
|
||||
[:span.icon {:on-click on-show} [i/actions]])
|
||||
|
||||
[:& dropdown {:show @show? :on-close on-hide}
|
||||
[:ul.dropdown.actions-dropdown
|
||||
(when is-you?
|
||||
[:li {:on-click leave} (tr "dashboard.leave-team")])
|
||||
[:li {:on-click on-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")])]]]))
|
||||
[:li {:on-click on-delete} (tr "labels.remove-member")])]]]))
|
||||
|
||||
(defn- set-role! [member-id role]
|
||||
(let [params {:member-id member-id :role role}]
|
||||
(st/emit! (dd/update-team-member-role params))))
|
||||
|
||||
(mf/defc team-member
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [team member members profile] :as props}]
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
[{:keys [team member members profile]}]
|
||||
|
||||
(let [owner? (dm/get-in team [:permissions :is-owner])
|
||||
set-role
|
||||
(let [member-id (:id member)
|
||||
on-set-admin (mf/use-fn (mf/deps member-id) (partial set-role! member-id :admin))
|
||||
on-set-editor (mf/use-fn (mf/deps member-id) (partial set-role! member-id :editor))
|
||||
owner? (dm/get-in team [:permissions :is-owner])
|
||||
|
||||
on-set-owner
|
||||
(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
|
||||
(fn [member _event]
|
||||
(let [params {: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-accept (partial set-role! member-id :owner)
|
||||
:accept-style :primary}]
|
||||
(st/emit! (modal/show params)))))
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
|
@ -308,14 +322,14 @@
|
|||
|
||||
(rx/throw error))))
|
||||
|
||||
delete-fn
|
||||
on-delete-accepted
|
||||
(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
|
||||
on-leave-accepted
|
||||
(mf/use-fn
|
||||
(mf/deps on-success on-error)
|
||||
(fn [member-id]
|
||||
|
@ -324,9 +338,9 @@
|
|||
{:on-success on-success
|
||||
:on-error on-error}))))))
|
||||
|
||||
leave-and-close
|
||||
on-leave-and-close
|
||||
(mf/use-fn
|
||||
(mf/deps delete-fn)
|
||||
(mf/deps on-delete-accepted)
|
||||
(fn []
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
|
@ -334,80 +348,100 @@
|
|||
: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}))))
|
||||
:on-accept on-delete-accepted}))))
|
||||
|
||||
change-owner-and-leave
|
||||
on-change-owner-and-leave
|
||||
(mf/use-fn
|
||||
(mf/deps profile team leave-fn)
|
||||
(mf/deps profile team on-leave-accepted)
|
||||
(fn []
|
||||
(st/emit! (dd/fetch-team-members)
|
||||
(modal/show
|
||||
{:type :leave-and-reassign
|
||||
:profile profile
|
||||
:team team
|
||||
:accept leave-fn}))))
|
||||
:accept on-leave-accepted}))))
|
||||
|
||||
leave
|
||||
on-leave
|
||||
(mf/use-fn
|
||||
(mf/deps leave-fn)
|
||||
(mf/deps on-leave-accepted)
|
||||
(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}))))
|
||||
:on-accept on-leave-accepted}))))
|
||||
|
||||
preset-leave (cond (= 1 (count members)) leave-and-close
|
||||
(= true owner?) change-owner-and-leave
|
||||
:else leave)
|
||||
|
||||
delete
|
||||
on-delete
|
||||
(mf/use-fn
|
||||
(mf/deps delete-member-fn)
|
||||
(mf/deps member-id)
|
||||
(fn []
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
(let [on-accept #(st/emit! (dd/delete-team-member {:member-id member-id}))
|
||||
params {: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}))))]
|
||||
:on-accept on-accept}]
|
||||
(st/emit! (modal/show params)))))
|
||||
|
||||
on-leave'
|
||||
(cond (= 1 (count members)) on-leave-and-close
|
||||
(= true owner?) on-change-owner-and-leave
|
||||
:else on-leave)]
|
||||
|
||||
[: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
|
||||
:on-set-admin on-set-admin
|
||||
:on-set-editor on-set-editor
|
||||
:on-set-owner on-set-owner
|
||||
:profile profile}]]
|
||||
|
||||
[:div.table-field.actions
|
||||
[:& member-actions {:member member
|
||||
:profile profile
|
||||
:team team
|
||||
:delete delete
|
||||
:leave preset-leave}]]]))
|
||||
:on-delete on-delete
|
||||
:on-leave on-leave'}]]]))
|
||||
|
||||
(mf/defc team-members
|
||||
[{:keys [members-map team profile] :as props}]
|
||||
(let [members (->> (vals members-map)
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [members-map team profile]}]
|
||||
(let [members (mf/with-memo [members-map]
|
||||
(->> (vals members-map)
|
||||
(sort-by :created-at)
|
||||
(remove :is-owner))
|
||||
owner (->> (vals members-map)
|
||||
(d/seek :is-owner))]
|
||||
(remove :is-owner)))
|
||||
owner (mf/with-memo [members-map]
|
||||
(->> (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}]
|
||||
[:& 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}])]]))
|
||||
[:& team-member
|
||||
{:member item
|
||||
:team team
|
||||
:profile profile
|
||||
:key (:id item)
|
||||
:members members-map}])]]))
|
||||
|
||||
(mf/defc team-members-page
|
||||
[{:keys [team profile] :as props}]
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [team profile]}]
|
||||
(let [members-map (mf/deref refs/dashboard-team-members)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
|
@ -417,14 +451,14 @@
|
|||
(tr "dashboard.your-penpot")
|
||||
(:name team)))))
|
||||
|
||||
(mf/with-effect
|
||||
(mf/with-effect []
|
||||
(st/emit! (dd/fetch-team-members)))
|
||||
|
||||
[:*
|
||||
[:& header {:section :dashboard-team-members
|
||||
:team team}]
|
||||
[:& header {:section :dashboard-team-members :team team}]
|
||||
[:section.dashboard-container.dashboard-team-members
|
||||
[:& team-members {:profile profile
|
||||
[:& team-members
|
||||
{:profile profile
|
||||
:team team
|
||||
:members-map members-map}]]]))
|
||||
|
||||
|
@ -433,58 +467,60 @@
|
|||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(mf/defc invitation-role-selector
|
||||
[{:keys [can-invite? role status change-to-admin change-to-editor] :as props}]
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [can-invite? role status on-change]}]
|
||||
(let [show? (mf/use-state false)
|
||||
role-label (cond
|
||||
(= role :owner) "labels.owner"
|
||||
(= role :admin) "labels.admin"
|
||||
(= role :editor) "labels.editor"
|
||||
:else "labels.viewer")]
|
||||
label (cond
|
||||
(= role :owner) (tr "labels.owner")
|
||||
(= role :admin) (tr "labels.admin")
|
||||
(= role :editor) (tr "labels.editor")
|
||||
:else (tr "labels.viewer"))
|
||||
|
||||
on-hide (mf/use-fn #(reset! show? false))
|
||||
on-show (mf/use-fn #(reset! show? true))
|
||||
|
||||
on-change'
|
||||
(mf/use-fn
|
||||
(mf/deps on-change)
|
||||
(fn [event]
|
||||
(let [role (-> (dom/get-current-target event)
|
||||
(dom/get-data "role")
|
||||
(keyword))]
|
||||
(on-change role event))))]
|
||||
|
||||
[:*
|
||||
(if (and can-invite? (= status :pending))
|
||||
[:div.rol-selector.has-priv {:on-click #(reset! show? true)}
|
||||
[:span.rol-label (tr role-label)]
|
||||
[:div.rol-selector.has-priv {:on-click on-show}
|
||||
[:span.rol-label label]
|
||||
[:span.icon i/arrow-down]]
|
||||
[:div.rol-selector
|
||||
[:span.rol-label (tr role-label)]])
|
||||
[:span.rol-label label]])
|
||||
|
||||
[:& dropdown {:show @show?
|
||||
:on-close #(reset! show? false)}
|
||||
[:& dropdown {:show @show? :on-close on-hide}
|
||||
[:ul.dropdown.options-dropdown
|
||||
[:li {:on-click change-to-admin} (tr "labels.admin")]
|
||||
[:li {:on-click change-to-editor} (tr "labels.editor")]]]]))
|
||||
[:li {:data-role "admin" :on-click on-change'} (tr "labels.admin")]
|
||||
[:li {:data-role "editor" :on-click on-change'} (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
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [status]}]
|
||||
[:div.status-badge
|
||||
{:class (dom/classnames
|
||||
:expired (= status :expired)
|
||||
:pending (= status :pending))}
|
||||
[:span.status-label (tr status-label)]]))
|
||||
[:span.status-label
|
||||
(if (= status :expired)
|
||||
(tr "labels.expired-invitation")
|
||||
(tr "labels.pending-invitation"))]])
|
||||
|
||||
(mf/defc invitation-actions
|
||||
[{:keys [invitation team] :as props}]
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [invitation team-id]}]
|
||||
(let [show? (mf/use-state false)
|
||||
|
||||
team-id (:id team)
|
||||
email (:email invitation)
|
||||
role (:role invitation)
|
||||
|
||||
on-resend-success
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
|
||||
(modal/hide)
|
||||
(dd/fetch-team-invitations))))
|
||||
|
||||
on-copy-success
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (msg/success (tr "notifications.invitation-link-copied"))
|
||||
(modal/hide))))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(mf/deps email)
|
||||
|
@ -505,7 +541,7 @@
|
|||
:else
|
||||
(rx/throw error))))
|
||||
|
||||
delete-fn
|
||||
on-delete
|
||||
(mf/use-fn
|
||||
(mf/deps email team-id)
|
||||
(fn []
|
||||
|
@ -513,7 +549,15 @@
|
|||
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
||||
(st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
|
||||
|
||||
resend-fn
|
||||
|
||||
on-resend-success
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (msg/success (tr "notifications.invitation-email-sent"))
|
||||
(modal/hide)
|
||||
(dd/fetch-team-invitations))))
|
||||
|
||||
on-resend
|
||||
(mf/use-fn
|
||||
(mf/deps email team-id)
|
||||
(fn []
|
||||
|
@ -527,7 +571,13 @@
|
|||
(-> (dd/invite-team-members params)
|
||||
(with-meta {::ev/origin :team}))))))
|
||||
|
||||
copy-fn
|
||||
on-copy-success
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (msg/success (tr "notifications.invitation-link-copied"))
|
||||
(modal/hide))))
|
||||
|
||||
on-copy
|
||||
(mf/use-fn
|
||||
(mf/deps email team-id)
|
||||
(fn []
|
||||
|
@ -536,52 +586,55 @@
|
|||
:on-error on-error})]
|
||||
(st/emit!
|
||||
(-> (dd/copy-invitation-link params)
|
||||
(with-meta {::ev/origin :team}))))))]
|
||||
(with-meta {::ev/origin :team}))))))
|
||||
|
||||
on-hide (mf/use-fn #(reset! show? false))
|
||||
on-show (mf/use-fn #(reset! show? true))]
|
||||
|
||||
[:*
|
||||
[:span.icon {:on-click #(reset! show? true)} [i/actions]]
|
||||
[:& dropdown {:show @show?
|
||||
:on-close #(reset! show? false)}
|
||||
[:span.icon {:on-click on-show} [i/actions]]
|
||||
[:& dropdown {:show @show? :on-close on-hide}
|
||||
[:ul.dropdown.actions-dropdown
|
||||
[:li {:on-click copy-fn} (tr "labels.copy-invitation-link")]
|
||||
[:li {:on-click resend-fn} (tr "labels.resend-invitation")]
|
||||
[:li {:on-click delete-fn} (tr "labels.delete-invitation")]]]]))
|
||||
[:li {:on-click on-copy} (tr "labels.copy-invitation-link")]
|
||||
[:li {:on-click on-resend} (tr "labels.resend-invitation")]
|
||||
[:li {:on-click on-delete} (tr "labels.delete-invitation")]]]]))
|
||||
|
||||
(mf/defc invitation-row
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [invitation can-invite? team] :as props}]
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
[{:keys [invitation can-invite? team-id] :as props}]
|
||||
|
||||
(let [expired? (:expired invitation)
|
||||
email (:email invitation)
|
||||
role (:role invitation)
|
||||
status (if expired? :expired :pending)
|
||||
|
||||
change-rol
|
||||
on-change-role
|
||||
(mf/use-fn
|
||||
(mf/deps team email)
|
||||
(fn [role]
|
||||
(let [params {:email email :team-id (:id team) :role role}
|
||||
(mf/deps email team-id)
|
||||
(fn [role _event]
|
||||
(let [params {:email email :team-id team-id :role role}
|
||||
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
|
||||
(st/emit! (dd/update-team-invitation-role (with-meta params mdata))))))]
|
||||
|
||||
[:div.table-row
|
||||
[:div.table-field.mail email]
|
||||
|
||||
[:div.table-field.roles
|
||||
[:& invitation-role-selector
|
||||
{:can-invite? can-invite?
|
||||
:role role
|
||||
:status status
|
||||
:change-to-editor (partial change-rol :editor)
|
||||
:change-to-admin (partial change-rol :admin)}]]
|
||||
:on-change on-change-role}]]
|
||||
|
||||
[:div.table-field.status
|
||||
[:& invitation-status-badge {:status status}]]
|
||||
|
||||
[:div.table-field.actions
|
||||
(when can-invite?
|
||||
[:& invitation-actions
|
||||
{:invitation invitation
|
||||
:team team}])]]))
|
||||
:team-id team-id}])]]))
|
||||
|
||||
(mf/defc empty-invitation-table
|
||||
[{:keys [can-invite?] :as props}]
|
||||
|
@ -595,7 +648,8 @@
|
|||
[{:keys [team invitations] :as props}]
|
||||
(let [owner? (dm/get-in team [:permissions :is-owner])
|
||||
admin? (dm/get-in team [:permissions :is-admin])
|
||||
can-invite? (or owner? admin?)]
|
||||
can-invite? (or owner? admin?)
|
||||
team-id (:id team)]
|
||||
|
||||
[:div.dashboard-table.invitations
|
||||
[:div.table-header
|
||||
|
@ -610,7 +664,7 @@
|
|||
{:key (:email invitation)
|
||||
:invitation invitation
|
||||
:can-invite? can-invite?
|
||||
:team team}])])]))
|
||||
:team-id team-id}])])]))
|
||||
|
||||
(mf/defc team-invitations-page
|
||||
[{:keys [team] :as props}]
|
||||
|
@ -767,6 +821,7 @@
|
|||
|
||||
|
||||
(mf/defc webhooks-hero
|
||||
{::mf/wrap-props false}
|
||||
[]
|
||||
[:div.banner
|
||||
[:div.title (tr "labels.webhooks")
|
||||
|
@ -785,18 +840,22 @@
|
|||
[:span (tr "dashboard.webhooks.create")]]]])
|
||||
|
||||
(mf/defc webhook-actions
|
||||
[{:keys [on-edit on-delete] :as props}]
|
||||
(let [show? (mf/use-state false)]
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [on-edit on-delete]}]
|
||||
(let [show? (mf/use-state false)
|
||||
on-show (mf/use-fn #(reset! show? true))
|
||||
on-hide (mf/use-fn #(reset! show? false))]
|
||||
|
||||
[:*
|
||||
[:span.icon {:on-click #(reset! show? true)} [i/actions]]
|
||||
[:& dropdown {:show @show?
|
||||
:on-close #(reset! show? false)}
|
||||
[:span.icon {:on-click on-show} [i/actions]]
|
||||
[:& dropdown {:show @show? :on-close on-hide}
|
||||
[:ul.dropdown.actions-dropdown
|
||||
[:li {:on-click on-edit} (tr "labels.edit")]
|
||||
[:li {:on-click on-delete} (tr "labels.delete")]]]]))
|
||||
|
||||
(mf/defc last-delivery-icon
|
||||
[{:keys [success? text] :as props}]
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [success? text]}]
|
||||
[:div.last-delivery-icon
|
||||
[:div.tooltip
|
||||
[:div.label text]
|
||||
|
@ -808,28 +867,38 @@
|
|||
(mf/defc webhook-item
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [webhook] :as props}]
|
||||
(let [on-edit #(st/emit! (modal/show :webhook {:webhook webhook}))
|
||||
error-code (:error-code webhook)
|
||||
(let [error-code (:error-code webhook)
|
||||
id (:id webhook)
|
||||
|
||||
delete-fn
|
||||
on-edit
|
||||
(mf/use-fn
|
||||
(mf/deps webhook)
|
||||
(fn []
|
||||
(let [params {:id (:id webhook)}
|
||||
(st/emit! (modal/show :webhook {:webhook webhook}))))
|
||||
|
||||
on-delete-accepted
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn []
|
||||
(let [params {:id id}
|
||||
mdata {:on-success #(st/emit! (dd/fetch-team-webhooks))}]
|
||||
(st/emit! (dd/delete-team-webhook (with-meta params mdata)))))
|
||||
(st/emit! (dd/delete-team-webhook (with-meta params mdata))))))
|
||||
|
||||
on-delete
|
||||
(mf/use-fn
|
||||
(mf/deps on-delete-accepted)
|
||||
(fn []
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
(let [params {:type :confirm
|
||||
:title (tr "modals.delete-webhook.title")
|
||||
:message (tr "modals.delete-webhook.message")
|
||||
:accept-label (tr "modals.delete-webhook.accept")
|
||||
:on-accept delete-fn})))
|
||||
:on-accept on-delete-accepted}]
|
||||
(st/emit! (modal/show params)))))
|
||||
|
||||
last-delivery-text
|
||||
(if (nil? error-code)
|
||||
(tr "webhooks.last-delivery.success")
|
||||
(str (tr "errors.webhooks.last-delivery")
|
||||
(dm/str (tr "errors.webhooks.last-delivery")
|
||||
(cond
|
||||
(= error-code "ssl-validation-error")
|
||||
(dm/str " " (tr "errors.webhooks.ssl-validation"))
|
||||
|
@ -855,14 +924,16 @@
|
|||
:on-delete on-delete}]]]))
|
||||
|
||||
(mf/defc webhooks-list
|
||||
[{:keys [webhooks] :as props}]
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [webhooks]}]
|
||||
[:div.dashboard-table
|
||||
[:div.table-rows
|
||||
(for [webhook webhooks]
|
||||
[:& webhook-item {:webhook webhook :key (:id webhook)}])]])
|
||||
|
||||
(mf/defc team-webhooks-page
|
||||
[{:keys [team] :as props}]
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [team]}]
|
||||
(let [webhooks (mf/deref refs/dashboard-team-webhooks)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
|
@ -891,7 +962,8 @@
|
|||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(mf/defc team-settings-page
|
||||
[{:keys [team] :as props}]
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [team]}]
|
||||
(let [finput (mf/use-ref)
|
||||
|
||||
members-map (mf/deref refs/dashboard-team-members)
|
||||
|
@ -912,22 +984,19 @@
|
|||
(st/emit! (dd/update-team-photo file)))]
|
||||
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(mf/with-effect [team]
|
||||
(dom/set-html-title (tr "title.team-settings"
|
||||
(if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team))))))
|
||||
(:name team)))))
|
||||
|
||||
|
||||
(mf/use-effect
|
||||
#(st/emit! (dd/fetch-team-members)
|
||||
(mf/with-effect []
|
||||
(st/emit! (dd/fetch-team-members)
|
||||
(dd/fetch-team-stats)))
|
||||
|
||||
[:*
|
||||
[:& header {:section :dashboard-team-settings
|
||||
:team team}]
|
||||
[:& header {:section :dashboard-team-settings :team team}]
|
||||
[:section.dashboard-container.dashboard-team-settings
|
||||
[:div.team-settings
|
||||
[:div.horizontal-blocks
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue