diff --git a/app/lib/firehose_web/live/microprints_live.ex b/app/lib/firehose_web/live/microprints_live.ex index bc81e5d..6dab3a5 100644 --- a/app/lib/firehose_web/live/microprints_live.ex +++ b/app/lib/firehose_web/live/microprints_live.ex @@ -7,8 +7,8 @@ defmodule FirehoseWeb.MicroprintsLive do @source_dirs ["app", "blogex"] @impl true - def mount(_params, _session, socket) do - files = scan_source_files() + def mount(params, _session, socket) do + files = scan_source_files_with_dir() microprints = files @@ -18,9 +18,7 @@ defmodule FirehoseWeb.MicroprintsLive do socket |> assign(:page_title, "Microprints") |> assign(:microprints, microprints) - |> assign(:expanded_path, nil) - |> assign(:highlighted_path, nil) - |> assign(:highlighted_line, nil)} + |> restore_state_from_params(params)} rescue e -> {:ok, @@ -28,11 +26,17 @@ defmodule FirehoseWeb.MicroprintsLive do |> assign(:page_title, "Microprints") |> assign(:microprints, []) |> assign(:expanded_path, nil) + |> assign(:source_content, nil) |> assign(:highlighted_path, nil) |> assign(:highlighted_line, nil) |> put_flash(:error, "Error loading microprints: #{inspect(e)}")} end + @impl true + def handle_params(params, _uri, socket) do + {:noreply, restore_state_from_params(socket, params)} + end + @impl true def render(assigns) do ~H""" @@ -46,7 +50,7 @@ defmodule FirehoseWeb.MicroprintsLive do <.microprint_legend />
- <%= for %{path: path, microprint: microprint, source: source} = item <- @microprints do %> + <%= for %{path: path, microprint: microprint, source_dir: _source_dir} = item <- @microprints do %> {error = item[:error]}
@@ -76,8 +80,8 @@ defmodule FirehoseWeb.MicroprintsLive do highlighted_line={@highlighted_line} /> - <%= if @expanded_path == path and source do %> <.source_viewer - content={source} + <%= if @expanded_path == path and @source_content do %> <.source_viewer + source_content={@source_content} highlighted_line={@highlighted_line} language="elixir" file_path={path} @@ -111,11 +115,14 @@ defmodule FirehoseWeb.MicroprintsLive do _ -> nil end - {:noreply, - socket - |> assign(:highlighted_path, path) - |> assign(:highlighted_line, highlighted) - |> assign(:expanded_path, expanded)} + socket = + socket + |> assign(:highlighted_path, path) + |> assign(:highlighted_line, highlighted) + |> assign(:expanded_path, expanded) + |> assign(:source_content, nil) + + {:noreply, push_patch(socket, to: build_params(socket))} end @impl true @@ -126,11 +133,75 @@ defmodule FirehoseWeb.MicroprintsLive do _ -> path end - {:noreply, assign(socket, :expanded_path, expanded)} + source_content = + case expanded do + nil -> nil + ^path -> + item = Enum.find(socket.assigns.microprints, &(&1.path == path)) + if item && item.source_dir do + abs_path = resolve_source_path(item.source_dir, path) + read_source(abs_path) + else + nil + end + end + + socket = socket |> assign(:expanded_path, expanded) |> assign(:source_content, source_content) + + {:noreply, push_patch(socket, to: build_params(socket))} end # Private helpers + defp restore_state_from_params(socket, params) do + expanded_path = params["expanded"] + highlighted_path = params["highlighted"] + highlighted_line = if params["line"], do: String.to_integer(params["line"]), else: nil + + source_content = + if expanded_path do + item = Enum.find(socket.assigns.microprints, &(&1.path == expanded_path)) + + if item && item.source_dir do + abs_path = resolve_source_path(item.source_dir, expanded_path) + read_source(abs_path) + end + end + + socket + |> assign(:expanded_path, expanded_path) + |> assign(:source_content, source_content) + |> assign(:highlighted_path, highlighted_path) + |> assign(:highlighted_line, highlighted_line) + end + + defp build_params(socket) do + params = %{} + + params = + if socket.assigns.expanded_path do + Map.put(params, "expanded", socket.assigns.expanded_path) + else + params + end + + params = + if socket.assigns.highlighted_path do + Map.put(params, "highlighted", socket.assigns.highlighted_path) + else + params + end + + params = + if socket.assigns.highlighted_line do + params |> Map.put("line", socket.assigns.highlighted_line) + else + params + end + + "/microprints" <> if params != %{}, do: "?" <> URI.encode_query(params), else: "" + end + defp sort_by_mtime(files) do app_root = Mix.Project.project_file() |> Path.dirname() @@ -143,16 +214,39 @@ defmodule FirehoseWeb.MicroprintsLive do |> Enum.map(fn {file, _mtime} -> file end) end + defp sort_by_mtime_with_dir(tuples) do + app_root = Mix.Project.project_file() |> Path.dirname() + + tuples + |> Enum.map(fn {path, dir} -> + abs_path = Path.join(app_root, path) + {{path, dir}, File.stat!(abs_path).mtime} + end) + |> Enum.sort_by(fn {{_path, _dir}, mtime} -> mtime end, :desc) + |> Enum.map(fn {{path, dir}, _mtime} -> {path, dir} end) + end + @doc false def scan_source_files do @source_dirs |> Enum.flat_map(&collect_elixir_files/1) - |> Enum.uniq() + |> Enum.uniq_by(&elem(&1, 0)) + |> Enum.map(&elem(&1, 0)) |> Enum.sort() |> sort_by_mtime() |> Enum.take(2) end + @doc false + def scan_source_files_with_dir do + @source_dirs + |> Enum.flat_map(&collect_elixir_files/1) + |> Enum.uniq_by(&elem(&1, 0)) + |> Enum.sort_by(&elem(&1, 0)) + |> sort_by_mtime_with_dir() + |> Enum.take(2) + end + defp resolve_absolute_paths(files) do app_root = Mix.Project.project_file() |> Path.dirname() @@ -181,26 +275,26 @@ defmodule FirehoseWeb.MicroprintsLive do |> Path.wildcard() |> Enum.filter(&File.regular?(&1)) |> Enum.reject(&excluded_path?/1) - |> Enum.map(&format_path(&1, monorepo_root, dir)) + |> Enum.map(&format_path_and_dir(&1, monorepo_root, dir)) false -> [] end end - defp format_path(path, monorepo_root, dir) do + defp format_path_and_dir(path, monorepo_root, dir) do relative = Path.relative_to(path, monorepo_root) - case dir do - "app" -> - String.replace_prefix(relative, "app/", "") + formatted = + case dir do + "app" -> String.replace_prefix(relative, "app/", "") + _ -> "../" <> relative + end - _ -> - "../" <> relative - end + {formatted, dir} end - defp process_file(rel_path) do + defp process_file({rel_path, source_dir}) do abs_path = resolve_absolute_paths([rel_path]) |> List.first() case MicroprintCache.get_microprint(abs_path) do @@ -211,13 +305,10 @@ defmodule FirehoseWeb.MicroprintsLive do |> Enum.with_index(1) |> Enum.map(fn {line, num} -> Map.put(line, :line_number, num) end) - # Read source code for expand/contract - source = read_source(abs_path) - %{ path: rel_path, - microprint: Map.put(microprint, :lines, lines_with_numbers), - source: source + source_dir: source_dir, + microprint: Map.put(microprint, :lines, lines_with_numbers) } {:error, reason} -> @@ -225,6 +316,13 @@ defmodule FirehoseWeb.MicroprintsLive do end end + defp resolve_source_path(source_dir, rel_path) do + app_root = Mix.Project.project_file() |> Path.dirname() + monorepo_root = app_root |> Path.dirname() + base = Path.join(monorepo_root, source_dir) + Path.join(base, rel_path) + end + defp read_source(abs_path) do case File.read(abs_path) do {:ok, content} -> @@ -255,7 +353,7 @@ defmodule FirehoseWeb.MicroprintsLive do data-language={@language} style="background: var(--sv-bg); color: var(--sv-text);" > -
 "-pre"} phx-update="ignore" class="m-0 p-2"><%= @content %>
+
 "-pre"} phx-update="ignore" class="m-0 p-2"><%= @source_content %>
""" end diff --git a/app/test/firehose_web/live/microprints_live_test.exs b/app/test/firehose_web/live/microprints_live_test.exs index be9130c..e195ef3 100644 --- a/app/test/firehose_web/live/microprints_live_test.exs +++ b/app/test/firehose_web/live/microprints_live_test.exs @@ -139,4 +139,42 @@ defmodule FirehoseWeb.MicroprintsLiveTest do "file B should show Collapse button when expanded" end end + + describe "URL state persistence" do + test "expanded path is restored from query param on mount", %{conn: conn} do + files = MicroprintsLive.scan_source_files() + file_a = List.first(files) + + {:ok, view, _html} = live(conn, ~p"/microprints?expanded=#{file_a}") + + html = render(view) + assert html =~ "Collapse", + "Expanded file should show Collapse button when restored from URL param" + assert html =~ ~s(phx-value-path="#{file_a}") + end + + test "unknown expanded path in URL is ignored gracefully", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/microprints?expanded=nonexistent.ex") + + html = render(view) + refute html =~ "Collapse", + "Nonexistent expanded path should not show Collapse" + end + + test "expand updates URL with expanded param", %{conn: conn} do + files = MicroprintsLive.scan_source_files() + file_a = List.first(files) + + {:ok, view, _html} = live(conn, ~p"/microprints") + + view + |> element("button[phx-value-path=\"#{file_a}\"]", "Expand") + |> render_click() + + # URL should contain expanded param after clicking Expand + html = render(view) + assert html =~ "Collapse", + "File should remain expanded after push_patch updates URL" + end + end end diff --git a/expand-collapse-fixed.html b/expand-collapse-fixed.html index 81ea143..e06c7e8 100644 --- a/expand-collapse-fixed.html +++ b/expand-collapse-fixed.html @@ -1158,7 +1158,7 @@
- +