This commit is contained in:
Adam Rutkowski 2023-05-16 16:55:28 +02:00
parent e3b8067628
commit 265d21efc8
7 changed files with 224 additions and 34 deletions

View File

@ -22,6 +22,11 @@ liveSocket.connect()
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket
window.addEventListener(`phx:update-value`, (e) => {
let el = document.getElementById(e.detail.id)
el.value = e.detail.value
})
const triggers = document.querySelectorAll('[data-dropdown-trigger]')
for (const trigger of triggers) {

View File

@ -22,12 +22,27 @@ defmodule PlausibleWeb.Live.FunnelSettings do
<%= if @add_funnel? do %>
<.live_component module={PlausibleWeb.Live.FunnelSettings.Form} id="funnelForm" site={@site} form={to_form(Plausible.Funnel.changeset())} goals={@goals} />
<% else %>
<.live_component module={PlausibleWeb.Live.FunnelSettings.List} id="funnelsList" funnels={@funnels} site={@site} />
<button type="button" class="button mt-6" phx-click="add_funnel">+ Add funnel</button>
<div :if={Enum.count(@goals) >= 2}>
<.live_component module={PlausibleWeb.Live.FunnelSettings.List} id="funnelsList" funnels={@funnels} site={@site} />
<button type="button" class="button mt-6" phx-click="add_funnel">+ Add funnel</button>
</div>
<div :if={Enum.count(@goals) < 2}>
<div class="rounded-md bg-yellow-100 p-4 mt-8">
<p class="text-sm leading-5 text-gray-900 dark:text-gray-100">
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, :new_goal, @site.domain), class: "text-indigo-500 w-full text-center" %> to proceed.
</p>
</div>
</div>
<% end %>
"""
end
def handle_event("save", data, socket) do
IO.inspect(data, label: :save)
{:noreply, socket}
end
def handle_event("add_funnel", _value, socket) do
{:noreply, assign(socket, :add_funnel?, true)}
end
@ -40,4 +55,9 @@ defmodule PlausibleWeb.Live.FunnelSettings do
IO.inspect(value, label: :validate)
{:noreply, socket}
end
def handle_info(:goal_picked, socket) do
IO.inspect("Picker notified")
{:noreply, socket}
end
end

View File

@ -16,23 +16,24 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
~H"""
<div class="grid grid-cols-4 gap-6 mt-6">
<div class="col-span-4 sm:col-span-2">
<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
<.form :let={f} for={@form} phx-change="validate" phx-submit="save" onkeydown="return event.key != 'Enter';">
<%= label f, "Funnel name", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<.input field={@form[:name]} />
<%= label f, "Funnel Steps", class: "mt-6 block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= for _step_number <- 1..@step_count do %>
<.select goals={@goals} myself={@myself} />
<% end %>
<.live_component
:for={step_number <- 1..@step_count}
module={PlausibleWeb.Live.FunnelSettings.InputPicker}
id={"step-#{step_number}"}
options={Enum.map(@goals, fn goal -> {goal.id, Plausible.Goal.display_name(goal)} end)}
/>
<%= if @step_count < 5 do %>
<a class="underline text-indigo-600 cursor-pointer" phx-click="add-step" phx-target={@myself}>Add another step</a>
<% end %>
<a :if={@step_count < 5} class="underline text-indigo-600 cursor-pointer" phx-click="add-step" phx-target={@myself}>Add another step</a>
<br/><hr/>
<button type="button" class="button mt-6">Save</button>
<button type="submit" class="button mt-6">Save</button>
<button type="button" class="button mt-6" phx-click="cancel_add_funnel">Cancel</button>
</.form>
</div>
@ -40,26 +41,11 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
"""
end
attr :field, Phoenix.HTML.FormField
attr(:field, Phoenix.HTML.FormField)
def input(assigns) do
~H"""
<input type="text" id={@field.id} name={@field.name} value={@field.value} class="focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-full rounded-md sm:text-sm border-gray-300 dark:border-gray-500" />
"""
end
attr :goals, :map, required: true
attr :myself, :any, required: true
def select(assigns) do
~H"""
<select phx-change="changed" phx-target={@myself} name="steps[]" class="dark:bg-gray-900 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100 cursor-pointer" id="site_timezone">
<%= for goal <- @goals do %>
<option value={goal.id}>
<%= Plausible.Goal.display_name(goal) %>
</option>
<% end %>
</select>
<input 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-full rounded-md sm:text-sm border-gray-300 dark:border-gray-500" />
"""
end
@ -80,7 +66,3 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
{:noreply, assign(socket, goals: except_first)}
end
end
# goal_options = [{"N/A", ""} | Enum.map(@goals, fn goal -> {Plausible.Goal.display_name(goal), goal.id} end)] %>
#
# end

View File

@ -0,0 +1,180 @@
defmodule PlausibleWeb.Live.FunnelSettings.InputPicker do
use Phoenix.LiveComponent
attr :placeholder, :string, default: "Select a Goal"
attr :id, :any, default: nil
attr :options, :list, default: []
attr :choices, :list, default: nil
attr :show_picker?, :boolean, default: false
attr :value, :string, default: ""
attr :goal_id, :string, default: ""
attr :higlighted, :integer, default: nil
def render(assigns) do
~H"""
<div class="mb-3">
<div class="relative w-full">
<div class="pl-2 pr-8 py-1 w-full dark:bg-gray-900 dark:text-gray-300 rounded-md shadow-sm border border-gray-300 dark:border-gray-700 focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500 ">
<input autocomplete="off" phx-debounce="10" phx-keyup="keypress" phx-target={@myself} name={@id} id={@id} class="border-none py-1 px-1 p-0 w-full inline-block rounded-md focus:outline-none focus:ring-0 text-sm" style="background-color: inherit;" placeholder={@placeholder} type="text" value={@value}>
<div phx-click="show-picker" phx-target={@myself} class="cursor-pointer absolute inset-y-0 right-0 flex items-center pr-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-4 w-4 text-gray-500"><path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"></path></svg>
</div>
</div>
</div>
<.picker id={"picker-#{@id}"} :if={@show_picker?} options={@options} choices={@choices || @options} target={@myself} higlighted={@higlighted} />
</div>
"""
end
attr :id, :any, default: nil
attr :options, :list, default: []
attr :choices, :list, default: []
attr :higlighted, :any
attr :target, :any
def picker(assigns) do
~H"""
<ul phx-click-away="hide-picker" phx-target={@target} class="z-50 absolute mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm dark:bg-gray-900">
<%= if @choices != [] do %>
<%= for {goal_id, goal_name} <- @choices do %>
<.li goal_id={goal_id} goal_name={goal_name} target={@target} higlighted={@higlighted}/>
<% end %>
<% else %>
<div class="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-gray-300">No matches found in the current goals. Try searching for something different.</div>
<% end %>
</ul>
"""
end
attr :higlighted, :any, default: nil
attr :goal_name, :string, required: true
attr :goal_id, :integer, required: true
attr :target, :any
def li(assigns) do
~H"""
<%= if @higlighted == @goal_id do %>
<li phx-value-goal-name={@goal_name}, phx-click="selected" phx-target={@target} class="relative select-none py-2 px-3 cursor-pointer dark:text-gray-300 bg-indigo-500 text-white">
<span class="block truncate"><%= @goal_name %></span>
</li>
<% else %>
<li phx-value-goal-name={@goal_name}, phx-click="selected" phx-target={@target} class="relative select-none py-2 px-3 cursor-pointer dark:text-gray-300 text-gray-500">
<span class="block truncate"><%= @goal_name %></span>
</li>
<% end %>
"""
end
def handle_event("selected", %{"goal-name" => goal_name}, socket) do
send(self(), :goal_picked)
{:noreply, assign(socket, %{show_picker?: false, value: goal_name})}
end
def handle_event("keypress", %{"key" => "ArrowDown"}, socket) do
choices = socket.assigns.choices
current = socket.assigns.higlighted
next =
Enum.drop_while(choices, fn {id, _} -> id != current end)
|> Enum.take(2)
|> List.last()
higlighted =
case next do
{next_id, _} ->
next_id
nil ->
current
end
{:noreply, assign(socket, %{higlighted: higlighted, show_picker?: true})}
end
def handle_event("keypress", %{"key" => "ArrowUp"}, socket) do
choices = socket.assigns.choices
current = socket.assigns.higlighted
next =
choices
|> Enum.reverse()
|> Enum.drop_while(fn {id, _} -> id != current end)
|> Enum.take(2)
|> List.last()
higlighted =
case next do
{next_id, _} ->
next_id
nil ->
current
end
{:noreply, assign(socket, %{higlighted: higlighted, show_picker?: true})}
end
def handle_event("keypress", %{"key" => "Enter"}, socket) do
choices = socket.assigns.choices
current = socket.assigns.higlighted
goal_name =
Enum.find_value(choices, fn {id, goal_name} ->
if id == current, do: goal_name
end)
socket =
socket
|> assign(%{show_picker?: false, value: goal_name, higlighted: nil})
|> push_event("update-value", %{id: socket.assigns.id, value: goal_name})
{:noreply, socket}
end
def handle_event("keypress", %{"key" => _other, "value" => typed}, socket) do
if String.length(typed) > 0 do
choices =
Enum.filter(socket.assigns.options, fn {_id, goal_name} ->
String.contains?(goal_name, typed) ||
String.jaro_distance(String.upcase(goal_name), String.upcase(typed)) > 0.6
end)
|> Enum.sort_by(fn {_id, goal_name} -> String.jaro_distance(goal_name, typed) end, :desc)
|> Enum.take(10)
first_id =
case List.first(choices) do
{id, _} -> id
nil -> nil
end
{:noreply, assign(socket, %{choices: choices, show_picker?: true, higlighted: first_id})}
else
{:noreply, socket}
end
end
def handle_event("hide-picker", _, socket) do
{:noreply, assign(socket, %{show_picker?: false})}
end
def handle_event("show-picker", _, socket) do
socket =
if socket.assigns[:choices] == [] do
assign(socket, %{show_picker?: true, choices: socket.assigns.options})
else
assign(socket, %{show_picker?: true})
end
{:noreply, socket}
end
def handle_event(event, payload, socket) do
IO.inspect(event, label: :event)
IO.inspect(payload, label: :payload)
{:noreply, socket}
end
end

View File

@ -250,7 +250,7 @@ defmodule PlausibleWeb.Router do
get "/:website/settings/funnels", SiteController, :settings_funnels
# post "/:website/settings/funnels", SiteController, :save_funnel
# live "/:website/settings/funnels", Live.FunnelSettings
# live "/:website/settings/funnel2", Live.FunnelSettings
get "/:website/settings/search-console", SiteController, :settings_search_console
get "/:website/settings/email-reports", SiteController, :settings_email_reports

View File

@ -23,4 +23,7 @@
<% end %>
<%= link("+ Add goal", to: "/#{URI.encode_www_form(@site.domain)}/goals/new", class: "button mt-6") %>
<%= if Enum.count(@goals) >= 2 do %>
<%= link("Set up funnels", to: Routes.site_path(@conn, :settings_funnels, @site.domain), class: "button mt-6 ml-2") %>
<% end %>
</div>

View File

@ -4,12 +4,12 @@ defmodule Plausible.Repo.Migrations.InitFunnels do
def change do
create table(:funnels) do
add :name, :string, null: false
add :site_id, references(:sites), null: false
add :site_id, references(:sites, on_delete: :delete_all), null: false
timestamps()
end
create table(:funnel_steps) do
add :goal_id, references(:goals), null: false
add :goal_id, references(:goals, on_delete: :delete_all), null: false
add :funnel_id, references(:funnels, on_delete: :delete_all), null: false
add :step_order, :integer, null: false
timestamps()