mirror of
https://github.com/plausible/analytics.git
synced 2024-11-23 03:04:43 +03:00
Implement UI to edit existing funnels (#4369)
* Update tailwind config Turns out `extra` wasn't scanned properly with previous wildcards. I don't care any more, it's not slow. * Add interface for updating funnels * Extend ComboBox with the ability to preselect an initial value * Implement editing funnels UI * Update CHANGELOG * s/add_funnel/setup_funnel * modal width 2/5 => 2/3 * Let's not make the list disappear on modal pop-up * Fix the damn modal width again * Watch extra dir * Format * Remove commented code The test is implemented elsewhere * Track funnel modified to drop default selection * Fix screen size adoption and format large numbers * Preserve currency so that string casting includes it * Format * Fix ComboBox attribute for preselected option
This commit is contained in:
parent
1da1da9421
commit
1488683d26
@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- UI to edit funnels
|
||||
- Add a search functionality in all Details views, except for Goal Conversions, Countries, Regions, Cities and Google Search terms
|
||||
- Icons for browsers plausible/analytics#4239
|
||||
- Automatic custom property selection in the dashboard Properties report
|
||||
|
@ -6,8 +6,7 @@ module.exports = {
|
||||
"./js/**/*.js",
|
||||
"../lib/*_web.ex",
|
||||
"../lib/*_web/**/*.*ex",
|
||||
"../extra/*_web.ex",
|
||||
"../extra/*_web/**/*.*ex"
|
||||
"../extra/**/*.*ex",
|
||||
],
|
||||
safelist: [
|
||||
// PlausibleWeb.StatsView.stats_container_class/1 uses this class
|
||||
@ -52,9 +51,9 @@ module.exports = {
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
|
||||
plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
|
||||
plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
|
||||
plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
|
||||
plugin(({ addVariant }) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
|
||||
plugin(({ addVariant }) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
|
||||
plugin(({ addVariant }) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
|
||||
plugin(({ addVariant }) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
|
||||
]
|
||||
}
|
||||
|
@ -15,6 +15,9 @@ config :plausible, PlausibleWeb.Endpoint,
|
||||
]
|
||||
],
|
||||
live_reload: [
|
||||
dirs: [
|
||||
"extra"
|
||||
],
|
||||
patterns: [
|
||||
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
|
||||
~r"lib/plausible_web/(controllers|live|components|templates|views|plugs)/.*(ex|heex)$"
|
||||
|
@ -46,7 +46,8 @@ defmodule Plausible.Funnel do
|
||||
has_many :steps, Step,
|
||||
preload_order: [
|
||||
asc: :step_order
|
||||
]
|
||||
],
|
||||
on_replace: :delete
|
||||
|
||||
has_many :goals, through: [:steps, :goal]
|
||||
timestamps()
|
||||
@ -56,21 +57,19 @@ defmodule Plausible.Funnel do
|
||||
funnel
|
||||
|> cast(attrs, [:name])
|
||||
|> validate_required([:name])
|
||||
|> cast_assoc(:steps, with: &Step.changeset/2, required: true)
|
||||
|> put_steps(attrs[:steps] || attrs["steps"])
|
||||
|> validate_length(:steps, min: @min_steps, max: @max_steps)
|
||||
|> put_step_orders()
|
||||
|> unique_constraint(:name,
|
||||
name: :funnels_name_site_id_index
|
||||
)
|
||||
end
|
||||
|
||||
def put_step_orders(changeset) do
|
||||
if steps = Ecto.Changeset.get_change(changeset, :steps) do
|
||||
steps
|
||||
|> Enum.with_index(fn step, step_order ->
|
||||
Ecto.Changeset.put_change(step, :step_order, step_order + 1)
|
||||
end)
|
||||
|> then(&Ecto.Changeset.put_change(changeset, :steps, &1))
|
||||
end
|
||||
def put_steps(changeset, steps) do
|
||||
steps
|
||||
|> Enum.map(&Step.changeset(%Step{}, &1))
|
||||
|> Enum.with_index(fn step, step_order ->
|
||||
Ecto.Changeset.put_change(step, :step_order, step_order + 1)
|
||||
end)
|
||||
|> then(&Ecto.Changeset.put_assoc(changeset, :steps, &1))
|
||||
end
|
||||
end
|
||||
|
@ -35,12 +35,35 @@ defmodule Plausible.Funnels do
|
||||
{:error, :invalid_funnel_size}
|
||||
end
|
||||
|
||||
@spec update(Funnel.t(), String.t(), [map()]) ::
|
||||
{:ok, Funnel.t()}
|
||||
| {:error, Ecto.Changeset.t() | :invalid_funnel_size | :upgrade_required}
|
||||
def update(funnel, name, steps) do
|
||||
site = Plausible.Repo.preload(funnel, site: :owner).site
|
||||
|
||||
case Plausible.Billing.Feature.Funnels.check_availability(site.owner) do
|
||||
{:error, _} = error ->
|
||||
error
|
||||
|
||||
:ok ->
|
||||
funnel
|
||||
|> Funnel.changeset(%{name: name, steps: steps})
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_changeset(Plausible.Site.t(), String.t(), [map()]) ::
|
||||
Ecto.Changeset.t()
|
||||
def create_changeset(site, name, steps) do
|
||||
Funnel.changeset(%Funnel{site_id: site.id}, %{name: name, steps: steps})
|
||||
end
|
||||
|
||||
@spec edit_changeset(Plausible.Funnel.t(), String.t(), [map()]) ::
|
||||
Ecto.Changeset.t()
|
||||
def edit_changeset(funnel, name, steps) do
|
||||
Funnel.changeset(funnel, %{name: name, steps: steps})
|
||||
end
|
||||
|
||||
@spec ephemeral_definition(Plausible.Site.t(), String.t(), [map()]) :: Funnel.t()
|
||||
def ephemeral_definition(site, name, steps) do
|
||||
site
|
||||
|
@ -30,9 +30,10 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
assign(socket,
|
||||
domain: domain,
|
||||
displayed_funnels: socket.assigns.all_funnels,
|
||||
add_funnel?: false,
|
||||
setup_funnel?: false,
|
||||
filter_text: "",
|
||||
current_user_id: user_id
|
||||
current_user_id: user_id,
|
||||
funnel_id: nil
|
||||
)}
|
||||
end
|
||||
|
||||
@ -42,36 +43,37 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
~H"""
|
||||
<div id="funnel-settings-main">
|
||||
<.flash_messages flash={@flash} />
|
||||
<%= if @add_funnel? do %>
|
||||
|
||||
<%= if @setup_funnel? do %>
|
||||
<%= live_render(
|
||||
@socket,
|
||||
PlausibleWeb.Live.FunnelSettings.Form,
|
||||
id: "funnels-form",
|
||||
session: %{
|
||||
"current_user_id" => @current_user_id,
|
||||
"domain" => @domain
|
||||
"domain" => @domain,
|
||||
"funnel_id" => @funnel_id
|
||||
}
|
||||
) %>
|
||||
<% else %>
|
||||
<div :if={@goal_count >= Funnel.min_steps()}>
|
||||
<.live_component
|
||||
module={PlausibleWeb.Live.FunnelSettings.List}
|
||||
id="funnels-list"
|
||||
funnels={@displayed_funnels}
|
||||
filter_text={@filter_text}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :if={@goal_count < Funnel.min_steps()}>
|
||||
<PlausibleWeb.Components.Generic.notice class="mt-4" title="Not enough goals">
|
||||
You need to define at least two goals to create a funnel. Go ahead and <%= link(
|
||||
"add goals",
|
||||
to: PlausibleWeb.Router.Helpers.site_path(@socket, :settings_goals, @domain),
|
||||
class: "text-indigo-500 w-full text-center"
|
||||
) %> to proceed.
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</div>
|
||||
<% end %>
|
||||
<div :if={@goal_count >= Funnel.min_steps()}>
|
||||
<.live_component
|
||||
module={PlausibleWeb.Live.FunnelSettings.List}
|
||||
id="funnels-list"
|
||||
funnels={@displayed_funnels}
|
||||
filter_text={@filter_text}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :if={@goal_count < Funnel.min_steps()}>
|
||||
<PlausibleWeb.Components.Generic.notice class="mt-4" title="Not enough goals">
|
||||
You need to define at least two goals to create a funnel. Go ahead and <%= link(
|
||||
"add goals",
|
||||
to: PlausibleWeb.Router.Helpers.site_path(@socket, :settings_goals, @domain),
|
||||
class: "text-indigo-500 w-full text-center"
|
||||
) %> to proceed.
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@ -92,7 +94,11 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
end
|
||||
|
||||
def handle_event("add-funnel", _value, socket) do
|
||||
{:noreply, assign(socket, add_funnel?: true)}
|
||||
{:noreply, assign(socket, setup_funnel?: true)}
|
||||
end
|
||||
|
||||
def handle_event("edit-funnel", %{"funnel-id" => id}, socket) do
|
||||
{:noreply, assign(socket, setup_funnel?: true, funnel_id: String.to_integer(id))}
|
||||
end
|
||||
|
||||
def handle_event("delete-funnel", %{"funnel-id" => id}, socket) do
|
||||
@ -110,18 +116,21 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info({:funnel_saved, funnel}, socket) do
|
||||
def handle_info({:funnel_saved, _funnel}, socket) do
|
||||
socket = put_live_flash(socket, :success, "Funnel saved successfully")
|
||||
|
||||
funnels = Funnels.list(socket.assigns.site)
|
||||
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
add_funnel?: false,
|
||||
all_funnels: [funnel | socket.assigns.all_funnels],
|
||||
displayed_funnels: [funnel | socket.assigns.displayed_funnels]
|
||||
setup_funnel?: false,
|
||||
all_funnels: funnels,
|
||||
funnel_id: nil,
|
||||
displayed_funnels: funnels
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info(:cancel_add_funnel, socket) do
|
||||
{:noreply, assign(socket, add_funnel?: false)}
|
||||
def handle_info(:cancel_setup_funnel, socket) do
|
||||
{:noreply, assign(socket, setup_funnel?: false, funnel_id: nil)}
|
||||
end
|
||||
end
|
||||
|
@ -9,9 +9,9 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
use Plausible.Funnel
|
||||
|
||||
import PlausibleWeb.Live.Components.Form
|
||||
alias Plausible.{Sites, Goals}
|
||||
alias Plausible.{Sites, Goals, Funnels}
|
||||
|
||||
def mount(_params, %{"current_user_id" => user_id, "domain" => domain}, socket) do
|
||||
def mount(_params, %{"current_user_id" => user_id, "domain" => domain} = session, socket) do
|
||||
site = Sites.get_for_user!(user_id, domain, [:owner, :admin, :super_admin])
|
||||
|
||||
# We'll have the options trimmed to only the data we care about, to keep
|
||||
@ -22,19 +22,16 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
site
|
||||
|> Goals.for_site()
|
||||
|> Enum.map(fn goal ->
|
||||
{goal.id, struct!(Plausible.Goal, Map.take(goal, [:id, :event_name, :page_path]))}
|
||||
{goal.id,
|
||||
struct!(Plausible.Goal, Map.take(goal, [:id, :event_name, :page_path, :currency]))}
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
step_ids: Enum.to_list(1..Funnel.min_steps()),
|
||||
form: to_form(Plausible.Funnels.create_changeset(site, "", [])),
|
||||
goals: goals,
|
||||
site: site,
|
||||
selections_made: Map.new(),
|
||||
evaluation_result: nil,
|
||||
evaluation_at: System.monotonic_time()
|
||||
)}
|
||||
socket =
|
||||
socket
|
||||
|> assign(goals: goals, site: site, evaluation_result: nil)
|
||||
|> prepare_socket(site, session["funnel_id"])
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
@ -46,7 +43,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
>
|
||||
</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 class="md:w-2/3 max-w-lg h-full">
|
||||
<div id="funnel-form">
|
||||
<.form
|
||||
:let={f}
|
||||
@ -58,7 +55,9 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
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"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-6">Add Funnel</h2>
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-6">
|
||||
<%= if @funnel, do: "Edit", else: "Add" %> Funnel
|
||||
</h2>
|
||||
|
||||
<label for={f[:name].name} class="block mb-3 font-medium dark:text-gray-100">
|
||||
Funnel Name
|
||||
@ -81,6 +80,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
<div :for={step_idx <- @step_ids} class="flex mb-3 mt-3">
|
||||
<div class="w-2/5 flex-1">
|
||||
<.live_component
|
||||
selected={find_preselected(@funnel, @funnel_modified?, step_idx)}
|
||||
submit_name={"funnel[steps][#{step_idx}][goal_id]"}
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={&PlausibleWeb.Live.Components.ComboBox.StaticSearch.suggest/2}
|
||||
@ -101,7 +101,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-4/12 align-middle ml-4 text-gray-500 dark:text-gray-400">
|
||||
<div class="w-4/12 mt-1 ml-4 text-gray-500 dark:text-gray-400">
|
||||
<.evaluation
|
||||
:if={@evaluation_result}
|
||||
result={@evaluation_result}
|
||||
@ -137,7 +137,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
length(@step_ids) > map_size(@selections_made)
|
||||
}
|
||||
>
|
||||
Add Funnel →
|
||||
<span><%= if @funnel, do: "Update", else: "Add" %> Funnel →</span>
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</div>
|
||||
@ -152,7 +152,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
|
||||
def remove_step_button(assigns) do
|
||||
~H"""
|
||||
<div class="inline-flex items-center ml-2 mb-4 text-red-500">
|
||||
<div class="inline-flex items-center ml-2 text-red-500">
|
||||
<svg
|
||||
id={"remove-step-#{@step_idx}"}
|
||||
class="feather feather-sm cursor-pointer"
|
||||
@ -196,11 +196,11 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
class="border-dotted border-b border-gray-400 "
|
||||
tooltip="Sample calculation for last month"
|
||||
>
|
||||
Entering Visitors: <strong><%= @result.entering_visitors %></strong>
|
||||
<span class="hidden md:inline">Entering Visitors: </span><strong><%= PlausibleWeb.StatsView.large_number_format(@result.entering_visitors) %></strong>
|
||||
</span>
|
||||
</span>
|
||||
<span :if={step && @at > 0}>
|
||||
Dropoff: <strong><%= Map.get(step, :dropoff_percentage) %>%</strong>
|
||||
<span class="hidden md:inline">Dropoff: </span><strong><%= Map.get(step, :dropoff_percentage) %>%</strong>
|
||||
</span>
|
||||
</span>
|
||||
"""
|
||||
@ -209,6 +209,8 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
def handle_event("add-step", _value, socket) do
|
||||
step_ids = socket.assigns.step_ids
|
||||
|
||||
socket = assign(socket, funnel_modified?: true)
|
||||
|
||||
if length(step_ids) < Funnel.max_steps() do
|
||||
first_free_idx = find_sequence_break(step_ids)
|
||||
new_ids = step_ids ++ [first_free_idx]
|
||||
@ -222,9 +224,11 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
idx = String.to_integer(idx)
|
||||
step_ids = List.delete(socket.assigns.step_ids, idx)
|
||||
selections_made = drop_selection(socket.assigns.selections_made, idx)
|
||||
|
||||
send(self(), :evaluate_funnel)
|
||||
|
||||
{:noreply, assign(socket, step_ids: step_ids, selections_made: selections_made)}
|
||||
{:noreply,
|
||||
assign(socket, step_ids: step_ids, selections_made: selections_made, funnel_modified?: true)}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"funnel" => params}, socket) do
|
||||
@ -238,7 +242,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
|
||||
changeset =
|
||||
socket.assigns.site
|
||||
|> Plausible.Funnels.create_changeset(
|
||||
|> Funnels.create_changeset(
|
||||
params["name"],
|
||||
steps_from_assigns
|
||||
)
|
||||
@ -247,10 +251,23 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"funnel" => params}, %{assigns: %{site: site}} = socket) do
|
||||
def handle_event(
|
||||
"save",
|
||||
%{"funnel" => params},
|
||||
%{assigns: %{site: site, funnel: funnel}} = socket
|
||||
) do
|
||||
steps = Enum.map(params["steps"], fn {_idx, payload} -> payload end)
|
||||
|
||||
case Plausible.Funnels.create(site, params["name"], steps) do
|
||||
save_fn =
|
||||
case funnel do
|
||||
%Plausible.Funnel{} ->
|
||||
fn -> Funnels.update(funnel, params["name"], steps) end
|
||||
|
||||
nil ->
|
||||
fn -> Funnels.create(site, params["name"], steps) end
|
||||
end
|
||||
|
||||
case save_fn.() do
|
||||
{:ok, funnel} ->
|
||||
send(
|
||||
socket.parent_pid,
|
||||
@ -268,7 +285,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
end
|
||||
|
||||
def handle_event("cancel-add-funnel", _value, socket) do
|
||||
send(socket.parent_pid, :cancel_add_funnel)
|
||||
send(socket.parent_pid, :cancel_setup_funnel)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@ -296,25 +313,19 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
%{
|
||||
assigns: %{
|
||||
site: site,
|
||||
selections_made: selections_made,
|
||||
evaluation_at: evaluation_at
|
||||
selections_made: selections_made
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
with true <- seconds_since_evaluation(evaluation_at) >= 1,
|
||||
{:ok, {definition, query}} <- build_ephemeral_funnel(site, selections_made),
|
||||
with {:ok, {definition, query}} <- build_ephemeral_funnel(site, selections_made),
|
||||
{:ok, funnel} <- Plausible.Stats.funnel(site, query, definition) do
|
||||
assign(socket, evaluation_result: funnel, evaluation_at: System.monotonic_time())
|
||||
assign(socket, evaluation_result: funnel)
|
||||
else
|
||||
_ ->
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp seconds_since_evaluation(evaluation_at) do
|
||||
System.convert_time_unit(System.monotonic_time() - evaluation_at, :native, :second)
|
||||
end
|
||||
|
||||
defp build_ephemeral_funnel(site, selections_made) do
|
||||
steps =
|
||||
selections_made
|
||||
@ -331,7 +342,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
end)
|
||||
|
||||
definition =
|
||||
Plausible.Funnels.ephemeral_definition(
|
||||
Funnels.ephemeral_definition(
|
||||
site,
|
||||
"Test funnel",
|
||||
steps
|
||||
@ -387,4 +398,54 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
send_update(PlausibleWeb.Live.Components.ComboBox, id: combo_box, suggestions: result)
|
||||
result
|
||||
end
|
||||
|
||||
defp find_preselected(%Funnel{} = funnel, false, idx) do
|
||||
if step = Enum.at(funnel.steps, idx - 1) do
|
||||
{step.goal.id, to_string(step.goal)}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_preselected(_, _, _), do: nil
|
||||
|
||||
defp prepare_socket(socket, site, funnel_id) when is_integer(funnel_id) do
|
||||
funnel = Funnels.get(site.id, funnel_id)
|
||||
|
||||
form =
|
||||
funnel
|
||||
|> Funnels.edit_changeset(
|
||||
funnel.name,
|
||||
Enum.map(funnel.steps, &%{goal_id: &1.goal.id})
|
||||
)
|
||||
|> to_form()
|
||||
|
||||
selections_made =
|
||||
Enum.reduce(Enum.with_index(funnel.steps, 1), %{}, fn {step, idx}, acc ->
|
||||
Map.put(acc, "step-#{idx}", step.goal)
|
||||
end)
|
||||
|
||||
socket =
|
||||
assign(
|
||||
socket,
|
||||
form: form,
|
||||
funnel: funnel,
|
||||
funnel_modified?: false,
|
||||
selections_made: selections_made,
|
||||
step_ids: Enum.to_list(1..Enum.count(funnel.steps))
|
||||
)
|
||||
|
||||
evaluate_funnel(socket)
|
||||
end
|
||||
|
||||
defp prepare_socket(socket, site, nil) do
|
||||
form = to_form(Funnels.create_changeset(site, "", []))
|
||||
|
||||
assign(
|
||||
socket,
|
||||
form: form,
|
||||
funnel: nil,
|
||||
funnel_modified?: false,
|
||||
selections_made: Map.new(),
|
||||
step_ids: Enum.to_list(1..Funnel.min_steps())
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -54,30 +54,35 @@ defmodule PlausibleWeb.Live.FunnelSettings.List do
|
||||
<%= funnel.steps_count %>-step funnel
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
id={"delete-funnel-#{funnel.id}"}
|
||||
phx-click="delete-funnel"
|
||||
phx-value-funnel-id={funnel.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm={"Are you sure you want to remove funnel '#{funnel.name}'? This will just affect the UI, all of your analytics data will stay intact."}
|
||||
>
|
||||
<svg
|
||||
class="feather feather-sm"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
<div class="flex items-center gap-x-4">
|
||||
<a href="#" phx-click="edit-funnel" phx-value-funnel-id={funnel.id}>
|
||||
<Heroicons.pencil_square class="feather feather-sm text-indigo-800 hover:text-indigo-500 dark:text-indigo-500 dark:hover:text-indigo-300" />
|
||||
</a>
|
||||
<button
|
||||
id={"delete-funnel-#{funnel.id}"}
|
||||
phx-click="delete-funnel"
|
||||
phx-value-funnel-id={funnel.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm={"Are you sure you want to remove funnel '#{funnel.name}'? This will just affect the UI, all of your analytics data will stay intact."}
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
|
||||
</path>
|
||||
<line x1="10" y1="11" x2="10" y2="17"></line>
|
||||
<line x1="14" y1="11" x2="14" y2="17"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<svg
|
||||
class="feather feather-sm"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
|
||||
</path>
|
||||
<line x1="10" y1="11" x2="10" y2="17"></line>
|
||||
<line x1="14" y1="11" x2="14" y2="17"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -42,7 +42,10 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
|
||||
@default_suggestions_limit 15
|
||||
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, assigns)
|
||||
socket =
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> select_default()
|
||||
|
||||
socket =
|
||||
if connected?(socket) do
|
||||
@ -62,6 +65,7 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
|
||||
attr(:submit_name, :string, required: true)
|
||||
attr(:display_value, :string, default: "")
|
||||
attr(:submit_value, :string, default: "")
|
||||
attr(:selected, :any)
|
||||
attr(:suggest_fun, :any, required: true)
|
||||
attr(:suggestions_limit, :integer)
|
||||
attr(:class, :string, default: "")
|
||||
@ -354,6 +358,16 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
|
||||
end
|
||||
end
|
||||
|
||||
defp select_default(socket) do
|
||||
case {socket.assigns[:selected], socket.assigns[:submit_value]} do
|
||||
{{submit_value, display_value}, nil} ->
|
||||
assign(socket, submit_value: submit_value, display_value: display_value)
|
||||
|
||||
_ ->
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp run_suggest_fun(input, options, %{id: id, suggest_fun: fun} = assigns, key_to_update) do
|
||||
if assigns[:async] do
|
||||
pid = self()
|
||||
|
@ -55,6 +55,28 @@ defmodule Plausible.FunnelsTest do
|
||||
assert fg3.step_order == 3
|
||||
end
|
||||
|
||||
test "update funnel", %{site: site, steps: [g1, g2, g3, g4, g5 | _]} do
|
||||
{:ok, funnel1} =
|
||||
Funnels.create(
|
||||
site,
|
||||
"Sample funnel",
|
||||
[g1, g2, g3]
|
||||
)
|
||||
|
||||
{:ok, funnel2} = Funnels.update(funnel1, "Updated funnel", [g4, g5])
|
||||
|
||||
assert funnel2.name == "Updated funnel"
|
||||
assert [fg1, fg2] = funnel2.steps
|
||||
|
||||
assert fg1.goal_id == g4["goal_id"]
|
||||
assert fg2.goal_id == g5["goal_id"]
|
||||
|
||||
assert fg1.step_order == 1
|
||||
assert fg2.step_order == 2
|
||||
|
||||
assert funnel1.id == funnel2.id
|
||||
end
|
||||
|
||||
test "retrieve a funnel by id and site, get steps in order", %{
|
||||
site: site,
|
||||
steps: [g1, g2, g3 | _]
|
||||
|
@ -24,6 +24,18 @@ defmodule PlausibleWeb.Live.Components.ComboBoxTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "renders preselected default value" do
|
||||
options = new_options(10)
|
||||
assert doc = render_sample_component(options, selected: List.last(options))
|
||||
|
||||
assert element_exists?(doc, "input[type=hidden][name=test-submit-name][value=10]")
|
||||
|
||||
assert element_exists?(
|
||||
doc,
|
||||
~s|input[type=text][name=display-test-component][value="TestOption 10"]|
|
||||
)
|
||||
end
|
||||
|
||||
test "renders up to 15 suggestions by default" do
|
||||
assert doc = render_sample_component(new_options(20))
|
||||
|
||||
|
@ -255,7 +255,101 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
refute element_exists?(doc, ~s/form button#save:disabled/)
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
test "save button saves a new funnel", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
setup_goals(site)
|
||||
lv = get_liveview(conn, site)
|
||||
lv |> element(~s/button[phx-click="add-funnel"]/) |> render_click()
|
||||
|
||||
assert lv = find_live_child(lv, "funnels-form")
|
||||
|
||||
lv
|
||||
|> element("li#dropdown-step-1-option-1 a")
|
||||
|> render_click()
|
||||
|
||||
lv
|
||||
|> element("li#dropdown-step-2-option-1 a")
|
||||
|> render_click()
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_change(%{
|
||||
funnel: %{
|
||||
name: "My test funnel",
|
||||
steps: [
|
||||
%{goal_id: 1},
|
||||
%{goal_id: 2}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
lv
|
||||
|> element(~s/form/)
|
||||
|> render_submit()
|
||||
|
||||
assert %Plausible.Funnel{steps: [_, _]} = Plausible.Funnels.get(site, "My test funnel")
|
||||
end
|
||||
|
||||
test "editing a funnel pre-renders it", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
{:ok, [f1_id, _]} = setup_funnels(site)
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv
|
||||
|> element(~s/a[phx-click="edit-funnel"][phx-value-funnel-id=#{f1_id}]/)
|
||||
|> render_click()
|
||||
|
||||
assert lv = find_live_child(lv, "funnels-form")
|
||||
|
||||
assert lv |> element("#step-1") |> render() |> text_of_attr("value") ==
|
||||
"Visit /go/to/blog/**"
|
||||
|
||||
assert lv |> element("#step-2") |> render() |> text_of_attr("value") == "Signup"
|
||||
end
|
||||
|
||||
test "clicking save after editing the funnel, updates it", %{
|
||||
conn: conn,
|
||||
site: site
|
||||
} do
|
||||
{:ok, [f1_id, _]} = setup_funnels(site)
|
||||
{:ok, %{id: goal_id}} = Plausible.Goals.create(site, %{"page_path" => "/"})
|
||||
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv
|
||||
|> element(~s/a[phx-click="edit-funnel"][phx-value-funnel-id=#{f1_id}]/)
|
||||
|> render_click()
|
||||
|
||||
assert lv = find_live_child(lv, "funnels-form")
|
||||
|
||||
lv
|
||||
|> element("li#dropdown-step-2-option-1 a")
|
||||
|> render_click()
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_change(%{
|
||||
funnel: %{
|
||||
name: "Updated funnel",
|
||||
steps: [
|
||||
%{goal_id: 1},
|
||||
%{goal_id: goal_id}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
lv
|
||||
|> element(~s/form/)
|
||||
|> render_submit()
|
||||
|
||||
assert %Plausible.Funnel{steps: [_, %Plausible.Funnel.Step{goal_id: ^goal_id}]} =
|
||||
Plausible.Funnels.get(site, "Updated funnel")
|
||||
end
|
||||
|
||||
test "funnel gets evaluated on every select, assuming a second has passed between selections",
|
||||
%{
|
||||
conn: conn,
|
||||
@ -269,8 +363,6 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
|
||||
|
||||
lv |> element("li#dropdown-step-1-option-1 a") |> render_click()
|
||||
|
||||
:timer.sleep(1001)
|
||||
|
||||
lv |> element("li#dropdown-step-2-option-1 a") |> render_click()
|
||||
|
||||
doc = lv |> element("#step-eval-0") |> render()
|
||||
|
Loading…
Reference in New Issue
Block a user