♻️ Refurbish create team slide

This commit is contained in:
Luis de Dios 2026-06-11 16:32:58 +02:00
parent 6eae360ddf
commit efeff85646
6 changed files with 405 additions and 314 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -5,13 +5,18 @@
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.main.ui.forms
(:require-macros [app.main.style :as stl])
(:require
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(def context (mf/create-context nil))
@ -49,8 +54,123 @@
[:> input* props]))
(defn- conj-dedup
"Adds item into a vector and removes possible duplicates."
[coll item]
(into [] (distinct) (conj coll item)))
(mf/defc form-multi-input*
[{:keys [name trim valid-item-fn caution-item-fn] :rest props}]
(let [form (mf/use-ctx context)
touched? (and (contains? (:data @form) name)
(get-in @form [:touched name]))
value (mf/use-state "")
focus? (mf/use-state false)
items
(mf/use-state
(fn []
(let [initial (get-in @form [:data name])]
(if (or (vector? initial) (set? initial))
(mapv (fn [val]
{:text val
:valid (valid-item-fn val)
:caution (caution-item-fn val)})
initial)
[]))))
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))))
on-key-down
(mf/use-fn
(mf/deps @value form name valid-item-fn caution-item-fn trim)
(fn [event]
(let [val (cond-> @value trim str/trim)]
(cond
(or (k/enter? event) (k/comma? event) (k/space? event))
(do
(dom/prevent-default event)
(dom/stop-propagation event)
(swap! form assoc-in [:touched name] true)
(when (and (valid-item-fn val) (not (str/empty? @value)))
(reset! value "")
(swap! form assoc-in [:touched name] false)
(doseq [v (str/split val #",|\s+")]
(let [v (str/trim v)]
(swap! items conj-dedup {:text v
:valid (valid-item-fn v)
:caution (caution-item-fn v)})))))
(and (k/backspace? event) (str/empty? @value))
(do
(dom/prevent-default event)
(dom/stop-propagation event)
(swap! items (fn [items]
(if (empty? items) items (pop items)))))))))
on-blur
(mf/use-fn
(fn []
(reset! focus? false)
(when-not (get-in @form [:touched name])
(swap! form assoc-in [:touched name] true))))
on-remove-item
(mf/use-fn
(fn [item]
(swap! items #(filterv (fn [x] (not= x item)) %))))
props
(mf/spread-props props {:value @value
:on-change on-change
:on-focus on-focus
:on-blur on-blur
:on-key-down on-key-down
:hint-type (when (and touched?
(not (str/empty? @value))
(not (valid-item-fn @value))) "error")})]
;; Sync form data whenever items or input value changes.
;; This ensures the current (unconfirmed) input value is included in the
;; form data when the user submits without pressing Enter/Space/Comma.
(mf/with-effect [@items @value]
(let [items-text (mapv :text @items)
val (cond-> @value trim str/trim)
combined (if (and (valid-item-fn val) (not (str/empty? val)))
(conj items-text val)
items-text)
data (str/join " " combined)]
(fm/update-input-value! form name data)))
[:div {:class (stl/css :multi-input)}
[:> input* props]
(when-let [items-seq (seq @items)]
[:div {:class (stl/css :multi-input-chips)}
(for [item items-seq]
[:div {:class (stl/css :multi-input-chip)
:key (:text item)}
[:span {:class (stl/css :multi-input-chip-text)}
(:text item)]
[:> icon-button* {:variant "ghost"
:class (stl/css :multi-input-chip-icon)
:icon i/close
:icon-size "s"
:aria-label (tr "labels.remove")
:on-click (partial on-remove-item item)}]])])]))
(mf/defc form-select*
[{:keys [name] :as props}]
[{:keys [name] :rest props}]
(let [select-name name
form (mf/use-ctx context)
value (get-in @form [:data select-name] "")

View File

@ -0,0 +1,43 @@
// 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) KALEIDOS INC Sucursal en España SL
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.multi-input {
display: flex;
flex-direction: column;
gap: var(--sp-s);
}
.multi-input-chips {
display: flex;
flex-wrap: wrap;
gap: var(--sp-xs);
}
.multi-input-chip {
display: flex;
align-items: center;
gap: var(--sp-xs);
block-size: $sz-24;
padding-inline-start: var(--sp-s);
border-radius: $br-6;
background-color: var(--color-background-tertiary);
}
.multi-input-chip-text {
@include t.use-typography("body-small");
color: var(--color-foreground-primary);
}
.multi-input-chip-icon {
inline-size: $sz-24;
block-size: $sz-24;
}

View File

@ -14,47 +14,60 @@
[app.main.data.profile :as du]
[app.main.data.team :as dtm]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.notifications.context-notification :refer [context-notification]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as fc]
[app.util.forms :as fm]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc left-sidebar*
(mf/defc team-info-feature*
{::mf/private true}
[{:keys [icon-id text]}]
[:li {:class (stl/css :modal-info-item)}
[:div {:class (stl/css :modal-info-icon)}
[:> icon* {:icon-id icon-id :size "m"}]]
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-light)}
text]])
(mf/defc team-info*
{::mf/private true}
[]
[:div {:class (stl/css :modal-left)}
[:h2 {:class (stl/css :modal-subtitle)}
[:div {:class (stl/css :modal-info)}
[:img {:src "images/form/slide-final-team.svg"}]
[:> heading* {:level 2
:typography t/title-medium
:class (stl/css :color-light)}
(tr "onboarding.team-modal.team-definition")]
[:p {:class (stl/css :modal-text)}
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-dimmed :margin-bottom)}
(tr "onboarding.team-modal.create-team-desc")]
[:ul {:class (stl/css :team-features)}
[:li {:class (stl/css :feature)}
[:span {:class (stl/css :icon)} deprecated-icon/document]
[:p {:class (stl/css :modal-desc)}
(tr "onboarding.team-modal.create-team-feature-1")]]
[:li {:class (stl/css :feature)}
[:span {:class (stl/css :icon)} deprecated-icon/move]
[:p {:class (stl/css :modal-desc)}
(tr "onboarding.team-modal.create-team-feature-2")]]
[:li {:class (stl/css :feature)}
[:span {:class (stl/css :icon)} deprecated-icon/tree]
[:p {:class (stl/css :modal-desc)}
(tr "onboarding.team-modal.create-team-feature-3")]]
[:li {:class (stl/css :feature)}
[:span {:class (stl/css :icon)} deprecated-icon/user]
[:p {:class (stl/css :modal-desc)}
(tr "onboarding.team-modal.create-team-feature-4")]]
[:li {:class (stl/css :feature)}
[:span {:class (stl/css :icon)} deprecated-icon/tick]
[:p {:class (stl/css :modal-desc)}
(tr "onboarding.team-modal.create-team-feature-5")]]]])
[:ul {:class (stl/css :modal-info-features)}
[:> team-info-feature* {:icon-id i/document
:text (tr "onboarding.team-modal.create-team-feature-1")}]
[:> team-info-feature* {:icon-id i/move
:text (tr "onboarding.team-modal.create-team-feature-2")}]
[:> team-info-feature* {:icon-id i/tree
:text (tr "onboarding.team-modal.create-team-feature-3")}]
[:> team-info-feature* {:icon-id i/user
:text (tr "onboarding.team-modal.create-team-feature-4")}]
[:> team-info-feature* {:icon-id i/tick
:text (tr "onboarding.team-modal.create-team-feature-5")}]]])
(defn- get-available-roles
[]
[{:value "viewer" :label (tr "labels.viewer")}
{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
[{:id "viewer" :value "viewer" :label (tr "labels.viewer")}
{:id "editor" :value "editor" :label (tr "labels.editor")}
{:id "admin" :value "admin" :label (tr "labels.admin")}])
(def ^:private schema:team-form
[:map {:title "TeamForm"}
@ -68,8 +81,8 @@
(let [initial (mf/with-memo []
{:role "editor"})
form (fm/use-form :schema schema:team-form
:initial initial)
form (fm/use-form :schema schema:team-form
:initial initial)
roles (mf/use-memo get-available-roles)
@ -109,7 +122,7 @@
:else
(swap! error* (tr "errors.generic"))))))
on-invite-later
on-create-without-invitations
(mf/use-fn
(fn [{:keys [name]}]
(let [mdata {:on-success on-success
@ -117,126 +130,125 @@
params {:name name}]
(st/emit! (-> (dtm/create-team (with-meta params mdata))
(with-meta {::ev/origin :onboarding-without-invitations}))
(ev/event
{::ev/name "onboarding-step"
:label "team:create-team-and-invite-later"
:team-name name
:step 8})
(ev/event
{::ev/name "onboarding-finish"})))))
(ev/event {::ev/name "onboarding-step"
:label "team:create-team-and-invite-later"
:team-name name
:step 8})
(ev/event {::ev/name "onboarding-finish"})))))
on-invite-now
on-create-with-invitations
(mf/use-fn
(fn [{:keys [name emails] :as params}]
(let [mdata {:on-success on-success
:on-error on-error}]
(st/emit! (-> (dtm/create-team-with-invitations (with-meta params mdata))
(with-meta {::ev/origin :onboarding-with-invitations}))
(ev/event
{::ev/name "onboarding-step"
:label "team:create-team-and-invite"
:invites (count emails)
:team-name name
:role (:role params)
:step 8})
(ev/event
{::ev/name "onboarding-finish"})))))
(ev/event {::ev/name "onboarding-step"
:label "team:create-team-and-invite"
:invites (count emails)
:team-name name
:role (:role params)
:step 8})
(ev/event {::ev/name "onboarding-finish"})))))
on-submit*
on-submit
(mf/use-fn
(fn [form]
(let [params (:clean-data @form)
emails (:emails params)]
(if (> (count emails) 0)
(on-invite-now params)
(on-invite-later params)))))
(on-create-with-invitations params)
(on-create-without-invitations params)))))
on-skip
(mf/use-fn
(fn []
(st/emit! (du/update-profile-props {:onboarding-viewed true})
(ev/event
{::ev/name "onboarding-step"
:label "team:skip-team-creation"
:step 7})
(ev/event
{::ev/name "onboarding-finish"}))))]
[:*
[:div {:class (stl/css :modal-right)}
[:div {:class (stl/css :first-block)}
[:& fm/form {:form form
:class (stl/css :modal-form)
:on-submit on-submit*}
[: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")]
(ev/event {::ev/name "onboarding-step"
:label "team:skip-team-creation"
:step 7})
(ev/event {::ev/name "onboarding-finish"}))))]
[:div {:class (stl/css :modal-team)}
[:> fc/form* {:form form
:class (stl/css :modal-team-form)
:on-submit on-submit}
[:& fm/input {:type "text"
:class (stl/css :team-name-input)
:name :name
:auto-focus? true
:placeholder "Team name"
:label (tr "onboarding.choice.team-up.create-team-placeholder")}]
[:div {:class (stl/css :modal-team-block)}
[:> heading* {:level 2
:typography t/title-medium
:class (stl/css :color-light)}
(tr "onboarding.team-modal.create-team")]
[:h2 {:class (stl/css :modal-subtitle :invite-subtitle)} (tr "onboarding.choice.team-up.invite-members")]
[:p {:class (stl/css :modal-text)} (tr "onboarding.choice.team-up.invite-members-info")]
[:> fc/form-input* {:type "text"
:name :name
:auto-focus true
:auto-complete "off"
:placeholder (tr "onboarding.choice.team-up.create-team-placeholder")}]
(when-let [content (deref error*)]
[:& context-notification {:content content :level :error}])
[:> text* {:as "div"
:typography t/body-small
:class (stl/css :color-dimmed)}
(tr "onboarding.choice.team-up.create-team-desc")]]
[:div {:class (stl/css :role-select)}
[:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")]
[:& fm/select {:name :role :options roles}]]
[:div {:class (stl/css :modal-team-block)}
[:> heading* {:level 2
:typography t/title-medium
:class (stl/css :color-light)}
(tr "onboarding.choice.team-up.invite-members")]
[:div {:class (stl/css :invitation-row)}
[:& fm/multi-input {:type "email"
:name :emails
:trim true
:valid-item-fn sm/parse-email
:caution-item-fn #{}
:label (tr "modals.invite-member.emails")}]]
(when-let [content (deref error*)]
[:> context-notification* {:level :error}
content])
[:div {:class (stl/css :modal-team-sub-block)}
[:> fc/form-select* {:name :role
:options roles}]
[:> fc/form-multi-input* {:type "email"
:name :emails
:trim true
:valid-item-fn sm/parse-email
:caution-item-fn #{}
:auto-complete "off"
:placeholder (tr "modals.invite-member.emails")}]]
[:> text* {:as "div"
:typography t/body-small
:class (stl/css :color-dimmed)}
(tr "onboarding.choice.team-up.invite-members-info")]
(let [params (:clean-data @form)
emails (:emails params)]
[:*
[:div {:class (stl/css :action-buttons)}
(let [params (:clean-data @form)
emails (:emails params)
num-emails (count emails)]
[:*
[:div {:class (stl/css :flex-align-right)}
[:> fc/form-submit* {:variant "primary"}
(if (> num-emails 0)
(tr "onboarding.choice.team-up.create-team-and-invite")
(tr "onboarding.choice.team-up.create-team-without-invite"))]]
[:> fm/submit-button*
{:class (stl/css :accept-button)
:label (if (> (count emails) 0)
(tr "onboarding.choice.team-up.create-team-and-invite")
(tr "onboarding.choice.team-up.create-team-without-invite"))}]]
(when (= num-emails 0)
[:> text* {:as "div"
:typography t/body-small
:class (stl/css :color-dimmed :text-align-right)}
"(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"])])]]
(when (= (count emails) 0)
[:> :div {:class (stl/css :modal-hint)}
"(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"])])]]
[: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")]]]]]))
[:> text* {:as "div"
:typography t/headline-small
:class (stl/css :link)
:on-click on-skip
:tab-index "0"}
(tr "onboarding.choice.team-up.continue-without-a-team")]]))
(mf/defc onboarding-team-modal*
[{:keys [go-to-team]}]
[:div {:class (stl/css-case
:modal-overlay true)}
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated.fade-in {:class (stl/css :modal-container)}
[:h1 {:class (stl/css :modal-title)}
[:> heading* {:level 1
:typography t/title-large
:class (stl/css :color-light)}
(tr "onboarding-v2.welcome.title")]
[:div {:class (stl/css :modal-sections)}
[:> left-sidebar*]
[:div {:class (stl/css :separator)}]
[:> team-info*]
[:div {:class (stl/css :modal-separator)}
[:div {:class (stl/css :modal-separator-line)}]]
[:> team-form* {:go-to-team go-to-team}]]]])

