mirror of
https://github.com/penpot/penpot.git
synced 2025-05-11 01:16:37 +02:00
Merge pull request #1655 from penpot/multiple-members-invitations
✨ Allow send multiple team invitations at once
This commit is contained in:
commit
192b9213ac
15 changed files with 326 additions and 62 deletions
|
@ -6,6 +6,7 @@
|
||||||
### :sparkles: New features
|
### :sparkles: New features
|
||||||
|
|
||||||
- Add border radius to our artboars [Taiga #2056](https://tree.taiga.io/project/penpot/us/2056)
|
- Add border radius to our artboars [Taiga #2056](https://tree.taiga.io/project/penpot/us/2056)
|
||||||
|
- Allow send multiple team invitations at once [Taiga #2798](https://tree.taiga.io/project/penpot/us/2798)
|
||||||
- Persist color palette and color picker across refresh [Taiga #1660](https://tree.taiga.io/project/penpot/issue/1660)
|
- Persist color palette and color picker across refresh [Taiga #1660](https://tree.taiga.io/project/penpot/issue/1660)
|
||||||
- Ability to add multiple strokes to a shape [Taiga #2778](https://tree.taiga.io/project/penpot/us/2778)
|
- Ability to add multiple strokes to a shape [Taiga #2778](https://tree.taiga.io/project/penpot/us/2778)
|
||||||
- Scroll to selected size in font size selector [Taiga #2825](https://tree.taiga.io/project/penpot/us/2825)
|
- Scroll to selected size in font size selector [Taiga #2825](https://tree.taiga.io/project/penpot/us/2825)
|
||||||
|
|
|
@ -354,15 +354,20 @@
|
||||||
(declare create-team-invitation)
|
(declare create-team-invitation)
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
(s/def ::email ::us/email)
|
||||||
|
(s/def ::emails ::us/set-of-emails)
|
||||||
(s/def ::invite-team-member
|
(s/def ::invite-team-member
|
||||||
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
|
(s/keys :req-un [::profile-id ::team-id ::role]
|
||||||
|
:opt-un [::email ::emails]))
|
||||||
|
|
||||||
(sv/defmethod ::invite-team-member
|
(sv/defmethod ::invite-team-member
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [perms (teams/get-permissions conn profile-id team-id)
|
(let [perms (teams/get-permissions conn profile-id team-id)
|
||||||
profile (db/get-by-id conn :profile profile-id)
|
profile (db/get-by-id conn :profile profile-id)
|
||||||
team (db/get-by-id conn :team team-id)]
|
team (db/get-by-id conn :team team-id)
|
||||||
|
emails (or emails #{})
|
||||||
|
emails (if email (conj emails email) emails)
|
||||||
|
]
|
||||||
|
|
||||||
(when-not (:is-admin perms)
|
(when-not (:is-admin perms)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
|
@ -374,6 +379,7 @@
|
||||||
:code :profile-is-muted
|
:code :profile-is-muted
|
||||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||||
|
|
||||||
|
(doseq [email emails]
|
||||||
(create-team-invitation
|
(create-team-invitation
|
||||||
(assoc cfg
|
(assoc cfg
|
||||||
:email email
|
:email email
|
||||||
|
@ -381,6 +387,7 @@
|
||||||
:team team
|
:team team
|
||||||
:profile profile
|
:profile profile
|
||||||
:role role))
|
:role role))
|
||||||
|
)
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
(def sql:upsert-team-invitation
|
(def sql:upsert-team-invitation
|
||||||
|
@ -408,12 +415,14 @@
|
||||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :member-is-muted
|
:code :member-is-muted
|
||||||
|
:email email
|
||||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||||
|
|
||||||
;; Secondly check if the invited member email is part of the global spam/bounce report.
|
;; Secondly check if the invited member email is part of the global spam/bounce report.
|
||||||
(when (eml/has-bounce-reports? conn email)
|
(when (eml/has-bounce-reports? conn email)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :email-has-permanent-bounces
|
:code :email-has-permanent-bounces
|
||||||
|
:email email
|
||||||
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -161,14 +161,14 @@
|
||||||
|
|
||||||
(def email-re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
|
(def email-re #"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
|
||||||
|
|
||||||
|
(defn parse-email
|
||||||
|
[s]
|
||||||
|
(some->> s (re-seq email-re) first))
|
||||||
|
|
||||||
(s/def ::email
|
(s/def ::email
|
||||||
(s/conformer
|
(s/conformer
|
||||||
(fn [v]
|
(fn [v]
|
||||||
(if (string? v)
|
(or (parse-email v) ::s/invalid))
|
||||||
(if-let [matches (re-seq email-re v)]
|
|
||||||
(first matches)
|
|
||||||
(do ::s/invalid))
|
|
||||||
::s/invalid))
|
|
||||||
str))
|
str))
|
||||||
|
|
||||||
(s/def ::set-of-emails
|
(s/def ::set-of-emails
|
||||||
|
|
1
frontend/resources/images/icons/cross.svg
Normal file
1
frontend/resources/images/icons/cross.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="3742 2512 500 500"><path fill="#b1b2b5" d="m4203 2512-211 211-212-211-38 39 211 211-211 211 39 39 211-211 211 211 39-39-211-211 211-211z"/><path d="m3761 2992-19-19 106-106 106-105-106-106-106-105 19-19 19-19 106 105 106 106 105-106 106-105 19 19 19 19-105 106-106 105 106 106 105 106-19 19-19 19-105-106-106-106-105 106-106 105-20-19z"/></svg>
|
After Width: | Height: | Size: 394 B |
|
@ -23,8 +23,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-select {
|
.custom-select {
|
||||||
width: 160px;
|
width: 180px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
justify-content: normal;
|
||||||
|
select {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
|
@ -38,6 +42,42 @@
|
||||||
.title {
|
.title {
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
fill: $color-gray-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #ffd9e0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
.icon {
|
||||||
|
background-color: $color-danger;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
svg {
|
||||||
|
fill: $color-white;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
color: $color-black;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-team-members,
|
.dashboard-team-members,
|
||||||
|
|
|
@ -227,6 +227,66 @@ textarea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-multi-input {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid $color-gray-20;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&.invalid {
|
||||||
|
label {
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: 0px;
|
||||||
|
|
||||||
|
&.no-padding {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-items {
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-left: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.around {
|
||||||
|
border: 1px solid $color-gray-20;
|
||||||
|
padding-left: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
&.invalid {
|
||||||
|
border: 1px solid $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 85%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $color-black;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.custom-select {
|
.custom-select {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -427,8 +427,8 @@
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
(defn invite-team-member
|
(defn invite-team-member
|
||||||
[{:keys [email role] :as params}]
|
[{:keys [emails role] :as params}]
|
||||||
(us/assert ::us/email email)
|
(us/assert ::us/set-of-emails emails)
|
||||||
(us/assert ::us/keyword role)
|
(us/assert ::us/keyword role)
|
||||||
(ptk/reify ::invite-team-member
|
(ptk/reify ::invite-team-member
|
||||||
IDeref
|
IDeref
|
||||||
|
@ -770,7 +770,6 @@
|
||||||
(rx/tap on-success)
|
(rx/tap on-success)
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Navigation
|
;; Navigation
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
@ -7,11 +7,14 @@
|
||||||
(ns app.main.ui.components.forms
|
(ns app.main.ui.components.forms
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.forms :as fm]
|
[app.util.forms :as fm]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
[app.util.keyboard :as kbd]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
|
[cljs.core :as c]
|
||||||
[clojure.string]
|
[clojure.string]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
@ -74,7 +77,9 @@
|
||||||
"password"))))
|
"password"))))
|
||||||
|
|
||||||
on-focus #(reset! focus? true)
|
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
|
on-blur
|
||||||
(fn [_]
|
(fn [_]
|
||||||
|
@ -136,12 +141,15 @@
|
||||||
:focus @focus?
|
:focus @focus?
|
||||||
:valid (and touched? (not error))
|
:valid (and touched? (not error))
|
||||||
:invalid (and touched? error)
|
:invalid (and touched? error)
|
||||||
:disabled disabled
|
:disabled disabled)
|
||||||
;; :empty (str/empty? value)
|
;; :empty (str/empty? value)
|
||||||
)
|
|
||||||
|
|
||||||
on-focus #(reset! focus? true)
|
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
|
on-blur
|
||||||
(fn [_]
|
(fn [_]
|
||||||
|
@ -177,7 +185,10 @@
|
||||||
form (or form (mf/use-ctx form-ctx))
|
form (or form (mf/use-ctx form-ctx))
|
||||||
value (or (get-in @form [:data input-name]) default)
|
value (or (get-in @form [:data input-name]) default)
|
||||||
cvalue (d/seek #(= value (:value %)) options)
|
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
|
[:div.custom-select
|
||||||
[:select {:value value
|
[:select {:value value
|
||||||
|
@ -215,3 +226,114 @@
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(on-submit form event))}
|
(on-submit form event))}
|
||||||
children]]))
|
children]]))
|
||||||
|
|
||||||
|
(defn- conj-dedup
|
||||||
|
"A helper that adds item into a vector and removes possible
|
||||||
|
duplicates. This is not very efficient implementation but is ok for
|
||||||
|
handling form input that will have a small number of items."
|
||||||
|
[coll item]
|
||||||
|
(into [] (distinct) (conj coll item)))
|
||||||
|
|
||||||
|
(mf/defc multi-input
|
||||||
|
[{:keys [form label class name trim valid-item-fn] :as props}]
|
||||||
|
(let [form (or form (mf/use-ctx form-ctx))
|
||||||
|
input-name (get props :name)
|
||||||
|
touched? (get-in @form [:touched input-name])
|
||||||
|
error (get-in @form [:errors input-name])
|
||||||
|
focus? (mf/use-state false)
|
||||||
|
|
||||||
|
items (mf/use-state [])
|
||||||
|
value (mf/use-state "")
|
||||||
|
result (hooks/use-equal-memo @items)
|
||||||
|
|
||||||
|
empty? (and (str/empty? @value)
|
||||||
|
(zero? (count @items)))
|
||||||
|
|
||||||
|
klass (str (get props :class) " "
|
||||||
|
(dom/classnames
|
||||||
|
:focus @focus?
|
||||||
|
:valid (and touched? (not error))
|
||||||
|
:invalid (and touched? error)
|
||||||
|
:empty empty?
|
||||||
|
:custom-multi-input true
|
||||||
|
:custom-input true))
|
||||||
|
|
||||||
|
in-klass (str class " "
|
||||||
|
(dom/classnames
|
||||||
|
:no-padding (pos? (count @items))))
|
||||||
|
|
||||||
|
on-focus
|
||||||
|
(mf/use-fn #(reset! focus? true))
|
||||||
|
|
||||||
|
on-change
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [event]
|
||||||
|
(let [content (-> event dom/get-target dom/get-input-value)]
|
||||||
|
(reset! value content))))
|
||||||
|
|
||||||
|
update-form!
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps form)
|
||||||
|
(fn [items]
|
||||||
|
(let [value (str/join " " (map :text items))]
|
||||||
|
(fm/update-input-value! form input-name value))))
|
||||||
|
|
||||||
|
on-key-down
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps @value)
|
||||||
|
(fn [event]
|
||||||
|
(cond
|
||||||
|
(or (kbd/enter? event)
|
||||||
|
(kbd/comma? event))
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(let [val (cond-> @value trim str/trim)]
|
||||||
|
(reset! value "")
|
||||||
|
(swap! items conj-dedup {:text val :valid (valid-item-fn val)})))
|
||||||
|
|
||||||
|
(and (kbd/backspace? event)
|
||||||
|
(str/empty? @value))
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(swap! items (fn [items] (if (c/empty? items) items (pop items))))))))
|
||||||
|
|
||||||
|
on-blur
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [_]
|
||||||
|
(reset! focus? false)
|
||||||
|
(when-not (get-in @form [:touched input-name])
|
||||||
|
(swap! form assoc-in [:touched input-name] true))))
|
||||||
|
|
||||||
|
remove-item!
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [item]
|
||||||
|
(swap! items #(into [] (remove (fn [x] (= x item))) %))))]
|
||||||
|
|
||||||
|
(mf/with-effect [result @value]
|
||||||
|
(let [val (cond-> @value trim str/trim)
|
||||||
|
values (conj-dedup result {:text val :valid (valid-item-fn val)})
|
||||||
|
values (filterv #(:valid %) values)]
|
||||||
|
(update-form! values)))
|
||||||
|
|
||||||
|
[:div {:class klass}
|
||||||
|
(when-let [items (seq @items)]
|
||||||
|
[:div.selected-items
|
||||||
|
(for [item items]
|
||||||
|
[:div.selected-item {:key (:text item)}
|
||||||
|
[:span.around {:class (when-not (:valid item) "invalid")}
|
||||||
|
[:span.text (:text item)]
|
||||||
|
[:span.icon {:on-click #(remove-item! item)} i/cross]]])])
|
||||||
|
|
||||||
|
[:input {:id (name input-name)
|
||||||
|
:class in-klass
|
||||||
|
:type "text"
|
||||||
|
:auto-focus true
|
||||||
|
:on-focus on-focus
|
||||||
|
:on-blur on-blur
|
||||||
|
:on-key-down on-key-down
|
||||||
|
:value @value
|
||||||
|
:on-change on-change
|
||||||
|
:placeholder (when empty? label)}]
|
||||||
|
[:label {:for (name input-name)} label]]))
|
||||||
|
|
|
@ -73,10 +73,10 @@
|
||||||
]
|
]
|
||||||
(filterv identity)))
|
(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 ::role ::us/keyword)
|
||||||
(s/def ::invite-member-form
|
(s/def ::invite-member-form
|
||||||
(s/keys :req-un [::role ::email]))
|
(s/keys :req-un [::role ::emails]))
|
||||||
|
|
||||||
(mf/defc invite-member-modal
|
(mf/defc invite-member-modal
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
|
@ -87,29 +87,29 @@
|
||||||
initial (mf/use-memo (constantly {:role "editor"}))
|
initial (mf/use-memo (constantly {:role "editor"}))
|
||||||
form (fm/use-form :spec ::invite-member-form
|
form (fm/use-form :spec ::invite-member-form
|
||||||
:initial initial)
|
:initial initial)
|
||||||
|
error-text (mf/use-state "")
|
||||||
|
|
||||||
on-success
|
on-success
|
||||||
(st/emitf (dm/success (tr "notifications.invitation-email-sent"))
|
(st/emitf (dm/success (tr "notifications.invitation-email-sent"))
|
||||||
(modal/hide)
|
(modal/hide)
|
||||||
(dd/fetch-team-invitations))
|
(dd/fetch-team-invitations))
|
||||||
|
|
||||||
on-error
|
on-error
|
||||||
(fn [form {:keys [type code] :as error}]
|
(fn [{:keys [type code] :as error}]
|
||||||
(let [email (get @form [:data :email])]
|
|
||||||
(cond
|
(cond
|
||||||
(and (= :validation type)
|
(and (= :validation type)
|
||||||
(= :profile-is-muted code))
|
(= :profile-is-muted code))
|
||||||
(dm/error (tr "errors.profile-is-muted"))
|
(st/emit! (dm/error (tr "errors.profile-is-muted"))
|
||||||
|
(modal/hide))
|
||||||
|
|
||||||
(and (= :validation type)
|
(and (= :validation type)
|
||||||
(= :member-is-muted code))
|
(or (= :member-is-muted code)
|
||||||
(dm/error (tr "errors.member-is-muted"))
|
(= :email-has-permanent-bounces code)))
|
||||||
|
(swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error)))
|
||||||
(and (= :validation type)
|
|
||||||
(= :email-has-permanent-bounces code))
|
|
||||||
(dm/error (tr "errors.email-has-permanent-bounces" email))
|
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(dm/error (tr "errors.generic")))))
|
(st/emit! (dm/error (tr "errors.generic"))
|
||||||
|
(modal/hide))))
|
||||||
|
|
||||||
on-submit
|
on-submit
|
||||||
(fn [form]
|
(fn [form]
|
||||||
|
@ -124,11 +124,19 @@
|
||||||
[:div.title
|
[:div.title
|
||||||
[:span.text (tr "modals.invite-member.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
|
[:div.form-row
|
||||||
[:& fm/input {:name :email
|
[:& fm/multi-input {:type "email"
|
||||||
:label (tr "labels.email")}]
|
:name :emails
|
||||||
[:& fm/select {:name :role
|
:auto-focus? true
|
||||||
:options roles}]]
|
:trim true
|
||||||
|
:valid-item-fn us/parse-email
|
||||||
|
:label (tr "modals.invite-member.emails")}]
|
||||||
|
[:& fm/select {:name :role :options roles}]]
|
||||||
|
|
||||||
[:div.action-buttons
|
[:div.action-buttons
|
||||||
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))
|
[:& fm/submit-button {:label (tr "modals.invite-member-confirm.accept")}]]]]))
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
(def component (icon-xref :component))
|
(def component (icon-xref :component))
|
||||||
(def copy (icon-xref :copy))
|
(def copy (icon-xref :copy))
|
||||||
(def curve (icon-xref :curve))
|
(def curve (icon-xref :curve))
|
||||||
|
(def cross (icon-xref :cross))
|
||||||
(def download (icon-xref :download))
|
(def download (icon-xref :download))
|
||||||
(def easing-linear (icon-xref :easing-linear))
|
(def easing-linear (icon-xref :easing-linear))
|
||||||
(def easing-ease (icon-xref :easing-ease))
|
(def easing-ease (icon-xref :easing-ease))
|
||||||
|
|
|
@ -108,6 +108,15 @@
|
||||||
(when (some? node)
|
(when (some? node)
|
||||||
(.-value 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
|
(defn get-attribute
|
||||||
"Extract the value of one attribute of a dom node."
|
"Extract the value of one attribute of a dom node."
|
||||||
[^js node ^string attr-name]
|
[^js node ^string attr-name]
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
(:refer-clojure :exclude [uuid])
|
(:refer-clojure :exclude [uuid])
|
||||||
(:require
|
(:require
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.util.dom :as dom]
|
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
@ -114,19 +113,20 @@
|
||||||
(render inc)))))
|
(render inc)))))
|
||||||
|
|
||||||
(defn on-input-change
|
(defn on-input-change
|
||||||
([form field]
|
([form field value]
|
||||||
(on-input-change form field false))
|
(on-input-change form field value false))
|
||||||
([form field trim?]
|
([form field value 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]
|
(swap! form (fn [state]
|
||||||
(-> state
|
(-> state
|
||||||
(assoc-in [:data field] (if trim? (str/trim value) value))
|
(assoc-in [:data field] (if trim? (str/trim value) value))
|
||||||
(update :errors dissoc field))))))))
|
(update :errors dissoc field))))))
|
||||||
|
|
||||||
|
(defn update-input-value!
|
||||||
|
[form field value]
|
||||||
|
(swap! form (fn [state]
|
||||||
|
(-> state
|
||||||
|
(assoc-in [:data field] value)
|
||||||
|
(update :errors dissoc field)))))
|
||||||
|
|
||||||
(defn on-input-blur
|
(defn on-input-blur
|
||||||
[form field]
|
[form field]
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
(def altKey? (is-key? "Alt"))
|
(def altKey? (is-key? "Alt"))
|
||||||
(def ctrlKey? (or (is-key? "Control")
|
(def ctrlKey? (or (is-key? "Control")
|
||||||
(is-key? "Meta")))
|
(is-key? "Meta")))
|
||||||
|
(def comma? (is-key? ","))
|
||||||
|
(def backspace? (is-key? "Backspace"))
|
||||||
|
|
||||||
(defn editing? [e]
|
(defn editing? [e]
|
||||||
(.-editing ^js e))
|
(.-editing ^js e))
|
||||||
|
|
|
@ -617,6 +617,9 @@ msgstr "You can't use your email as password"
|
||||||
msgid "errors.email-has-permanent-bounces"
|
msgid "errors.email-has-permanent-bounces"
|
||||||
msgstr "The email «%s» has many permanent bounce reports."
|
msgstr "The email «%s» has many permanent bounce reports."
|
||||||
|
|
||||||
|
msgid "errors.email-spam-or-permanent-bounces"
|
||||||
|
msgstr "The email «%s» has been reported as spam or permanently bounce."
|
||||||
|
|
||||||
#: src/app/main/ui/settings/change_email.cljs
|
#: src/app/main/ui/settings/change_email.cljs
|
||||||
msgid "errors.email-invalid-confirmation"
|
msgid "errors.email-invalid-confirmation"
|
||||||
msgstr "Confirmation email must match"
|
msgstr "Confirmation email must match"
|
||||||
|
@ -1551,6 +1554,9 @@ msgstr "Are you sure you want to delete this member from the team?"
|
||||||
msgid "modals.delete-team-member-confirm.title"
|
msgid "modals.delete-team-member-confirm.title"
|
||||||
msgstr "Delete team member"
|
msgstr "Delete team member"
|
||||||
|
|
||||||
|
msgid "modals.invite-member.emails"
|
||||||
|
msgstr "Emails, comma separated"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs
|
#: src/app/main/ui/dashboard/team.cljs
|
||||||
msgid "modals.invite-member-confirm.accept"
|
msgid "modals.invite-member-confirm.accept"
|
||||||
msgstr "Send invitation"
|
msgstr "Send invitation"
|
||||||
|
|
|
@ -620,6 +620,9 @@ msgstr "No puedes usar tu email como password"
|
||||||
msgid "errors.email-has-permanent-bounces"
|
msgid "errors.email-has-permanent-bounces"
|
||||||
msgstr "El email «%s» tiene varios reportes de rebote permanente."
|
msgstr "El email «%s» tiene varios reportes de rebote permanente."
|
||||||
|
|
||||||
|
msgid "errors.email-spam-or-permanent-bounces"
|
||||||
|
msgstr "El email «%s» tiene reportes de spam o de rebote permanente."
|
||||||
|
|
||||||
#: src/app/main/ui/settings/change_email.cljs
|
#: src/app/main/ui/settings/change_email.cljs
|
||||||
msgid "errors.email-invalid-confirmation"
|
msgid "errors.email-invalid-confirmation"
|
||||||
msgstr "El correo de confirmación debe coincidir"
|
msgstr "El correo de confirmación debe coincidir"
|
||||||
|
@ -1553,6 +1556,9 @@ msgstr "¿Seguro que quieres eliminar este integrante del equipo?"
|
||||||
msgid "modals.delete-team-member-confirm.title"
|
msgid "modals.delete-team-member-confirm.title"
|
||||||
msgstr "Eliminar integrante del equipo"
|
msgstr "Eliminar integrante del equipo"
|
||||||
|
|
||||||
|
msgid "modals.invite-member.emails"
|
||||||
|
msgstr "Emails, separados por coma"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs
|
#: src/app/main/ui/dashboard/team.cljs
|
||||||
msgid "modals.invite-member-confirm.accept"
|
msgid "modals.invite-member-confirm.accept"
|
||||||
msgstr "Enviar invitacion"
|
msgstr "Enviar invitacion"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue