mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 01:22:15 +03:00
Edit goals with display names (#4415)
* Update Goal schema
* Equip ComboBox with the ability of JS selection callbacks
* Update factory so display_name is always present
* Extend Goals context interface
* Update seeds
Also farming unsuspecting BEAM programmers for better
sample page paths :)
* Update ComboBox test
* Unify error message color class with helpers seen elsewhere
* Use goal.display_name where applicable
* Implement LiveView extensions for editing goals
* Sprinkle display name in external stats controller tests
* Format
* Fix goal list mobile view
* Update lib/plausible_web/live/goal_settings/list.ex
Co-authored-by: Artur Pata <artur.pata@gmail.com>
* Update lib/plausible_web/live/goal_settings/form.ex
Co-authored-by: Artur Pata <artur.pata@gmail.com>
* Update the APIs: plugins and external
* Update test so the intent is clearer
* Format
* Update CHANGELOG
* Simplify form tabs tests
* Revert "Format"
This reverts commit c1647b5307
.
* Fixup format commit that went too far
* ComboBox: select the input contents on first focus
* Update lib/plausible/goal/schema.ex
Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
* Update lib/plausible/goals/goals.ex
Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
* Update lib/plausible_web/live/goal_settings/form.ex
Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
* Pass form goal instead of just ID
* Make tab component dumber
* Extract separate render functions for edit and create forms
* Update test to account for extracted forms
* Inline goal get query
* Extract revenue goal settings to a component and avoid computing assigns in flight
* Make LV modal preload optional
* Disable preload for goal settings form modal
* Get rid of phash component ID hack
* For another render after render_submit when testing goal updates
* Fix LV preload option
* Enable preload back for goals modal for now
* Make formatter happy
* Implement support for preopening of LV modal
* Preopen goals modal to avoid feedback gap on loading edited goal
* Remove `console.log` call from modal JS
* Clean up display name input IDs
* Make revenue settings functional on first edit again
* Display names: 2nd stage migration
* Update migration with data backfill
---------
Co-authored-by: Artur Pata <artur.pata@gmail.com>
Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
This commit is contained in:
parent
2e6ba4ba42
commit
cc769dfb3d
@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- UI to edit goals along with display names
|
||||
- Support contains filter for goals
|
||||
- UI to edit funnels
|
||||
- Add Details views for browsers, browser versions, os-s, os versions, and screen sizes reports
|
||||
|
@ -6,12 +6,17 @@ export default (id) => ({
|
||||
id: id,
|
||||
focus: null,
|
||||
selectionInProgress: false,
|
||||
firstFocusRegistered: false,
|
||||
setFocus(f) {
|
||||
this.focus = f;
|
||||
},
|
||||
initFocus() {
|
||||
if (this.focus === null) {
|
||||
this.setFocus(this.leastFocusableIndex())
|
||||
if (!this.firstFocusRegistered) {
|
||||
document.getElementById(this.id).select();
|
||||
this.firstFocusRegistered = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
trackSubmitValueChange() {
|
||||
|
@ -11,3 +11,8 @@ window.addEventListener(`phx:js-exec`, ({ detail }) => {
|
||||
window.liveSocket.execJS(el, el.getAttribute(detail.attr))
|
||||
})
|
||||
})
|
||||
|
||||
window.addEventListener(`phx:notify-selection-change`, (event) => {
|
||||
let el = document.getElementById(event.detail.id)
|
||||
el.dispatchEvent(new CustomEvent("selection-change", { detail: event.detail }))
|
||||
})
|
||||
|
@ -14,9 +14,18 @@ defmodule Plausible.Goal.Revenue do
|
||||
def currency_options() do
|
||||
options =
|
||||
for code <- valid_currencies() do
|
||||
{code, "#{code} - #{Cldr.Currency.display_name!(code)}"}
|
||||
{code, display(code)}
|
||||
end
|
||||
|
||||
options
|
||||
end
|
||||
|
||||
def currency_option(code) do
|
||||
true = "#{code}" in valid_currencies()
|
||||
{"#{code}", display(code)}
|
||||
end
|
||||
|
||||
def display(code) do
|
||||
"#{code} - #{Cldr.Currency.display_name!(code)}"
|
||||
end
|
||||
end
|
||||
|
@ -41,7 +41,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||
Enum.map(page.entries, fn goal ->
|
||||
%{
|
||||
id: goal.id,
|
||||
display_name: Goal.display_name(goal),
|
||||
display_name: goal.display_name,
|
||||
goal_type: Goal.type(goal),
|
||||
event_name: goal.event_name,
|
||||
page_path: goal.page_path
|
||||
|
@ -23,7 +23,10 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
|> Goals.for_site()
|
||||
|> Enum.map(fn goal ->
|
||||
{goal.id,
|
||||
struct!(Plausible.Goal, Map.take(goal, [:id, :event_name, :page_path, :currency]))}
|
||||
struct!(
|
||||
Plausible.Goal,
|
||||
Map.take(goal, [:id, :display_name, :event_name, :page_path, :currency])
|
||||
)}
|
||||
end)
|
||||
|
||||
socket =
|
||||
|
@ -8,6 +8,7 @@ defmodule Plausible.Goal do
|
||||
schema "goals" do
|
||||
field :event_name, :string
|
||||
field :page_path, :string
|
||||
field :display_name, :string
|
||||
|
||||
on_ee do
|
||||
field :currency, Ecto.Enum, values: Money.Currency.known_current_currencies()
|
||||
@ -22,7 +23,8 @@ defmodule Plausible.Goal do
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@fields [:id, :site_id, :event_name, :page_path] ++ on_ee(do: [:currency], else: [])
|
||||
@fields [:id, :site_id, :event_name, :page_path, :display_name] ++
|
||||
on_ee(do: [:currency], else: [])
|
||||
|
||||
@max_event_name_length 120
|
||||
|
||||
@ -35,10 +37,10 @@ defmodule Plausible.Goal do
|
||||
|> cast_assoc(:site)
|
||||
|> update_leading_slash()
|
||||
|> validate_event_name_and_page_path()
|
||||
|> update_change(:event_name, &String.trim/1)
|
||||
|> update_change(:page_path, &String.trim/1)
|
||||
|> maybe_put_display_name()
|
||||
|> unique_constraint(:event_name, name: :goals_event_name_unique)
|
||||
|> unique_constraint(:page_path, name: :goals_page_path_unique)
|
||||
|> unique_constraint(:display_name, name: :goals_site_id_display_name_index)
|
||||
|> validate_length(:event_name, max: @max_event_name_length)
|
||||
|> check_constraint(:event_name,
|
||||
name: :check_event_name_or_page_path,
|
||||
@ -48,12 +50,8 @@ defmodule Plausible.Goal do
|
||||
end
|
||||
|
||||
@spec display_name(t()) :: String.t()
|
||||
def display_name(%{page_path: path}) when is_binary(path) do
|
||||
"Visit " <> path
|
||||
end
|
||||
|
||||
def display_name(%{event_name: name}) when is_binary(name) do
|
||||
name
|
||||
def display_name(goal) do
|
||||
goal.display_name
|
||||
end
|
||||
|
||||
@spec type(t()) :: :event | :page
|
||||
@ -76,6 +74,8 @@ defmodule Plausible.Goal do
|
||||
defp validate_event_name_and_page_path(changeset) do
|
||||
if validate_page_path(changeset) || validate_event_name(changeset) do
|
||||
changeset
|
||||
|> update_change(:event_name, &String.trim/1)
|
||||
|> update_change(:page_path, &String.trim/1)
|
||||
else
|
||||
changeset
|
||||
|> add_error(:event_name, "this field is required and cannot be blank")
|
||||
@ -100,6 +100,24 @@ defmodule Plausible.Goal do
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_put_display_name(changeset) do
|
||||
clause =
|
||||
Enum.map([:display_name, :page_path, :event_name], &get_field(changeset, &1))
|
||||
|
||||
case clause do
|
||||
[nil, path, _] when is_binary(path) ->
|
||||
put_change(changeset, :display_name, "Visit " <> path)
|
||||
|
||||
[nil, _, event_name] when is_binary(event_name) ->
|
||||
put_change(changeset, :display_name, event_name)
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
|> update_change(:display_name, &String.trim/1)
|
||||
|> validate_required(:display_name)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Jason.Encoder, for: Plausible.Goal do
|
||||
@ -110,14 +128,14 @@ defimpl Jason.Encoder, for: Plausible.Goal do
|
||||
|> Map.put(:goal_type, Plausible.Goal.type(value))
|
||||
|> Map.take([:id, :goal_type, :event_name, :page_path])
|
||||
|> Map.put(:domain, domain)
|
||||
|> Map.put(:display_name, Plausible.Goal.display_name(value))
|
||||
|> Map.put(:display_name, value.display_name)
|
||||
|> Jason.Encode.map(opts)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: Plausible.Goal do
|
||||
def to_string(goal) do
|
||||
Plausible.Goal.display_name(goal)
|
||||
goal.display_name
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -8,6 +8,17 @@ defmodule Plausible.Goals do
|
||||
alias Plausible.Goal
|
||||
alias Ecto.Multi
|
||||
|
||||
@spec get(Plausible.Site.t(), pos_integer()) :: nil | Plausible.Goal.t()
|
||||
def get(site, id) when is_integer(id) do
|
||||
q =
|
||||
from g in Plausible.Goal,
|
||||
where: g.site_id == ^site.id,
|
||||
order_by: [desc: g.id],
|
||||
where: g.id == ^id
|
||||
|
||||
Repo.one(q)
|
||||
end
|
||||
|
||||
@spec create(Plausible.Site.t(), map(), Keyword.t()) ::
|
||||
{:ok, Goal.t()} | {:error, Ecto.Changeset.t()} | {:error, :upgrade_required}
|
||||
@doc """
|
||||
@ -41,14 +52,31 @@ defmodule Plausible.Goals do
|
||||
end)
|
||||
end
|
||||
|
||||
def find_or_create(site, %{
|
||||
"goal_type" => "event",
|
||||
"event_name" => event_name,
|
||||
"currency" => currency
|
||||
})
|
||||
when is_binary(event_name) and is_binary(currency) do
|
||||
params = %{"event_name" => event_name, "currency" => currency}
|
||||
@spec update(Plausible.Goal.t(), map()) ::
|
||||
{:ok, Goal.t()} | {:error, Ecto.Changeset.t()} | {:error, :upgrade_required}
|
||||
def update(goal, params) do
|
||||
changeset = Goal.changeset(goal, params)
|
||||
site = Repo.preload(goal, :site).site
|
||||
|
||||
with :ok <- maybe_check_feature_access(site, changeset),
|
||||
{:ok, goal} <- Repo.update(changeset) do
|
||||
on_ee do
|
||||
{:ok, Repo.preload(goal, :funnels)}
|
||||
else
|
||||
{:ok, goal}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create(
|
||||
site,
|
||||
%{
|
||||
"goal_type" => "event",
|
||||
"event_name" => event_name,
|
||||
"currency" => currency
|
||||
} = params
|
||||
)
|
||||
when is_binary(event_name) and is_binary(currency) do
|
||||
with {:ok, goal} <- create(site, params, upsert?: true) do
|
||||
if to_string(goal.currency) == currency do
|
||||
{:ok, goal}
|
||||
@ -67,15 +95,15 @@ defmodule Plausible.Goals do
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create(site, %{"goal_type" => "event", "event_name" => event_name})
|
||||
def find_or_create(site, %{"goal_type" => "event", "event_name" => event_name} = params)
|
||||
when is_binary(event_name) do
|
||||
create(site, %{"event_name" => event_name}, upsert?: true)
|
||||
create(site, params, upsert?: true)
|
||||
end
|
||||
|
||||
def find_or_create(_, %{"goal_type" => "event"}), do: {:missing, "event_name"}
|
||||
|
||||
def find_or_create(site, %{"goal_type" => "page", "page_path" => page_path}) do
|
||||
create(site, %{"page_path" => page_path}, upsert?: true)
|
||||
def find_or_create(site, %{"goal_type" => "page", "page_path" => _} = params) do
|
||||
create(site, params, upsert?: true)
|
||||
end
|
||||
|
||||
def find_or_create(_, %{"goal_type" => "page"}), do: {:missing, "page_path"}
|
||||
|
@ -5,7 +5,6 @@ defmodule Plausible.Plugins.API.Goals do
|
||||
"""
|
||||
use Plausible
|
||||
|
||||
import Ecto.Query
|
||||
import Plausible.Pagination
|
||||
|
||||
alias Plausible.Repo
|
||||
@ -36,10 +35,7 @@ defmodule Plausible.Plugins.API.Goals do
|
||||
|
||||
@spec get(Plausible.Site.t(), pos_integer()) :: nil | Plausible.Goal.t()
|
||||
def get(site, id) when is_integer(id) do
|
||||
site
|
||||
|> get_query()
|
||||
|> where([g], g.id == ^id)
|
||||
|> Repo.one()
|
||||
Plausible.Goals.get(site, id)
|
||||
end
|
||||
|
||||
@spec delete(Plausible.Site.t(), [pos_integer()] | pos_integer()) :: :ok
|
||||
@ -55,13 +51,6 @@ defmodule Plausible.Plugins.API.Goals do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp get_query(site) do
|
||||
from g in Plausible.Goal,
|
||||
where: g.site_id == ^site.id,
|
||||
order_by: [desc: g.id],
|
||||
group_by: g.id
|
||||
end
|
||||
|
||||
defp convert_to_create_params(%CreateRequest.CustomEvent{goal: %{event_name: event_name}}) do
|
||||
%{"goal_type" => "event", "event_name" => event_name}
|
||||
end
|
||||
|
@ -155,7 +155,7 @@ defmodule Plausible.Stats.FilterSuggestions do
|
||||
def filter_suggestions(site, _query, "goal", filter_search) do
|
||||
site
|
||||
|> Plausible.Goals.for_site()
|
||||
|> Enum.map(&Plausible.Goal.display_name/1)
|
||||
|> Enum.map(& &1.display_name)
|
||||
|> Enum.filter(fn goal ->
|
||||
String.contains?(
|
||||
String.downcase(goal),
|
||||
|
@ -341,8 +341,9 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
||||
|
||||
defp validate_filter(site, [_type, "event:goal", goal_filter]) do
|
||||
configured_goals =
|
||||
Plausible.Goals.for_site(site)
|
||||
|> Enum.map(&Plausible.Goal.display_name/1)
|
||||
site
|
||||
|> Plausible.Goals.for_site()
|
||||
|> Enum.map(& &1.display_name)
|
||||
|
||||
goals_in_filter = List.wrap(goal_filter)
|
||||
|
||||
|
@ -104,6 +104,7 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
|
||||
name={"display-#{@id}"}
|
||||
placeholder={@placeholder}
|
||||
x-on:focus="open"
|
||||
x-on:selection-change={assigns[:"x-on-selection-change"]}
|
||||
phx-change="search"
|
||||
x-on:keydown="open"
|
||||
phx-target={@myself}
|
||||
@ -268,8 +269,14 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
|
||||
"""
|
||||
end
|
||||
|
||||
def select_option(js \\ %JS{}, _id, submit_value, display_value) do
|
||||
def select_option(js \\ %JS{}, id, submit_value, display_value) do
|
||||
js
|
||||
|> JS.dispatch("phx:notify-selection-change",
|
||||
detail: %{
|
||||
id: id,
|
||||
value: %{"submitValue" => submit_value, "displayValue" => display_value}
|
||||
}
|
||||
)
|
||||
|> JS.push("select-option",
|
||||
value: %{"submit-value" => submit_value, "display-value" => display_value}
|
||||
)
|
||||
@ -363,6 +370,12 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
|
||||
{{submit_value, display_value}, nil} ->
|
||||
assign(socket, submit_value: submit_value, display_value: display_value)
|
||||
|
||||
{submit_and_display_value, nil} when is_binary(submit_and_display_value) ->
|
||||
assign(socket,
|
||||
submit_value: submit_and_display_value,
|
||||
display_value: submit_and_display_value
|
||||
)
|
||||
|
||||
_ ->
|
||||
socket
|
||||
end
|
||||
|
@ -52,13 +52,22 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
be skipped if embedded component handles state reset explicitly
|
||||
(via, for instance, `phx-click-away` callback).
|
||||
|
||||
`Modal` exposes two functions for managing window state:
|
||||
`Modal` exposes a number of functions for managing window state:
|
||||
|
||||
* `Modal.JS.preopen/1` - to preopen the modal on the frontend.
|
||||
Useful when the actual opening is done server-side with
|
||||
`Modal.open/2` - helps avoid lack of feedback to the end user
|
||||
when server-side state change before opening the modal is
|
||||
still in progress.
|
||||
* `Modal.JS.open/1` - to open the modal from the frontend. It's
|
||||
important to make sure the element triggering that call is
|
||||
wrapped in an Alpine UI component - or is an Alpine component
|
||||
itself - adding `x-data` attribute without any value is enough
|
||||
to ensure that.
|
||||
* `Modal.open/2` - to open the modal from the backend; usually
|
||||
called from `handle_event/2` of component wrapping the modal
|
||||
and providing the state. Should be used together with
|
||||
`Modal.JS.preopen/1` for optimal user experience.
|
||||
* `Modal.close/2` - to close the modal from the backend; usually
|
||||
done inside wrapped component's `handle_event/2`. The example
|
||||
quoted above shows one way to implement this, under that assumption
|
||||
@ -104,6 +113,11 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
def open(id) do
|
||||
"$dispatch('open-modal', '#{id}')"
|
||||
end
|
||||
|
||||
@spec preopen(String.t()) :: String.t()
|
||||
def preopen(id) do
|
||||
"$dispatch('preopen-modal', '#{id}')"
|
||||
end
|
||||
end
|
||||
|
||||
@spec open(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
|
||||
@ -118,6 +132,8 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
preload? = Map.get(assigns, :preload?, true)
|
||||
|
||||
socket =
|
||||
assign(socket,
|
||||
id: assigns.id,
|
||||
@ -127,7 +143,8 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
# established. Otherwise, there will be problems
|
||||
# with live components relying on ID for setup
|
||||
# on mount (using AlpineJS, for instance).
|
||||
load_content?: true,
|
||||
load_content?: preload?,
|
||||
preload?: preload?,
|
||||
modal_sequence_id: 0
|
||||
)
|
||||
|
||||
@ -136,6 +153,7 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
|
||||
attr :id, :any, required: true
|
||||
attr :class, :string, default: ""
|
||||
attr :preload?, :boolean, default: true
|
||||
slot :inner_block, required: true
|
||||
|
||||
def render(assigns) do
|
||||
@ -159,6 +177,12 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
x-data="{
|
||||
firstLoadDone: false,
|
||||
modalOpen: false,
|
||||
modalPreopen: false,
|
||||
preopenModal() {
|
||||
document.body.style['overflow-y'] = 'hidden';
|
||||
|
||||
this.modalPreopen = true;
|
||||
},
|
||||
openModal() {
|
||||
document.body.style['overflow-y'] = 'hidden';
|
||||
|
||||
@ -168,9 +192,11 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
this.firstLoadDone = true;
|
||||
}
|
||||
|
||||
this.modalPreopen = false;
|
||||
this.modalOpen = true;
|
||||
},
|
||||
closeModal() {
|
||||
this.modalPreopen = false;
|
||||
this.modalOpen = false;
|
||||
liveSocket.execJS($el, $el.dataset.onclose);
|
||||
|
||||
@ -179,6 +205,8 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
}, 200);
|
||||
}
|
||||
}"
|
||||
x-init={"firstLoadDone = #{not @preload?}"}
|
||||
x-on:preopen-modal.window={"if ($event.detail === '#{@id}') preopenModal()"}
|
||||
x-on:open-modal.window={"if ($event.detail === '#{@id}') openModal()"}
|
||||
x-on:close-modal.window={"if ($event.detail === '#{@id}') closeModal()"}
|
||||
data-onopen={LiveView.JS.push("open", target: @myself)}
|
||||
@ -188,7 +216,7 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
x-show="modalOpen"
|
||||
x-show="modalOpen || modalPreopen"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="bg-opacity-0"
|
||||
x-transition:enter-end="bg-opacity-75"
|
||||
@ -198,6 +226,16 @@ defmodule PlausibleWeb.Live.Components.Modal do
|
||||
class="fixed inset-0 bg-gray-500 bg-opacity-75 z-50"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
x-show="modalPreopen"
|
||||
class="fixed flex inset-0 items-start z-50 overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<div class="modal-pre-loading w-full self-center">
|
||||
<div class="text-center">
|
||||
<PlausibleWeb.Components.Generic.spinner class="inline-block h-8 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
x-show="modalOpen"
|
||||
class="fixed flex inset-0 items-start z-50 overflow-y-auto overflow-x-hidden"
|
||||
|
@ -43,7 +43,8 @@ defmodule PlausibleWeb.Live.GoalSettings do
|
||||
site_id: site_id,
|
||||
domain: domain,
|
||||
displayed_goals: socket.assigns.all_goals,
|
||||
filter_text: ""
|
||||
filter_text: "",
|
||||
form_goal: nil
|
||||
)}
|
||||
end
|
||||
|
||||
@ -53,6 +54,7 @@ defmodule PlausibleWeb.Live.GoalSettings do
|
||||
~H"""
|
||||
<div id="goal-settings-main">
|
||||
<.flash_messages flash={@flash} />
|
||||
|
||||
<.live_component :let={modal_unique_id} module={Modal} id="goals-form-modal">
|
||||
<.live_component
|
||||
module={PlausibleWeb.Live.GoalSettings.Form}
|
||||
@ -63,6 +65,7 @@ defmodule PlausibleWeb.Live.GoalSettings do
|
||||
site={@site}
|
||||
current_user={@current_user}
|
||||
existing_goals={@all_goals}
|
||||
goal={@form_goal}
|
||||
on_save_goal={
|
||||
fn goal, socket ->
|
||||
send(self(), {:goal_added, goal})
|
||||
@ -103,6 +106,19 @@ defmodule PlausibleWeb.Live.GoalSettings do
|
||||
{:noreply, assign(socket, displayed_goals: new_list, filter_text: filter_text)}
|
||||
end
|
||||
|
||||
def handle_event("edit-goal", %{"goal-id" => goal_id}, socket) do
|
||||
goal_id = String.to_integer(goal_id)
|
||||
form_goal = Plausible.Goals.get(socket.assigns.site, goal_id)
|
||||
|
||||
socket = socket |> assign(form_goal: form_goal) |> Modal.open("goals-form-modal")
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("add-goal", _, socket) do
|
||||
socket = socket |> assign(form_goal: nil) |> Modal.open("goals-form-modal")
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("delete-goal", %{"goal-id" => goal_id} = params, socket) do
|
||||
goal_id = String.to_integer(goal_id)
|
||||
|
||||
@ -132,14 +148,17 @@ defmodule PlausibleWeb.Live.GoalSettings do
|
||||
end
|
||||
|
||||
def handle_info({:goal_added, goal}, socket) do
|
||||
all_goals = Goals.for_site(socket.assigns.site, preload_funnels?: true)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(
|
||||
filter_text: "",
|
||||
all_goals: [goal | socket.assigns.all_goals],
|
||||
all_goals: all_goals,
|
||||
event_name_options:
|
||||
Enum.reject(socket.assigns.event_name_options, &(&1 == goal.event_name)),
|
||||
displayed_goals: [goal | socket.assigns.all_goals]
|
||||
displayed_goals: all_goals,
|
||||
form_goal: nil
|
||||
)
|
||||
|> put_live_flash(:success, "Goal saved successfully")
|
||||
|
||||
|
@ -11,7 +11,7 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
alias Plausible.Repo
|
||||
|
||||
def update(assigns, socket) do
|
||||
site = Repo.preload(assigns.site, [:owner])
|
||||
site = Repo.preload(assigns.site, :owner)
|
||||
owner = Plausible.Users.with_subscription(site.owner)
|
||||
site = %{site | owner: owner}
|
||||
|
||||
@ -19,10 +19,17 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
Plausible.Billing.Feature.RevenueGoals.check_availability(owner) == :ok
|
||||
|
||||
form =
|
||||
%Plausible.Goal{}
|
||||
(assigns.goal || %Plausible.Goal{})
|
||||
|> Plausible.Goal.changeset()
|
||||
|> to_form()
|
||||
|
||||
selected_tab =
|
||||
if assigns.goal && assigns.goal.page_path do
|
||||
"pageviews"
|
||||
else
|
||||
"custom_events"
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(
|
||||
@ -33,87 +40,135 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
event_name_options: Enum.map(assigns.event_name_options, &{&1, &1}),
|
||||
current_user: assigns.current_user,
|
||||
domain: assigns.domain,
|
||||
selected_tab: "custom_events",
|
||||
selected_tab: selected_tab,
|
||||
tab_sequence_id: 0,
|
||||
site: site,
|
||||
has_access_to_revenue_goals?: has_access_to_revenue_goals?,
|
||||
existing_goals: assigns.existing_goals,
|
||||
on_save_goal: assigns.on_save_goal,
|
||||
on_autoconfigure: assigns.on_autoconfigure
|
||||
on_autoconfigure: assigns.on_autoconfigure,
|
||||
goal: assigns.goal
|
||||
)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
# Regular functions instead of component calls are used here
|
||||
# explicitly to avoid breaking change tracking. Done following
|
||||
# advice from https://hexdocs.pm/phoenix_live_view/assigns-eex.html#the-assigns-variable.
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={@id}>
|
||||
<.form
|
||||
:let={f}
|
||||
x-data="{ tabSelectionInProgress: false }"
|
||||
for={@form}
|
||||
phx-submit="save-goal"
|
||||
<%= if @goal, do: edit_form(assigns) %>
|
||||
<%= if is_nil(@goal), do: create_form(assigns) %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def edit_form(assigns) do
|
||||
~H"""
|
||||
<.form :let={f} for={@form} phx-submit="save-goal" phx-target={@myself}>
|
||||
<h2 class="text-xl font-black dark:text-gray-100">
|
||||
Edit Goal for <%= @domain %>
|
||||
</h2>
|
||||
|
||||
<.custom_event_fields
|
||||
:if={@selected_tab == "custom_events"}
|
||||
f={f}
|
||||
suffix={@context_unique_id}
|
||||
current_user={@current_user}
|
||||
site={@site}
|
||||
goal={@goal}
|
||||
existing_goals={@existing_goals}
|
||||
goal_options={@event_name_options}
|
||||
has_access_to_revenue_goals?={@has_access_to_revenue_goals?}
|
||||
/>
|
||||
<.pageview_fields
|
||||
:if={@selected_tab == "pageviews"}
|
||||
f={f}
|
||||
goal={@goal}
|
||||
suffix={@context_unique_id}
|
||||
site={@site}
|
||||
/>
|
||||
|
||||
<div class="py-4">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Update Goal →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</.form>
|
||||
"""
|
||||
end
|
||||
|
||||
def create_form(assigns) do
|
||||
~H"""
|
||||
<.form
|
||||
:let={f}
|
||||
x-data="{ tabSelectionInProgress: false }"
|
||||
for={@form}
|
||||
phx-submit="save-goal"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.spinner
|
||||
class="spinner block absolute right-9 top-8"
|
||||
x-show="tabSelectionInProgress"
|
||||
/>
|
||||
|
||||
<h2 class="text-xl font-black dark:text-gray-100">
|
||||
Add Goal for <%= @domain %>
|
||||
</h2>
|
||||
|
||||
<.tabs selected_tab={@selected_tab} myself={@myself} />
|
||||
|
||||
<.custom_event_fields
|
||||
:if={@selected_tab == "custom_events"}
|
||||
x-show="!tabSelectionInProgress"
|
||||
f={f}
|
||||
suffix={suffix(@context_unique_id, @tab_sequence_id)}
|
||||
current_user={@current_user}
|
||||
site={@site}
|
||||
existing_goals={@existing_goals}
|
||||
goal_options={@event_name_options}
|
||||
has_access_to_revenue_goals?={@has_access_to_revenue_goals?}
|
||||
x-init="tabSelectionInProgress = false"
|
||||
/>
|
||||
<.pageview_fields
|
||||
:if={@selected_tab == "pageviews"}
|
||||
x-show="!tabSelectionInProgress"
|
||||
f={f}
|
||||
suffix={suffix(@context_unique_id, @tab_sequence_id)}
|
||||
site={@site}
|
||||
x-init="tabSelectionInProgress = false"
|
||||
/>
|
||||
|
||||
<div class="py-4" x-show="!tabSelectionInProgress">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Goal →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:if={@selected_tab == "custom_events" && @event_name_options_count > 0}
|
||||
x-show="!tabSelectionInProgress"
|
||||
class="mt-2 text-sm hover:underline text-indigo-600 dark:text-indigo-400 text-left"
|
||||
phx-click="autoconfigure"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.spinner
|
||||
class="spinner block absolute right-9 top-8"
|
||||
x-show="tabSelectionInProgress"
|
||||
/>
|
||||
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Add Goal for <%= @domain %></h2>
|
||||
|
||||
<.tabs selected_tab={@selected_tab} myself={@myself} />
|
||||
|
||||
<.custom_event_fields
|
||||
:if={@selected_tab == "custom_events"}
|
||||
x-show="!tabSelectionInProgress"
|
||||
f={f}
|
||||
suffix={suffix(@context_unique_id, @tab_sequence_id)}
|
||||
current_user={@current_user}
|
||||
site={@site}
|
||||
existing_goals={@existing_goals}
|
||||
goal_options={@event_name_options}
|
||||
has_access_to_revenue_goals?={@has_access_to_revenue_goals?}
|
||||
x-init="tabSelectionInProgress = false"
|
||||
/>
|
||||
<.pageview_fields
|
||||
:if={@selected_tab == "pageviews"}
|
||||
x-show="!tabSelectionInProgress"
|
||||
f={f}
|
||||
suffix={suffix(@context_unique_id, @tab_sequence_id)}
|
||||
site={@site}
|
||||
x-init="tabSelectionInProgress = false"
|
||||
/>
|
||||
|
||||
<div class="py-4" x-show="!tabSelectionInProgress">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Goal →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:if={@selected_tab == "custom_events" && @event_name_options_count > 0}
|
||||
x-show="!tabSelectionInProgress"
|
||||
class="mt-2 text-sm hover:underline text-indigo-600 dark:text-indigo-400 text-left"
|
||||
phx-click="autoconfigure"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<span :if={@event_name_options_count > 1}>
|
||||
Already sending custom events? We've found <%= @event_name_options_count %> custom events from the last 6 months that are not yet configured as goals. Click here to add them.
|
||||
</span>
|
||||
<span :if={@event_name_options_count == 1}>
|
||||
Already sending custom events? We've found 1 custom event from the last 6 months that is not yet configured as a goal. Click here to add it.
|
||||
</span>
|
||||
</button>
|
||||
</.form>
|
||||
</div>
|
||||
<span :if={@event_name_options_count > 1}>
|
||||
Already sending custom events? We've found <%= @event_name_options_count %> custom events from the last 6 months that are not yet configured as goals. Click here to add them.
|
||||
</span>
|
||||
<span :if={@event_name_options_count == 1}>
|
||||
Already sending custom events? We've found 1 custom event from the last 6 months that is not yet configured as a goal. Click here to add it.
|
||||
</span>
|
||||
</button>
|
||||
</.form>
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:f, Phoenix.HTML.Form)
|
||||
attr(:site, Plausible.Site)
|
||||
attr(:suffix, :string)
|
||||
|
||||
attr(:goal, Plausible.Goal, default: nil)
|
||||
attr(:rest, :global)
|
||||
|
||||
def pageview_fields(assigns) do
|
||||
@ -131,14 +186,29 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
]}
|
||||
module={ComboBox}
|
||||
suggest_fun={fn input, _options -> suggest_page_paths(input, @site) end}
|
||||
selected={if @goal && @goal.page_path, do: @goal.page_path}
|
||||
creatable
|
||||
x-on-selection-change="document.getElementById('pageview_display_name_input').setAttribute('value', 'Visit ' + $event.detail.value.displayValue)"
|
||||
/>
|
||||
|
||||
<.error :for={{msg, opts} <- @f[:page_path].errors}>
|
||||
<%= Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
||||
end) %>
|
||||
<.error :for={msg <- Enum.map(@f[:page_path].errors, &translate_error/1)}>
|
||||
<%= msg %>
|
||||
</.error>
|
||||
|
||||
<div class="mt-2">
|
||||
<.label for="pageview_display_name_input">
|
||||
Display Name
|
||||
</.label>
|
||||
|
||||
<.input
|
||||
id="pageview_display_name_input"
|
||||
field={@f[:display_name]}
|
||||
type="text"
|
||||
x-data="{ firstFocus: true }"
|
||||
x-on:focus="if (firstFocus) { $el.select(); firstFocus = false; }"
|
||||
class="mt-2 dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@ -149,6 +219,7 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
attr(:suffix, :string)
|
||||
attr(:existing_goals, :list)
|
||||
attr(:goal_options, :list)
|
||||
attr(:goal, Plausible.Goal, default: nil)
|
||||
attr(:has_access_to_revenue_goals?, :boolean)
|
||||
|
||||
attr(:rest, :global)
|
||||
@ -181,92 +252,124 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
module={ComboBox}
|
||||
suggest_fun={fn input, _options -> suggest_event_names(input, @site, @existing_goals) end}
|
||||
options={@goal_options}
|
||||
selected={if @goal && @goal.event_name, do: @goal.event_name}
|
||||
creatable
|
||||
x-on-selection-change="document.getElementById('custom_event_display_name_input').setAttribute('value', $event.detail.value.displayValue)"
|
||||
/>
|
||||
|
||||
<.error :for={msg <- Enum.map(@f[:event_name].errors, &translate_error/1)}>
|
||||
<%= msg %>
|
||||
</.error>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<.label for="custom_event_display_name_input">
|
||||
Display Name
|
||||
</.label>
|
||||
|
||||
<.input
|
||||
id="custom_event_display_name_input"
|
||||
field={@f[:display_name]}
|
||||
type="text"
|
||||
x-data="{ firstFocus: true }"
|
||||
x-on:focus="if (firstFocus) { $el.select(); firstFocus = false; }"
|
||||
class="mt-2 dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<.revenue_goal_settings
|
||||
:if={ee?()}
|
||||
class="mt-6 space-y-3"
|
||||
x-data={
|
||||
Jason.encode!(%{
|
||||
active: !!@f[:currency].value and @f[:currency].value != "",
|
||||
currency: @f[:currency].value
|
||||
})
|
||||
}
|
||||
f={@f}
|
||||
site={@site}
|
||||
current_user={@current_user}
|
||||
has_access_to_revenue_goals?={@has_access_to_revenue_goals?}
|
||||
goal={@goal}
|
||||
suffix={@suffix}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def revenue_goal_settings(assigns) do
|
||||
js_data =
|
||||
Jason.encode!(%{
|
||||
active: !!assigns.f[:currency].value and assigns.f[:currency].value != "",
|
||||
currency: assigns.f[:currency].value
|
||||
})
|
||||
|
||||
assigns = assign(assigns, selected_currency: currency_option(assigns.goal), js_data: js_data)
|
||||
|
||||
~H"""
|
||||
<div class="mt-6 space-y-3" x-data={@js_data}>
|
||||
<PlausibleWeb.Components.Billing.Notice.premium_feature
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
feature_mod={Plausible.Billing.Feature.RevenueGoals}
|
||||
size={:xs}
|
||||
class="rounded-b-md"
|
||||
/>
|
||||
<button
|
||||
id={"currency-toggle-#{@suffix}"}
|
||||
class={[
|
||||
"flex items-center w-max mb-3",
|
||||
if @has_access_to_revenue_goals? and is_nil(@goal) do
|
||||
"cursor-pointer"
|
||||
else
|
||||
"cursor-not-allowed"
|
||||
end
|
||||
]}
|
||||
aria-labelledby="enable-revenue-tracking"
|
||||
role="switch"
|
||||
type="button"
|
||||
x-on:click="active = !active; currency = ''"
|
||||
x-bind:aria-checked="active"
|
||||
disabled={not @has_access_to_revenue_goals? or not is_nil(@goal)}
|
||||
>
|
||||
<span
|
||||
id={"currency-container1-#{@suffix}"}
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
|
||||
x-bind:class="active ? 'bg-indigo-600' : 'dark:bg-gray-700 bg-gray-200'"
|
||||
>
|
||||
<PlausibleWeb.Components.Billing.Notice.premium_feature
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
feature_mod={Plausible.Billing.Feature.RevenueGoals}
|
||||
size={:xs}
|
||||
class="rounded-b-md"
|
||||
<span
|
||||
id={"currency-container2-#{@suffix}"}
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
x-bind:class="active ? 'dark:bg-gray-800 translate-x-5' : 'dark:bg-gray-800 translate-x-0'"
|
||||
/>
|
||||
<button
|
||||
class={[
|
||||
"flex items-center w-max mb-3",
|
||||
if @has_access_to_revenue_goals? do
|
||||
"cursor-pointer"
|
||||
else
|
||||
"cursor-not-allowed"
|
||||
</span>
|
||||
<span
|
||||
class={[
|
||||
"ml-3 font-medium",
|
||||
if(@has_access_to_revenue_goals?,
|
||||
do: "text-gray-900 dark:text-gray-100",
|
||||
else: "text-gray-500 dark:text-gray-300"
|
||||
)
|
||||
]}
|
||||
id="enable-revenue-tracking"
|
||||
>
|
||||
Enable Revenue Tracking
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div x-show="active" id={"revenue-input-#{@suffix}"}>
|
||||
<.live_component
|
||||
id={"currency_input_#{@suffix}"}
|
||||
submit_name={@f[:currency].name}
|
||||
module={ComboBox}
|
||||
selected={@selected_currency}
|
||||
suggest_fun={
|
||||
on_ee do
|
||||
fn
|
||||
"", [] ->
|
||||
Plausible.Goal.Revenue.currency_options()
|
||||
|
||||
input, options ->
|
||||
ComboBox.StaticSearch.suggest(input, options, weight_threshold: 0.8)
|
||||
end
|
||||
]}
|
||||
aria-labelledby="enable-revenue-tracking"
|
||||
role="switch"
|
||||
type="button"
|
||||
x-on:click="active = !active; currency = ''"
|
||||
x-bind:aria-checked="active"
|
||||
disabled={not @has_access_to_revenue_goals?}
|
||||
>
|
||||
<span
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
|
||||
x-bind:class="active ? 'bg-indigo-600' : 'dark:bg-gray-700 bg-gray-200'"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
x-bind:class="active ? 'dark:bg-gray-800 translate-x-5' : 'dark:bg-gray-800 translate-x-0'"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class={[
|
||||
"ml-3 font-medium",
|
||||
if(assigns.has_access_to_revenue_goals?,
|
||||
do: "text-gray-900 dark:text-gray-100",
|
||||
else: "text-gray-500 dark:text-gray-300"
|
||||
)
|
||||
]}
|
||||
id="enable-revenue-tracking"
|
||||
>
|
||||
Enable Revenue Tracking
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div x-show="active">
|
||||
<.live_component
|
||||
id={"currency_input_#{@suffix}"}
|
||||
submit_name={@f[:currency].name}
|
||||
module={ComboBox}
|
||||
suggest_fun={
|
||||
on_ee do
|
||||
fn
|
||||
"", [] ->
|
||||
Plausible.Goal.Revenue.currency_options()
|
||||
|
||||
input, options ->
|
||||
ComboBox.StaticSearch.suggest(input, options, weight_threshold: 0.8)
|
||||
end
|
||||
end
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.error :for={{msg, opts} <- @f[:event_name].errors}>
|
||||
<%= Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
||||
end) %>
|
||||
</.error>
|
||||
end
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
@ -330,8 +433,8 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("save-goal", %{"goal" => goal}, socket) do
|
||||
case Plausible.Goals.create(socket.assigns.site, goal) do
|
||||
def handle_event("save-goal", %{"goal" => goal_params}, %{assigns: %{goal: nil}} = socket) do
|
||||
case Plausible.Goals.create(socket.assigns.site, goal_params) do
|
||||
{:ok, goal} ->
|
||||
socket =
|
||||
goal
|
||||
@ -345,6 +448,22 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"save-goal",
|
||||
%{"goal" => goal_params},
|
||||
%{assigns: %{goal: %Plausible.Goal{} = goal}} = socket
|
||||
) do
|
||||
case Plausible.Goals.update(goal, goal_params) do
|
||||
{:ok, goal} ->
|
||||
socket = socket.assigns.on_save_goal.(goal, socket)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("autoconfigure", _params, socket) do
|
||||
{:noreply, socket.assigns.on_autoconfigure.(socket)}
|
||||
end
|
||||
@ -371,4 +490,15 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
defp suffix(context_unique_id, tab_sequence_id) do
|
||||
"#{context_unique_id}-tabseq#{tab_sequence_id}"
|
||||
end
|
||||
|
||||
on_ee do
|
||||
defp currency_option(nil), do: nil
|
||||
|
||||
defp currency_option(goal) do
|
||||
Plausible.Goal.Revenue.revenue?(goal) &&
|
||||
Plausible.Goal.Revenue.currency_option(goal.currency)
|
||||
end
|
||||
else
|
||||
defp currency_option(_), do: nil
|
||||
end
|
||||
end
|
||||
|
@ -14,8 +14,7 @@ defmodule PlausibleWeb.Live.GoalSettings.List do
|
||||
|
||||
def render(assigns) do
|
||||
revenue_goals_enabled? = Plausible.Billing.Feature.RevenueGoals.enabled?(assigns.site)
|
||||
goals = Enum.map(assigns.goals, &{goal_label(&1), &1})
|
||||
assigns = assign(assigns, goals: goals, revenue_goals_enabled?: revenue_goals_enabled?)
|
||||
assigns = assign(assigns, revenue_goals_enabled?: revenue_goals_enabled?)
|
||||
|
||||
~H"""
|
||||
<div>
|
||||
@ -47,8 +46,9 @@ defmodule PlausibleWeb.Live.GoalSettings.List do
|
||||
<div class="mt-4 flex sm:ml-4 sm:mt-0">
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
id="add-goal-button"
|
||||
phx-click="add-goal"
|
||||
x-data
|
||||
x-on:click={Modal.JS.open("goals-form-modal")}
|
||||
x-on:click={Modal.JS.preopen("goals-form-modal")}
|
||||
>
|
||||
+ Add Goal
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
@ -56,43 +56,71 @@ defmodule PlausibleWeb.Live.GoalSettings.List do
|
||||
</div>
|
||||
<%= if Enum.count(@goals) > 0 do %>
|
||||
<div class="mt-12">
|
||||
<%= for {goal_label, goal} <- @goals 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 w-3/4">
|
||||
<div class="flex">
|
||||
<span class="truncate">
|
||||
<%= for goal <- @goals do %>
|
||||
<div class="border-b border-gray-300 dark:border-gray-500 py-3 flex justify-between items-center h-16">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 w-2/3 cursor-help pr-4">
|
||||
<div class="flex" title={goal.page_path || goal.event_name}>
|
||||
<div class="truncate block">
|
||||
<div class="text-xs text-gray-400 block mb-1 font-normal">
|
||||
<.goal_description goal={goal} revenue_goals_enabled?={@revenue_goals_enabled?} />
|
||||
</div>
|
||||
<%= if not @revenue_goals_enabled? && goal.currency do %>
|
||||
<div class="text-gray-600 flex items-center">
|
||||
<Heroicons.lock_closed class="w-4 h-4 mr-1 inline" />
|
||||
<span><%= goal_label %></span>
|
||||
<div class="truncate"><%= goal %></div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= goal_label %>
|
||||
<div class="truncate"><%= goal %></div>
|
||||
<% end %>
|
||||
<span class="text-sm text-gray-400 block mt-1 font-normal">
|
||||
<span :if={goal.page_path}>Pageview</span>
|
||||
<span :if={goal.event_name && !goal.currency}>Custom Event</span>
|
||||
<span :if={goal.currency && @revenue_goals_enabled?}>
|
||||
Revenue Goal
|
||||
</span>
|
||||
<span :if={goal.currency && not @revenue_goals_enabled?} class="text-red-600">
|
||||
Unlock Revenue Goals by upgrading to a business plan
|
||||
</span>
|
||||
<span :if={not Enum.empty?(goal.funnels)}> - belongs to funnel(s)</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<button
|
||||
id={"delete-goal-#{goal.id}"}
|
||||
phx-click="delete-goal"
|
||||
phx-value-goal-id={goal.id}
|
||||
phx-value-goal-name={goal.event_name}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm={delete_confirmation_text(goal)}
|
||||
>
|
||||
<Heroicons.trash class="feather feather-sm" />
|
||||
</button>
|
||||
|
||||
<div class="flex items-center w-1/3">
|
||||
<div class="text-xs w-full mr-6 text-gray-400">
|
||||
<div class="hidden md:block">
|
||||
<div :if={goal.page_path} class="text-gray-600">Pageview</div>
|
||||
<div :if={goal.event_name && !goal.currency} class="text-gray-600">
|
||||
Custom Event
|
||||
</div>
|
||||
<div :if={goal.currency} class="text-gray-600">
|
||||
Revenue Goal (<%= goal.currency %>)
|
||||
</div>
|
||||
<div :if={not Enum.empty?(goal.funnels)}>Belongs to funnel(s)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:if={!goal.currency || (goal.currency && @revenue_goals_enabled?)}
|
||||
x-data
|
||||
x-on:click={Modal.JS.preopen("goals-form-modal")}
|
||||
phx-click="edit-goal"
|
||||
phx-value-goal-id={goal.id}
|
||||
id={"edit-goal-#{goal.id}"}
|
||||
class="mr-4"
|
||||
>
|
||||
<Heroicons.pencil_square class="feather feather-sm text-indigo-800 hover:text-indigo-500 dark:text-indigo-500 dark:hover:text-indigo-300" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
:if={goal.currency && !@revenue_goals_enabled?}
|
||||
id={"edit-goal-#{goal.id}-disabled"}
|
||||
disabled
|
||||
class="mr-4 cursor-not-allowed"
|
||||
>
|
||||
<Heroicons.pencil_square class="feather feather-sm text-gray-400 dark:text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
id={"delete-goal-#{goal.id}"}
|
||||
phx-click="delete-goal"
|
||||
phx-value-goal-id={goal.id}
|
||||
phx-value-goal-name={goal.event_name}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm={delete_confirmation_text(goal)}
|
||||
>
|
||||
<Heroicons.trash class="feather feather-sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@ -117,12 +145,33 @@ defmodule PlausibleWeb.Live.GoalSettings.List do
|
||||
"""
|
||||
end
|
||||
|
||||
defp goal_label(%{currency: currency} = goal) when not is_nil(currency) do
|
||||
to_string(goal) <> " (#{currency})"
|
||||
def pageview_description(goal) do
|
||||
path = goal.page_path
|
||||
|
||||
case goal.display_name do
|
||||
"Visit " <> ^path -> ""
|
||||
_ -> "#{path}"
|
||||
end
|
||||
end
|
||||
|
||||
defp goal_label(goal) do
|
||||
to_string(goal)
|
||||
def custom_event_description(goal) do
|
||||
if goal.display_name == goal.event_name, do: "", else: goal.event_name
|
||||
end
|
||||
|
||||
def goal_description(assigns) do
|
||||
~H"""
|
||||
<span :if={@goal.page_path} class="block w-full truncate">
|
||||
<%= pageview_description(@goal) %>
|
||||
</span>
|
||||
|
||||
<span :if={@goal.event_name}>
|
||||
<%= custom_event_description(@goal) %>
|
||||
</span>
|
||||
|
||||
<span :if={@goal.currency && not @revenue_goals_enabled?} class="text-red-600">
|
||||
Unlock Revenue Goals by upgrading to a business plan
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp delete_confirmation_text(goal) do
|
||||
|
@ -34,7 +34,7 @@ defmodule PlausibleWeb.Plugins.API.Views.Goal do
|
||||
goal_type: "Goal.Pageview",
|
||||
goal: %{
|
||||
id: pageview.id,
|
||||
display_name: to_string(pageview),
|
||||
display_name: pageview.display_name,
|
||||
path: pageview.page_path
|
||||
}
|
||||
}
|
||||
@ -47,7 +47,7 @@ defmodule PlausibleWeb.Plugins.API.Views.Goal do
|
||||
goal_type: "Goal.CustomEvent",
|
||||
goal: %{
|
||||
id: custom_event.id,
|
||||
display_name: to_string(custom_event),
|
||||
display_name: custom_event.display_name,
|
||||
event_name: custom_event.event_name
|
||||
}
|
||||
}
|
||||
@ -61,7 +61,7 @@ defmodule PlausibleWeb.Plugins.API.Views.Goal do
|
||||
goal_type: "Goal.Revenue",
|
||||
goal: %{
|
||||
id: revenue_goal.id,
|
||||
display_name: to_string(revenue_goal),
|
||||
display_name: revenue_goal.display_name,
|
||||
event_name: revenue_goal.event_name,
|
||||
currency: revenue_goal.currency
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ defmodule PlausibleWeb.ErrorHelpers do
|
||||
|
||||
def error_tag(%{errors: errors}, field) do
|
||||
Enum.map(Keyword.get_values(errors, field), fn error ->
|
||||
content_tag(:div, translate_error(error), class: "mt-2 text-sm text-red-600")
|
||||
content_tag(:div, translate_error(error), class: "mt-2 text-sm text-red-500")
|
||||
end)
|
||||
end
|
||||
|
||||
@ -11,7 +11,7 @@ defmodule PlausibleWeb.ErrorHelpers do
|
||||
error = assigns[field]
|
||||
|
||||
if error do
|
||||
content_tag(:div, error, class: "mt-2 text-sm text-red-600")
|
||||
content_tag(:div, error, class: "mt-2 text-sm text-red-500")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
defmodule Plausible.Repo.Migrations.MakeGoalDisplayNamesUnique do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
fill_display_names()
|
||||
|
||||
alter table(:goals) do
|
||||
modify :display_name, :text, null: false
|
||||
end
|
||||
|
||||
create unique_index(:goals, [:site_id, :display_name])
|
||||
end
|
||||
|
||||
def down do
|
||||
drop unique_index(:goals, [:site_id, :display_name])
|
||||
|
||||
alter table(:goals) do
|
||||
modify :display_name, :text, null: true
|
||||
end
|
||||
|
||||
execute """
|
||||
UPDATE goals
|
||||
SET display_name = NULL
|
||||
"""
|
||||
end
|
||||
|
||||
def fill_display_names do
|
||||
execute """
|
||||
UPDATE goals
|
||||
SET display_name =
|
||||
CASE
|
||||
WHEN page_path IS NOT NULL THEN 'Visit ' || page_path
|
||||
WHEN event_name IS NOT NULL THEN event_name
|
||||
END
|
||||
"""
|
||||
end
|
||||
end
|
@ -10,6 +10,10 @@
|
||||
# We recommend using the bang functions (`insert!`, `update!`
|
||||
# and so on) as they will fail if something goes wrong.
|
||||
|
||||
words =
|
||||
for i <- 0..(:erlang.system_info(:atom_count) - 1),
|
||||
do: :erlang.binary_to_term(<<131, 75, i::24>>)
|
||||
|
||||
user = Plausible.Factory.insert(:user, email: "user@plausible.test", password: "plausible")
|
||||
|
||||
native_stats_range =
|
||||
@ -32,8 +36,13 @@ imported_stats_range =
|
||||
|
||||
long_random_paths =
|
||||
for _ <- 1..100 do
|
||||
l = Enum.random(40..300)
|
||||
"/long/#{l}/path/#{String.duplicate("0x", l)}/end"
|
||||
path =
|
||||
words
|
||||
|> Enum.shuffle()
|
||||
|> Enum.take(Enum.random(1..20))
|
||||
|> Enum.join("/")
|
||||
|
||||
"/#{path}.html"
|
||||
end
|
||||
|
||||
long_random_urls =
|
||||
@ -74,8 +83,17 @@ seeded_token = Plausible.Plugins.API.Token.generate("seed-token")
|
||||
|
||||
{:ok, goal1} = Plausible.Goals.create(site, %{"page_path" => "/"})
|
||||
{:ok, goal2} = Plausible.Goals.create(site, %{"page_path" => "/register"})
|
||||
{:ok, goal3} = Plausible.Goals.create(site, %{"page_path" => "/login"})
|
||||
{:ok, goal4} = Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "USD"})
|
||||
|
||||
{:ok, goal3} =
|
||||
Plausible.Goals.create(site, %{"page_path" => "/login", "display_name" => "User logs in"})
|
||||
|
||||
{:ok, goal4} =
|
||||
Plausible.Goals.create(site, %{
|
||||
"event_name" => "Purchase",
|
||||
"currency" => "USD",
|
||||
"display_name" => "North America Purchases"
|
||||
})
|
||||
|
||||
{:ok, _goal5} = Plausible.Goals.create(site, %{"page_path" => Enum.random(long_random_paths)})
|
||||
{:ok, outbound} = Plausible.Goals.create(site, %{"event_name" => "Outbound Link: Click"})
|
||||
|
||||
|
@ -7,9 +7,16 @@ defmodule Plausible.GoalsTest do
|
||||
site = insert(:site)
|
||||
{:ok, goal} = Goals.create(site, %{"page_path" => "/foo bar "})
|
||||
assert goal.page_path == "/foo bar"
|
||||
assert goal.display_name == "Visit /foo bar"
|
||||
|
||||
{:ok, goal} =
|
||||
Goals.create(site, %{
|
||||
"event_name" => " some event name ",
|
||||
"display_name" => " DisplayName "
|
||||
})
|
||||
|
||||
{:ok, goal} = Goals.create(site, %{"event_name" => " some event name "})
|
||||
assert goal.event_name == "some event name"
|
||||
assert goal.display_name == "DisplayName"
|
||||
end
|
||||
|
||||
test "create/2 creates pageview goal and adds a leading slash if missing" do
|
||||
@ -92,6 +99,15 @@ defmodule Plausible.GoalsTest do
|
||||
assert [currency: {"is invalid", _}] = changeset.errors
|
||||
end
|
||||
|
||||
test "update/2 updates a goal" do
|
||||
site = insert(:site)
|
||||
{:ok, goal1} = Goals.create(site, %{"page_path" => "/foo bar "})
|
||||
{:ok, goal2} = Goals.update(goal1, %{"page_path" => "/", "display_name" => "Homepage"})
|
||||
assert goal1.id == goal2.id
|
||||
assert goal2.page_path == "/"
|
||||
assert goal2.display_name == "Homepage"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "list_revenue_goals/1 lists event_names and currencies for each revenue goal" do
|
||||
site = insert(:site)
|
||||
|
@ -305,6 +305,38 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||
assert res["domain"] == new_domain
|
||||
end
|
||||
|
||||
test "can add a goal as event with display name", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "event",
|
||||
event_name: "Signup",
|
||||
display_name: "Customer Acquired"
|
||||
})
|
||||
|
||||
res = json_response(conn, 200)
|
||||
assert res["goal_type"] == "event"
|
||||
assert res["event_name"] == "Signup"
|
||||
assert res["display_name"] == "Customer Acquired"
|
||||
assert res["domain"] == site.domain
|
||||
end
|
||||
|
||||
test "can add a goal as page with display name", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
site_id: site.domain,
|
||||
goal_type: "page",
|
||||
page_path: "/foo",
|
||||
display_name: "Visit the foo page"
|
||||
})
|
||||
|
||||
res = json_response(conn, 200)
|
||||
assert res["goal_type"] == "page"
|
||||
assert res["display_name"] == "Visit the foo page"
|
||||
assert res["page_path"] == "/foo"
|
||||
assert res["domain"] == site.domain
|
||||
end
|
||||
|
||||
test "is idempotent find or create op", %{conn: conn, site: site} do
|
||||
conn =
|
||||
put(conn, "/api/v1/sites/goals", %{
|
||||
|
@ -415,7 +415,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
||||
build(:pageview, timestamp: today)
|
||||
])
|
||||
|
||||
insert(:goal, %{site: site, event_name: "Signup"})
|
||||
insert(:goal, %{site: site, event_name: "Signup", display_name: "Signup Display Name"})
|
||||
|
||||
conn =
|
||||
get(conn, "/api/v1/stats/aggregate", %{
|
||||
@ -423,7 +423,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
||||
"period" => "day",
|
||||
"date" => "2023-05-05",
|
||||
"metrics" => "conversion_rate",
|
||||
"filters" => "event:goal==Signup",
|
||||
"filters" => "event:goal==Signup Display Name",
|
||||
"compare" => "previous_period"
|
||||
})
|
||||
|
||||
@ -712,7 +712,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
||||
|
||||
test "returns stats with page + pageview goal filter",
|
||||
%{conn: conn, site: site, site_import: site_import} do
|
||||
insert(:goal, site: site, page_path: "/blog/**")
|
||||
insert(:goal, site: site, page_path: "/blog/**", display_name: "Blog Visit")
|
||||
|
||||
populate_stats(site, site_import.id, [
|
||||
build(:imported_pages, page: "/blog/1", visitors: 1, pageviews: 1),
|
||||
@ -725,7 +725,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do
|
||||
"site_id" => site.domain,
|
||||
"period" => "day",
|
||||
"metrics" => "visitors,events,conversion_rate",
|
||||
"filters" => "event:page==/blog/2;event:goal==Visit /blog/**",
|
||||
"filters" => "event:page==/blog/2;event:goal==Blog Visit",
|
||||
"with_imported" => "true"
|
||||
})
|
||||
|
||||
|
@ -24,7 +24,7 @@ defmodule PlausibleWeb.Live.Components.ComboBoxTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "renders preselected default value" do
|
||||
test "renders preselected default value (tuple)" do
|
||||
options = new_options(10)
|
||||
assert doc = render_sample_component(options, selected: List.last(options))
|
||||
|
||||
@ -36,6 +36,18 @@ defmodule PlausibleWeb.Live.Components.ComboBoxTest do
|
||||
)
|
||||
end
|
||||
|
||||
test "renders preselected default value (string)" do
|
||||
options = [{"a", "a"}, {"b", "b"}, {"c", "c"}]
|
||||
assert doc = render_sample_component(options, selected: "b")
|
||||
|
||||
assert element_exists?(doc, "input[type=hidden][name=test-submit-name][value=b]")
|
||||
|
||||
assert element_exists?(
|
||||
doc,
|
||||
~s|input[type=text][name=display-test-component][value="b"]|
|
||||
)
|
||||
end
|
||||
|
||||
test "renders up to 15 suggestions by default" do
|
||||
assert doc = render_sample_component(new_options(20))
|
||||
|
||||
|
@ -27,43 +27,65 @@ defmodule PlausibleWeb.Live.GoalSettings.FormTest do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
@tag :ee_only
|
||||
test "renders form fields (with currency)", %{conn: conn, site: site} do
|
||||
test "renders form fields per tab, with currency", %{conn: conn, site: site} do
|
||||
lv = get_liveview(conn, site)
|
||||
html = render(lv)
|
||||
|
||||
[event_name_display, event_name, currency_display, currency_submit] =
|
||||
find(html, "#goals-form-modalseq0 input")
|
||||
refute element_exists?(html, "#pageviews-form")
|
||||
|
||||
assert name_of(event_name_display) == "display-event_name_input_modalseq0-tabseq0"
|
||||
assert name_of(event_name) == "goal[event_name]"
|
||||
assert name_of(currency_display) == "display-currency_input_modalseq0-tabseq0"
|
||||
assert name_of(currency_submit) == "goal[currency]"
|
||||
input_names = html |> find("#custom-events-form input") |> Enum.map(&name_of/1)
|
||||
|
||||
assert input_names ==
|
||||
[
|
||||
"display-event_name_input_modalseq0-tabseq0",
|
||||
"goal[event_name]",
|
||||
"goal[display_name]",
|
||||
"display-currency_input_modalseq0-tabseq0",
|
||||
"goal[currency]"
|
||||
]
|
||||
|
||||
lv |> element(~s/a#pageview-tab/) |> render_click()
|
||||
html = lv |> render()
|
||||
|
||||
html = render(lv)
|
||||
refute element_exists?(html, "#custom-events-form")
|
||||
|
||||
[page_path_display, page_path] = find(html, "#goals-form-modalseq0 input")
|
||||
assert name_of(page_path_display) == "display-page_path_input_modalseq0-tabseq1"
|
||||
assert name_of(page_path) == "goal[page_path]"
|
||||
input_names = html |> find("#pageviews-form input") |> Enum.map(&name_of/1)
|
||||
|
||||
assert input_names == [
|
||||
"display-page_path_input_modalseq0-tabseq1",
|
||||
"goal[page_path]",
|
||||
"goal[display_name]"
|
||||
]
|
||||
end
|
||||
|
||||
@tag :ce_build_only
|
||||
test "renders form fields (no currency)", %{conn: conn, site: site} do
|
||||
test "renders form fields per tab (no currency)", %{conn: conn, site: site} do
|
||||
lv = get_liveview(conn, site)
|
||||
html = render(lv)
|
||||
|
||||
[event_name_display, event_name | _] = find(html, "#goals-form-modalseq0 input")
|
||||
assert name_of(event_name_display) == "display-event_name_input_modalseq0-tabseq0"
|
||||
assert name_of(event_name) == "goal[event_name]"
|
||||
refute element_exists?(html, "#pageviews-form")
|
||||
|
||||
input_names = html |> find("#custom-events-form input") |> Enum.map(&name_of/1)
|
||||
|
||||
assert input_names ==
|
||||
[
|
||||
"display-event_name_input_modalseq0-tabseq0",
|
||||
"goal[event_name]",
|
||||
"goal[display_name]"
|
||||
]
|
||||
|
||||
lv |> element(~s/a#pageview-tab/) |> render_click()
|
||||
html = lv |> render()
|
||||
|
||||
html = render(lv)
|
||||
refute element_exists?(html, "#custom-events-form")
|
||||
|
||||
[page_path_display, page_path] = find(html, "#goals-form-modalseq0 input")
|
||||
assert name_of(page_path_display) == "display-page_path_input_modalseq0-tabseq1"
|
||||
assert name_of(page_path) == "goal[page_path]"
|
||||
input_names = html |> find("#pageviews-form input") |> Enum.map(&name_of/1)
|
||||
|
||||
assert input_names == [
|
||||
"display-page_path_input_modalseq0-tabseq1",
|
||||
"goal[page_path]",
|
||||
"goal[display_name]"
|
||||
]
|
||||
end
|
||||
|
||||
test "renders error on empty submission", %{conn: conn, site: site} do
|
||||
@ -96,12 +118,19 @@ defmodule PlausibleWeb.Live.GoalSettings.FormTest do
|
||||
|
||||
lv
|
||||
|> element("#goals-form-modalseq0 form")
|
||||
|> render_submit(%{goal: %{event_name: "SampleRevenueGoal", currency: "EUR"}})
|
||||
|> render_submit(%{
|
||||
goal: %{
|
||||
event_name: "SampleRevenueGoal",
|
||||
currency: "EUR",
|
||||
display_name: "Sample Display Name"
|
||||
}
|
||||
})
|
||||
|
||||
html = render(lv)
|
||||
|
||||
assert html =~ "SampleRevenueGoal (EUR)"
|
||||
assert html =~ "Revenue Goal"
|
||||
assert html =~ "SampleRevenueGoal"
|
||||
assert html =~ "Revenue Goal (EUR)"
|
||||
assert html =~ "Sample Display Name"
|
||||
end
|
||||
|
||||
test "creates a pageview goal", %{conn: conn, site: site} do
|
||||
@ -118,6 +147,119 @@ defmodule PlausibleWeb.Live.GoalSettings.FormTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "Editing goals" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
@tag :ee_only
|
||||
test "tabless view is rendered with goal type change disabled", %{conn: conn, site: site} do
|
||||
{:ok, [pageview, custom_event, revenue_goal]} = setup_goals(site)
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
# pageviews
|
||||
lv |> element(~s/button#edit-goal-#{pageview.id}/) |> render_click()
|
||||
html = render(lv)
|
||||
|
||||
assert element_exists?(html, "#pageviews-form")
|
||||
refute element_exists?(html, "#custom-events-form")
|
||||
|
||||
refute element_exists?(
|
||||
html,
|
||||
~s/button[role=switch][aria-labelledby=enable-revenue-tracking]/
|
||||
)
|
||||
|
||||
# custom events
|
||||
lv |> element(~s/button#edit-goal-#{custom_event.id}/) |> render_click()
|
||||
html = render(lv)
|
||||
|
||||
refute element_exists?(html, "#pageviews-form")
|
||||
assert element_exists?(html, "#custom-events-form")
|
||||
|
||||
assert element_exists?(
|
||||
html,
|
||||
~s/button[role=switch][aria-labelledby=enable-revenue-tracking][disabled="disabled"]/
|
||||
)
|
||||
|
||||
# revenue goals
|
||||
lv |> element(~s/button#edit-goal-#{revenue_goal.id}/) |> render_click()
|
||||
html = render(lv)
|
||||
|
||||
refute element_exists?(html, "#pageviews-form")
|
||||
assert element_exists?(html, "#custom-events-form")
|
||||
|
||||
assert element_exists?(
|
||||
html,
|
||||
~s/button[role=switch][aria-labelledby=enable-revenue-tracking][disabled="disabled"]/
|
||||
)
|
||||
end
|
||||
|
||||
test "updates a custom event", %{conn: conn, site: site} do
|
||||
{:ok, [_, g, _]} = setup_goals(site)
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv |> element(~s/button#edit-goal-#{g.id}/) |> render_click()
|
||||
|
||||
html = render(lv)
|
||||
assert element_exists?(html, "#event_name_input_modalseq0[value=Signup]")
|
||||
|
||||
lv
|
||||
|> element("#goals-form-modalseq0 form")
|
||||
|> render_submit(%{goal: %{event_name: "Updated", display_name: "UPDATED"}})
|
||||
|
||||
_html = render(lv)
|
||||
|
||||
updated = Plausible.Goals.get(site, g.id)
|
||||
assert updated.event_name == "Updated"
|
||||
assert updated.display_name == "UPDATED"
|
||||
assert updated.id == g.id
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "updates a revenue goal", %{conn: conn, site: site} do
|
||||
{:ok, [_, _, g]} = setup_goals(site)
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv |> element(~s/button#edit-goal-#{g.id}/) |> render_click()
|
||||
|
||||
html = render(lv)
|
||||
assert element_exists?(html, "#event_name_input_modalseq0[value=Purchase]")
|
||||
assert element_exists?(html, ~s/#currency_input_modalseq0[value="EUR - Euro"]/)
|
||||
|
||||
lv
|
||||
|> element("#goals-form-modalseq0 form")
|
||||
|> render_submit(%{goal: %{event_name: "Updated", currency: "USD"}})
|
||||
|
||||
_html = render(lv)
|
||||
|
||||
updated = Plausible.Goals.get(site, g.id)
|
||||
assert updated.event_name == "Updated"
|
||||
assert updated.display_name == "Purchase"
|
||||
assert updated.currency == :USD
|
||||
assert updated.currency != g.currency
|
||||
assert updated.id == g.id
|
||||
end
|
||||
|
||||
test "updates a pageview", %{conn: conn, site: site} do
|
||||
{:ok, [g, _, _]} = setup_goals(site)
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv |> element(~s/button#edit-goal-#{g.id}/) |> render_click()
|
||||
|
||||
html = render(lv)
|
||||
assert element_exists?(html, ~s|#page_path_input_modalseq0[value="/go/to/blog/**"|)
|
||||
|
||||
lv
|
||||
|> element("#goals-form-modalseq0 form")
|
||||
|> render_submit(%{goal: %{page_path: "/updated", display_name: "Visit /updated"}})
|
||||
|
||||
_html = render(lv)
|
||||
|
||||
updated = Plausible.Goals.get(site, g.id)
|
||||
assert updated.page_path == "/updated"
|
||||
assert updated.display_name == "Visit /updated"
|
||||
assert updated.id == g.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "Combos integration" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
|
@ -41,9 +41,14 @@ defmodule PlausibleWeb.Live.GoalSettingsTest do
|
||||
assert g3.currency
|
||||
assert resp =~ to_string(g3)
|
||||
assert resp =~ "Unlock Revenue Goals by upgrading to a business plan"
|
||||
|
||||
refute element_exists?(
|
||||
resp,
|
||||
~s/button[phx-click="edit-goal"][phx-value-goal-id=#{g3.id}][disabled]#edit-goal-#{g3.id}/
|
||||
)
|
||||
end
|
||||
|
||||
test "lists goals with delete actions", %{conn: conn, site: site} do
|
||||
test "lists goals with actions", %{conn: conn, site: site} do
|
||||
{:ok, goals} = setup_goals(site)
|
||||
conn = get(conn, "/#{site.domain}/settings/goals")
|
||||
resp = html_response(conn, 200)
|
||||
@ -53,6 +58,11 @@ defmodule PlausibleWeb.Live.GoalSettingsTest do
|
||||
resp,
|
||||
~s/button[phx-click="delete-goal"][phx-value-goal-id=#{g.id}]#delete-goal-#{g.id}/
|
||||
)
|
||||
|
||||
assert element_exists?(
|
||||
resp,
|
||||
~s/button[phx-click="edit-goal"][phx-value-goal-id=#{g.id}]#edit-goal-#{g.id}/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@ -75,10 +85,7 @@ defmodule PlausibleWeb.Live.GoalSettingsTest do
|
||||
test "add goal button is rendered", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/#{site.domain}/settings/goals")
|
||||
resp = html_response(conn, 200)
|
||||
assert element_exists?(resp, ~s/button#add-goal-button[x-data]/)
|
||||
attr = text_of_attr(resp, ~s/button#add-goal-button/, "x-on:click")
|
||||
assert attr =~ "open-modal"
|
||||
assert attr =~ "goals-form-modal"
|
||||
assert element_exists?(resp, ~s/button#add-goal-button[phx-click="add-goal"]/)
|
||||
end
|
||||
|
||||
test "search goals input is rendered", %{conn: conn, site: site} do
|
||||
|
@ -107,8 +107,32 @@ defmodule Plausible.Factory do
|
||||
}
|
||||
end
|
||||
|
||||
def goal_factory do
|
||||
%Plausible.Goal{}
|
||||
def goal_factory(attrs) do
|
||||
display_name_provided? = Map.has_key?(attrs, :display_name)
|
||||
|
||||
attrs =
|
||||
case {attrs, display_name_provided?} do
|
||||
{%{page_path: path}, false} when is_binary(path) ->
|
||||
Map.put(attrs, :display_name, "Visit " <> path)
|
||||
|
||||
{%{page_path: path}, false} when is_function(path, 0) ->
|
||||
attrs
|
||||
|> Map.put(:display_name, "Visit " <> path.())
|
||||
|> Map.put(:page_path, path.())
|
||||
|
||||
{%{event_name: event_name}, false} when is_binary(event_name) ->
|
||||
Map.put(attrs, :display_name, event_name)
|
||||
|
||||
{%{event_name: event_name}, false} when is_function(event_name, 0) ->
|
||||
attrs
|
||||
|> Map.put(:display_name, event_name.())
|
||||
|> Map.put(:event_name, event_name.())
|
||||
|
||||
_ ->
|
||||
attrs
|
||||
end
|
||||
|
||||
merge_attributes(%Plausible.Goal{}, attrs)
|
||||
end
|
||||
|
||||
def subscription_factory do
|
||||
|
Loading…
Reference in New Issue
Block a user