analytics/lib/plausible_web/live/props_settings.ex
hq1 b3ff695797
Improve goal settings UX (#3293)
* Add Heroicons dependency

* Add name_of/1 html helper

Currently with Floki there's no way to query for
`[name=foo[some]]` selector

* Update changelog

* Make goal deletion possible with only goal id

* Remove stale goal controllers

* Improve ComboBox component

- make sure the list options are always of the parent input width
- allow passing a suggestion function instead of a module

* Stale fixup

* Update routes

* Use the new goals route in funnel settings

* Use a function in the funnel combo

* Use function in the props combo

* Remove old goals form

* Implement new goal settings

* Update moduledoc

* Fix revenue switch in dark mode

* Connect live socket on goal settings page

* Fixup

* Use Heroicons.trash icon

* Tweak goals search input

* Remove unused alias

* Fix search/button alignment

* Fix backspace icon alignment

* Delegate :superadmin check to get_for_user/3

I'll do props settings separately, it's work in progress
in a branch on top of this one already. cc @ukutaht

* Rename socket assigns

* Fixup to 5c9f58e

* Fixup

* Render ComboBox suggestions asynchronously

This commit:
  - prevents redundant work by checking the socket connection
  - allows passing no options to the ComboBox component,
    so that when combined with the `async` option, the options
    are asynchronously initialized post-render
  - allows updating the suggestions asynchronously with the
    `async` option set to `true` - helpful in case of DB
    queries used for suggestions

* Update tests

* Throttle comboboxes

* Update tests

* Dim the search input

* Use debounce=200 in ComboBox component

* Move creatable option to the top

* Ensure there's always a leading slash for goals

* Test pageview goals with leading / missing

* Make the modal scrollable on small viewports
2023-09-04 13:44:22 +02:00

179 lines
5.9 KiB
Elixir

defmodule PlausibleWeb.Live.PropsSettings do
@moduledoc """
LiveView allowing listing, allowing and disallowing custom event properties.
"""
use Phoenix.LiveView
use Phoenix.HTML
alias PlausibleWeb.Live.Components.ComboBox
def mount(
_params,
%{"site_id" => _site_id, "domain" => domain, "current_user_id" => user_id},
socket
) do
site =
if Plausible.Auth.is_super_admin?(user_id) do
Plausible.Sites.get_by_domain(domain)
else
Plausible.Sites.get_for_user!(user_id, domain, [:owner, :admin])
end
suggestions =
site
|> Plausible.Props.suggest_keys_to_allow()
|> Enum.map(&{&1, &1})
{:ok,
assign(socket,
site: site,
current_user_id: user_id,
form: new_form(site),
suggestions: suggestions
)}
end
def render(assigns) do
~H"""
<section id="props-settings-main">
<.live_component id="embedded_liveview_flash" module={PlausibleWeb.Live.Flash} flash={@flash} />
<h1 class="text-normal leading-6 font-medium text-gray-900 dark:text-gray-100">
Configured properties
</h1>
<h2 class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-400">
In order for the properties to show up on your dashboard, you need to
explicitly add them below first
</h2>
<.form :let={f} for={@form} id="props-form" phx-submit="allow" class="mt-5">
<div class="flex space-x-2">
<.live_component
id={:prop_input}
submit_name="prop"
class="flex-1"
module={ComboBox}
suggest_fun={&ComboBox.StaticSearch.suggest/2}
options={@suggestions}
required
creatable
/>
<button id="allow" type="submit" class="button">+ Add</button>
</div>
<div :if={length(f[:allowed_event_props].errors) > 0} id="prop-errors" role="alert">
<%= PlausibleWeb.ErrorHelpers.error_tag(f, :allowed_event_props) %>
</div>
</.form>
<button
:if={length(@suggestions) > 0}
title="Use this to add any existing properties from your past events into your settings. This allows you to set up properties without having to manually enter each item."
class="mt-1 text-sm hover:underline text-indigo-600 dark:text-indigo-400"
phx-click="allow-existing-props"
>
Already sending custom properties? Click to add all existing properties
</button>
<div class="mt-5">
<%= if is_list(@site.allowed_event_props) && length(@site.allowed_event_props) > 0 do %>
<ul id="allowed-props" class="divide-gray-200 divide-y dark:divide-gray-600">
<li
:for={{prop, index} <- Enum.with_index(@site.allowed_event_props)}
id={"prop-#{index}"}
class="flex py-4"
>
<span class="flex-1 truncate font-medium text-sm text-gray-800 dark:text-gray-200">
<%= prop %>
</span>
<button
data-confirm={"Are you sure you want to remove property '#{prop}'? This will just affect the UI, all of your analytics data will stay intact."}
phx-click="disallow"
phx-value-prop={prop}
class="w-4 h-4 text-red-600 hover:text-red-700"
aria-label={"Remove #{prop} property"}
>
<svg
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"
aria-hidden="true"
focusable="false"
>
<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>
</li>
</ul>
<% else %>
<p class="text-sm text-gray-800 dark:text-gray-200">
No properties configured for this site yet
</p>
<% end %>
</div>
</section>
"""
end
def handle_event("allow", %{"prop" => prop}, socket) do
case Plausible.Props.allow(socket.assigns.site, prop) do
{:ok, site} ->
send_update(ComboBox, id: :prop_input, display_value: "", submit_value: "")
{:noreply,
assign(socket,
site: site,
form: new_form(site),
suggestions: rebuild_suggestions(socket.assigns.suggestions, site.allowed_event_props)
)}
{:error, changeset} ->
{:noreply,
assign(socket,
form: to_form(Map.put(changeset, :action, :validate))
)}
end
end
def handle_event("disallow", %{"prop" => prop}, socket) do
{:ok, site} = Plausible.Props.disallow(socket.assigns.site, prop)
{:noreply, assign(socket, site: site)}
end
def handle_event("allow-existing-props", _params, socket) do
{:ok, site} = Plausible.Props.allow_existing_props(socket.assigns.site)
{:noreply,
assign(socket,
site: site,
suggestions: rebuild_suggestions(socket.assigns.suggestions, site.allowed_event_props)
)}
end
defp rebuild_suggestions(suggestions, allowed_event_props) do
allowed_event_props = allowed_event_props || []
suggestions =
for {suggestion, _} <- suggestions,
suggestion not in allowed_event_props,
do: {suggestion, suggestion}
send_update(ComboBox, id: :prop_input, suggestions: suggestions)
suggestions
end
defp new_form(site) do
to_form(Plausible.Props.allow_changeset(site, []))
end
end