View File

@ -5,216 +5,131 @@
// Copyright (c) KALEIDOS INC Sucursal en España SL
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.color-light {
color: var(--color-foreground-primary);
}
.color-dimmed {
color: var(--color-foreground-secondary);
}
.text-align-right {
text-align: right;
}
.flex-align-right {
display: flex;
justify-content: flex-end;
}
.margin-bottom {
margin-bottom: var(--sp-xxl);
}
.link {
cursor: pointer;
text-decoration: underline;
text-align: center;
color: var(--color-foreground-secondary);
&:hover {
color: var(--color-foreground-primary);
}
}
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
position: relative;
width: deprecated.$s-908;
max-height: deprecated.$s-800;
height: 100%;
padding-inline: deprecated.$s-100;
padding-block: deprecated.$s-40 deprecated.$s-40;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
display: flex;
flex-direction: column;
gap: deprecated.$s-24;
gap: var(--sp-xxl);
position: relative;
inline-size: px2rem(908);
padding: $sz-64 px2rem(100);
border-radius: $br-8;
background-color: var(--color-background-primary);
border: $b-2 solid var(--color-background-quaternary);
}
.modal-sections {
display: grid;
grid-template-columns: 1fr deprecated.$s-32 1fr;
gap: deprecated.$s-24;
height: 100%;
overflow: hidden;
grid-template-columns: px2rem(310) $sz-32 1fr;
gap: var(--sp-xxl);
block-size: 100%;
}
.paginator {
@include deprecated.body-small-typography;
position: absolute;
top: deprecated.$s-40;
right: deprecated.$s-100;
padding: deprecated.$s-4;
border-radius: deprecated.$br-6;
color: var(--color-foreground-secondary);
.modal-info {
display: flex;
flex-direction: column;
gap: var(--sp-xl);
border: $b-1 solid var(--color-foreground-secondary);
padding: var(--sp-m);
border-radius: $br-12;
background-color: var(--color-background-quaternary);
}
// MODAL LEFT
.modal-left {
.modal-info-features {
display: flex;
flex-direction: column;
gap: var(--sp-m);
}
.modal-info-item {
display: flex;
align-items: center;
gap: var(--sp-l);
}
.modal-info-icon {
display: flex;
align-items: center;
justify-content: center;
inline-size: $sz-32;
block-size: $sz-32;
border-radius: $br-circle;
border: $b-1 solid var(--color-accent-primary);
color: var(--color-accent-primary);
}
.modal-separator {
display: flex;
justify-content: center;
}
.modal-separator-line {
inline-size: px2rem(8);
block-size: 100%;
border-radius: $br-8;
opacity: 0.5;
background-color: var(--color-background-quaternary);
}
.modal-team {
display: flex;
flex-direction: column;
gap: var(--sp-xl);
}
.modal-team-form {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: deprecated.$s-32 auto auto 1fr;
gap: deprecated.$s-16;
overflow: auto;
gap: var(--sp-xxl);
}
.modal-title {
@include deprecated.big-title-typography;
color: var(--modal-title-foreground-color);
.modal-team-block {
display: flex;
flex-direction: column;
gap: var(--sp-m);
}
.modal-subtitle {
@include deprecated.med-title-typography;
color: var(--modal-title-foreground-color);
}
.invite-subtitle {
padding-top: deprecated.$s-16;
}
.modal-text {
@include deprecated.body-large-typography;
color: var(--modal-text-foreground-color);
margin: 0;
}
.modal-desc {
@include deprecated.small-title-typography;
margin: 0;
color: var(--modal-title-foreground-color);
}
.team-features {
@include deprecated.flex-column;
gap: deprecated.$s-16;
margin: 0;
}
.feature {
@include deprecated.flex-row;
gap: deprecated.$s-16;
}
.icon {
@include deprecated.flex-center;
height: deprecated.$s-32;
width: deprecated.$s-32;
border-radius: deprecated.$br-circle;
border: deprecated.$s-1 solid var(--color-accent-primary);
svg {
@extend %button-icon;
stroke: var(--color-accent-primary);
}
}
.action-buttons {
@extend %modal-action-btns;
justify-content: flex-end;
}
.accept-button {
@extend %modal-accept-btn;
}
.back-button {
@extend %modal-cancel-btn;
}
// SEPARATOR
.separator {
width: deprecated.$s-8;
height: 100%;
border-radius: deprecated.$br-8;
opacity: 0.42;
background-color: var(--modal-separator-background-color);
}
// MODAL RIGHT TEAM
.modal-right {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
gap: deprecated.$s-24;
overflow: hidden;
}
.first-block {
overflow: auto;
flex-grow: 1;
}
.first-block,
.second-block {
@include deprecated.flex-column;
gap: deprecated.$s-16;
}
.modal-form {
display: grid;
grid-template-columns: 1fr;
gap: deprecated.$s-16;
}
.team-name-input {
@extend %input-element-label;
label {
@include deprecated.flex-column;
@include deprecated.body-small-typography;
align-items: flex-start;
width: 100%;
border: none;
background-color: transparent;
height: 100%;
input {
@include deprecated.body-small-typography;
margin-top: deprecated.$s-8;
}
}
}
// MODAL RIGHT INVITATIONS
.modal-right-invitations {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr;
gap: deprecated.$s-16;
max-height: deprecated.$s-512;
}
.modal-form-invitations {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto auto;
margin-block-end: deprecated.$s-72;
gap: deprecated.$s-8;
}
.role-title {
@include deprecated.uppercase-title-typography;
margin-block-end: deprecated.$s-8;
color: var(--modal-title-foreground-color);
}
.invitation-row {
margin: 0;
color: var(--modal-title-foreground-color);
}
.modal-hint {
@include deprecated.body-small-typography;
color: var(--modal-text-foreground-color);
text-align: right;
.modal-team-sub-block {
display: flex;
flex-direction: column;
gap: var(--sp-s);
}

View File

@ -4750,7 +4750,7 @@ msgstr "Invite members"
#: src/app/main/ui/onboarding/team_choice.cljs:187
msgid "onboarding.choice.team-up.invite-members-info"
msgstr ""
"Remember to include everyone. Developers, designers, managers... diversity "
"Remember to include everyone. Developers, designers, managers... Diversity "
"adds up :)"
#: src/app/main/ui/dashboard/team.cljs:258, src/app/main/ui/onboarding/team_choice.cljs:193