Improve state management on onboarding team modal

This commit is contained in:
Andrey Antukh 2024-06-07 16:45:46 +02:00
parent 0dda893d73
commit 273a5f7a0a

View file

@ -7,34 +7,29 @@
(ns app.main.ui.onboarding.team-choice (ns app.main.ui.onboarding.team-choice
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data.macros :as dmc] [app.common.data.macros :as dm]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.messages :as msg] [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.store :as st]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.timers :as tm]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(s/def ::name ::us/not-empty-string) (mf/defc left-sidebar
(s/def ::team-form {::mf/props :obj
(s/keys :req-un [::name])) ::mf/private true}
(mf/defc team-modal-left
[] []
[:div {:class (stl/css :modal-left)} [:div {:class (stl/css :modal-left)}
[:h1 {:class (stl/css :modal-title)} [:h1 {:class (stl/css :modal-title)}
(tr "onboarding-v2.welcome.title")] (tr "onboarding-v2.welcome.title")]
[:h2 {:class (stl/css :modal-subtitle)} [:h2 {:class (stl/css :modal-subtitle)}
(tr "onboarding.team-modal.team-definition")] (tr "onboarding.team-modal.team-definition")]
[:p {:class (stl/css :modal-text)} [:p {:class (stl/css :modal-text)}
@ -61,99 +56,27 @@
[:p {:class (stl/css :modal-desc)} [:p {:class (stl/css :modal-desc)}
(tr "onboarding.team-modal.create-team-feature-5")]]]]) (tr "onboarding.team-modal.create-team-feature-5")]]]])
(mf/defc onboarding-team-modal
{::mf/register modal/components
::mf/register-as :onboarding-team}
[]
(let [form (fm/use-form :spec ::team-form
:initial {}
:validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space"))
(fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))])
on-submit
(mf/use-fn
(fn [form _]
(let [tname (get-in @form [:clean-data :name])]
(st/emit! (modal/show {:type :onboarding-team-invitations :name tname})
(ptk/event ::ev/event {::ev/name "choose-team-name"
::ev/origin "onboarding"
:name tname
:step 1})))))
on-skip
(fn []
(tm/schedule 400 #(st/emit! (modal/hide)
(ptk/event ::ev/event {::ev/name "create-team-later"
::ev/origin "onboarding"
:step 1}))))
teams (mf/deref refs/teams)
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
(mf/with-effect [teams]
(when (> (count teams) 1)
(st/emit! (modal/hide))))
(when (< (count teams) 2)
[:div {:class (stl/css-case :modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated.fadeIn {:class (stl/css :modal-container)}
[:& team-modal-left]
[:div {:class (stl/css :separator)}]
[:div {:class (stl/css :modal-right)}
[:div {:class (stl/css :first-block)}
[:h2 {:class (stl/css :modal-subtitle)}
(tr "onboarding.team-modal.create-team")]
[:p {:class (stl/css :modal-text)}
(tr "onboarding.choice.team-up.create-team-desc")]
[:& fm/form {:form form
:class (stl/css :modal-form)
:on-submit on-submit}
[:& fm/input {:type "text"
:class (stl/css :team-name-input)
:name :name
:placeholder "Team name"
:label (tr "onboarding.choice.team-up.create-team-placeholder")}]
[:div {:class (stl/css :action-buttons)}
[:> fm/submit-button*
{:class (stl/css :accept-button)
:label (tr "onboarding.choice.team-up.continue-creating-team")}]]]]
[:div {:class (stl/css :second-block)}
[:h2 {:class (stl/css :modal-subtitle)}
(tr "onboarding.choice.team-up.start-without-a-team")]
[:p {:class (stl/css :modal-text)}
(tr "onboarding.choice.team-up.start-without-a-team-description")]
[:div {:class (stl/css :action-buttons)}
[:button {:class (stl/css :accept-button)
:on-click on-skip}
(tr "onboarding.choice.team-up.continue-without-a-team")]]]]
[:div {:class (stl/css :paginator)} "1/2"]]])))
(defn get-available-roles
[]
[{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
(s/def ::emails (s/and ::us/set-of-valid-emails)) (s/def ::emails (s/and ::us/set-of-valid-emails))
(s/def ::role ::us/keyword) (s/def ::role ::us/keyword)
(s/def ::invite-form (s/def ::invite-form
(s/keys :req-un [::role ::emails])) (s/keys :req-un [::role ::emails]))
;; This is the final step of team creation, consists in provide a (defn- get-available-roles
;; shortcut for invite users. []
[{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
(mf/defc onboarding-team-invitations-modal (mf/defc team-form-step-2
{::mf/register modal/components {::mf/props :obj}
::mf/register-as :onboarding-team-invitations [{:keys [name on-back]}]
::mf/props :obj} (let [initial (mf/use-memo
[{:keys [name]}] #(do {:role "editor"
(let [initial (mf/use-memo (constantly
{:role "editor"
:name name})) :name name}))
form (fm/use-form :spec ::invite-form form (fm/use-form :spec ::invite-form
:initial initial) :initial initial)
params (:clean-data @form) params (:clean-data @form)
emails (:emails params) emails (:emails params)
@ -161,51 +84,47 @@
on-success on-success
(mf/use-fn (mf/use-fn
(fn [_form response] (fn [response]
(let [team-id (:id response)] (let [team-id (:id response)]
(st/emit! (st/emit! (du/update-profile-props {:onboarding-team-id team-id})
(modal/hide) (rt/nav :dashboard-projects {:team-id team-id})))))
(rt/nav :dashboard-projects {:team-id team-id}))
(tm/schedule 400 #(st/emit!
(modal/hide))))))
on-error on-error
(mf/use-fn (mf/use-fn
(fn [_form _cause] (fn [_]
(st/emit! (msg/error "Error on creating team.")))) (st/emit! (msg/error (tr "errors.generic")))))
;; The SKIP branch only creates the team, without invitations
on-invite-later on-invite-later
(mf/use-fn (mf/use-fn
(fn [_] (fn [{:keys [name]}]
(let [mdata {:on-success (partial on-success form) (let [mdata {:on-success on-success
:on-error (partial on-error form)} :on-error on-error}
params {:name name}] params {:name name}]
(st/emit! (dd/create-team (with-meta params mdata)) (st/emit! (dd/create-team (with-meta params mdata))
(ptk/event ::ev/event {::ev/name "create-team-and-invite-later" (ptk/data-event ::ev/event
::ev/origin "onboarding" {::ev/name "onboarding-step"
:name name :label "team:create-team-and-invite-later"
:step 2}))))) :team-name name
:step 7})
(ptk/data-event ::ev/event
{::ev/name "onboarding-finish"})))))
;; The SUBMIT branch creates the team with the invitations
on-invite-now on-invite-now
(mf/use-fn (mf/use-fn
(fn [form] (fn [{:keys [name] :as params}]
(let [mdata {:on-success (partial on-success form) (let [mdata {:on-success on-success
:on-error (partial on-error form)} :on-error on-error}]
params (:clean-data @form)
emails (:emails params)]
(st/emit! (if (> (count emails) 0) (st/emit! (dd/create-team-with-invitations (with-meta params mdata))
;; If the user is only inviting to itself we don't call to create-team-with-invitations (ptk/data-event ::ev/event
(dd/create-team-with-invitations (with-meta params mdata)) {::ev/name "onboarding-step"
(dd/create-team (with-meta {:name name} mdata))) :label "team:create-team-and-invite"
(ptk/event ::ev/event {::ev/name "create-team-and-send-invitations"
::ev/origin "onboarding"
:invites (count emails) :invites (count emails)
:team-name name
:role (:role params) :role (:role params)
:name name :step 7})
:step 2}))))) (ptk/data-event ::ev/event
{::ev/name "onboarding-finish"})))))
on-submit on-submit
(mf/use-fn (mf/use-fn
@ -213,17 +132,10 @@
(let [params (:clean-data @form) (let [params (:clean-data @form)
emails (:emails params)] emails (:emails params)]
(if (> (count emails) 0) (if (> (count emails) 0)
(on-invite-now form) (on-invite-now params)
(on-invite-later form)) (on-invite-later params)))))]
(modal/hide!))))
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
[:div {:class (stl/css-case :modal-overlay true [:*
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated.fadeIn {:class (stl/css :modal-container)}
[:& team-modal-left]
[:div {:class (stl/css :separator)}]
[:div {:class (stl/css :modal-right-invitations)} [:div {:class (stl/css :modal-right-invitations)}
[:h2 {:class (stl/css :modal-subtitle)} (tr "onboarding.choice.team-up.invite-members")] [:h2 {:class (stl/css :modal-subtitle)} (tr "onboarding.choice.team-up.invite-members")]
[:p {:class (stl/css :modal-text)} (tr "onboarding.choice.team-up.invite-members-info")] [:p {:class (stl/css :modal-text)} (tr "onboarding.choice.team-up.invite-members-info")]
@ -246,11 +158,7 @@
[:div {:class (stl/css :action-buttons)} [:div {:class (stl/css :action-buttons)}
[:button {:class (stl/css :back-button) [:button {:class (stl/css :back-button)
:on-click #(st/emit! (modal/show {:type :onboarding-team}) :on-click on-back}
(ptk/event ::ev/event {::ev/name "invite-members-back"
::ev/origin "onboarding"
:name name
:step 2}))}
(tr "labels.back")] (tr "labels.back")]
[:> fm/submit-button* [:> fm/submit-button*
@ -259,9 +167,111 @@
(tr "onboarding.choice.team-up.create-team-and-invite") (tr "onboarding.choice.team-up.create-team-and-invite")
(tr "onboarding.choice.team-up.create-team-without-invite"))}]] (tr "onboarding.choice.team-up.create-team-without-invite"))}]]
[:div {:class (stl/css :modal-hint)} [:div {:class (stl/css :modal-hint)}
(dmc/str "(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")")]]] "(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"]]]
[:div {:class (stl/css :paginator)} "2/2"]]])) [:div {:class (stl/css :paginator)} "2/2"]]))
(mf/defc team-form-step-1
{::mf/props :obj
::mf/private true}
[{:keys [on-submit]}]
(let [validators (mf/with-memo []
[(fm/validate-not-empty :name (tr "auth.name.not-all-space"))
(fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))])
form (fm/use-form
:spec ::team-form
:initial {}
:validators validators)
on-submit*
(mf/use-fn
(fn [form]
(let [name (dm/get-in @form [:clean-data :name])]
(st/emit! (ptk/data-event ::ev/event
{::ev/name "onboarding-step"
:label "team:choice-team-name"
:step 7}))
(on-submit name))))
on-skip
(mf/use-fn
(fn []
(st/emit! (du/update-profile-props {:onboarding-viewed true})
(ptk/data-event ::ev/event
{::ev/name "onboarding-step"
:label "team:skip-team-creation"
:step 7})
(ptk/data-event ::ev/event
{::ev/name "onboarding-finish"}))))]
[:*
[:div {:class (stl/css :modal-right)}
[:div {:class (stl/css :first-block)}
[:h2 {:class (stl/css :modal-subtitle)}
(tr "onboarding.team-modal.create-team")]
[:p {:class (stl/css :modal-text)}
(tr "onboarding.choice.team-up.create-team-desc")]
[:& fm/form {:form form
:class (stl/css :modal-form)
:on-submit on-submit*}
[:& fm/input {:type "text"
:class (stl/css :team-name-input)
:name :name
:placeholder "Team name"
:label (tr "onboarding.choice.team-up.create-team-placeholder")}]
[:div {:class (stl/css :action-buttons)}
[:> fm/submit-button*
{:class (stl/css :accept-button)
:label (tr "onboarding.choice.team-up.continue-creating-team")}]]]]
[:div {:class (stl/css :second-block)}
[:h2 {:class (stl/css :modal-subtitle)}
(tr "onboarding.choice.team-up.start-without-a-team")]
[:p {:class (stl/css :modal-text)}
(tr "onboarding.choice.team-up.start-without-a-team-description")]
[:div {:class (stl/css :action-buttons)}
[:button {:class (stl/css :accept-button)
:on-click on-skip}
(tr "onboarding.choice.team-up.continue-without-a-team")]]]]
[:div {:class (stl/css :paginator)} "1/2"]]))
(s/def ::name ::us/not-empty-string)
(s/def ::team-form
(s/keys :req-un [::name]))
(mf/defc onboarding-team-modal
{::mf/props :obj}
[]
(let [name* (mf/use-state nil)
name (deref name*)
on-submit
(mf/use-fn
(fn [tname]
(swap! name* (constantly tname))))
on-back
(mf/use-fn
(fn []
(swap! name* (constantly nil))))
onboarding-a-b-test?
(cf/external-feature-flag "signup-background" "test")]
[:div {:class (stl/css-case
:modal-overlay true
:onboarding-a-b-test onboarding-a-b-test?)}
[:div.animated.fadeIn {:class (stl/css :modal-container)}
[:& left-sidebar]
[:div {:class (stl/css :separator)}]
(if name
[:& team-form-step-2 {:name name :on-back on-back}]
[:& team-form-step-1 {:on-submit on-submit}])]]))