mirror of
https://github.com/penpot/penpot.git
synced 2025-05-25 09:26:11 +02:00
✨ Allow send multiple team invitations at once
This commit is contained in:
parent
087d896569
commit
1cf9ad55c6
13 changed files with 287 additions and 54 deletions
|
@ -427,8 +427,8 @@
|
|||
(rx/catch on-error))))))
|
||||
|
||||
(defn invite-team-member
|
||||
[{:keys [email role] :as params}]
|
||||
(us/assert ::us/email email)
|
||||
[{:keys [emails role] :as params}]
|
||||
(us/assert ::us/set-of-emails emails)
|
||||
(us/assert ::us/keyword role)
|
||||
(ptk/reify ::invite-team-member
|
||||
IDeref
|
||||
|
@ -770,7 +770,6 @@
|
|||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Navigation
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -890,7 +889,7 @@
|
|||
:team-id team-id})
|
||||
action-name (if in-project? :create-file :create-project)
|
||||
action (if in-project? file-created project-created)]
|
||||
|
||||
|
||||
(->> (rp/mutation! action-name params)
|
||||
(rx/map action))))))
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[clojure.string]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -74,7 +75,9 @@
|
|||
"password"))))
|
||||
|
||||
on-focus #(reset! focus? true)
|
||||
on-change (fm/on-input-change form input-name trim)
|
||||
on-change (fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-input-value)]
|
||||
(fm/on-input-change form input-name value trim)))
|
||||
|
||||
on-blur
|
||||
(fn [_]
|
||||
|
@ -141,7 +144,10 @@
|
|||
)
|
||||
|
||||
on-focus #(reset! focus? true)
|
||||
on-change (fm/on-input-change form input-name trim)
|
||||
on-change (fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)]
|
||||
(fm/on-input-change form input-name value trim)))
|
||||
|
||||
on-blur
|
||||
(fn [_]
|
||||
|
@ -177,7 +183,10 @@
|
|||
form (or form (mf/use-ctx form-ctx))
|
||||
value (or (get-in @form [:data input-name]) default)
|
||||
cvalue (d/seek #(= value (:value %)) options)
|
||||
on-change (fm/on-input-change form input-name)]
|
||||
on-change (fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)]
|
||||
(fm/on-input-change form input-name value)))]
|
||||
|
||||
[:div.custom-select
|
||||
[:select {:value value
|
||||
|
@ -215,3 +224,93 @@
|
|||
(dom/prevent-default event)
|
||||
(on-submit form event))}
|
||||
children]]))
|
||||
|
||||
|
||||
|
||||
(mf/defc multi-input-row
|
||||
[{:keys [item, remove-item!, class, invalid-class]}]
|
||||
(let [valid (val item)
|
||||
text (key item)]
|
||||
[:div {:class class}
|
||||
[:span.around {:class (when-not valid invalid-class)}
|
||||
[:span.text text]
|
||||
[:span.icon {:on-click #(remove-item! (key item))} i/cross]]]))
|
||||
|
||||
(mf/defc multi-input
|
||||
[{:keys [form hint class container-class row-class row-invalid-class] :as props}]
|
||||
(let [multi-input-name (get props :name)
|
||||
single-input-name (keyword (str "single-" (name multi-input-name)))
|
||||
single-input-element (dom/get-element (name single-input-name))
|
||||
hint-element (dom/get-element-by-class "hint")
|
||||
form (or form (mf/use-ctx form-ctx))
|
||||
value (get-in @form [:data multi-input-name] "")
|
||||
single-mail-value (get-in @form [:data single-input-name] "")
|
||||
items (mf/use-state {})
|
||||
|
||||
comma-items
|
||||
(fn [items]
|
||||
(if (= "" single-mail-value)
|
||||
(str/join "," (keys items))
|
||||
(str/join "," (conj (keys items) single-mail-value))))
|
||||
|
||||
update-multi-input
|
||||
(fn [all]
|
||||
(fm/on-input-change form multi-input-name all true)
|
||||
|
||||
(if (= "" all)
|
||||
(do
|
||||
(dom/add-class! single-input-element "empty")
|
||||
(dom/add-class! hint-element "hidden"))
|
||||
(do
|
||||
(dom/remove-class! single-input-element "empty")
|
||||
(dom/remove-class! hint-element "hidden")))
|
||||
|
||||
(dom/focus! single-input-element))
|
||||
|
||||
remove-item!
|
||||
(fn [item]
|
||||
(swap! items
|
||||
(fn [items]
|
||||
(let [temp-items (dissoc items item)
|
||||
all (comma-items temp-items)]
|
||||
(update-multi-input all)
|
||||
temp-items))))
|
||||
|
||||
add-item!
|
||||
(fn [item valid]
|
||||
(swap! items assoc item valid))
|
||||
|
||||
input-key-down (fn [event]
|
||||
(let [target (dom/event->target event)
|
||||
value (dom/get-value target)
|
||||
valid (and (not (= value "")) (dom/valid? target))]
|
||||
|
||||
(when (kbd/comma? event)
|
||||
(dom/prevent-default event)
|
||||
(add-item! value valid)
|
||||
(fm/on-input-change form single-input-name ""))))
|
||||
|
||||
input-key-up #(update-multi-input (comma-items @items))
|
||||
|
||||
single-props (-> props
|
||||
(dissoc :hint :row-class :row-invalid-class :container-class :class)
|
||||
(assoc
|
||||
:label hint
|
||||
:name single-input-name
|
||||
:on-key-down input-key-down
|
||||
:on-key-up input-key-up
|
||||
:class (str/join " " [class "empty"])))]
|
||||
|
||||
[:div {:class container-class}
|
||||
(when (string? hint)
|
||||
[:span.hint.hidden hint])
|
||||
(for [item @items]
|
||||
[:& multi-input-row {:item item
|
||||
:remove-item! remove-item!
|
||||
:class row-class
|
||||
:invalid-class row-invalid-class}])
|
||||
[:& input single-props]
|
||||
[:input {:id (name multi-input-name)
|
||||
:read-only true
|
||||
:type "hidden"
|
||||
:value value}]]))
|
|
@ -73,10 +73,10 @@
|
|||
]
|
||||
(filterv identity)))
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::emails (s/and ::us/set-of-emails d/not-empty?))
|
||||
(s/def ::role ::us/keyword)
|
||||
(s/def ::invite-member-form
|
||||
(s/keys :req-un [::role ::email]))
|
||||
(s/keys :req-un [::role ::emails]))
|
||||
|
||||
(mf/defc invite-member-modal
|
||||
{::mf/register modal/components
|
||||
|
@ -87,29 +87,29 @@
|
|||
initial (mf/use-memo (constantly {:role "editor"}))
|
||||
form (fm/use-form :spec ::invite-member-form
|
||||
:initial initial)
|
||||
error-text (mf/use-state "")
|
||||
|
||||
on-success
|
||||
(st/emitf (dm/success (tr "notifications.invitation-email-sent"))
|
||||
(modal/hide)
|
||||
(dd/fetch-team-invitations))
|
||||
|
||||
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"))
|
||||
(fn [{:keys [type code] :as error}]
|
||||
(cond
|
||||
(and (= :validation type)
|
||||
(= :profile-is-muted code))
|
||||
(st/emit! (dm/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)))
|
||||
|
||||
(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")))))
|
||||
:else
|
||||
(st/emit! (dm/error (tr "errors.generic"))
|
||||
(modal/hide))))
|
||||
|
||||
on-submit
|
||||
(fn [form]
|
||||
|
@ -123,10 +123,23 @@
|
|||
[:& fm/form {:on-submit on-submit :form form}
|
||||
[:div.title
|
||||
[:span.text (tr "modals.invite-member.title")]]
|
||||
|
||||
(when-not (= "" @error-text)
|
||||
[:div.error
|
||||
[:span.icon i/msg-error]
|
||||
[:span.text @error-text]]
|
||||
)
|
||||
|
||||
|
||||
[:div.form-row
|
||||
[:& fm/input {:name :email
|
||||
:label (tr "labels.email")}]
|
||||
[:& fm/multi-input {:type "email"
|
||||
:name :emails
|
||||
:auto-focus? true
|
||||
:hint (tr "modals.invite-member.emails")
|
||||
:class "invite-member-email-input"
|
||||
:container-class "invite-member-email-container"
|
||||
:row-class "invite-member-email-text"
|
||||
:row-invalid-class "invalid"}]
|
||||
[:& fm/select {:name :role
|
||||
:options roles}]]
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
(def component (icon-xref :component))
|
||||
(def copy (icon-xref :copy))
|
||||
(def curve (icon-xref :curve))
|
||||
(def cross (icon-xref :cross))
|
||||
(def download (icon-xref :download))
|
||||
(def easing-linear (icon-xref :easing-linear))
|
||||
(def easing-ease (icon-xref :easing-ease))
|
||||
|
|
|
@ -108,6 +108,15 @@
|
|||
(when (some? node)
|
||||
(.-value node)))
|
||||
|
||||
(defn get-input-value
|
||||
"Extract the value from dom input node taking into account the type."
|
||||
[^js node]
|
||||
(when (some? node)
|
||||
(if (or (= (.-type node) "checkbox")
|
||||
(= (.-type node) "radio"))
|
||||
(.-checked node)
|
||||
(.-value node))))
|
||||
|
||||
(defn get-attribute
|
||||
"Extract the value of one attribute of a dom node."
|
||||
[^js node ^string attr-name]
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
(:refer-clojure :exclude [uuid])
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -114,19 +113,13 @@
|
|||
(render inc)))))
|
||||
|
||||
(defn on-input-change
|
||||
([form field]
|
||||
(on-input-change form field false))
|
||||
([form field trim?]
|
||||
(fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
value (if (or (= (.-type target) "checkbox")
|
||||
(= (.-type target) "radio"))
|
||||
(.-checked target)
|
||||
(dom/get-value target))]
|
||||
(swap! form (fn [state]
|
||||
(-> state
|
||||
(assoc-in [:data field] (if trim? (str/trim value) value))
|
||||
(update :errors dissoc field))))))))
|
||||
([form field value]
|
||||
(on-input-change form field value false))
|
||||
([form field value trim?]
|
||||
(swap! form (fn [state]
|
||||
(-> state
|
||||
(assoc-in [:data field] (if trim? (str/trim value) value))
|
||||
(update :errors dissoc field))))))
|
||||
|
||||
(defn on-input-blur
|
||||
[form field]
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
(def altKey? (is-key? "Alt"))
|
||||
(def ctrlKey? (or (is-key? "Control")
|
||||
(is-key? "Meta")))
|
||||
(def comma? (is-key? ","))
|
||||
|
||||
(defn editing? [e]
|
||||
(.-editing ^js e))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue