Remember microprint state on reload

This commit is contained in:
Firehose Bot 2026-05-18 20:17:38 +01:00
parent 7afe5c1fe8
commit 790d2f0e1d
3 changed files with 167 additions and 31 deletions

View File

@ -7,8 +7,8 @@ defmodule FirehoseWeb.MicroprintsLive do
@source_dirs ["app", "blogex"] @source_dirs ["app", "blogex"]
@impl true @impl true
def mount(_params, _session, socket) do def mount(params, _session, socket) do
files = scan_source_files() files = scan_source_files_with_dir()
microprints = microprints =
files files
@ -18,9 +18,7 @@ defmodule FirehoseWeb.MicroprintsLive do
socket socket
|> assign(:page_title, "Microprints") |> assign(:page_title, "Microprints")
|> assign(:microprints, microprints) |> assign(:microprints, microprints)
|> assign(:expanded_path, nil) |> restore_state_from_params(params)}
|> assign(:highlighted_path, nil)
|> assign(:highlighted_line, nil)}
rescue rescue
e -> e ->
{:ok, {:ok,
@ -28,11 +26,17 @@ defmodule FirehoseWeb.MicroprintsLive do
|> assign(:page_title, "Microprints") |> assign(:page_title, "Microprints")
|> assign(:microprints, []) |> assign(:microprints, [])
|> assign(:expanded_path, nil) |> assign(:expanded_path, nil)
|> assign(:source_content, nil)
|> assign(:highlighted_path, nil) |> assign(:highlighted_path, nil)
|> assign(:highlighted_line, nil) |> assign(:highlighted_line, nil)
|> put_flash(:error, "Error loading microprints: #{inspect(e)}")} |> put_flash(:error, "Error loading microprints: #{inspect(e)}")}
end end
@impl true
def handle_params(params, _uri, socket) do
{:noreply, restore_state_from_params(socket, params)}
end
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
@ -46,7 +50,7 @@ defmodule FirehoseWeb.MicroprintsLive do
<.microprint_legend /> <.microprint_legend />
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-6"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
<%= 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]} {error = item[:error]}
<div class="card bg-base-100 shadow-sm border border-zinc-200"> <div class="card bg-base-100 shadow-sm border border-zinc-200">
<div class="card-body p-4"> <div class="card-body p-4">
@ -76,8 +80,8 @@ defmodule FirehoseWeb.MicroprintsLive do
highlighted_line={@highlighted_line} highlighted_line={@highlighted_line}
/> />
<%= if @expanded_path == path and source do %> <.source_viewer <%= if @expanded_path == path and @source_content do %> <.source_viewer
content={source} source_content={@source_content}
highlighted_line={@highlighted_line} highlighted_line={@highlighted_line}
language="elixir" language="elixir"
file_path={path} file_path={path}
@ -111,11 +115,14 @@ defmodule FirehoseWeb.MicroprintsLive do
_ -> nil _ -> nil
end end
{:noreply, socket =
socket socket
|> assign(:highlighted_path, path) |> assign(:highlighted_path, path)
|> assign(:highlighted_line, highlighted) |> assign(:highlighted_line, highlighted)
|> assign(:expanded_path, expanded)} |> assign(:expanded_path, expanded)
|> assign(:source_content, nil)
{:noreply, push_patch(socket, to: build_params(socket))}
end end
@impl true @impl true
@ -126,11 +133,75 @@ defmodule FirehoseWeb.MicroprintsLive do
_ -> path _ -> path
end 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 end
# Private helpers # 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 defp sort_by_mtime(files) do
app_root = Mix.Project.project_file() |> Path.dirname() app_root = Mix.Project.project_file() |> Path.dirname()
@ -143,16 +214,39 @@ defmodule FirehoseWeb.MicroprintsLive do
|> Enum.map(fn {file, _mtime} -> file end) |> Enum.map(fn {file, _mtime} -> file end)
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 @doc false
def scan_source_files do def scan_source_files do
@source_dirs @source_dirs
|> Enum.flat_map(&collect_elixir_files/1) |> Enum.flat_map(&collect_elixir_files/1)
|> Enum.uniq() |> Enum.uniq_by(&elem(&1, 0))
|> Enum.map(&elem(&1, 0))
|> Enum.sort() |> Enum.sort()
|> sort_by_mtime() |> sort_by_mtime()
|> Enum.take(2) |> Enum.take(2)
end 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 defp resolve_absolute_paths(files) do
app_root = Mix.Project.project_file() |> Path.dirname() app_root = Mix.Project.project_file() |> Path.dirname()
@ -181,26 +275,26 @@ defmodule FirehoseWeb.MicroprintsLive do
|> Path.wildcard() |> Path.wildcard()
|> Enum.filter(&File.regular?(&1)) |> Enum.filter(&File.regular?(&1))
|> Enum.reject(&excluded_path?/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 -> false ->
[] []
end end
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) relative = Path.relative_to(path, monorepo_root)
formatted =
case dir do case dir do
"app" -> "app" -> String.replace_prefix(relative, "app/", "")
String.replace_prefix(relative, "app/", "") _ -> "../" <> relative
_ ->
"../" <> relative
end
end end
defp process_file(rel_path) do {formatted, dir}
end
defp process_file({rel_path, source_dir}) do
abs_path = resolve_absolute_paths([rel_path]) |> List.first() abs_path = resolve_absolute_paths([rel_path]) |> List.first()
case MicroprintCache.get_microprint(abs_path) do case MicroprintCache.get_microprint(abs_path) do
@ -211,13 +305,10 @@ defmodule FirehoseWeb.MicroprintsLive do
|> Enum.with_index(1) |> Enum.with_index(1)
|> Enum.map(fn {line, num} -> Map.put(line, :line_number, num) end) |> 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, path: rel_path,
microprint: Map.put(microprint, :lines, lines_with_numbers), source_dir: source_dir,
source: source microprint: Map.put(microprint, :lines, lines_with_numbers)
} }
{:error, reason} -> {:error, reason} ->
@ -225,6 +316,13 @@ defmodule FirehoseWeb.MicroprintsLive do
end end
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 defp read_source(abs_path) do
case File.read(abs_path) do case File.read(abs_path) do
{:ok, content} -> {:ok, content} ->
@ -255,7 +353,7 @@ defmodule FirehoseWeb.MicroprintsLive do
data-language={@language} data-language={@language}
style="background: var(--sv-bg); color: var(--sv-text);" style="background: var(--sv-bg); color: var(--sv-text);"
> >
<pre id={@viewer_id <> "-pre"} phx-update="ignore" class="m-0 p-2"><code class={"language-#{@language || "plaintext"}"}><%= @content %></code></pre> <pre id={@viewer_id <> "-pre"} phx-update="ignore" class="m-0 p-2"><code class={"language-#{@language || "plaintext"}"}><%= @source_content %></code></pre>
</div> </div>
""" """
end end

View File

@ -139,4 +139,42 @@ defmodule FirehoseWeb.MicroprintsLiveTest do
"file B should show Collapse button when expanded" "file B should show Collapse button when expanded"
end end
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 end

File diff suppressed because one or more lines are too long