mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-29 23:05:59 +08:00
Implement reactive input (#389)
* Implement reactive input * Store reactive attribute only when truthy * Polishing
This commit is contained in:
parent
1689dae71c
commit
4ca6f9eb5e
14 changed files with 477 additions and 116 deletions
|
|
@ -52,12 +52,14 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
value = if cell.type == :password, do: "", else: cell.value
|
||||
|
||||
json =
|
||||
Jason.encode!(%{
|
||||
%{
|
||||
livebook_object: :cell_input,
|
||||
type: cell.type,
|
||||
name: cell.name,
|
||||
value: value
|
||||
})
|
||||
}
|
||||
|> put_truthy(reactive: cell.reactive)
|
||||
|> Jason.encode!()
|
||||
|
||||
"<!-- livebook:#{json} -->"
|
||||
|> prepend_metadata(cell.metadata)
|
||||
|
|
@ -120,4 +122,14 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
_ -> code
|
||||
end
|
||||
end
|
||||
|
||||
defp put_truthy(map, entries) do
|
||||
Enum.reduce(entries, map, fn {key, value}, map ->
|
||||
if value do
|
||||
Map.put(map, key, value)
|
||||
else
|
||||
map
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -201,7 +201,9 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
| metadata: metadata,
|
||||
type: data["type"] |> String.to_existing_atom(),
|
||||
name: data["name"],
|
||||
value: data["value"]
|
||||
value: data["value"],
|
||||
# Optional flags
|
||||
reactive: Map.get(data, "reactive", false)
|
||||
}
|
||||
|
||||
build_notebook(elems, [cell | cells], sections)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ defmodule Livebook.Notebook.Cell.Input do
|
|||
# It consists of an input that the user may fill
|
||||
# and then read during code evaluation.
|
||||
|
||||
defstruct [:id, :metadata, :type, :name, :value]
|
||||
defstruct [:id, :metadata, :type, :name, :value, :reactive]
|
||||
|
||||
alias Livebook.Utils
|
||||
alias Livebook.Notebook.Cell
|
||||
|
|
@ -16,7 +16,8 @@ defmodule Livebook.Notebook.Cell.Input do
|
|||
metadata: Cell.metadata(),
|
||||
type: type(),
|
||||
name: String.t(),
|
||||
value: String.t()
|
||||
value: String.t(),
|
||||
reactive: boolean()
|
||||
}
|
||||
|
||||
@type type :: :text | :url | :number | :password
|
||||
|
|
@ -31,7 +32,8 @@ defmodule Livebook.Notebook.Cell.Input do
|
|||
metadata: %{},
|
||||
type: :text,
|
||||
name: "input",
|
||||
value: ""
|
||||
value: "",
|
||||
reactive: false
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -62,4 +64,20 @@ defmodule Livebook.Notebook.Cell.Input do
|
|||
_ -> {:error, "not a valid number"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the input changed in terms of content.
|
||||
"""
|
||||
@spec invalidated?(t(), t()) :: boolean()
|
||||
def invalidated?(cell, prev_cell) do
|
||||
cell.value != prev_cell.value or cell.name != prev_cell.name
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the input change should trigger reactive update.
|
||||
"""
|
||||
@spec reactive_update?(t(), t()) :: boolean()
|
||||
def reactive_update?(cell, prev_cell) do
|
||||
cell.reactive and cell.value != prev_cell.value and validate(cell) == :ok
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -495,8 +495,10 @@ defmodule Livebook.Session do
|
|||
end
|
||||
|
||||
def handle_info({:evaluation_input, cell_id, reply_to, prompt}, state) do
|
||||
input_cell = Notebook.input_cell_for_prompt(state.data.notebook, cell_id, prompt)
|
||||
|
||||
reply =
|
||||
with {:ok, cell} <- Notebook.input_cell_for_prompt(state.data.notebook, cell_id, prompt),
|
||||
with {:ok, cell} <- input_cell,
|
||||
:ok <- Cell.Input.validate(cell) do
|
||||
{:ok, cell.value <> "\n"}
|
||||
else
|
||||
|
|
@ -505,6 +507,15 @@ defmodule Livebook.Session do
|
|||
|
||||
send(reply_to, {:evaluation_input_reply, reply})
|
||||
|
||||
state =
|
||||
case input_cell do
|
||||
{:ok, input_cell} ->
|
||||
handle_operation(state, {:bind_input, self(), cell_id, input_cell.id})
|
||||
|
||||
:error ->
|
||||
state
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ defmodule Livebook.Session.Data do
|
|||
deltas: list(Delta.t()),
|
||||
revision_by_client_pid: %{pid() => cell_revision()},
|
||||
evaluation_digest: String.t() | nil,
|
||||
evaluation_time_ms: integer() | nil
|
||||
evaluation_time_ms: integer() | nil,
|
||||
number_of_evaluations: non_neg_integer(),
|
||||
bound_to_input_ids: MapSet.t(Cell.id())
|
||||
}
|
||||
|
||||
@type cell_revision :: non_neg_integer()
|
||||
|
|
@ -87,6 +89,7 @@ defmodule Livebook.Session.Data do
|
|||
| {:evaluation_started, pid(), Cell.id(), binary()}
|
||||
| {:add_cell_evaluation_output, pid(), Cell.id(), term()}
|
||||
| {:add_cell_evaluation_response, pid(), Cell.id(), term()}
|
||||
| {:bind_input, pid(), elixir_cell_id :: Cell.id(), input_cell_id :: Cell.id()}
|
||||
| {:reflect_evaluation_failure, pid()}
|
||||
| {:cancel_cell_evaluation, pid(), Cell.id()}
|
||||
| {:set_notebook_name, pid(), String.t()}
|
||||
|
|
@ -307,6 +310,20 @@ defmodule Livebook.Session.Data do
|
|||
end
|
||||
end
|
||||
|
||||
def apply_operation(data, {:bind_input, _client_pid, id, input_id}) do
|
||||
with {:ok, %Cell.Elixir{} = cell, _section} <-
|
||||
Notebook.fetch_cell_and_section(data.notebook, id),
|
||||
{:ok, %Cell.Input{} = input_cell, _section} <-
|
||||
Notebook.fetch_cell_and_section(data.notebook, input_id) do
|
||||
data
|
||||
|> with_actions()
|
||||
|> bind_input(cell, input_cell)
|
||||
|> wrap_ok()
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def apply_operation(data, {:reflect_evaluation_failure, _client_pid}) do
|
||||
data
|
||||
|> with_actions()
|
||||
|
|
@ -421,21 +438,15 @@ defmodule Livebook.Session.Data do
|
|||
def apply_operation(data, {:set_cell_attributes, _client_pid, cell_id, attrs}) do
|
||||
with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
|
||||
true <- Enum.all?(attrs, fn {key, _} -> Map.has_key?(cell, key) end) do
|
||||
invalidates_dependent =
|
||||
case cell do
|
||||
%Cell.Input{} -> Map.has_key?(attrs, :value) or Map.has_key?(attrs, :name)
|
||||
_ -> false
|
||||
end
|
||||
|
||||
data
|
||||
|> with_actions()
|
||||
|> set_cell_attributes(cell, attrs)
|
||||
|> then(fn data_actions ->
|
||||
if invalidates_dependent do
|
||||
mark_dependent_cells_as_stale(data_actions, cell)
|
||||
else
|
||||
data_actions
|
||||
end
|
||||
|> then(fn {data, _} = data_actions ->
|
||||
{:ok, updated_cell, _} = Notebook.fetch_cell_and_section(data.notebook, cell_id)
|
||||
|
||||
data_actions
|
||||
|> maybe_invalidate_bound_cells(updated_cell, cell)
|
||||
|> maybe_queue_bound_cells(updated_cell, cell)
|
||||
end)
|
||||
|> set_dirty()
|
||||
|> wrap_ok()
|
||||
|
|
@ -548,11 +559,14 @@ defmodule Livebook.Session.Data do
|
|||
defp queue_cell_evaluation(data_actions, cell, section) do
|
||||
data_actions
|
||||
|> update_section_info!(section.id, fn section ->
|
||||
%{section | evaluation_queue: section.evaluation_queue ++ [cell.id]}
|
||||
%{section | evaluation_queue: append_new(section.evaluation_queue, cell.id)}
|
||||
end)
|
||||
|> update_cell_info!(cell.id, fn info ->
|
||||
update_in(info.evaluation_status, fn
|
||||
:ready -> :queued
|
||||
other -> other
|
||||
end)
|
||||
end)
|
||||
|> set_cell_info!(cell.id,
|
||||
evaluation_status: :queued
|
||||
)
|
||||
end
|
||||
|
||||
defp unqueue_cell_evaluation(data_actions, cell, section) do
|
||||
|
|
@ -605,20 +619,20 @@ defmodule Livebook.Session.Data do
|
|||
|
||||
defp finish_cell_evaluation(data_actions, cell, section, metadata) do
|
||||
data_actions
|
||||
|> set_cell_info!(cell.id,
|
||||
evaluation_status: :ready,
|
||||
evaluation_time_ms: metadata.evaluation_time_ms
|
||||
)
|
||||
|> update_cell_info!(cell.id, fn info ->
|
||||
%{
|
||||
info
|
||||
| evaluation_status: :ready,
|
||||
evaluation_time_ms: metadata.evaluation_time_ms,
|
||||
number_of_evaluations: info.number_of_evaluations + 1
|
||||
}
|
||||
end)
|
||||
|> set_section_info!(section.id, evaluating_cell_id: nil)
|
||||
end
|
||||
|
||||
defp mark_dependent_cells_as_stale({data, _} = data_actions, cell) do
|
||||
child_cells =
|
||||
data.notebook
|
||||
|> Notebook.child_cells_with_section(cell.id)
|
||||
|> Enum.filter(fn {cell, _} -> is_struct(cell, Cell.Elixir) end)
|
||||
|
||||
mark_cells_as_stale(data_actions, child_cells)
|
||||
dependent = dependent_cells_with_section(data, cell.id)
|
||||
mark_cells_as_stale(data_actions, dependent)
|
||||
end
|
||||
|
||||
defp mark_cells_as_stale({data, _} = data_actions, cells_with_section) do
|
||||
|
|
@ -672,7 +686,8 @@ defmodule Livebook.Session.Data do
|
|||
# During evaluation notebook changes may invalidate the cell,
|
||||
# so we mark it as up-to-date straight away and possibly mark
|
||||
# it as stale during evaluation
|
||||
validity_status: :evaluated
|
||||
validity_status: :evaluated,
|
||||
bound_to_input_ids: MapSet.new()
|
||||
}
|
||||
end)
|
||||
|> set_section_info!(section.id, evaluating_cell_id: id, evaluation_queue: ids)
|
||||
|
|
@ -686,6 +701,13 @@ defmodule Livebook.Session.Data do
|
|||
end
|
||||
end
|
||||
|
||||
defp bind_input(data_actions, cell, input_cell) do
|
||||
data_actions
|
||||
|> update_cell_info!(cell.id, fn info ->
|
||||
%{info | bound_to_input_ids: MapSet.put(info.bound_to_input_ids, input_cell.id)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp clear_evaluation({data, _} = data_actions) do
|
||||
data_actions
|
||||
|> reduce(data.notebook.sections, &clear_section_evaluation/2)
|
||||
|
|
@ -730,8 +752,8 @@ defmodule Livebook.Session.Data do
|
|||
end
|
||||
|
||||
defp unqueue_dependent_cells_evaluation({data, _} = data_actions, cell) do
|
||||
dependent_cells = Notebook.child_cells_with_section(data.notebook, cell.id)
|
||||
unqueue_cells_evaluation(data_actions, dependent_cells)
|
||||
dependent = dependent_cells_with_section(data, cell.id)
|
||||
unqueue_cells_evaluation(data_actions, dependent)
|
||||
end
|
||||
|
||||
defp unqueue_cells_evaluation({data, _} = data_actions, cells_with_section) do
|
||||
|
|
@ -838,6 +860,40 @@ defmodule Livebook.Session.Data do
|
|||
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, &Map.merge(&1, attrs)))
|
||||
end
|
||||
|
||||
defp maybe_invalidate_bound_cells({data, _} = data_actions, %Cell.Input{} = cell, prev_cell) do
|
||||
if Cell.Input.invalidated?(cell, prev_cell) do
|
||||
bound_cells = bound_cells_with_section(data, cell.id)
|
||||
|
||||
data_actions
|
||||
|> reduce(bound_cells, fn data_actions, {bound_cell, section} ->
|
||||
dependent = dependent_cells_with_section(data, bound_cell.id)
|
||||
mark_cells_as_stale(data_actions, [{bound_cell, section} | dependent])
|
||||
end)
|
||||
else
|
||||
data_actions
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_invalidate_bound_cells(data_actions, _cell, _prev_cell), do: data_actions
|
||||
|
||||
defp maybe_queue_bound_cells({data, _} = data_actions, %Cell.Input{} = cell, prev_cell) do
|
||||
if Cell.Input.reactive_update?(cell, prev_cell) do
|
||||
bound_cells = bound_cells_with_section(data, cell.id)
|
||||
|
||||
data_actions
|
||||
|> reduce(bound_cells, fn data_actions, {bound_cell, section} ->
|
||||
data_actions
|
||||
|> queue_prerequisite_cells_evaluation(bound_cell)
|
||||
|> queue_cell_evaluation(bound_cell, section)
|
||||
end)
|
||||
|> maybe_evaluate_queued()
|
||||
else
|
||||
data_actions
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_queue_bound_cells(data_actions, _cell, _prev_cell), do: data_actions
|
||||
|
||||
defp set_runtime(data_actions, prev_data, runtime) do
|
||||
{data, _} = data_actions = set!(data_actions, runtime: runtime)
|
||||
|
||||
|
|
@ -872,6 +928,14 @@ defmodule Livebook.Session.Data do
|
|||
{data, actions ++ [action]}
|
||||
end
|
||||
|
||||
defp append_new(list, item) do
|
||||
if item in list do
|
||||
list
|
||||
else
|
||||
list ++ [item]
|
||||
end
|
||||
end
|
||||
|
||||
defp new_section_info() do
|
||||
%{
|
||||
evaluating_cell_id: nil,
|
||||
|
|
@ -889,7 +953,9 @@ defmodule Livebook.Session.Data do
|
|||
validity_status: :fresh,
|
||||
evaluation_status: :ready,
|
||||
evaluation_digest: nil,
|
||||
evaluation_time_ms: nil
|
||||
evaluation_time_ms: nil,
|
||||
number_of_evaluations: 0,
|
||||
bound_to_input_ids: MapSet.new()
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -951,4 +1017,23 @@ defmodule Livebook.Session.Data do
|
|||
info = data.section_infos[section_id]
|
||||
info && info.evaluating_cell_id
|
||||
end
|
||||
|
||||
@doc """
|
||||
Find child cells bound to the given input cell.
|
||||
"""
|
||||
@spec bound_cells_with_section(t(), Cell.id()) :: list(Cell.t())
|
||||
def bound_cells_with_section(data, cell_id) do
|
||||
data
|
||||
|> dependent_cells_with_section(cell_id)
|
||||
|> Enum.filter(fn {child_cell, _} ->
|
||||
info = data.cell_infos[child_cell.id]
|
||||
MapSet.member?(info.bound_to_input_ids, cell_id)
|
||||
end)
|
||||
end
|
||||
|
||||
defp dependent_cells_with_section(data, cell_id) do
|
||||
data.notebook
|
||||
|> Notebook.child_cells_with_section(cell_id)
|
||||
|> Enum.filter(fn {cell, _} -> is_struct(cell, Cell.Elixir) end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -118,7 +118,6 @@ defmodule LivebookWeb.Helpers do
|
|||
@doc """
|
||||
Renders a list of select input options with the given one selected.
|
||||
"""
|
||||
|
||||
def render_select(name, options, selected) do
|
||||
assigns = %{name: name, options: options, selected: selected}
|
||||
|
||||
|
|
@ -132,4 +131,21 @@ defmodule LivebookWeb.Helpers do
|
|||
</select>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a checkbox input styled as a switch.
|
||||
"""
|
||||
def render_switch(name, checked, label) do
|
||||
assigns = %{name: name, checked: checked, label: label}
|
||||
|
||||
~L"""
|
||||
<div class="flex space-x-3 items-center justify-between">
|
||||
<span class="text-gray-700"><%= @label %></span>
|
||||
<label class="switch-button">
|
||||
<%= tag :input, class: "switch-button__checkbox", type: "checkbox", name: @name, checked: @checked %>
|
||||
<div class="switch-button__bg"></div>
|
||||
</label>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -489,6 +489,18 @@ defmodule LivebookWeb.SessionLive do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("queue_bound_cells_evaluation", %{"cell_id" => cell_id}, socket) do
|
||||
data = socket.private.data
|
||||
|
||||
with {:ok, cell, _section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id) do
|
||||
for {bound_cell, _} <- Session.Data.bound_cells_with_section(data, cell.id) do
|
||||
Session.queue_cell_evaluation(socket.assigns.session_id, bound_cell.id)
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("cancel_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
||||
Session.cancel_cell_evaluation(socket.assigns.session_id, cell_id)
|
||||
|
||||
|
|
@ -851,7 +863,8 @@ defmodule LivebookWeb.SessionLive do
|
|||
outputs: cell.outputs,
|
||||
validity_status: info.validity_status,
|
||||
evaluation_status: info.evaluation_status,
|
||||
evaluation_time_ms: info.evaluation_time_ms
|
||||
evaluation_time_ms: info.evaluation_time_ms,
|
||||
number_of_evaluations: info.number_of_evaluations
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
<div class="w-1 rounded-lg relative -left-3" data-element="cell-focus-indicator">
|
||||
</div>
|
||||
<div>
|
||||
<form phx-change="set_cell_value" onsubmit="return false">
|
||||
<form phx-change="set_cell_value" phx-submit="queue_bound_cells_evaluation">
|
||||
<input type="hidden" name="cell_id" value="<%= @cell_view.id %>" />
|
||||
<div class="input-label">
|
||||
<%= @cell_view.name %>
|
||||
|
|
@ -196,6 +196,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
class="input <%= if(@cell_view.error, do: "input--error") %>"
|
||||
name="value"
|
||||
value="<%= @cell_view.value %>"
|
||||
phx-debounce="300"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
tabindex="-1" />
|
||||
|
|
@ -224,7 +225,12 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
|
||||
<%= if @cell_view.type == :elixir do %>
|
||||
<div class="absolute bottom-2 right-2">
|
||||
<%= render_cell_status(@cell_view.validity_status, @cell_view.evaluation_status, @cell_view.evaluation_time_ms) %>
|
||||
<%= render_cell_status(
|
||||
@cell_view.validity_status,
|
||||
@cell_view.evaluation_status,
|
||||
@cell_view.evaluation_time_ms,
|
||||
"cell-#{@cell_view.id}-evaluation#{@cell_view.number_of_evaluations}"
|
||||
) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -359,13 +365,14 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp render_cell_status(cell_view, evaluation_status, evaluation_time_ms)
|
||||
defp render_cell_status(cell_view, evaluation_status, evaluation_time_ms, evaluation_id)
|
||||
|
||||
defp render_cell_status(_, :evaluating, _) do
|
||||
defp render_cell_status(_, :evaluating, _, evaluation_id) do
|
||||
timer =
|
||||
content_tag(:span, nil,
|
||||
phx_hook: "Timer",
|
||||
id: "evaluating-cell-timer",
|
||||
# Make sure each evaluation gets its own timer
|
||||
id: "#{evaluation_id}-timer",
|
||||
phx_update: "ignore",
|
||||
class: "font-mono"
|
||||
)
|
||||
|
|
@ -376,29 +383,29 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
)
|
||||
end
|
||||
|
||||
defp render_cell_status(_, :queued, _) do
|
||||
defp render_cell_status(_, :queued, _, _) do
|
||||
render_status_indicator("Queued", "bg-gray-500", animated_circle_class: "bg-gray-400")
|
||||
end
|
||||
|
||||
defp render_cell_status(:evaluated, _, evaluation_time_ms) do
|
||||
defp render_cell_status(:evaluated, _, evaluation_time_ms, _) do
|
||||
render_status_indicator("Evaluated", "bg-green-400",
|
||||
change_indicator: true,
|
||||
tooltip: evaluated_label(evaluation_time_ms)
|
||||
)
|
||||
end
|
||||
|
||||
defp render_cell_status(:stale, _, evaluation_time_ms) do
|
||||
defp render_cell_status(:stale, _, evaluation_time_ms, _) do
|
||||
render_status_indicator("Stale", "bg-yellow-200",
|
||||
change_indicator: true,
|
||||
tooltip: evaluated_label(evaluation_time_ms)
|
||||
)
|
||||
end
|
||||
|
||||
defp render_cell_status(:aborted, _, _) do
|
||||
defp render_cell_status(:aborted, _, _, _) do
|
||||
render_status_indicator("Aborted", "bg-red-400")
|
||||
end
|
||||
|
||||
defp render_cell_status(_, _, _), do: nil
|
||||
defp render_cell_status(_, _, _, _), do: nil
|
||||
|
||||
defp render_status_indicator(element, circle_class, opts \\ []) do
|
||||
assigns = %{
|
||||
|
|
|
|||
|
|
@ -21,14 +21,8 @@ defmodule LivebookWeb.SessionLive.ElixirCellSettingsComponent do
|
|||
Cell settings
|
||||
</h3>
|
||||
<form phx-submit="save" phx-target="<%= @myself %>">
|
||||
<div class="w-full flex-col space-y-3">
|
||||
<div class="flex space-x-3 items-center justify-between">
|
||||
<span class="text-gray-700">Disable code formatting (when saving to file)</span>
|
||||
<label class="switch-button">
|
||||
<%= tag :input, class: "switch-button__checkbox", type: "checkbox", name: "disable_formatting", checked: @disable_formatting %>
|
||||
<div class="switch-button__bg"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-full flex-col space-y-6">
|
||||
<%= render_switch("disable_formatting", @disable_formatting, "Disable code formatting (when saving to file)") %>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
|||
|> assign(assigns)
|
||||
|> assign_new(:name, fn -> cell.name end)
|
||||
|> assign_new(:type, fn -> cell.type end)
|
||||
|> assign_new(:reactive, fn -> cell.reactive end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
|
@ -24,12 +25,18 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
|||
Cell settings
|
||||
</h3>
|
||||
<form phx-submit="save" phx-change="validate" phx-target="<%= @myself %>">
|
||||
<div class="flex space-x-8 items-center">
|
||||
<%= render_select("type", [number: "Number", password: "Password", text: "Text", url: "URL"], @type) %>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="input-label">Name</div>
|
||||
<input type="text" class="input" name="name" value="<%= @name %>" spellcheck="false" autocomplete="off" autofocus />
|
||||
<div class="flex flex-col space-y-6">
|
||||
<div>
|
||||
<div class="input-label">Type</div>
|
||||
<%= render_select("type", [number: "Number", password: "Password", text: "Text", url: "URL"], @type) %>
|
||||
</div>
|
||||
<div>
|
||||
<div class="input-label">Name</div>
|
||||
<input type="text" class="input" name="name" value="<%= @name %>" spellcheck="false" autocomplete="off" autofocus />
|
||||
</div>
|
||||
<div>
|
||||
<%= render_switch("reactive", @reactive, "Reactive (reevaluates dependent cells on change)") %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
|
||||
|
|
@ -44,15 +51,22 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"name" => name, "type" => type}, socket) do
|
||||
type = String.to_existing_atom(type)
|
||||
{:noreply, assign(socket, name: name, type: type)}
|
||||
def handle_event("validate", params, socket) do
|
||||
attrs = params_to_attrs(params)
|
||||
{:noreply, assign(socket, attrs)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"name" => name, "type" => type}, socket) do
|
||||
type = String.to_existing_atom(type)
|
||||
attrs = %{name: name, type: type}
|
||||
def handle_event("save", params, socket) do
|
||||
attrs = params_to_attrs(params)
|
||||
Session.set_cell_attributes(socket.assigns.session_id, socket.assigns.cell.id, attrs)
|
||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
||||
end
|
||||
|
||||
defp params_to_attrs(params) do
|
||||
name = params["name"]
|
||||
type = params["type"] |> String.to_existing_atom()
|
||||
reactive = Map.has_key?(params, "reactive")
|
||||
|
||||
%{name: name, type: type, reactive: reactive}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
Notebook.Cell.new(:input)
|
||||
| type: :text,
|
||||
name: "length",
|
||||
value: "100"
|
||||
value: "100",
|
||||
reactive: true
|
||||
},
|
||||
%{
|
||||
Notebook.Cell.new(:elixir)
|
||||
|
|
@ -92,7 +93,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
|
||||
## Section 2
|
||||
|
||||
<!-- livebook:{"livebook_object":"cell_input","name":"length","type":"text","value":"100"} -->
|
||||
<!-- livebook:{"livebook_object":"cell_input","name":"length","reactive":true,"type":"text","value":"100"} -->
|
||||
|
||||
```elixir
|
||||
IO.gets("length: ")
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
|
||||
## Section 2
|
||||
|
||||
<!-- livebook:{"livebook_object":"cell_input","name":"length","type":"text","value":"100"} -->
|
||||
<!-- livebook:{"livebook_object":"cell_input","name":"length","reactive":true,"type":"text","value":"100"} -->
|
||||
|
||||
```elixir
|
||||
IO.gets("length: ")
|
||||
|
|
@ -84,7 +84,8 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
metadata: %{},
|
||||
type: :text,
|
||||
name: "length",
|
||||
value: "100"
|
||||
value: "100",
|
||||
reactive: true
|
||||
},
|
||||
%Cell.Elixir{
|
||||
metadata: %{},
|
||||
|
|
|
|||
|
|
@ -46,4 +46,50 @@ defmodule Livebook.Notebook.Cell.InputText do
|
|||
assert Input.validate(input) == {:error, "not a valid number"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "invalidated?/2" do
|
||||
test "returns false if only the type changes" do
|
||||
input = %{Input.new() | type: :text}
|
||||
updated_input = %{input | type: :url}
|
||||
|
||||
refute Input.invalidated?(updated_input, input)
|
||||
end
|
||||
|
||||
test "returns true if the name changes" do
|
||||
input = %{Input.new() | name: "Name"}
|
||||
updated_input = %{input | name: "Full name"}
|
||||
|
||||
assert Input.invalidated?(updated_input, input)
|
||||
end
|
||||
|
||||
test "returns true if the value changes" do
|
||||
input = %{Input.new() | value: "Jake Peralta"}
|
||||
updated_input = %{input | value: "Amy Santiago"}
|
||||
|
||||
assert Input.invalidated?(updated_input, input)
|
||||
end
|
||||
end
|
||||
|
||||
describe "reactive_change?/2" do
|
||||
test "returns false if the input is not reactive" do
|
||||
input = %{Input.new() | reactive: false, value: "Jake Peralta"}
|
||||
updated_input = %{input | value: "Amy Santiago"}
|
||||
|
||||
refute Input.reactive_update?(updated_input, input)
|
||||
end
|
||||
|
||||
test "returns true if the input is reactive and value changes" do
|
||||
input = %{Input.new() | reactive: true, value: "Jake Peralta"}
|
||||
updated_input = %{input | value: "Amy Santiago"}
|
||||
|
||||
assert Input.reactive_update?(updated_input, input)
|
||||
end
|
||||
|
||||
test "returns false if the new value is invalid" do
|
||||
input = %{Input.new() | reactive: true, type: :number, value: "10"}
|
||||
updated_input = %{input | value: "invalid"}
|
||||
|
||||
refute Input.reactive_update?(updated_input, input)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -8,6 +8,9 @@ defmodule Livebook.Session.DataTest do
|
|||
|
||||
alias Livebook.Runtime.NoopRuntime
|
||||
|
||||
@eval_resp {:ok, [1, 2, 3]}
|
||||
@eval_meta %{evaluation_time_ms: 10}
|
||||
|
||||
describe "new/1" do
|
||||
test "called with no arguments defaults to a blank notebook" do
|
||||
empty_map = %{}
|
||||
|
|
@ -191,11 +194,9 @@ defmodule Livebook.Session.DataTest do
|
|||
# Evaluate both cells
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:queue_cell_evaluation, self(), "c1"},
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 10}},
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
|
||||
{:queue_cell_evaluation, self(), "c2"},
|
||||
{:add_cell_evaluation_response, self(), "c2", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 20}}
|
||||
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
|
||||
])
|
||||
|
||||
operation = {:delete_cell, self(), "c1"}
|
||||
|
|
@ -816,15 +817,12 @@ defmodule Livebook.Session.DataTest do
|
|||
# Evaluate first 2 cells
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:queue_cell_evaluation, self(), "c1"},
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 10}},
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
|
||||
{:queue_cell_evaluation, self(), "c2"},
|
||||
{:add_cell_evaluation_response, self(), "c2", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 20}},
|
||||
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
|
||||
# Evaluate the first cell, so the second becomes stale
|
||||
{:queue_cell_evaluation, self(), "c1"},
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 30}}
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
])
|
||||
|
||||
# The above leads to:
|
||||
|
|
@ -959,8 +957,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:queue_cell_evaluation, self(), "c1"},
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 10}}
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
])
|
||||
|
||||
operation = {:add_cell_evaluation_output, self(), "c1", "Hello!"}
|
||||
|
|
@ -1012,8 +1009,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:queue_cell_evaluation, self(), "c1"}
|
||||
])
|
||||
|
||||
operation =
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}, %{evaluation_time_ms: 10}}
|
||||
operation = {:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
|
|
@ -1031,16 +1027,14 @@ defmodule Livebook.Session.DataTest do
|
|||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
# Evaluate the first cell
|
||||
{:queue_cell_evaluation, self(), "c1"},
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 10}},
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
|
||||
# Start evaluating the second cell
|
||||
{:queue_cell_evaluation, self(), "c2"},
|
||||
# Remove the first cell, marking the second as stale
|
||||
{:delete_cell, self(), "c1"}
|
||||
])
|
||||
|
||||
operation =
|
||||
{:add_cell_evaluation_response, self(), "c2", {:ok, [1, 2, 3]}, %{evaluation_time_ms: 10}}
|
||||
operation = {:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
|
|
@ -1059,8 +1053,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:queue_cell_evaluation, self(), "c2"}
|
||||
])
|
||||
|
||||
operation =
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}, %{evaluation_time_ms: 10}}
|
||||
operation = {:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
|
|
@ -1083,8 +1076,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:queue_cell_evaluation, self(), "c2"}
|
||||
])
|
||||
|
||||
operation =
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}, %{evaluation_time_ms: 10}}
|
||||
operation = {:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
|
|
@ -1132,20 +1124,16 @@ defmodule Livebook.Session.DataTest do
|
|||
# Evaluate all cells
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:queue_cell_evaluation, self(), "c1"},
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 10}},
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
|
||||
{:queue_cell_evaluation, self(), "c2"},
|
||||
{:add_cell_evaluation_response, self(), "c2", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 20}},
|
||||
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
|
||||
{:queue_cell_evaluation, self(), "c3"},
|
||||
{:add_cell_evaluation_response, self(), "c3", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 30}},
|
||||
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
|
||||
# Queue the first cell again
|
||||
{:queue_cell_evaluation, self(), "c1"}
|
||||
])
|
||||
|
||||
operation =
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}, %{evaluation_time_ms: 10}}
|
||||
operation = {:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
|
|
@ -1165,8 +1153,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:queue_cell_evaluation, self(), "c1"}
|
||||
])
|
||||
|
||||
operation =
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}, %{evaluation_time_ms: 10}}
|
||||
operation = {:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
|
||||
Process.sleep(10)
|
||||
|
||||
|
|
@ -1179,6 +1166,51 @@ defmodule Livebook.Session.DataTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "apply_operation/2 given :bind_input" do
|
||||
test "returns an error given invalid input cell id" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :elixir, "c1"}
|
||||
])
|
||||
|
||||
operation = {:bind_input, self(), "c1", "nonexistent"}
|
||||
assert :error = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "returns an error given non-input cell" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
|
||||
{:insert_cell, self(), "s1", 1, :elixir, "c2"}
|
||||
])
|
||||
|
||||
operation = {:bind_input, self(), "c2", "c1"}
|
||||
assert :error = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "updates elixir cell info with binding to the input cell" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :input, "c1"},
|
||||
{:insert_cell, self(), "s1", 1, :elixir, "c2"}
|
||||
])
|
||||
|
||||
operation = {:bind_input, self(), "c2", "c1"}
|
||||
|
||||
bound_to_input_ids = MapSet.new(["c1"])
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
cell_infos: %{
|
||||
"c2" => %{bound_to_input_ids: ^bound_to_input_ids}
|
||||
}
|
||||
}, _actions} = Data.apply_operation(data, operation)
|
||||
end
|
||||
end
|
||||
|
||||
describe "apply_operation/2 given :reflect_evaluation_failure" do
|
||||
test "clears evaluation queue and marks evaluated and evaluating cells as aborted" do
|
||||
data =
|
||||
|
|
@ -1191,8 +1223,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:queue_cell_evaluation, self(), "c1"},
|
||||
{:queue_cell_evaluation, self(), "c2"},
|
||||
{:queue_cell_evaluation, self(), "c3"},
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 10}}
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
])
|
||||
|
||||
operation = {:reflect_evaluation_failure, self()}
|
||||
|
|
@ -1225,8 +1256,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:queue_cell_evaluation, self(), "c1"},
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 10}}
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
])
|
||||
|
||||
operation = {:cancel_cell_evaluation, self(), "c1"}
|
||||
|
|
@ -1243,8 +1273,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:insert_cell, self(), "s2", 0, :elixir, "c3"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:queue_cell_evaluation, self(), "c1"},
|
||||
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 10}},
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta},
|
||||
{:queue_cell_evaluation, self(), "c2"},
|
||||
{:queue_cell_evaluation, self(), "c3"}
|
||||
])
|
||||
|
|
@ -1764,16 +1793,24 @@ defmodule Livebook.Session.DataTest do
|
|||
}, _} = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "given input value change, marks evaluated child cells as stale" do
|
||||
test "given input value change, marks evaluated bound cells and their dependants as stale" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :input, "c1"},
|
||||
{:set_cell_attributes, self(), "c1", %{reactive: false}},
|
||||
# Insert three evaluated cells and bind the second one to the input
|
||||
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
|
||||
{:insert_cell, self(), "s1", 2, :elixir, "c3"},
|
||||
{:insert_cell, self(), "s1", 3, :elixir, "c4"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:queue_cell_evaluation, self(), "c2"},
|
||||
{:add_cell_evaluation_response, self(), "c2", {:ok, [1, 2, 3]},
|
||||
%{evaluation_time_ms: 10}}
|
||||
{:queue_cell_evaluation, self(), "c3"},
|
||||
{:queue_cell_evaluation, self(), "c4"},
|
||||
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
|
||||
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
|
||||
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta},
|
||||
{:bind_input, self(), "c3", "c1"}
|
||||
])
|
||||
|
||||
attrs = %{value: "stuff"}
|
||||
|
|
@ -1781,7 +1818,74 @@ defmodule Livebook.Session.DataTest do
|
|||
|
||||
assert {:ok,
|
||||
%{
|
||||
cell_infos: %{"c2" => %{validity_status: :stale}}
|
||||
cell_infos: %{
|
||||
"c2" => %{validity_status: :evaluated},
|
||||
"c3" => %{validity_status: :stale},
|
||||
"c4" => %{validity_status: :stale}
|
||||
}
|
||||
}, _} = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "given reactive input value change, triggers bound cells evaluation" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :input, "c1"},
|
||||
{:set_cell_attributes, self(), "c1", %{reactive: true}},
|
||||
# Insert three evaluated cells and bind the second and third one to the input
|
||||
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
|
||||
{:insert_cell, self(), "s1", 2, :elixir, "c3"},
|
||||
{:insert_cell, self(), "s1", 3, :elixir, "c4"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:queue_cell_evaluation, self(), "c2"},
|
||||
{:queue_cell_evaluation, self(), "c3"},
|
||||
{:queue_cell_evaluation, self(), "c4"},
|
||||
{:add_cell_evaluation_response, self(), "c2", @eval_resp, @eval_meta},
|
||||
{:add_cell_evaluation_response, self(), "c3", @eval_resp, @eval_meta},
|
||||
{:add_cell_evaluation_response, self(), "c4", @eval_resp, @eval_meta},
|
||||
{:bind_input, self(), "c3", "c1"},
|
||||
{:bind_input, self(), "c4", "c1"}
|
||||
])
|
||||
|
||||
attrs = %{value: "stuff"}
|
||||
operation = {:set_cell_attributes, self(), "c1", attrs}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
cell_infos: %{
|
||||
"c2" => %{evaluation_status: :ready},
|
||||
"c3" => %{evaluation_status: :evaluating},
|
||||
"c4" => %{evaluation_status: :queued}
|
||||
}
|
||||
}, _} = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "given reactive input value change, queues bound cell evaluation even if evaluating" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :input, "c1"},
|
||||
{:set_cell_attributes, self(), "c1", %{reactive: true}},
|
||||
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:queue_cell_evaluation, self(), "c2"},
|
||||
{:bind_input, self(), "c2", "c1"}
|
||||
])
|
||||
|
||||
attrs = %{value: "stuff"}
|
||||
operation = {:set_cell_attributes, self(), "c1", attrs}
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
cell_infos: %{
|
||||
"c2" => %{evaluation_status: :evaluating}
|
||||
},
|
||||
section_infos: %{
|
||||
"s1" => %{
|
||||
evaluating_cell_id: "c2",
|
||||
evaluation_queue: ["c2"]
|
||||
}
|
||||
}
|
||||
}, _} = Data.apply_operation(data, operation)
|
||||
end
|
||||
end
|
||||
|
|
@ -1890,4 +1994,41 @@ defmodule Livebook.Session.DataTest do
|
|||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "bound_cells_with_section/2" do
|
||||
test "returns an empty list when an invalid cell id is given" do
|
||||
data = Data.new()
|
||||
assert [] = Data.bound_cells_with_section(data, "nonexistent")
|
||||
end
|
||||
|
||||
test "returns elixir cells bound to the given input cell" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :input, "c1"},
|
||||
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
|
||||
{:insert_cell, self(), "s1", 2, :elixir, "c3"},
|
||||
{:insert_cell, self(), "s1", 4, :elixir, "c4"},
|
||||
{:bind_input, self(), "c2", "c1"},
|
||||
{:bind_input, self(), "c4", "c1"}
|
||||
])
|
||||
|
||||
assert [{%{id: "c2"}, _}, {%{id: "c4"}, _}] = Data.bound_cells_with_section(data, "c1")
|
||||
end
|
||||
|
||||
test "returns only child cells" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :elixir, "c4"},
|
||||
{:insert_cell, self(), "s1", 1, :input, "c1"},
|
||||
{:insert_cell, self(), "s1", 2, :elixir, "c2"},
|
||||
{:insert_cell, self(), "s1", 3, :elixir, "c3"},
|
||||
{:bind_input, self(), "c2", "c1"},
|
||||
{:bind_input, self(), "c4", "c1"}
|
||||
])
|
||||
|
||||
assert [{%{id: "c2"}, _}] = Data.bound_cells_with_section(data, "c1")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue