mirror of
https://github.com/plausible/analytics.git
synced 2024-09-11 18:07:33 +03:00
Funnel Settings UI tweaks (to match Custom Props/Goals) (#3323)
* Add hint to creatable ComboBoxes without suggestions available * Load external resources once in funnel settings * Load external resources once in goal settings * Make Custom Props Settings UI match Goal Settings * Remove unnecessary goals query This should be done only once in the live view * Remove funnels feature flag * fixup * Make the modal scrollable * By default, focus first suggestion for creatables * Update StaticSearch So it's capable of casting custom data structures into weighted items. Missing tests added. * Add Search + modal to funnel settings * Add sample props to seeds * Load all suggestions asynchronously, unless `Mix.env == :test` * ComboBox: Fix inconsistent suggestions We require "Create ..." element to be only focused when there are no suggestions available. This causes some issues, depending on the state, the least focusable index might be either 0 ("Create...") or 1. This patch addresses all the quirks with focus. * Fix ComboBox max results message So that AlpineJS doesn't think it's a focusable option. * Keep the state up to date when changing props * Add hint to creatable ComboBoxes without suggestions available * Load external resources once in funnel settings * Load external resources once in goal settings * Make Custom Props Settings UI match Goal Settings * Remove unnecessary goals query This should be done only once in the live view * Remove funnels feature flag * fixup * Make the modal scrollable * By default, focus first suggestion for creatables * Add sample props to seeds * Load all suggestions asynchronously, unless `Mix.env == :test` * ComboBox: Fix inconsistent suggestions We require "Create ..." element to be only focused when there are no suggestions available. This causes some issues, depending on the state, the least focusable index might be either 0 ("Create...") or 1. This patch addresses all the quirks with focus. * Fix ComboBox max results message So that AlpineJS doesn't think it's a focusable option. * Keep the state up to date when changing props * Fixup site_id * Fix typo * fixup
This commit is contained in:
parent
0822bc61df
commit
43be271836
@ -19,10 +19,10 @@ defmodule PlausibleWeb.Live.Components.ComboBox.StaticSearch do
|
||||
choices
|
||||
|> Enum.map(fn
|
||||
{_, value} = choice ->
|
||||
{choice, weight(value, input)}
|
||||
{choice, weight(value, input, opts)}
|
||||
|
||||
value ->
|
||||
{value, weight(value, input)}
|
||||
{value, weight(value, input, opts)}
|
||||
end)
|
||||
|> Enum.reject(fn {_choice, weight} -> weight < weight_threshold end)
|
||||
|> Enum.sort_by(fn {_choice, weight} -> weight end, :desc)
|
||||
@ -32,8 +32,9 @@ defmodule PlausibleWeb.Live.Components.ComboBox.StaticSearch do
|
||||
end
|
||||
end
|
||||
|
||||
defp weight(value, input) do
|
||||
value = to_string(value)
|
||||
defp weight(value, input, opts) do
|
||||
to_str = Keyword.get(opts, :to_string, &to_string/1)
|
||||
value = to_str.(value)
|
||||
|
||||
case {value, input} do
|
||||
{value, input} when value == input ->
|
||||
|
@ -19,7 +19,7 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
|> assign_new(:site, fn ->
|
||||
Sites.get_for_user!(user_id, domain, [:owner, :admin, :super_admin])
|
||||
end)
|
||||
|> assign_new(:funnels, fn %{site: site} ->
|
||||
|> assign_new(:all_funnels, fn %{site: %{id: ^site_id} = site} ->
|
||||
Funnels.list(site)
|
||||
end)
|
||||
|> assign_new(:goal_count, fn %{site: site} ->
|
||||
@ -28,9 +28,10 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
site_id: site_id,
|
||||
domain: domain,
|
||||
displayed_funnels: socket.assigns.all_funnels,
|
||||
add_funnel?: false,
|
||||
filter_text: "",
|
||||
current_user_id: user_id
|
||||
)}
|
||||
end
|
||||
@ -56,9 +57,9 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
<.live_component
|
||||
module={PlausibleWeb.Live.FunnelSettings.List}
|
||||
id="funnels-list"
|
||||
funnels={@funnels}
|
||||
funnels={@displayed_funnels}
|
||||
filter_text={@filter_text}
|
||||
/>
|
||||
<button type="button" class="button mt-6" phx-click="add-funnel">+ Add Funnel</button>
|
||||
</div>
|
||||
|
||||
<div :if={@goal_count < Funnel.min_steps()}>
|
||||
@ -75,12 +76,23 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_event("add-funnel", _value, socket) do
|
||||
{:noreply, assign(socket, add_funnel?: true)}
|
||||
def handle_event("reset-filter-text", _params, socket) do
|
||||
{:noreply, assign(socket, filter_text: "", displayed_funnels: socket.assigns.all_funnels)}
|
||||
end
|
||||
|
||||
def handle_event("cancel-add-funnel", _value, socket) do
|
||||
{:noreply, assign(socket, add_funnel?: false)}
|
||||
def handle_event("filter", %{"filter-text" => filter_text}, socket) do
|
||||
new_list =
|
||||
PlausibleWeb.Live.Components.ComboBox.StaticSearch.suggest(
|
||||
filter_text,
|
||||
socket.assigns.all_funnels,
|
||||
to_string: & &1.name
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, displayed_funnels: new_list, filter_text: filter_text)}
|
||||
end
|
||||
|
||||
def handle_event("add-funnel", _value, socket) do
|
||||
{:noreply, assign(socket, add_funnel?: true)}
|
||||
end
|
||||
|
||||
def handle_event("delete-funnel", %{"funnel-id" => id}, socket) do
|
||||
@ -91,13 +103,28 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
:ok = Funnels.delete(site, id)
|
||||
socket = put_flash(socket, :success, "Funnel deleted successfully")
|
||||
Process.send_after(self(), :clear_flash, 5000)
|
||||
{:noreply, assign(socket, funnels: Enum.reject(socket.assigns.funnels, &(&1.id == id)))}
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
all_funnels: Enum.reject(socket.assigns.all_funnels, &(&1.id == id)),
|
||||
displayed_funnels: Enum.reject(socket.assigns.displayed_funnels, &(&1.id == id))
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info({:funnel_saved, funnel}, socket) do
|
||||
socket = put_flash(socket, :success, "Funnel saved successfully")
|
||||
Process.send_after(self(), :clear_flash, 5000)
|
||||
{:noreply, assign(socket, add_funnel?: false, funnels: [funnel | socket.assigns.funnels])}
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
add_funnel?: false,
|
||||
all_funnels: [funnel | socket.assigns.all_funnels],
|
||||
displayed_funnels: [funnel | socket.assigns.displayed_funnels]
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info(:cancel_add_funnel, socket) do
|
||||
{:noreply, assign(socket, add_funnel?: false)}
|
||||
end
|
||||
|
||||
def handle_info(:clear_flash, socket) do
|
||||
|
@ -6,9 +6,9 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
"""
|
||||
|
||||
use Phoenix.LiveView
|
||||
use Phoenix.HTML
|
||||
use Plausible.Funnel
|
||||
|
||||
import PlausibleWeb.Live.Components.Form
|
||||
alias Plausible.{Sites, Goals}
|
||||
|
||||
def mount(_params, %{"current_user_id" => user_id, "domain" => domain}, socket) do
|
||||
@ -39,119 +39,109 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id="funnel-form" class="grid grid-cols-2 gap-6 mt-6">
|
||||
<div class="col-span-2 sm:col-span-2">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
phx-target="#funnel-form"
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
>
|
||||
<label
|
||||
for={f[:name].name}
|
||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
<div
|
||||
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity z-50"
|
||||
phx-window-keydown="cancel-add-funnel"
|
||||
phx-key="Escape"
|
||||
>
|
||||
</div>
|
||||
<div class="fixed inset-0 flex items-center justify-center mt-16 z-50 overlofw-y-auto overflow-x-hidden">
|
||||
<div class="w-2/5 h-full">
|
||||
<div id="funnel-form">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
phx-target="#funnel-form"
|
||||
phx-click-away="cancel-add-funnel"
|
||||
onkeydown="return event.key != 'Enter';"
|
||||
class="bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"
|
||||
>
|
||||
Funnel Name
|
||||
</label>
|
||||
<.input field={f[:name]} />
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-6">Add Funnel</h2>
|
||||
|
||||
<div id="steps-builder">
|
||||
<label class="mt-6 block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Funnel Steps
|
||||
<label for={f[:name].name} class="block mb-3 font-medium dark:text-gray-100">
|
||||
Funnel Name
|
||||
</label>
|
||||
|
||||
<div :for={step_idx <- @step_ids} class="flex mb-3">
|
||||
<div class="w-2/5 flex-1">
|
||||
<.live_component
|
||||
submit_name="funnel[steps][][goal_id]"
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={&PlausibleWeb.Live.Components.ComboBox.StaticSearch.suggest/2}
|
||||
id={"step-#{step_idx}"}
|
||||
options={reject_alrady_selected("step-#{step_idx}", @goals, @selections_made)}
|
||||
/>
|
||||
<.input
|
||||
field={f[:name]}
|
||||
phx-debounce={200}
|
||||
autocomplete="off"
|
||||
placeholder="e.g. From Blog to Purchase"
|
||||
autofocus
|
||||
class="w-full focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-7/12 rounded-md sm:text-sm border-gray-300 dark:border-gray-500 w-full p-2 mt-2"
|
||||
/>
|
||||
|
||||
<div id="steps-builder" class="mt-6">
|
||||
<label class="font-medium dark:text-gray-100">
|
||||
Funnel Steps
|
||||
</label>
|
||||
|
||||
<div :for={step_idx <- @step_ids} class="flex mb-3 mt-3">
|
||||
<div class="w-2/5 flex-1">
|
||||
<.live_component
|
||||
submit_name="funnel[steps][][goal_id]"
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={&PlausibleWeb.Live.Components.ComboBox.StaticSearch.suggest/2}
|
||||
id={"step-#{step_idx}"}
|
||||
options={reject_already_selected("step-#{step_idx}", @goals, @selections_made)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-min inline-flex items-center align-middle">
|
||||
<.remove_step_button
|
||||
:if={length(@step_ids) > Funnel.min_steps()}
|
||||
step_idx={step_idx}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-4/12 align-middle ml-4 text-gray-500 dark:text-gray-400">
|
||||
<.evaluation
|
||||
:if={@evaluation_result}
|
||||
result={@evaluation_result}
|
||||
at={Enum.find_index(@step_ids, &(&1 == step_idx))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-min inline-flex items-center align-middle">
|
||||
<.remove_step_button :if={length(@step_ids) > Funnel.min_steps()} step_idx={step_idx} />
|
||||
<.add_step_button :if={
|
||||
length(@step_ids) < Funnel.max_steps() and
|
||||
map_size(@selections_made) < length(@goals)
|
||||
} />
|
||||
|
||||
<div class="mt-6">
|
||||
<p id="funnel-eval" class="text-gray-500 dark:text-gray-400 text-sm mt-2 mb-2">
|
||||
<%= if @evaluation_result do %>
|
||||
Last month conversion rate: <strong><%= List.last(@evaluation_result.steps).conversion_rate %></strong>%
|
||||
<% else %>
|
||||
<span class="text-red-600 text-sm">
|
||||
Choose minimum <%= Funnel.min_steps() %> steps to evaluate funnel.
|
||||
</span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="w-2/5 inline-flex items-center ml-2 mb-3 text-gray-500 dark:text-gray-400">
|
||||
<.evaluation
|
||||
:if={@evaluation_result}
|
||||
result={@evaluation_result}
|
||||
at={Enum.find_index(@step_ids, &(&1 == step_idx))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.add_step_button :if={
|
||||
length(@step_ids) < Funnel.max_steps() and
|
||||
map_size(@selections_made) < length(@goals)
|
||||
} />
|
||||
|
||||
<div class="mt-6">
|
||||
<p id="funnel-eval" class="text-gray-500 dark:text-gray-400 text-sm mt-2 mb-2">
|
||||
<%= if @evaluation_result do %>
|
||||
Last month conversion rate: <strong><%= List.last(@evaluation_result.steps).conversion_rate %></strong>%
|
||||
<div class="mt-6">
|
||||
<%= if has_steps_errors?(f) or map_size(@selections_made) < Funnel.min_steps() or length(@step_ids) > map_size(@selections_made) do %>
|
||||
<.submit_button_inactive />
|
||||
<% else %>
|
||||
<span class="text-red-600 text-sm">
|
||||
Choose minimum <%= Funnel.min_steps() %> steps to evaluate funnel.
|
||||
</span>
|
||||
<.submit_button />
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<%= if has_steps_errors?(f) or map_size(@selections_made) < Funnel.min_steps() or length(@step_ids) > map_size(@selections_made) do %>
|
||||
<.submit_button_inactive />
|
||||
<% else %>
|
||||
<.submit_button />
|
||||
<% end %>
|
||||
<.cancel_button />
|
||||
</div>
|
||||
</div>
|
||||
</.form>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:field, Phoenix.HTML.FormField)
|
||||
|
||||
def input(assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@field.name}>
|
||||
<input
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
type="text"
|
||||
id={@field.id}
|
||||
name={@field.name}
|
||||
value={@field.value}
|
||||
phx-debounce="300"
|
||||
class="focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-7/12 rounded-md sm:text-sm border-gray-300 dark:border-gray-500"
|
||||
/>
|
||||
|
||||
<.error :for={{msg, _} <- @field.errors}>Funnel Name <%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def error(assigns) do
|
||||
~H"""
|
||||
<div class="mt-2 text-sm text-red-600">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:step_idx, :integer, required: true)
|
||||
|
||||
def remove_step_button(assigns) do
|
||||
~H"""
|
||||
<div class="inline-flex items-center ml-2 mb-4 text-red-600">
|
||||
<div class="inline-flex items-center ml-2 mb-4 text-red-500">
|
||||
<svg
|
||||
id={"remove-step-#{@step_idx}"}
|
||||
class="feather feather-sm cursor-pointer"
|
||||
@ -185,7 +175,9 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
|
||||
def submit_button(assigns) do
|
||||
~H"""
|
||||
<button id="save" type="submit" class="button mt-6">Save</button>
|
||||
<button id="save" type="submit" class="button text-base font-bold w-full">
|
||||
Add Funnel →
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
@ -194,23 +186,9 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
<button
|
||||
type="none"
|
||||
id="save"
|
||||
class="inline-block mt-6 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-gray-300 bg-white dark:bg-gray-800 hover:text-gray-500 dark:hover:text-gray-400 focus:outline-none focus:border-blue-300 focus:ring active:text-gray-800 active:bg-gray-50 transition ease-in-out duration-150 cursor-not-allowed"
|
||||
class="w-full text-base font-bold py-2 border border-gray-300 dark:border-gray-500 rounded-md text-gray-300 bg-white dark:bg-gray-800 hover:text-gray-500 dark:hover:text-gray-400 focus:outline-none focus:border-blue-300 focus:ring active:text-gray-800 active:bg-gray-50 transition ease-in-out duration-150 cursor-not-allowed"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
def cancel_button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type="button"
|
||||
id="cancel"
|
||||
class="inline-block mt-4 ml-2 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150 "
|
||||
phx-click="cancel-add-funnel"
|
||||
phx-target="#funnel-settings-main"
|
||||
>
|
||||
Cancel
|
||||
Add Funnel →
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
@ -220,7 +198,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
|
||||
def evaluation(assigns) do
|
||||
~H"""
|
||||
<span class="text-xs" id={"step-eval-#{@at}"}>
|
||||
<span class="text-sm" id={"step-eval-#{@at}"}>
|
||||
<% step = Enum.at(@result.steps, @at) %>
|
||||
<span :if={step && @at == 0}>
|
||||
<span
|
||||
@ -288,6 +266,11 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("cancel-add-funnel", _value, socket) do
|
||||
send(socket.parent_pid, :cancel_add_funnel)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_info({:selection_made, %{submit_value: goal_id, by: combo_box}}, socket) do
|
||||
selections_made = store_selection(socket.assigns, combo_box, goal_id)
|
||||
|
||||
@ -353,7 +336,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
steps
|
||||
)
|
||||
|
||||
query = Plausible.Stats.Query.from(site, %{"period" => "all"})
|
||||
query = Plausible.Stats.Query.from(site, %{"period" => "month"})
|
||||
{:ok, {definition, query}}
|
||||
end
|
||||
|
||||
@ -392,7 +375,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
Map.delete(selections_made, step_input_id)
|
||||
end
|
||||
|
||||
defp reject_alrady_selected(combo_box, goals, selections_made) do
|
||||
defp reject_already_selected(combo_box, goals, selections_made) do
|
||||
selection_ids =
|
||||
Enum.map(selections_made, fn
|
||||
{_, %{id: goal_id}} -> goal_id
|
||||
|
@ -13,14 +13,44 @@ defmodule PlausibleWeb.Live.FunnelSettings.List do
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<div class="border-t border-gray-200 pt-4 sm:flex sm:items-center sm:justify-between">
|
||||
<form id="filter-form" phx-change="filter">
|
||||
<div class="text-gray-800 text-sm inline-flex items-center">
|
||||
<div class="relative rounded-md shadow-sm flex">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<Heroicons.magnifying_glass class="feather mr-1 dark:text-gray-300" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="filter-text"
|
||||
id="filter-text"
|
||||
class="pl-8 shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800"
|
||||
placeholder="Search Funnels"
|
||||
value={@filter_text}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Heroicons.backspace
|
||||
:if={String.trim(@filter_text) != ""}
|
||||
class="feather ml-2 cursor-pointer hover:text-red-500 dark:text-gray-300 dark:hover:text-red-500"
|
||||
phx-click="reset-filter-text"
|
||||
id="reset-filter"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 flex sm:ml-4 sm:mt-0">
|
||||
<button type="button" phx-click="add-funnel" class="button">
|
||||
+ Add Funnel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<%= if Enum.count(@funnels) > 0 do %>
|
||||
<div class="mt-4">
|
||||
<%= for funnel <- @funnels do %>
|
||||
<div class="border-b border-gray-300 dark:border-gray-500 py-3 flex justify-between">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<%= funnel.name %>
|
||||
<br />
|
||||
<span class="text-sm text-gray-400 font-normal">
|
||||
<span class="text-sm text-gray-400 font-normal block mt-1">
|
||||
<%= funnel.steps_count %>-step funnel
|
||||
</span>
|
||||
</span>
|
||||
@ -52,7 +82,21 @@ defmodule PlausibleWeb.Live.FunnelSettings.List do
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="mt-4 dark:text-gray-100">No funnels configured for this site yet</div>
|
||||
<p class="text-sm text-gray-800 dark:text-gray-200 mt-12 mb-8 text-center">
|
||||
<span :if={String.trim(@filter_text) != ""}>
|
||||
No funnels found for this site. Please refine or
|
||||
<a
|
||||
class="text-indigo-500 cursor-pointer underline"
|
||||
phx-click="reset-filter-text"
|
||||
id="reset-filter-hint"
|
||||
>
|
||||
reset your search.
|
||||
</a>
|
||||
</span>
|
||||
<span :if={String.trim(@filter_text) == "" && Enum.empty?(@funnels)}>
|
||||
No funnels configured for this site.
|
||||
</span>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
|
@ -149,7 +149,7 @@ defmodule PlausibleWeb.Live.PropsSettings do
|
||||
%{assigns: %{site: site}} = socket
|
||||
)
|
||||
when is_binary(prop) do
|
||||
allowed_event_props = [prop | site.allowed_event_props]
|
||||
allowed_event_props = [prop | site.allowed_event_props || []]
|
||||
|
||||
socket =
|
||||
socket
|
||||
|
@ -31,6 +31,17 @@ defmodule PlausibleWeb.Live.Components.ComboBox.StaticSearchTest do
|
||||
options = fake_options(["OS", "Version", "Logged In"])
|
||||
assert [] = StaticSearch.suggest("cow", options)
|
||||
end
|
||||
|
||||
test "uses custom to_string/1 function" do
|
||||
options = fake_options([%{key: "OS"}, %{key: "Version"}, %{key: "Logged In"}])
|
||||
assert [{_, %{key: "Version"}}] = StaticSearch.suggest("vers", options, to_string: & &1.key)
|
||||
end
|
||||
|
||||
test "accepts custom jaro distance threshold" do
|
||||
options = fake_options(["Joanna", "Joel", "John"])
|
||||
assert [_, _, _] = StaticSearch.suggest("Joa", options, weight_threshold: 0.5)
|
||||
assert [{_, "Joanna"}] = StaticSearch.suggest("Joa", options, weight_threshold: 0.9)
|
||||
end
|
||||
end
|
||||
|
||||
defp fake_options(option_names) do
|
||||
|
@ -17,6 +17,14 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
assert element_exists?(resp, "a[href=\"https://plausible.io/docs/funnel-analysis\"]")
|
||||
end
|
||||
|
||||
test "search funnels input is rendered", %{conn: conn, site: site} do
|
||||
setup_goals(site)
|
||||
conn = get(conn, "/#{site.domain}/settings/funnels")
|
||||
resp = html_response(conn, 200)
|
||||
assert element_exists?(resp, ~s/input[type="text"]#filter-text/)
|
||||
assert element_exists?(resp, ~s/form[phx-change="filter"]#filter-form/)
|
||||
end
|
||||
|
||||
test "lists funnels with delete actions", %{conn: conn, site: site} do
|
||||
{:ok, [f1_id, f2_id]} = setup_funnels(site)
|
||||
conn = get(conn, "/#{site.domain}/settings/funnels")
|
||||
@ -41,7 +49,7 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
assert element_exists?(resp, ~S/button[phx-click="add-funnel"]/)
|
||||
end
|
||||
|
||||
test "if not enough goals are present, a hint to create goals is rendered", %{
|
||||
test "if not enough goals are present, renders a hint to create goals + no search", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
@ -53,12 +61,59 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
|
||||
add_goals_path = Routes.site_path(conn, :settings_goals, site.domain)
|
||||
assert element_exists?(doc, ~s/a[href="#{add_goals_path}"]/)
|
||||
|
||||
refute element_exists?(doc, ~s/input[type="text"]#filter-text/)
|
||||
refute element_exists?(doc, ~s/form[phx-change="filter"]#filter-form/)
|
||||
end
|
||||
end
|
||||
|
||||
describe "FunnelSettings live view" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "allows list filtering / search", %{conn: conn, site: site} do
|
||||
{:ok, _} = setup_funnels(site, ["Funnel One", "Search Me"])
|
||||
{lv, html} = get_liveview(conn, site, with_html?: true)
|
||||
|
||||
assert html =~ "Funnel One"
|
||||
assert html =~ "Search Me"
|
||||
|
||||
html = type_into_search(lv, "search")
|
||||
|
||||
refute html =~ "Funnel One"
|
||||
assert html =~ "Search Me"
|
||||
end
|
||||
|
||||
test "allows resetting filter text via backspace icon", %{conn: conn, site: site} do
|
||||
{:ok, _} = setup_funnels(site, ["Funnel One", "Another"])
|
||||
{lv, html} = get_liveview(conn, site, with_html?: true)
|
||||
|
||||
refute element_exists?(html, ~s/svg[phx-click="reset-filter-text"]#reset-filter/)
|
||||
|
||||
html = type_into_search(lv, "one")
|
||||
refute html =~ "Another"
|
||||
|
||||
assert element_exists?(html, ~s/svg[phx-click="reset-filter-text"]#reset-filter/)
|
||||
|
||||
html = lv |> element(~s/svg#reset-filter/) |> render_click()
|
||||
|
||||
assert html =~ "Funnel One"
|
||||
assert html =~ "Another"
|
||||
end
|
||||
|
||||
test "allows resetting filter text via no match link", %{conn: conn, site: site} do
|
||||
{:ok, _} = setup_funnels(site)
|
||||
lv = get_liveview(conn, site)
|
||||
html = type_into_search(lv, "Definitely this is not going to render any matches")
|
||||
|
||||
assert html =~ "No funnels found for this site. Please refine or"
|
||||
assert html =~ "reset your search"
|
||||
|
||||
assert element_exists?(html, ~s/a[phx-click="reset-filter-text"]#reset-filter-hint/)
|
||||
html = lv |> element(~s/a#reset-filter-hint/) |> render_click()
|
||||
|
||||
refute html =~ "No funnels found for this site. Please refine or"
|
||||
end
|
||||
|
||||
test "allows to delete funnels", %{conn: conn, site: site} do
|
||||
{:ok, [f1_id, _f2_id]} = setup_funnels(site)
|
||||
|
||||
@ -83,7 +138,11 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
lv = get_liveview(conn, site)
|
||||
doc = render_click(lv, "add-funnel")
|
||||
|
||||
assert element_exists?(doc, ~s/form[phx-change="validate"][phx-submit="save"]/)
|
||||
assert element_exists?(
|
||||
doc,
|
||||
~s/form[phx-change="validate"][phx-submit="save"][phx-click-away="cancel-add-funnel"]/
|
||||
)
|
||||
|
||||
assert element_exists?(doc, ~s/form input[type="text"][name="funnel[name]"]/)
|
||||
|
||||
assert element_exists?(
|
||||
@ -222,43 +281,22 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
doc = lv |> element("#funnel-eval") |> render()
|
||||
assert text_of_element(doc, ~s/#funnel-eval/) =~ "Last month conversion rate: 0%"
|
||||
end
|
||||
|
||||
test "cancel buttons renders the funnel list", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
setup_goals(site)
|
||||
lv = get_liveview(conn, site)
|
||||
doc = lv |> element(~s/button[phx-click="add-funnel"]/) |> render_click()
|
||||
|
||||
cancel_button = ~s/button#cancel[phx-click="cancel-add-funnel"]/
|
||||
|
||||
assert element_exists?(doc, cancel_button)
|
||||
|
||||
doc =
|
||||
lv
|
||||
|> element(cancel_button)
|
||||
|> render_click()
|
||||
|
||||
assert doc =~ "No funnels configured for this site yet"
|
||||
assert element_exists?(doc, ~S/button[phx-click="add-funnel"]/)
|
||||
end
|
||||
end
|
||||
|
||||
defp setup_funnels(site) do
|
||||
defp setup_funnels(site, names \\ []) do
|
||||
{:ok, [g1, g2]} = setup_goals(site)
|
||||
|
||||
{:ok, f1} =
|
||||
Plausible.Funnels.create(
|
||||
site,
|
||||
"From blog to signup",
|
||||
Enum.at(names, 0) || "From blog to signup",
|
||||
[%{"goal_id" => g1.id}, %{"goal_id" => g2.id}]
|
||||
)
|
||||
|
||||
{:ok, f2} =
|
||||
Plausible.Funnels.create(
|
||||
site,
|
||||
"From signup to blog",
|
||||
Enum.at(names, 1) || "From signup to blog",
|
||||
[%{"goal_id" => g2.id}, %{"goal_id" => g1.id}]
|
||||
)
|
||||
|
||||
@ -281,4 +319,13 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
lv
|
||||
end
|
||||
end
|
||||
|
||||
defp type_into_search(lv, text) do
|
||||
lv
|
||||
|> element("form#filter-form")
|
||||
|> render_change(%{
|
||||
"_target" => ["filter-text"],
|
||||
"filter-text" => "#{text}"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user