Skip to content

Commit 84c0754

Browse files
authored
Fix email validation not displaying in Collaborators modals (#4838)
* Fix email validation not displaying in Add Collaborators modal - Set action: :validate on the changeset in the validate handler so used_input? surfaces errors correctly for embedded schema fields - Add phx-debounce="300" to the email input to avoid errors appearing on every keystroke - Use a friendlier error message for invalid email format Relates to #4765 * Update changelog * Fix email validation display and layout shift in collaborator modals - Position validation error absolutely to prevent layout shift in both Add and Invite Collaborator modals - Fix Invite Collaborator not showing validation by adding missing changeset action and consistent error message - Add phx-debounce to email input in Invite Collaborator modal * tweak changelog * Fixes changelog * Adds test to check if invalid email message is visible for invite/add collaborator
1 parent 817e445 commit 84c0754

6 files changed

Lines changed: 219 additions & 105 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ and this project adheres to
3838

3939
### Fixed
4040

41+
- Fix email format validation not displaying in the Add Collaborators and Invite
42+
Collaborator modal. [#4765](https://github.com/OpenFn/lightning/issues/4765)
4143
- Fix a `workflows_pkey` duplicate-key crash when reconnecting to the
4244
collaborative editor after a save. Workflow resolution is now centralised in a
4345
single `Lightning.Collaboration.WorkflowResolver`, so the channel join and the

lib/lightning_web/live/project_live/invite_collaborator_component.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ defmodule LightningWeb.ProjectLive.InviteCollaboratorComponent do
3333
assign(socket,
3434
changeset:
3535
InvitedCollaborators.changeset(socket.assigns.collaborators, params)
36+
|> Map.put(:action, :validate)
3637
)
3738

3839
with :ok <- limit_adding_users(socket, params) do

lib/lightning_web/live/project_live/invite_collaborator_component.html.heex

Lines changed: 65 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -55,69 +55,74 @@
5555
field={f[:invited_collaborators]}
5656
prepend={[%InvitedCollaborators.InvitedCollaborator{}]}
5757
>
58-
<div class="flex flex-wrap space-x-4">
59-
<div class="flex-1">
60-
<.input
61-
type="text"
62-
field={cf[:first_name]}
63-
placeholder="First Name"
64-
required="true"
65-
class="w-full"
66-
/>
58+
<div class="relative">
59+
<div class="flex flex-wrap space-x-4">
60+
<div class="flex-1">
61+
<.input
62+
type="text"
63+
field={cf[:first_name]}
64+
placeholder="First Name"
65+
required="true"
66+
class="w-full"
67+
/>
68+
</div>
69+
<div class="flex-1">
70+
<.input
71+
type="text"
72+
field={cf[:last_name]}
73+
placeholder="Last Name"
74+
required="true"
75+
class="w-full"
76+
/>
77+
</div>
78+
<div class="flex-1">
79+
<.input
80+
type="email"
81+
field={cf[:email]}
82+
required="true"
83+
class="w-full"
84+
display_errors={false}
85+
phx-debounce="300"
86+
/>
87+
</div>
88+
<div>
89+
<.input
90+
type="select"
91+
prompt="Select Role"
92+
field={cf[:role]}
93+
options={
94+
Enum.map(
95+
["viewer", "editor", "admin"],
96+
&{String.capitalize(&1), &1}
97+
)
98+
}
99+
required="true"
100+
class="w-full"
101+
/>
102+
</div>
103+
<div class="flex items-center">
104+
<input
105+
type="hidden"
106+
name={"#{f.name}[collaborators_sort][]"}
107+
value={cf.index}
108+
/>
109+
<.button
110+
:if={Enum.count(f[:invited_collaborators].value) > 1}
111+
type="button"
112+
name={"#{f.name}[collaborators_drop][]"}
113+
value={cf.index}
114+
phx-click={JS.dispatch("change")}
115+
theme="secondary"
116+
class="inline-flex items-center"
117+
>
118+
<Heroicons.minus_circle class="w-5 h-5" />
119+
</.button>
120+
</div>
67121
</div>
68-
<div class="flex-1">
69-
<.input
70-
type="text"
71-
field={cf[:last_name]}
72-
placeholder="Last Name"
73-
required="true"
74-
class="w-full"
75-
/>
76-
</div>
77-
<div class="flex-1">
78-
<.input
79-
type="email"
80-
field={cf[:email]}
81-
required="true"
82-
class="w-full"
83-
display_errors={false}
84-
/>
85-
</div>
86-
<div>
87-
<.input
88-
type="select"
89-
prompt="Select Role"
90-
field={cf[:role]}
91-
options={
92-
Enum.map(
93-
["viewer", "editor", "admin"],
94-
&{String.capitalize(&1), &1}
95-
)
96-
}
97-
required="true"
98-
class="w-full"
99-
/>
100-
</div>
101-
<div class="flex items-center">
102-
<input
103-
type="hidden"
104-
name={"#{f.name}[collaborators_sort][]"}
105-
value={cf.index}
106-
/>
107-
<.button
108-
:if={Enum.count(f[:invited_collaborators].value) > 1}
109-
type="button"
110-
name={"#{f.name}[collaborators_drop][]"}
111-
value={cf.index}
112-
phx-click={JS.dispatch("change")}
113-
theme="secondary"
114-
class="inline-flex items-center"
115-
>
116-
<Heroicons.minus_circle class="w-5 h-5" />
117-
</.button>
122+
<div class="absolute top-full left-0">
123+
<.error_field field={cf[:email]} />
118124
</div>
119125
</div>
120-
<.error_field field={cf[:email]} />
121126
</.inputs_for>
122127
</div>
123128
<.modal_footer class="mx-6 mt-6">

lib/lightning_web/live/project_live/new_collaborator_component.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ defmodule LightningWeb.ProjectLive.NewCollaboratorComponent do
2424
def handle_event("validate", %{"project" => params}, socket) do
2525
socket =
2626
assign(socket,
27-
changeset: Collaborators.changeset(socket.assigns.collaborators, params)
27+
changeset:
28+
Collaborators.changeset(socket.assigns.collaborators, params)
29+
|> Map.put(:action, :validate)
2830
)
2931

3032
with :ok <- limit_adding_users(socket, params) do

lib/lightning_web/live/project_live/new_collaborator_component.html.heex

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -55,55 +55,60 @@
5555
field={f[:collaborators]}
5656
prepend={[%Collaborators.Collaborator{}]}
5757
>
58-
<div class="flex items-center space-x-4">
59-
<div class="flex-1">
60-
<.input
61-
type="text"
62-
field={cf[:email]}
63-
placeholder="email@example.com"
64-
required="true"
65-
class="w-full"
66-
display_errors={false}
67-
/>
58+
<div class="relative">
59+
<div class="flex items-center space-x-4">
60+
<div class="flex-1">
61+
<.input
62+
type="text"
63+
field={cf[:email]}
64+
placeholder="email@example.com"
65+
required="true"
66+
class="w-full"
67+
display_errors={false}
68+
phx-debounce="300"
69+
/>
70+
</div>
71+
<div>
72+
<.input
73+
type="select"
74+
prompt="Select Role"
75+
field={cf[:role]}
76+
options={
77+
Enum.map(
78+
["viewer", "editor", "admin"],
79+
&{String.capitalize(&1), &1}
80+
)
81+
}
82+
required="true"
83+
class="w-full"
84+
/>
85+
</div>
86+
<div>
87+
<input
88+
type="hidden"
89+
name={"#{f.name}[collaborators_sort][]"}
90+
value={cf.index}
91+
/>
92+
<.button
93+
:if={Enum.count(f[:collaborators].value) > 1}
94+
type="button"
95+
name={"#{f.name}[collaborators_drop][]"}
96+
value={cf.index}
97+
phx-click={JS.dispatch("change")}
98+
theme="secondary"
99+
class="inline-flex items-center"
100+
>
101+
<.icon name="hero-minus-circle" class="w-5 h-5" />
102+
</.button>
103+
</div>
68104
</div>
69-
<div>
70-
<.input
71-
type="select"
72-
prompt="Select Role"
73-
field={cf[:role]}
74-
options={
75-
Enum.map(
76-
["viewer", "editor", "admin"],
77-
&{String.capitalize(&1), &1}
78-
)
79-
}
80-
required="true"
81-
class="w-full"
82-
/>
83-
</div>
84-
<div>
85-
<input
86-
type="hidden"
87-
name={"#{f.name}[collaborators_sort][]"}
88-
value={cf.index}
89-
/>
90-
<.button
91-
:if={Enum.count(f[:collaborators].value) > 1}
92-
type="button"
93-
name={"#{f.name}[collaborators_drop][]"}
94-
value={cf.index}
95-
phx-click={JS.dispatch("change")}
96-
theme="secondary"
97-
class="inline-flex items-center"
98-
>
99-
<.icon name="hero-minus-circle" class="w-5 h-5" />
100-
</.button>
105+
<div class="absolute top-full left-0">
106+
<.error_field field={cf[:email]} />
101107
</div>
102108
</div>
103-
<.error_field field={cf[:email]} />
104109
</.inputs_for>
105110

106-
<div class="mt-5 flex justify-start">
111+
<div class="mt-6 flex justify-start">
107112
<input type="hidden" name={"#{f.name}[collaborators_drop][]"} />
108113
<.button
109114
type="button"

test/lightning_web/live/project_live_test.exs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3477,6 +3477,105 @@ defmodule LightningWeb.ProjectLiveTest do
34773477
end
34783478
end
34793479

3480+
test "validate event with invalid email shows error in modal", %{
3481+
conn: conn
3482+
} do
3483+
project = insert(:project)
3484+
{conn, _user} = setup_project_user(conn, project, :owner)
3485+
3486+
{:ok, view, _html} =
3487+
live(conn, ~p"/projects/#{project.id}/settings#collaboration")
3488+
3489+
view |> element("#show_collaborators_modal_button") |> render_click()
3490+
3491+
modal = element(view, "#add_collaborators_modal")
3492+
3493+
view
3494+
|> form("#add_collaborators_modal_form",
3495+
project: %{
3496+
"collaborators" => %{
3497+
"0" => %{"email" => "not-an-email", "role" => "editor"}
3498+
}
3499+
}
3500+
)
3501+
|> render_change()
3502+
3503+
assert render(modal) =~ "Email address not valid."
3504+
3505+
view
3506+
|> form("#add_collaborators_modal_form",
3507+
project: %{
3508+
"collaborators" => %{
3509+
"0" => %{"email" => "valid@example.com", "role" => "editor"}
3510+
}
3511+
}
3512+
)
3513+
|> render_change()
3514+
3515+
refute render(modal) =~ "Email address not valid."
3516+
end
3517+
3518+
test "validate event with invalid email shows error in invite modal", %{
3519+
conn: conn
3520+
} do
3521+
project = insert(:project, name: "my-project")
3522+
{conn, _user} = setup_project_user(conn, project, :owner)
3523+
3524+
{:ok, view, _html} =
3525+
live(conn, ~p"/projects/#{project.id}/settings#collaboration")
3526+
3527+
view |> element("#show_collaborators_modal_button") |> render_click()
3528+
3529+
# Submit with a non-existent email to open the invite form
3530+
view
3531+
|> form("#add_collaborators_modal_form",
3532+
project: %{
3533+
"collaborators" => %{
3534+
"0" => %{"email" => "newuser@example.com", "role" => "editor"}
3535+
}
3536+
}
3537+
)
3538+
|> render_submit()
3539+
3540+
assert has_element?(view, "#invite_collaborators_modal_form")
3541+
3542+
invite_modal = element(view, "#invite_collaborators_modal")
3543+
3544+
view
3545+
|> form("#invite_collaborators_modal_form",
3546+
project: %{
3547+
"invited_collaborators" => %{
3548+
"0" => %{
3549+
"email" => "not-an-email",
3550+
"role" => "editor",
3551+
"first_name" => "Test",
3552+
"last_name" => "User"
3553+
}
3554+
}
3555+
}
3556+
)
3557+
|> render_change()
3558+
3559+
assert render(invite_modal) =~ "Email address not valid."
3560+
3561+
view
3562+
|> form("#invite_collaborators_modal_form",
3563+
project: %{
3564+
"invited_collaborators" => %{
3565+
"0" => %{
3566+
"email" => "valid@example.com",
3567+
"role" => "editor",
3568+
"first_name" => "Test",
3569+
"last_name" => "User"
3570+
}
3571+
}
3572+
}
3573+
)
3574+
|> render_change()
3575+
3576+
refute render(invite_modal) =~ "Email address not valid."
3577+
end
3578+
34803579
test "adding a non existent user triggers the invite users process", %{
34813580
conn: conn
34823581
} do

0 commit comments

Comments
 (0)