mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
Add support for input properties (#440)
This commit is contained in:
parent
73845e4f6a
commit
7203813f8d
|
@ -79,28 +79,27 @@
|
|||
@apply py-0 w-16;
|
||||
}
|
||||
|
||||
.input[type="range"] {
|
||||
@apply outline-none appearance-none bg-gray-200 border-none;
|
||||
padding: 0;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.input[type="range"]::-webkit-slider-thumb {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
@apply appearance-none border-transparent bg-blue-600 hover:bg-blue-700 cursor-pointer rounded-xl;
|
||||
}
|
||||
|
||||
.input[type="range"]::-moz-range-thumb {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
@apply appearance-none border-transparent bg-blue-600 hover:bg-blue-700 cursor-pointer rounded-xl;
|
||||
}
|
||||
|
||||
.input--error {
|
||||
@apply bg-red-50 border-red-600 text-red-600;
|
||||
}
|
||||
|
||||
.input-range {
|
||||
height: 8px;
|
||||
@apply appearance-none bg-gray-200 rounded-lg;
|
||||
}
|
||||
|
||||
.input-range::-webkit-slider-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@apply appearance-none border-transparent bg-blue-600 hover:bg-blue-700 cursor-pointer rounded-xl;
|
||||
}
|
||||
|
||||
.input-range::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@apply appearance-none border-transparent bg-blue-600 hover:bg-blue-700 cursor-pointer rounded-xl;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
@apply mb-0.5 text-sm text-gray-800 font-medium;
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
name: cell.name,
|
||||
value: value
|
||||
}
|
||||
|> put_truthy(reactive: cell.reactive)
|
||||
|> put_unless_implicit(reactive: cell.reactive, props: cell.props)
|
||||
|> Jason.encode!()
|
||||
|
||||
"<!-- livebook:#{json} -->"
|
||||
|
@ -135,12 +135,12 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
String.duplicate("`", max_streak + 1)
|
||||
end
|
||||
|
||||
defp put_truthy(map, entries) do
|
||||
defp put_unless_implicit(map, entries) do
|
||||
Enum.reduce(entries, map, fn {key, value}, map ->
|
||||
if value do
|
||||
Map.put(map, key, value)
|
||||
else
|
||||
if value in [false, %{}] do
|
||||
map
|
||||
else
|
||||
Map.put(map, key, value)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
|
|
@ -195,17 +195,8 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
|
||||
defp build_notebook([{:cell, :input, data} | elems], cells, sections) do
|
||||
{metadata, elems} = grab_metadata(elems)
|
||||
|
||||
cell = %{
|
||||
Notebook.Cell.new(:input)
|
||||
| metadata: metadata,
|
||||
type: data["type"] |> String.to_existing_atom(),
|
||||
name: data["name"],
|
||||
value: data["value"],
|
||||
# Optional flags
|
||||
reactive: Map.get(data, "reactive", false)
|
||||
}
|
||||
|
||||
attrs = parse_input_attrs(data)
|
||||
cell = %{Notebook.Cell.new(:input) | metadata: metadata} |> Map.merge(attrs)
|
||||
build_notebook(elems, [cell | cells], sections)
|
||||
end
|
||||
|
||||
|
@ -246,4 +237,26 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
end
|
||||
|
||||
defp grab_metadata(elems), do: {%{}, elems}
|
||||
|
||||
defp parse_input_attrs(data) do
|
||||
type = data["type"] |> String.to_existing_atom()
|
||||
|
||||
%{
|
||||
type: type,
|
||||
name: data["name"],
|
||||
value: data["value"],
|
||||
# Fields with implicit value
|
||||
reactive: Map.get(data, "reactive", false),
|
||||
props: data |> Map.get("props", %{}) |> parse_input_props(type)
|
||||
}
|
||||
end
|
||||
|
||||
defp parse_input_props(data, type) do
|
||||
default_props = Notebook.Cell.Input.default_props(type)
|
||||
|
||||
Map.new(default_props, fn {key, default_value} ->
|
||||
value = Map.get(data, to_string(key), default_value)
|
||||
{key, value}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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, :reactive]
|
||||
defstruct [:id, :metadata, :type, :name, :value, :reactive, :props]
|
||||
|
||||
alias Livebook.Utils
|
||||
alias Livebook.Notebook.Cell
|
||||
|
@ -17,10 +17,16 @@ defmodule Livebook.Notebook.Cell.Input do
|
|||
type: type(),
|
||||
name: String.t(),
|
||||
value: String.t(),
|
||||
reactive: boolean()
|
||||
reactive: boolean(),
|
||||
props: props()
|
||||
}
|
||||
|
||||
@type type :: :text | :url | :number | :password | :textarea | :range
|
||||
@type type :: :text | :url | :number | :password | :textarea | :color | :range
|
||||
|
||||
@typedoc """
|
||||
Additional properties adjusting the given input type.
|
||||
"""
|
||||
@type props :: %{atom() => term()}
|
||||
|
||||
@doc """
|
||||
Returns an empty cell.
|
||||
|
@ -33,7 +39,8 @@ defmodule Livebook.Notebook.Cell.Input do
|
|||
type: :text,
|
||||
name: "input",
|
||||
value: "",
|
||||
reactive: false
|
||||
reactive: false,
|
||||
props: %{}
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -42,17 +49,9 @@ defmodule Livebook.Notebook.Cell.Input do
|
|||
for its type.
|
||||
"""
|
||||
@spec validate(t()) :: :ok | {:error, String.t()}
|
||||
def validate(cell) do
|
||||
validate_value(cell.value, cell.type)
|
||||
end
|
||||
def validate(cell)
|
||||
|
||||
defp validate_value(_value, :text), do: :ok
|
||||
|
||||
defp validate_value(_value, :password), do: :ok
|
||||
|
||||
defp validate_value(_value, :textarea), do: :ok
|
||||
|
||||
defp validate_value(value, :url) do
|
||||
def validate(%{value: value, type: :url}) do
|
||||
if Utils.valid_url?(value) do
|
||||
:ok
|
||||
else
|
||||
|
@ -60,14 +59,14 @@ defmodule Livebook.Notebook.Cell.Input do
|
|||
end
|
||||
end
|
||||
|
||||
defp validate_value(value, :number) do
|
||||
def validate(%{value: value, type: :number}) do
|
||||
case Float.parse(value) do
|
||||
{_number, ""} -> :ok
|
||||
_ -> {:error, "not a valid number"}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_value(value, :color) do
|
||||
def validate(%{value: value, type: :color}) do
|
||||
if Utils.valid_hex_color?(value) do
|
||||
:ok
|
||||
else
|
||||
|
@ -75,13 +74,31 @@ defmodule Livebook.Notebook.Cell.Input do
|
|||
end
|
||||
end
|
||||
|
||||
defp validate_value(value, :range) do
|
||||
case Integer.parse(value) do
|
||||
{_number, ""} -> :ok
|
||||
_ -> {:error, "not a valid number"}
|
||||
def validate(%{value: value, type: :range, props: props}) do
|
||||
case Float.parse(value) do
|
||||
{number, ""} ->
|
||||
cond do
|
||||
number < props.min -> {:error, "number too small"}
|
||||
number > props.max -> {:error, "number too big"}
|
||||
true -> :ok
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, "not a valid number"}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(_cell), do: :ok
|
||||
|
||||
@doc """
|
||||
Returns default properties for input of the given type.
|
||||
"""
|
||||
@spec default_props(type()) :: props()
|
||||
def default_props(type)
|
||||
|
||||
def default_props(:range), do: %{min: 0, max: 100, step: 1}
|
||||
def default_props(_type), do: %{}
|
||||
|
||||
@doc """
|
||||
Checks if the input changed in terms of content.
|
||||
"""
|
||||
|
|
|
@ -987,7 +987,8 @@ defmodule LivebookWeb.SessionLive do
|
|||
case Cell.Input.validate(cell) do
|
||||
:ok -> nil
|
||||
{:error, error} -> error
|
||||
end
|
||||
end,
|
||||
props: cell.props
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -115,24 +115,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
<%= @cell_view.name %>
|
||||
</div>
|
||||
|
||||
<%= if @cell_view.input_type == :textarea do %>
|
||||
<textarea
|
||||
data-element="input"
|
||||
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
|
||||
name="value"
|
||||
spellcheck="false"
|
||||
tabindex="-1"><%= [?\n, @cell_view.value] %></textarea>
|
||||
<% else %>
|
||||
<input type={html_input_type(@cell_view.input_type)}
|
||||
data-element="input"
|
||||
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
|
||||
name="value"
|
||||
value={@cell_view.value}
|
||||
phx-debounce="300"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
tabindex="-1" />
|
||||
<% end %>
|
||||
<.cell_input cell_view={@cell_view} />
|
||||
|
||||
<%= if @cell_view.error do %>
|
||||
<div class="input-error">
|
||||
|
@ -144,6 +127,58 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp cell_input(%{cell_view: %{input_type: :textarea}} = assigns) do
|
||||
~H"""
|
||||
<textarea
|
||||
data-element="input"
|
||||
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
|
||||
name="value"
|
||||
spellcheck="false"
|
||||
tabindex="-1"><%= [?\n, @cell_view.value] %></textarea>
|
||||
"""
|
||||
end
|
||||
|
||||
defp cell_input(%{cell_view: %{input_type: :range}} = assigns) do
|
||||
~H"""
|
||||
<div class="flex items-center space-x-2">
|
||||
<div><%= @cell_view.props.min %></div>
|
||||
<input type="range"
|
||||
data-element="input"
|
||||
class="input-range"
|
||||
name="value"
|
||||
value={@cell_view.value}
|
||||
phx-debounce="300"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
tabindex="-1"
|
||||
min={@cell_view.props.min}
|
||||
max={@cell_view.props.max}
|
||||
step={@cell_view.props.step} />
|
||||
<div><%= @cell_view.props.max %></div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp cell_input(assigns) do
|
||||
~H"""
|
||||
<input type={html_input_type(@cell_view.input_type)}
|
||||
data-element="input"
|
||||
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
|
||||
name="value"
|
||||
value={@cell_view.value}
|
||||
phx-debounce="300"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
tabindex="-1" />
|
||||
"""
|
||||
end
|
||||
|
||||
defp html_input_type(:password), do: "password"
|
||||
defp html_input_type(:number), do: "number"
|
||||
defp html_input_type(:color), do: "color"
|
||||
defp html_input_type(:range), do: "range"
|
||||
defp html_input_type(_), do: "text"
|
||||
|
||||
defp cell_body(assigns) do
|
||||
~H"""
|
||||
<div class="flex relative">
|
||||
|
@ -433,10 +468,4 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
<div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @message %></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp html_input_type(:password), do: "password"
|
||||
defp html_input_type(:number), do: "number"
|
||||
defp html_input_type(:color), do: "color"
|
||||
defp html_input_type(:range), do: "range"
|
||||
defp html_input_type(_), do: "text"
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
|||
use LivebookWeb, :live_component
|
||||
|
||||
alias Livebook.Session
|
||||
alias Livebook.Notebook.Cell
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
|
@ -10,9 +11,11 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
|||
socket =
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_new(:name, fn -> cell.name end)
|
||||
|> assign_new(:type, fn -> cell.type end)
|
||||
|> assign_new(:reactive, fn -> cell.reactive end)
|
||||
|> assign(:current_type, cell.type)
|
||||
|> assign_new(:attrs, fn ->
|
||||
Map.take(cell, [:name, :type, :reactive, :props])
|
||||
end)
|
||||
|> assign_new(:valid, fn -> true end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -24,26 +27,30 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
|||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Cell settings
|
||||
</h3>
|
||||
<form phx-submit="save" phx-change="validate" phx-target={@myself}>
|
||||
<form
|
||||
phx-submit="save"
|
||||
phx-change="validate"
|
||||
phx-target={@myself}
|
||||
spellcheck="false"
|
||||
autocomplete="off">
|
||||
<div class="flex flex-col space-y-6">
|
||||
<div>
|
||||
<div class="input-label">Type</div>
|
||||
<.select name="type" selected={@type} options={input_types()} />
|
||||
<.select name="attrs[type]" selected={@attrs.type} options={input_types()} />
|
||||
</div>
|
||||
<div>
|
||||
<div class="input-label">Name</div>
|
||||
<input type="text" class="input" name="name" value={@name} spellcheck="false" autocomplete="off" autofocus />
|
||||
</div>
|
||||
<div>
|
||||
<.switch_checkbox
|
||||
name="reactive"
|
||||
label="Reactive (reevaluates dependent cells on change)"
|
||||
checked={@reactive} />
|
||||
<input type="text" class="input" name="attrs[name]" value={@attrs.name} autofocus />
|
||||
</div>
|
||||
<.extra_fields type={@attrs.type} props={@attrs.props} />
|
||||
<.switch_checkbox
|
||||
name="attrs[reactive]"
|
||||
label="Reactive (reevaluates dependent cells on change)"
|
||||
checked={@attrs.reactive} />
|
||||
</div>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
|
||||
<button class="button button-blue" type="submit" disabled={@name == ""}>
|
||||
<button class="button button-blue" type="submit" disabled={not @valid}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
@ -52,26 +59,92 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp extra_fields(%{type: :range} = assigns) do
|
||||
~H"""
|
||||
<div class="flex space-x-4">
|
||||
<div class="flex-grow">
|
||||
<div class="input-label">Min</div>
|
||||
<input type="number" class="input" name="attrs[props][min]" value={@props.min} />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="input-label">Max</div>
|
||||
<input type="number" class="input" name="attrs[props][max]" value={@props.max} />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="input-label">Step</div>
|
||||
<input type="number" class="input" name="attrs[props][step]" value={@props.step} />
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp extra_fields(assigns), do: ~H""
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", params, socket) do
|
||||
attrs = params_to_attrs(params)
|
||||
{:noreply, assign(socket, attrs)}
|
||||
{valid?, attrs} = validate_attrs(params["attrs"], socket.assigns.attrs)
|
||||
{:noreply, socket |> assign(attrs: attrs) |> assign(:valid, valid?)}
|
||||
end
|
||||
|
||||
def handle_event("save", params, socket) do
|
||||
attrs = params_to_attrs(params)
|
||||
{true, attrs} = validate_attrs(params["attrs"], socket.assigns.attrs)
|
||||
|
||||
attrs =
|
||||
if attrs.type != socket.assigns.current_type do
|
||||
Map.put(attrs, :value, default_value(attrs.type, attrs.props))
|
||||
else
|
||||
attrs
|
||||
end
|
||||
|
||||
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")
|
||||
defp validate_attrs(data, prev_attrs) do
|
||||
name = data["name"]
|
||||
type = data["type"] |> String.to_existing_atom()
|
||||
reactive = Map.has_key?(data, "reactive")
|
||||
|
||||
%{name: name, type: type, reactive: reactive}
|
||||
{props_valid?, props} =
|
||||
if type == prev_attrs.type do
|
||||
data |> Map.get("props", %{}) |> validate_props(type)
|
||||
else
|
||||
{true, Cell.Input.default_props(type)}
|
||||
end
|
||||
|
||||
valid? = name != "" and props_valid?
|
||||
|
||||
{valid?, %{name: name, type: type, reactive: reactive, props: props}}
|
||||
end
|
||||
|
||||
defp validate_props(data, :range) do
|
||||
min = parse_number(data["min"])
|
||||
max = parse_number(data["max"])
|
||||
step = parse_number(data["step"])
|
||||
valid? = min != nil and max != nil and step != nil and min < max and step > 0
|
||||
data = %{min: min, max: max, step: step}
|
||||
{valid?, data}
|
||||
end
|
||||
|
||||
defp validate_props(_data, _type) do
|
||||
{true, %{}}
|
||||
end
|
||||
|
||||
defp parse_number(string) do
|
||||
case Float.parse(string) do
|
||||
{number, _} ->
|
||||
integer = round(number)
|
||||
if integer == number, do: integer, else: number
|
||||
|
||||
:error ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp default_value(:color, _props), do: "#3E64FF"
|
||||
defp default_value(:range, %{min: min}), do: to_string(min)
|
||||
defp default_value(_type, _props), do: ""
|
||||
|
||||
defp input_types do
|
||||
[
|
||||
color: "Color",
|
||||
|
|
|
@ -60,6 +60,13 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
source: """
|
||||
IO.gets("length: ")
|
||||
"""
|
||||
},
|
||||
%{
|
||||
Notebook.Cell.new(:input)
|
||||
| type: :range,
|
||||
name: "length",
|
||||
value: "100",
|
||||
props: %{min: 50, max: 150, step: 2}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -98,6 +105,8 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
```elixir
|
||||
IO.gets("length: ")
|
||||
```
|
||||
|
||||
<!-- livebook:{"livebook_object":"cell_input","name":"length","props":{"max":150,"min":50,"step":2},"type":"range","value":"100"} -->
|
||||
"""
|
||||
|
||||
document = Export.notebook_to_markdown(notebook)
|
||||
|
@ -384,7 +393,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
assert expected_document == document
|
||||
end
|
||||
|
||||
test "save password as empty string" do
|
||||
test "saves password as empty string" do
|
||||
notebook = %{
|
||||
Notebook.new()
|
||||
| name: "My Notebook",
|
||||
|
|
|
@ -38,6 +38,8 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
```elixir
|
||||
IO.gets("length: ")
|
||||
```
|
||||
|
||||
<!-- livebook:{"livebook_object":"cell_input","name":"length","props":{"max":150,"min":50,"step":2},"type":"range","value":"100"} -->
|
||||
"""
|
||||
|
||||
{notebook, []} = Import.notebook_from_markdown(markdown)
|
||||
|
@ -92,6 +94,13 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
source: """
|
||||
IO.gets("length: ")\
|
||||
"""
|
||||
},
|
||||
%Cell.Input{
|
||||
metadata: %{},
|
||||
type: :range,
|
||||
name: "length",
|
||||
value: "100",
|
||||
props: %{min: 50, max: 150, step: 2}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -442,4 +451,33 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
]
|
||||
} = notebook
|
||||
end
|
||||
|
||||
test "sets default input types props if not provided" do
|
||||
markdown = """
|
||||
# My Notebook
|
||||
|
||||
## Section 1
|
||||
|
||||
<!-- livebook:{"livebook_object":"cell_input","name":"length","props":{"extra":100,"max":150},"type":"range","value":"100"} -->
|
||||
"""
|
||||
|
||||
{notebook, []} = Import.notebook_from_markdown(markdown)
|
||||
|
||||
expected_props = %{min: 0, max: 150, step: 1}
|
||||
|
||||
assert %Notebook{
|
||||
name: "My Notebook",
|
||||
sections: [
|
||||
%Notebook.Section{
|
||||
name: "Section 1",
|
||||
cells: [
|
||||
%Cell.Input{
|
||||
type: :range,
|
||||
props: ^expected_props
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
} = notebook
|
||||
end
|
||||
end
|
||||
|
|
|
@ -45,6 +45,28 @@ defmodule Livebook.Notebook.Cell.InputText do
|
|||
input = %{Input.new() | type: :number, value: "-"}
|
||||
assert Input.validate(input) == {:error, "not a valid number"}
|
||||
end
|
||||
|
||||
test "given color input allows valid hex colors" do
|
||||
input = %{Input.new() | type: :color, value: "#111111"}
|
||||
assert Input.validate(input) == :ok
|
||||
|
||||
input = %{Input.new() | type: :color, value: "ABCDEF"}
|
||||
assert Input.validate(input) == {:error, "not a valid hex color"}
|
||||
end
|
||||
|
||||
test "given range input allows numbers in the configured range" do
|
||||
input = %{Input.new() | type: :range, value: "0", props: %{min: -5, max: 5, step: 1}}
|
||||
assert Input.validate(input) == :ok
|
||||
|
||||
input = %{Input.new() | type: :range, value: "", props: %{min: -5, max: 5, step: 1}}
|
||||
assert Input.validate(input) == {:error, "not a valid number"}
|
||||
|
||||
input = %{Input.new() | type: :range, value: "-10", props: %{min: -5, max: 5, step: 1}}
|
||||
assert Input.validate(input) == {:error, "number too small"}
|
||||
|
||||
input = %{Input.new() | type: :range, value: "10", props: %{min: -5, max: 5, step: 1}}
|
||||
assert Input.validate(input) == {:error, "number too big"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "invalidated?/2" do
|
||||
|
|
|
@ -360,6 +360,51 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "input cell settings" do
|
||||
test "setting input cell attributes updates data", %{conn: conn, session_id: session_id} do
|
||||
section_id = insert_section(session_id)
|
||||
cell_id = insert_input_cell(session_id, section_id)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}/cell-settings/#{cell_id}")
|
||||
|
||||
form_selector = ~s/[role="dialog"] form/
|
||||
|
||||
assert view
|
||||
|> element(form_selector)
|
||||
|> render_change(%{attrs: %{type: "range"}}) =~
|
||||
~s{<div class="input-label">Min</div>}
|
||||
|
||||
view
|
||||
|> element(form_selector)
|
||||
|> render_change(%{attrs: %{name: "length"}})
|
||||
|
||||
view
|
||||
|> element(form_selector)
|
||||
|> render_change(%{attrs: %{props: %{min: "10"}}})
|
||||
|
||||
view
|
||||
|> element(form_selector)
|
||||
|> render_submit()
|
||||
|
||||
assert %{
|
||||
notebook: %{
|
||||
sections: [
|
||||
%{
|
||||
cells: [
|
||||
%{
|
||||
id: ^cell_id,
|
||||
type: :range,
|
||||
name: "length",
|
||||
props: %{min: 10, max: 100, step: 1}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
} = Session.get_data(session_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Helpers
|
||||
|
||||
defp wait_for_session_update(session_id) do
|
||||
|
|
Loading…
Reference in a new issue