Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **Nested route support** - InboxLive now correctly handles URLs when mounted in nested scopes (e.g., `/admin/fyi` instead of just `/fyi`)
- Event detail URLs now respect the route prefix where the LiveView is mounted
- Navigation between index and detail views works correctly regardless of scope nesting

## [1.0.1] - 2025-12-27

### Added
Expand Down
45 changes: 36 additions & 9 deletions lib/fyi/web/inbox_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ if Code.ensure_loaded?(Phoenix.LiveView) do
end

@impl true
def handle_params(params, _uri, socket) do
def handle_params(params, uri, socket) do
time_range = params["range"] || "7d"
event_type = params["type"] || ""
event_id = params["id"]

# Extract route prefix from URI
route_prefix = extract_route_prefix_from_uri(uri)

socket =
socket
|> assign(:route_prefix, route_prefix)
|> assign(:time_range, time_range)
|> assign(:event_type, event_type)
|> load_event_types()
Expand Down Expand Up @@ -71,24 +75,39 @@ if Code.ensure_loaded?(Phoenix.LiveView) do

@impl true
def handle_event("time_range", %{"range" => range}, socket) do
{:noreply, push_patch(socket, to: build_url(range, socket.assigns.event_type))}
{:noreply, push_patch(socket, to: build_url(socket, range, socket.assigns.event_type))}
end

@impl true
def handle_event("event_type", %{"type" => type}, socket) do
{:noreply, push_patch(socket, to: build_url(socket.assigns.time_range, type))}
{:noreply, push_patch(socket, to: build_url(socket, socket.assigns.time_range, type))}
end

@impl true
def handle_event("close_detail", _, socket) do
{:noreply,
push_patch(socket, to: build_url(socket.assigns.time_range, socket.assigns.event_type))}
push_patch(socket,
to: build_url(socket, socket.assigns.time_range, socket.assigns.event_type)
)}
end

@doc false
def extract_route_prefix_from_uri(uri) do
# Extract the route prefix from the URI
# For example: "http://localhost:4000/admin/fyi?range=7d" -> "/admin/fyi"
# "http://localhost:4000/fyi/events/123?range=7d" -> "/fyi"
uri
|> URI.parse()
|> Map.get(:path, "/fyi")
|> String.split("/events/")
|> List.first()
end

defp build_url(range, type) do
@doc false
def build_url(socket, range, type) do
params = [{"range", range}]
params = if type != "", do: params ++ [{"type", type}], else: params
"/fyi?" <> URI.encode_query(params)
"#{socket.assigns.route_prefix}?" <> URI.encode_query(params)
end

@impl true
Expand Down Expand Up @@ -697,7 +716,7 @@ if Code.ensure_loaded?(Phoenix.LiveView) do
</div>
<% else %>
<%= for event <- @events do %>
<.link patch={event_url(event.id, @time_range, @event_type)} class="fyi-event-row">
<.link patch={event_url(assigns, event.id, @time_range, @event_type)} class="fyi-event-row">
<svg class="fyi-event-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
</svg>
Expand Down Expand Up @@ -849,10 +868,18 @@ if Code.ensure_loaded?(Phoenix.LiveView) do
end
end

defp event_url(event_id, range, type) do
@doc false
def event_url(socket_or_assigns, event_id, range, type) do
route_prefix =
case socket_or_assigns do
%{assigns: %{route_prefix: prefix}} -> prefix
%{route_prefix: prefix} -> prefix
_ -> "/fyi"
end

params = [{"range", range}]
params = if type != "", do: params ++ [{"type", type}], else: params
"/fyi/events/#{event_id}?" <> URI.encode_query(params)
"#{route_prefix}/events/#{event_id}?" <> URI.encode_query(params)
end

defp time_range_since(range) do
Expand Down
82 changes: 82 additions & 0 deletions test/fyi/web/inbox_live_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
defmodule FYI.Web.InboxLiveTest do
use ExUnit.Case, async: true

alias FYI.Web.InboxLive

describe "extract_route_prefix_from_uri/1" do
test "extracts route prefix from root-level /fyi path" do
uri = "http://localhost:4000/fyi?range=7d"
assert InboxLive.extract_route_prefix_from_uri(uri) == "/fyi"
end

test "extracts route prefix from nested /admin/fyi path" do
uri = "http://localhost:4000/admin/fyi?range=7d"
assert InboxLive.extract_route_prefix_from_uri(uri) == "/admin/fyi"
end

test "extracts route prefix from deeply nested path" do
uri = "http://localhost:4000/admin/dashboard/fyi?range=7d"
assert InboxLive.extract_route_prefix_from_uri(uri) == "/admin/dashboard/fyi"
end

test "extracts route prefix from event detail URL at root level" do
uri = "http://localhost:4000/fyi/events/123?range=7d"
assert InboxLive.extract_route_prefix_from_uri(uri) == "/fyi"
end

test "extracts route prefix from event detail URL in nested scope" do
uri = "http://localhost:4000/admin/fyi/events/456?range=7d&type=user"
assert InboxLive.extract_route_prefix_from_uri(uri) == "/admin/fyi"
end

test "handles URI without query params" do
uri = "http://localhost:4000/admin/fyi"
assert InboxLive.extract_route_prefix_from_uri(uri) == "/admin/fyi"
end

test "handles URI with trailing slash" do
uri = "http://localhost:4000/admin/fyi/"
assert InboxLive.extract_route_prefix_from_uri(uri) == "/admin/fyi/"
end
end

describe "event_url/4" do
test "generates correct URL for root-level scope" do
socket = %{assigns: %{route_prefix: "/fyi"}}
url = InboxLive.event_url(socket, "event-123", "7d", "")
assert url == "/fyi/events/event-123?range=7d"
end

test "generates correct URL for nested scope" do
socket = %{assigns: %{route_prefix: "/admin/fyi"}}
url = InboxLive.event_url(socket, "event-456", "24h", "user.signup")
assert url == "/admin/fyi/events/event-456?range=24h&type=user.signup"
end

test "generates correct URL without event type filter" do
socket = %{assigns: %{route_prefix: "/admin/dashboard/fyi"}}
url = InboxLive.event_url(socket, "event-789", "1h", "")
assert url == "/admin/dashboard/fyi/events/event-789?range=1h"
end
end

describe "build_url/3" do
test "generates correct index URL for root-level scope" do
socket = %{assigns: %{route_prefix: "/fyi"}}
url = InboxLive.build_url(socket, "7d", "")
assert url == "/fyi?range=7d"
end

test "generates correct index URL for nested scope" do
socket = %{assigns: %{route_prefix: "/admin/fyi"}}
url = InboxLive.build_url(socket, "24h", "error.occurred")
assert url == "/admin/fyi?range=24h&type=error.occurred"
end

test "generates correct URL without event type" do
socket = %{assigns: %{route_prefix: "/admin/dashboard/fyi"}}
url = InboxLive.build_url(socket, "1h", "")
assert url == "/admin/dashboard/fyi?range=1h"
end
end
end