mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-01-30 18:58:45 +08:00
Implement relative navigation between notebooks (#441)
* Use live redirect for local links in rendered markdown * Resolve relative notebook URLs * Bump LV * Adds tests * Handle nested relative path * Handle child nested paths
This commit is contained in:
parent
44ae4ecf94
commit
bd8e06b5ce
7 changed files with 253 additions and 36 deletions
|
@ -4,8 +4,22 @@ import DOMPurify from "dompurify";
|
||||||
import katex from "katex";
|
import katex from "katex";
|
||||||
import { highlight } from "./live_editor/monaco";
|
import { highlight } from "./live_editor/monaco";
|
||||||
|
|
||||||
// Reuse Monaco highlighter for Markdown code blocks
|
// Custom renderer overrides
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
renderer.link = function (href, title, text) {
|
||||||
|
// Browser normalizes URLs with .. so we use a __parent__ modifier
|
||||||
|
// instead and handle it on the server
|
||||||
|
href = href
|
||||||
|
.split("/")
|
||||||
|
.map((part) => (part === ".." ? "__parent__" : part))
|
||||||
|
.join("/");
|
||||||
|
|
||||||
|
return marked.Renderer.prototype.link.call(this, href, title, text);
|
||||||
|
};
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
|
renderer,
|
||||||
|
// Reuse Monaco highlighter for Markdown code blocks
|
||||||
highlight: (code, lang, callback) => {
|
highlight: (code, lang, callback) => {
|
||||||
highlight(code, lang)
|
highlight(code, lang)
|
||||||
.then((html) => callback(null, html))
|
.then((html) => callback(null, html))
|
||||||
|
@ -16,12 +30,14 @@ marked.setOptions({
|
||||||
// Modify external links, so that they open in a new tab.
|
// Modify external links, so that they open in a new tab.
|
||||||
// See https://github.com/cure53/DOMPurify/tree/main/demos#hook-to-open-all-links-in-a-new-window-link
|
// See https://github.com/cure53/DOMPurify/tree/main/demos#hook-to-open-all-links-in-a-new-window-link
|
||||||
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
||||||
if (
|
if (node.tagName.toLowerCase() === "a") {
|
||||||
node.tagName.toLowerCase() === "a" &&
|
if (node.host !== window.location.host) {
|
||||||
node.host !== window.location.host
|
node.setAttribute("target", "_blank");
|
||||||
) {
|
node.setAttribute("rel", "noreferrer noopener");
|
||||||
node.setAttribute("target", "_blank");
|
} else {
|
||||||
node.setAttribute("rel", "noreferrer noopener");
|
node.setAttribute("data-phx-link", "redirect");
|
||||||
|
node.setAttribute("data-phx-link-state", "push");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -311,22 +311,6 @@ defmodule LivebookWeb.HomeLive do
|
||||||
LiveMarkdown.Import.notebook_from_markdown(content)
|
LiveMarkdown.Import.notebook_from_markdown(content)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp put_import_flash_messages(socket, []), do: socket
|
|
||||||
|
|
||||||
defp put_import_flash_messages(socket, messages) do
|
|
||||||
list =
|
|
||||||
messages
|
|
||||||
|> Enum.map(fn message -> ["- ", message] end)
|
|
||||||
|> Enum.intersperse("\n")
|
|
||||||
|
|
||||||
flash =
|
|
||||||
IO.iodata_to_binary([
|
|
||||||
"We found problems while importing the file and tried to autofix them:\n" | list
|
|
||||||
])
|
|
||||||
|
|
||||||
put_flash(socket, :info, flash)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp session_id_by_path(path, session_summaries) do
|
defp session_id_by_path(path, session_summaries) do
|
||||||
summary = Enum.find(session_summaries, &(&1.path == path))
|
summary = Enum.find(session_summaries, &(&1.path == path))
|
||||||
summary.session_id
|
summary.session_id
|
||||||
|
|
|
@ -18,4 +18,28 @@ defmodule LivebookWeb.SessionHelpers do
|
||||||
put_flash(socket, :error, "Failed to create session: #{reason}")
|
put_flash(socket, :error, "Failed to create session: #{reason}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Formats the given list of notebook import messages and puts
|
||||||
|
into the info flash.
|
||||||
|
"""
|
||||||
|
@spec put_import_flash_messages(Phoenix.LiveView.Socket.t(), list(String.t())) ::
|
||||||
|
Phoenix.LiveView.Socket.t()
|
||||||
|
def put_import_flash_messages(socket, messages)
|
||||||
|
|
||||||
|
def put_import_flash_messages(socket, []), do: socket
|
||||||
|
|
||||||
|
def put_import_flash_messages(socket, messages) do
|
||||||
|
list =
|
||||||
|
messages
|
||||||
|
|> Enum.map(fn message -> ["- ", message] end)
|
||||||
|
|> Enum.intersperse("\n")
|
||||||
|
|
||||||
|
flash =
|
||||||
|
IO.iodata_to_binary([
|
||||||
|
"We found problems while importing the file and tried to autofix them:\n" | list
|
||||||
|
])
|
||||||
|
|
||||||
|
put_flash(socket, :info, flash)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,10 +2,11 @@ defmodule LivebookWeb.SessionLive do
|
||||||
use LivebookWeb, :live_view
|
use LivebookWeb, :live_view
|
||||||
|
|
||||||
import LivebookWeb.UserHelpers
|
import LivebookWeb.UserHelpers
|
||||||
|
import LivebookWeb.SessionHelpers
|
||||||
import Livebook.Utils, only: [access_by_id: 1]
|
import Livebook.Utils, only: [access_by_id: 1]
|
||||||
|
|
||||||
alias LivebookWeb.SidebarHelpers
|
alias LivebookWeb.SidebarHelpers
|
||||||
alias Livebook.{SessionSupervisor, Session, Delta, Notebook, Runtime}
|
alias Livebook.{SessionSupervisor, Session, Delta, Notebook, Runtime, LiveMarkdown}
|
||||||
alias Livebook.Notebook.Cell
|
alias Livebook.Notebook.Cell
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
@ -326,6 +327,21 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:noreply, assign(socket, section: section, first_section_id: first_section_id)}
|
{:noreply, assign(socket, section: section, first_section_id: first_section_id)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_params(
|
||||||
|
%{"path_parts" => path_parts},
|
||||||
|
_url,
|
||||||
|
%{assigns: %{live_action: :catch_all}} = socket
|
||||||
|
) do
|
||||||
|
path_parts =
|
||||||
|
Enum.map(path_parts, fn
|
||||||
|
"__parent__" -> ".."
|
||||||
|
part -> part
|
||||||
|
end)
|
||||||
|
|
||||||
|
path = Path.join(path_parts)
|
||||||
|
{:noreply, handle_relative_path(socket, path)}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_params(_params, _url, socket) do
|
def handle_params(_params, _url, socket) do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
@ -608,7 +624,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
data = Session.get_data(socket.assigns.session_id)
|
data = Session.get_data(socket.assigns.session_id)
|
||||||
notebook = Notebook.forked(data.notebook)
|
notebook = Notebook.forked(data.notebook)
|
||||||
%{images_dir: images_dir} = Session.get_summary(socket.assigns.session_id)
|
%{images_dir: images_dir} = Session.get_summary(socket.assigns.session_id)
|
||||||
create_session(socket, notebook: notebook, copy_images_from: images_dir)
|
{:noreply, create_session(socket, notebook: notebook, copy_images_from: images_dir)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("location_report", report, socket) do
|
def handle_event("location_report", report, socket) do
|
||||||
|
@ -635,16 +651,6 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:reply, %{code: formatted}, socket}
|
{:reply, %{code: formatted}, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp create_session(socket, opts) do
|
|
||||||
case SessionSupervisor.create_session(opts) do
|
|
||||||
{:ok, id} ->
|
|
||||||
{:noreply, push_redirect(socket, to: Routes.session_path(socket, :page, id))}
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
{:noreply, put_flash(socket, :error, "Failed to create a notebook: #{reason}")}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:operation, operation}, socket) do
|
def handle_info({:operation, operation}, socket) do
|
||||||
case Session.Data.apply_operation(socket.private.data, operation) do
|
case Session.Data.apply_operation(socket.private.data, operation) do
|
||||||
|
@ -720,6 +726,67 @@ defmodule LivebookWeb.SessionLive do
|
||||||
|
|
||||||
def handle_info(_message, socket), do: {:noreply, socket}
|
def handle_info(_message, socket), do: {:noreply, socket}
|
||||||
|
|
||||||
|
defp handle_relative_path(socket, path) do
|
||||||
|
cond do
|
||||||
|
String.ends_with?(path, LiveMarkdown.extension()) ->
|
||||||
|
handle_relative_notebook_path(socket, path)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
socket
|
||||||
|
|> push_patch(to: Routes.session_path(socket, :page, socket.assigns.session_id))
|
||||||
|
|> put_flash(
|
||||||
|
:error,
|
||||||
|
"Got unrecognised session path: #{path}\nIf you want to link another notebook, make sure to include the .livemd extension"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_relative_notebook_path(socket, relative_path) do
|
||||||
|
case socket.private.data.path do
|
||||||
|
nil ->
|
||||||
|
socket
|
||||||
|
|> put_flash(
|
||||||
|
:info,
|
||||||
|
"Cannot resolve notebook path #{relative_path}, because the current notebook has no file"
|
||||||
|
)
|
||||||
|
|> push_patch(to: Routes.session_path(socket, :page, socket.assigns.session_id))
|
||||||
|
|
||||||
|
path ->
|
||||||
|
target_path = path |> Path.dirname() |> Path.join(relative_path) |> Path.expand()
|
||||||
|
maybe_open_notebook(socket, target_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_open_notebook(socket, path) do
|
||||||
|
if session_id = session_id_by_path(path) do
|
||||||
|
push_redirect(socket, to: Routes.session_path(socket, :page, session_id))
|
||||||
|
else
|
||||||
|
case File.read(path) do
|
||||||
|
{:ok, content} ->
|
||||||
|
{notebook, messages} = LiveMarkdown.Import.notebook_from_markdown(content)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> put_import_flash_messages(messages)
|
||||||
|
|> create_session(notebook: notebook, path: path)
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
message = :file.format_error(error)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> put_flash(:error, "Failed to open #{path}, reason: #{message}")
|
||||||
|
|> push_patch(to: Routes.session_path(socket, :page, socket.assigns.session_id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp session_id_by_path(path) do
|
||||||
|
session_summaries = SessionSupervisor.get_session_summaries()
|
||||||
|
|
||||||
|
Enum.find_value(session_summaries, fn summary ->
|
||||||
|
summary.path == path && summary.session_id
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp after_operation(socket, _prev_socket, {:client_join, client_pid, user}) do
|
defp after_operation(socket, _prev_socket, {:client_join, client_pid, user}) do
|
||||||
push_event(socket, "client_joined", %{client: client_info(client_pid, user)})
|
push_event(socket, "client_joined", %{client: client_info(client_pid, user)})
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,9 +23,11 @@ defmodule LivebookWeb.Router do
|
||||||
live "/home/user-profile", HomeLive, :user
|
live "/home/user-profile", HomeLive, :user
|
||||||
live "/home/import/:tab", HomeLive, :import
|
live "/home/import/:tab", HomeLive, :import
|
||||||
live "/home/sessions/:session_id/close", HomeLive, :close_session
|
live "/home/sessions/:session_id/close", HomeLive, :close_session
|
||||||
|
|
||||||
live "/explore", ExploreLive, :page
|
live "/explore", ExploreLive, :page
|
||||||
live "/explore/user-profile", ExploreLive, :user
|
live "/explore/user-profile", ExploreLive, :user
|
||||||
live "/explore/notebooks/:slug", ExploreLive, :notebook
|
live "/explore/notebooks/:slug", ExploreLive, :notebook
|
||||||
|
|
||||||
live "/sessions/:id", SessionLive, :page
|
live "/sessions/:id", SessionLive, :page
|
||||||
live "/sessions/:id/user-profile", SessionLive, :user
|
live "/sessions/:id/user-profile", SessionLive, :user
|
||||||
live "/sessions/:id/shortcuts", SessionLive, :shortcuts
|
live "/sessions/:id/shortcuts", SessionLive, :shortcuts
|
||||||
|
@ -36,6 +38,7 @@ defmodule LivebookWeb.Router do
|
||||||
live "/sessions/:id/cell-upload/:cell_id", SessionLive, :cell_upload
|
live "/sessions/:id/cell-upload/:cell_id", SessionLive, :cell_upload
|
||||||
live "/sessions/:id/delete-section/:section_id", SessionLive, :delete_section
|
live "/sessions/:id/delete-section/:section_id", SessionLive, :delete_section
|
||||||
get "/sessions/:id/images/:image", SessionController, :show_image
|
get "/sessions/:id/images/:image", SessionController, :show_image
|
||||||
|
live "/sessions/:id/*path_parts", SessionLive, :catch_all
|
||||||
|
|
||||||
live_dashboard "/dashboard",
|
live_dashboard "/dashboard",
|
||||||
metrics: LivebookWeb.Telemetry,
|
metrics: LivebookWeb.Telemetry,
|
||||||
|
|
2
mix.lock
2
mix.lock
|
@ -14,7 +14,7 @@
|
||||||
"phoenix_html": {:git, "https://github.com/phoenixframework/phoenix_html.git", "d35bebbea395569573ef0e1757cbec735da0573b", []},
|
"phoenix_html": {:git, "https://github.com/phoenixframework/phoenix_html.git", "d35bebbea395569573ef0e1757cbec735da0573b", []},
|
||||||
"phoenix_live_dashboard": {:git, "https://github.com/phoenixframework/phoenix_live_dashboard.git", "1cc67e3c7275b8e68d8201e5dc3660893ae9e4ec", []},
|
"phoenix_live_dashboard": {:git, "https://github.com/phoenixframework/phoenix_live_dashboard.git", "1cc67e3c7275b8e68d8201e5dc3660893ae9e4ec", []},
|
||||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
|
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
|
||||||
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "92100b658b257a9dffc11a4ca13e4e9054048f61", []},
|
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "dcdde14ba2a42908bc7b55d1662e9e33c667ed0e", []},
|
||||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
|
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
|
||||||
"phoenix_view": {:git, "https://github.com/phoenixframework/phoenix_view.git", "90ce9c9ef5f832f80e956b77d079f79171ed45d0", []},
|
"phoenix_view": {:git, "https://github.com/phoenixframework/phoenix_view.git", "90ce9c9ef5f832f80e956b77d079f79171ed45d0", []},
|
||||||
"plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"},
|
"plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"},
|
||||||
|
|
|
@ -405,6 +405,129 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "relative paths" do
|
||||||
|
test "renders an info message when the path doesn't have notebook extension",
|
||||||
|
%{conn: conn, session_id: session_id} do
|
||||||
|
session_path = "/sessions/#{session_id}"
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: ^session_path}}} =
|
||||||
|
result = live(conn, "/sessions/#{session_id}/document.pdf")
|
||||||
|
|
||||||
|
{:ok, view, _} = follow_redirect(result, conn)
|
||||||
|
assert render(view) =~ "Got unrecognised session path: document.pdf"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders an info message when the session has no associated path",
|
||||||
|
%{conn: conn, session_id: session_id} do
|
||||||
|
session_path = "/sessions/#{session_id}"
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: ^session_path}}} =
|
||||||
|
result = live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||||
|
|
||||||
|
{:ok, view, _} = follow_redirect(result, conn)
|
||||||
|
|
||||||
|
assert render(view) =~
|
||||||
|
"Cannot resolve notebook path notebook.livemd, because the current notebook has no file"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "renders an error message when the relative notebook does not exist",
|
||||||
|
%{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||||
|
index_path = Path.join(tmp_dir, "index.livemd")
|
||||||
|
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
||||||
|
|
||||||
|
Session.set_path(session_id, index_path)
|
||||||
|
wait_for_session_update(session_id)
|
||||||
|
|
||||||
|
session_path = "/sessions/#{session_id}"
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: ^session_path}}} =
|
||||||
|
result = live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||||
|
|
||||||
|
{:ok, view, _} = follow_redirect(result, conn)
|
||||||
|
|
||||||
|
assert render(view) =~
|
||||||
|
"Failed to open #{notebook_path}, reason: no such file or directory"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "opens a relative notebook if it exists",
|
||||||
|
%{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||||
|
index_path = Path.join(tmp_dir, "index.livemd")
|
||||||
|
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
||||||
|
|
||||||
|
Session.set_path(session_id, index_path)
|
||||||
|
wait_for_session_update(session_id)
|
||||||
|
|
||||||
|
File.write!(notebook_path, "# Sibling notebook")
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: _session_path}}} =
|
||||||
|
result = live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||||
|
|
||||||
|
{:ok, view, _} = follow_redirect(result, conn)
|
||||||
|
|
||||||
|
assert render(view) =~ "Sibling notebook"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "if the notebook is already open, redirects to the session",
|
||||||
|
%{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||||
|
index_path = Path.join(tmp_dir, "index.livemd")
|
||||||
|
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
||||||
|
|
||||||
|
Session.set_path(session_id, index_path)
|
||||||
|
wait_for_session_update(session_id)
|
||||||
|
|
||||||
|
File.write!(notebook_path, "# Sibling notebook")
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: session_path}}} =
|
||||||
|
live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||||
|
|
||||||
|
assert {:error, {:live_redirect, %{to: ^session_path}}} =
|
||||||
|
live(conn, "/sessions/#{session_id}/notebook.livemd")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "handles nested paths", %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||||
|
parent_path = Path.join(tmp_dir, "parent.livemd")
|
||||||
|
child_dir = Path.join(tmp_dir, "dir")
|
||||||
|
child_path = Path.join(child_dir, "child.livemd")
|
||||||
|
|
||||||
|
Session.set_path(session_id, parent_path)
|
||||||
|
wait_for_session_update(session_id)
|
||||||
|
|
||||||
|
File.mkdir!(child_dir)
|
||||||
|
File.write!(child_path, "# Child notebook")
|
||||||
|
|
||||||
|
{:ok, view, _} =
|
||||||
|
conn
|
||||||
|
|> live("/sessions/#{session_id}/dir/child.livemd")
|
||||||
|
|> follow_redirect(conn)
|
||||||
|
|
||||||
|
assert render(view) =~ "Child notebook"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "handles parent paths", %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do
|
||||||
|
parent_path = Path.join(tmp_dir, "parent.livemd")
|
||||||
|
child_dir = Path.join(tmp_dir, "dir")
|
||||||
|
child_path = Path.join(child_dir, "child.livemd")
|
||||||
|
|
||||||
|
File.mkdir!(child_dir)
|
||||||
|
Session.set_path(session_id, child_path)
|
||||||
|
wait_for_session_update(session_id)
|
||||||
|
|
||||||
|
File.write!(parent_path, "# Parent notebook")
|
||||||
|
|
||||||
|
{:ok, view, _} =
|
||||||
|
conn
|
||||||
|
|> live("/sessions/#{session_id}/__parent__/parent.livemd")
|
||||||
|
|> follow_redirect(conn)
|
||||||
|
|
||||||
|
assert render(view) =~ "Parent notebook"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
||||||
defp wait_for_session_update(session_id) do
|
defp wait_for_session_update(session_id) do
|
||||||
|
|
Loading…
Reference in a new issue