Remember microprint state on reload
This commit is contained in:
parent
7afe5c1fe8
commit
790d2f0e1d
@ -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
|
||||||
|
|||||||
@ -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
Loading…
x
Reference in New Issue
Block a user