feat(contacts): hide Private column when the viewer has nothing private

Row-level privacy was already correct: private rows only render for
the owner and admins. The header column was still showing for every
viewer, leaving an always-empty slot for anyone without private
contacts of their own. Drop `:private` from the fields list unless
the scope can actually see at least one private row.
This commit is contained in:
Graham McIntire 2026-04-19 12:36:11 -05:00
parent c38f4e2bfe
commit 4b155c5419
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 79 additions and 1 deletions

View file

@ -61,9 +61,31 @@ defmodule MicrowavepropWeb.ContactLive.Index do
socket
|> assign(:page_title, "Contacts")
|> assign(:total_contacts, total)
|> assign(:visible_fields, visible_fields_for(scope))
|> assign(:data_provider, {__MODULE__, :visible_query_provider, [scope_token(scope)]})}
end
# Hide the Private column entirely unless this scope can actually see
# at least one private contact — avoids an always-empty column for
# visitors and owners who've never flagged one as private.
defp visible_fields_for(scope) do
if has_viewable_private?(scope) do
fields()
else
Keyword.delete(fields(), :private)
end
end
defp has_viewable_private?(%Scope{user: %User{is_admin: true}}) do
Repo.exists?(from(c in Contact, where: c.private == true))
end
defp has_viewable_private?(%Scope{user: %User{id: user_id}}) do
Repo.exists?(from(c in Contact, where: c.private == true and c.user_id == ^user_id))
end
defp has_viewable_private?(_), do: false
@doc false
# Called by LiveTable to get the base queryable. Must be a
# named function because the helper persists MFA across renders.
@ -183,7 +205,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
</.header>
<.live_table
fields={fields()}
fields={@visible_fields}
filters={filters()}
options={@options}
streams={@streams}

View file

@ -167,6 +167,62 @@ defmodule MicrowavepropWeb.ContactLiveTest do
end
end
describe "Index private column visibility" do
import Microwaveprop.AccountsFixtures
test "column hidden for anonymous viewer when no private contacts exist", %{conn: conn} do
_public = create_contact(%{station1: "W5PUB"})
{:ok, _lv, html} = live(conn, ~p"/contacts")
refute html =~ ">Private<"
end
test "column hidden for anonymous viewer even when others have private contacts", %{conn: conn} do
_private = create_contact(%{station1: "W5PRV", private: true})
_public = create_contact(%{station1: "W5PUB"})
{:ok, _lv, html} = live(conn, ~p"/contacts")
refute html =~ ">Private<"
end
test "column hidden for a user who has no private contacts of their own", %{conn: conn} do
owner = user_fixture()
viewer = user_fixture()
_others_private = create_contact(%{station1: "W5PRV", private: true, user_id: owner.id})
conn = log_in_user(conn, viewer)
{:ok, _lv, html} = live(conn, ~p"/contacts")
refute html =~ ">Private<"
end
test "column visible for a user with at least one of their own private contacts", %{conn: conn} do
owner = user_fixture()
_private = create_contact(%{station1: "W5PRV", private: true, user_id: owner.id})
conn = log_in_user(conn, owner)
{:ok, _lv, html} = live(conn, ~p"/contacts")
assert html =~ ">Private<"
end
test "column hidden for admin when there are no private contacts anywhere", %{conn: conn} do
admin = user_fixture() |> Ecto.Changeset.change(is_admin: true) |> Repo.update!()
_public = create_contact(%{station1: "W5PUB"})
conn = log_in_user(conn, admin)
{:ok, _lv, html} = live(conn, ~p"/contacts")
refute html =~ ">Private<"
end
test "column visible for admin as soon as any private contact exists", %{conn: conn} do
admin = user_fixture() |> Ecto.Changeset.change(is_admin: true) |> Repo.update!()
_private = create_contact(%{station1: "W5PRV", private: true})
conn = log_in_user(conn, admin)
{:ok, _lv, html} = live(conn, ~p"/contacts")
assert html =~ ">Private<"
end
end
describe "Show private visibility" do
import Microwaveprop.AccountsFixtures