Skip to content

Conversation

@sanne-san
Copy link
Contributor

Changes

  • All empty states have been enhanced by center-aligned copy, link to docs, and CTA.
  • Whenever empty state is shown, the setting's subtitle is hidden.

Tests

  • Automated tests have been added
  • This PR does not require tests

Dark mode

  • The UI has been tested both in dark and light mode

@ukutaht ukutaht added the preview label Nov 5, 2025
@github-actions
Copy link

github-actions bot commented Nov 5, 2025

Preview environment👷🏼‍♀️🏗️
PR-5874

Comment on lines +166 to +174
has_shared_links? =
Repo.exists?(
from(l in Plausible.Site.SharedLink,
where:
l.site_id == ^site.id and
l.name not in ^Plausible.Sites.shared_link_special_names()
)
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this so that I can hide the setting subtitle when the empty state is shown. I'm not sure this is the right place to put it, and it works but requires a page refresh for it to take effect.

Copy link
Member

@aerosol aerosol Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @sanne-san - this is unfortunately, not the most lucky problem to have. Looks like we have a lot to unpack with regards to how we've been randomly migrating settings sections to live views.

A smol background story: settings were originally all static, standard HTML forms etc. With need of having many dynamic forms over the time (and us getting a safe-ish playground to learn LiveView the hard way), we've started turning them into live views. Because rewriting everything was risky we've started embedding live views (modules operating on @socket / websocket) in static templates (as in "rendered once", a.k.a "dead" - using @conn / HTTP) bit by bit, where needed. Live views are where react-like diffs are applied. That's why you're struggling with refresh - the part that renders the tile and its slots is static 💀.

Normally it'd best to move the <.tile></.tile> element to the live view that's embedded underneath - PlausibleWeb.Live.SharedLinkSettings in this case. But there are cans of worms ahead: you'll get the compiler complain about site_role missing. You can of course provide it in the live view's mount() function (either execute the query or pass it down via the session params in the static template and assign it to the socket).

But it also turns out that the tile component is a bit complicated - in an attempt of generic handling of toggling features and/or upgrade CTAs in case of insufficient subscriptions, it is why it wants the site_role assign provided. It also needs @conn - which we don't have access to in live views.. And SharedLinks is indeed a feature that's not available to everyone, so we need some handling of billing availability still.

With Goals, Funnels, CustomProps it might get even weirder, since we have live views embedding live views there and they're separate processes so they'd need to exchange messages with each other 🙈

I wouldn't recommend getting there now; sounds like a massive undertaking to hide the subtitle. 😬

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's possible to remove the :subtitle slot from static template and render its markup in live view, verbatim? Just so we don't have to deal with all the tile problems.

@sanne-san sanne-san requested a review from a team November 6, 2025 09:07
- Add show_content? attribute to generic tile component
- Ensure content is hidden when toggled off
- Avoid rendering border and empty space when toggled off
- Fix formatting
Add funnel
</.button>
</.filter_bar>
<%= if String.trim(@filter_text) != "" || Enum.count(@funnels) > 0 do %>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or is perfectly valid here since both left and right hand side expressions return a boolean

Comment on lines +166 to +174
has_shared_links? =
Repo.exists?(
from(l in Plausible.Site.SharedLink,
where:
l.site_id == ^site.id and
l.name not in ^Plausible.Sites.shared_link_special_names()
)
)

Copy link
Member

@aerosol aerosol Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @sanne-san - this is unfortunately, not the most lucky problem to have. Looks like we have a lot to unpack with regards to how we've been randomly migrating settings sections to live views.

A smol background story: settings were originally all static, standard HTML forms etc. With need of having many dynamic forms over the time (and us getting a safe-ish playground to learn LiveView the hard way), we've started turning them into live views. Because rewriting everything was risky we've started embedding live views (modules operating on @socket / websocket) in static templates (as in "rendered once", a.k.a "dead" - using @conn / HTTP) bit by bit, where needed. Live views are where react-like diffs are applied. That's why you're struggling with refresh - the part that renders the tile and its slots is static 💀.

Normally it'd best to move the <.tile></.tile> element to the live view that's embedded underneath - PlausibleWeb.Live.SharedLinkSettings in this case. But there are cans of worms ahead: you'll get the compiler complain about site_role missing. You can of course provide it in the live view's mount() function (either execute the query or pass it down via the session params in the static template and assign it to the socket).

But it also turns out that the tile component is a bit complicated - in an attempt of generic handling of toggling features and/or upgrade CTAs in case of insufficient subscriptions, it is why it wants the site_role assign provided. It also needs @conn - which we don't have access to in live views.. And SharedLinks is indeed a feature that's not available to everyone, so we need some handling of billing availability still.

With Goals, Funnels, CustomProps it might get even weirder, since we have live views embedding live views there and they're separate processes so they'd need to exchange messages with each other 🙈

I wouldn't recommend getting there now; sounds like a massive undertaking to hide the subtitle. 😬

Add funnel
</.button>
</.filter_bar>
<%= if String.trim(@filter_text) != "" || Enum.count(@funnels) > 0 do %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the String.trim(@filter_text) != "" condition is redundant here as it's always true when Enum.count(@funnels) > 0 is true. The search input isn't rendered without any funnels, so there cannot be any text in there either, right? Or is there a specific case that I'm missing here?

<% else %>
<p class="mt-12 mb-8 text-sm text-center">
<span :if={String.trim(@filter_text) != ""}>
<%= if String.trim(@filter_text) != "" do %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This render function is getting a bit hard to read with the nested if/else conditionals. How about we extract some functions? I'm thinking:

def render(assigns) do
  assigns = assign(assigns, :searching?, String.trim(assigns.filter_text) != "")

  ~H"""
  <%= if Enum.count(@funnels) > 0 do %>
    # ... keep the filter bar, add button, table
  <% else %>
    <.no_search_results :if={@searching?}/>
    <.create_first_funnel :if={not @searching?}
  <% end %>
  """
end

defp no_search_results(assigns) do
  ~H"""
  <p class="mt-12 mb-8 text-sm text-center">
    No funnels found for this site. Please refine or
    <.styled_link phx-click="reset-filter-text" id="reset-filter-hint">
       reset your search.
    </.styled_link>
  </p>
  """
end

Set up a few goals first (e.g. <b>"Signup"</b>, <b>"Visit /"</b>, or <b>"Scroll 50% on /blog/*"</b>) and return here to build your first funnel!
</h3>
<p class="text-center text-sm mt-1 text-gray-500 dark:text-gray-400 leading-5 text-pretty">
Set up a few goals like <span class="font-medium text-indigo-600 dark:text-gray-100">Signup</span>, <span class="font-medium text-indigo-600 dark:text-gray-100">Visit /</span>, or
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we extract a small inline text component (perhaps <.highlighted> in PlausibleWeb.Components.Generic?) to not have to repeat these classnames for every element? Or if it's something that will definitely not be reused, it could also be a private function in this file. WDYT?

Add goal
</.button>
</.filter_bar>
<%= if String.trim(@filter_text) != "" || Enum.count(@goals) > 0 do %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto: String.trim(@filter_text) != "" || irrelevant?

<% else %>
<p class="mt-12 mb-8 text-center text-sm">
<span :if={String.trim(@filter_text) != ""}>
<%= if String.trim(@filter_text) != "" do %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe follow the same logic here too (as I suggested for funnels), extracting the components?

Add property
</.button>
</.filter_bar>
<%= if String.trim(@filter_text) != "" || (is_list(@props) && length(@props) > 0) do %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto: String.trim(@filter_text) != "" || irrelevant?

<% else %>
<p class="mt-12 mb-8 text-center text-sm">
<span :if={String.trim(@filter_text) != ""}>
<%= if String.trim(@filter_text) != "" do %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto: I think extracting functions (like in goals and funnels) could make the render function more readable.

@sanne-san sanne-san mentioned this pull request Nov 14, 2025
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants