diff --git a/CHANGELOG.md b/CHANGELOG.md index f9264ca7e2e6..882286bfe3ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index e665c42f87f4..ec7657dab822 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -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 &"])), ] } diff --git a/config/dev.exs b/config/dev.exs index 5b80c4c199b5..e1ede12af03b 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -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)$" diff --git a/extra/lib/plausible/funnel.ex b/extra/lib/plausible/funnel.ex index 208fec2afe42..abd3724f8737 100644 --- a/extra/lib/plausible/funnel.ex +++ b/extra/lib/plausible/funnel.ex @@ -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 diff --git a/extra/lib/plausible/funnels.ex b/extra/lib/plausible/funnels.ex index 831228cffd4f..84f30295d61a 100644 --- a/extra/lib/plausible/funnels.ex +++ b/extra/lib/plausible/funnels.ex @@ -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 diff --git a/extra/lib/plausible_web/live/funnel_settings.ex b/extra/lib/plausible_web/live/funnel_settings.ex index 2a00a028b6b3..a5859f961022 100644 --- a/extra/lib/plausible_web/live/funnel_settings.ex +++ b/extra/lib/plausible_web/live/funnel_settings.ex @@ -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"""
<.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 %> -
= Funnel.min_steps()}> - <.live_component - module={PlausibleWeb.Live.FunnelSettings.List} - id="funnels-list" - funnels={@displayed_funnels} - filter_text={@filter_text} - /> -
- -
- - 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. - -
<% end %> +
= Funnel.min_steps()}> + <.live_component + module={PlausibleWeb.Live.FunnelSettings.List} + id="funnels-list" + funnels={@displayed_funnels} + filter_text={@filter_text} + /> +
+ +
+ + 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. + +
""" 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 diff --git a/extra/lib/plausible_web/live/funnel_settings/form.ex b/extra/lib/plausible_web/live/funnel_settings/form.ex index e3f5248a5ea9..89cef4e79e07 100644 --- a/extra/lib/plausible_web/live/funnel_settings/form.ex +++ b/extra/lib/plausible_web/live/funnel_settings/form.ex @@ -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 >
-
+
<.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" > -

Add Funnel

+

+ <%= if @funnel, do: "Edit", else: "Add" %> Funnel +