mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 18:15:56 +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;
|
@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 {
|
.input--error {
|
||||||
@apply bg-red-50 border-red-600 text-red-600;
|
@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 {
|
.input-label {
|
||||||
@apply mb-0.5 text-sm text-gray-800 font-medium;
|
@apply mb-0.5 text-sm text-gray-800 font-medium;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
||||||
name: cell.name,
|
name: cell.name,
|
||||||
value: value
|
value: value
|
||||||
}
|
}
|
||||||
|> put_truthy(reactive: cell.reactive)
|
|> put_unless_implicit(reactive: cell.reactive, props: cell.props)
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
|
|
||||||
"<!-- livebook:#{json} -->"
|
"<!-- livebook:#{json} -->"
|
||||||
|
@ -135,12 +135,12 @@ defmodule Livebook.LiveMarkdown.Export do
|
||||||
String.duplicate("`", max_streak + 1)
|
String.duplicate("`", max_streak + 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp put_truthy(map, entries) do
|
defp put_unless_implicit(map, entries) do
|
||||||
Enum.reduce(entries, map, fn {key, value}, map ->
|
Enum.reduce(entries, map, fn {key, value}, map ->
|
||||||
if value do
|
if value in [false, %{}] do
|
||||||
Map.put(map, key, value)
|
|
||||||
else
|
|
||||||
map
|
map
|
||||||
|
else
|
||||||
|
Map.put(map, key, value)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
@ -195,17 +195,8 @@ defmodule Livebook.LiveMarkdown.Import do
|
||||||
|
|
||||||
defp build_notebook([{:cell, :input, data} | elems], cells, sections) do
|
defp build_notebook([{:cell, :input, data} | elems], cells, sections) do
|
||||||
{metadata, elems} = grab_metadata(elems)
|
{metadata, elems} = grab_metadata(elems)
|
||||||
|
attrs = parse_input_attrs(data)
|
||||||
cell = %{
|
cell = %{Notebook.Cell.new(:input) | metadata: metadata} |> Map.merge(attrs)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
build_notebook(elems, [cell | cells], sections)
|
build_notebook(elems, [cell | cells], sections)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -246,4 +237,26 @@ defmodule Livebook.LiveMarkdown.Import do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp grab_metadata(elems), do: {%{}, elems}
|
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
|
end
|
||||||
|
|
|
@ -6,7 +6,7 @@ defmodule Livebook.Notebook.Cell.Input do
|
||||||
# It consists of an input that the user may fill
|
# It consists of an input that the user may fill
|
||||||
# and then read during code evaluation.
|
# 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.Utils
|
||||||
alias Livebook.Notebook.Cell
|
alias Livebook.Notebook.Cell
|
||||||
|
@ -17,10 +17,16 @@ defmodule Livebook.Notebook.Cell.Input do
|
||||||
type: type(),
|
type: type(),
|
||||||
name: String.t(),
|
name: String.t(),
|
||||||
value: 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 """
|
@doc """
|
||||||
Returns an empty cell.
|
Returns an empty cell.
|
||||||
|
@ -33,7 +39,8 @@ defmodule Livebook.Notebook.Cell.Input do
|
||||||
type: :text,
|
type: :text,
|
||||||
name: "input",
|
name: "input",
|
||||||
value: "",
|
value: "",
|
||||||
reactive: false
|
reactive: false,
|
||||||
|
props: %{}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,17 +49,9 @@ defmodule Livebook.Notebook.Cell.Input do
|
||||||
for its type.
|
for its type.
|
||||||
"""
|
"""
|
||||||
@spec validate(t()) :: :ok | {:error, String.t()}
|
@spec validate(t()) :: :ok | {:error, String.t()}
|
||||||
def validate(cell) do
|
def validate(cell)
|
||||||
validate_value(cell.value, cell.type)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_value(_value, :text), do: :ok
|
def validate(%{value: value, type: :url}) do
|
||||||
|
|
||||||
defp validate_value(_value, :password), do: :ok
|
|
||||||
|
|
||||||
defp validate_value(_value, :textarea), do: :ok
|
|
||||||
|
|
||||||
defp validate_value(value, :url) do
|
|
||||||
if Utils.valid_url?(value) do
|
if Utils.valid_url?(value) do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
|
@ -60,14 +59,14 @@ defmodule Livebook.Notebook.Cell.Input do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_value(value, :number) do
|
def validate(%{value: value, type: :number}) do
|
||||||
case Float.parse(value) do
|
case Float.parse(value) do
|
||||||
{_number, ""} -> :ok
|
{_number, ""} -> :ok
|
||||||
_ -> {:error, "not a valid number"}
|
_ -> {:error, "not a valid number"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_value(value, :color) do
|
def validate(%{value: value, type: :color}) do
|
||||||
if Utils.valid_hex_color?(value) do
|
if Utils.valid_hex_color?(value) do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
|
@ -75,13 +74,31 @@ defmodule Livebook.Notebook.Cell.Input do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_value(value, :range) do
|
def validate(%{value: value, type: :range, props: props}) do
|
||||||
case Integer.parse(value) do
|
case Float.parse(value) do
|
||||||
{_number, ""} -> :ok
|
{number, ""} ->
|
||||||
_ -> {:error, "not a valid 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
|
||||||
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 """
|
@doc """
|
||||||
Checks if the input changed in terms of content.
|
Checks if the input changed in terms of content.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -987,7 +987,8 @@ defmodule LivebookWeb.SessionLive do
|
||||||
case Cell.Input.validate(cell) do
|
case Cell.Input.validate(cell) do
|
||||||
:ok -> nil
|
:ok -> nil
|
||||||
{:error, error} -> error
|
{:error, error} -> error
|
||||||
end
|
end,
|
||||||
|
props: cell.props
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -115,24 +115,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
<%= @cell_view.name %>
|
<%= @cell_view.name %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if @cell_view.input_type == :textarea do %>
|
<.cell_input cell_view={@cell_view} />
|
||||||
<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 %>
|
|
||||||
|
|
||||||
<%= if @cell_view.error do %>
|
<%= if @cell_view.error do %>
|
||||||
<div class="input-error">
|
<div class="input-error">
|
||||||
|
@ -144,6 +127,58 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
||||||
"""
|
"""
|
||||||
end
|
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
|
defp cell_body(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex relative">
|
<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>
|
<div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @message %></div>
|
||||||
"""
|
"""
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
||||||
use LivebookWeb, :live_component
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
alias Livebook.Session
|
alias Livebook.Session
|
||||||
|
alias Livebook.Notebook.Cell
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(assigns, socket) do
|
def update(assigns, socket) do
|
||||||
|
@ -10,9 +11,11 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(assigns)
|
|> assign(assigns)
|
||||||
|> assign_new(:name, fn -> cell.name end)
|
|> assign(:current_type, cell.type)
|
||||||
|> assign_new(:type, fn -> cell.type end)
|
|> assign_new(:attrs, fn ->
|
||||||
|> assign_new(:reactive, fn -> cell.reactive end)
|
Map.take(cell, [:name, :type, :reactive, :props])
|
||||||
|
end)
|
||||||
|
|> assign_new(:valid, fn -> true end)
|
||||||
|
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
@ -24,26 +27,30 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
||||||
<h3 class="text-2xl font-semibold text-gray-800">
|
<h3 class="text-2xl font-semibold text-gray-800">
|
||||||
Cell settings
|
Cell settings
|
||||||
</h3>
|
</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 class="flex flex-col space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<div class="input-label">Type</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>
|
<div>
|
||||||
<div class="input-label">Name</div>
|
<div class="input-label">Name</div>
|
||||||
<input type="text" class="input" name="name" value={@name} spellcheck="false" autocomplete="off" autofocus />
|
<input type="text" class="input" name="attrs[name]" value={@attrs.name} autofocus />
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<.switch_checkbox
|
|
||||||
name="reactive"
|
|
||||||
label="Reactive (reevaluates dependent cells on change)"
|
|
||||||
checked={@reactive} />
|
|
||||||
</div>
|
</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>
|
||||||
<div class="mt-8 flex justify-end space-x-2">
|
<div class="mt-8 flex justify-end space-x-2">
|
||||||
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
|
<%= 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
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,26 +59,92 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
|
||||||
"""
|
"""
|
||||||
end
|
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
|
@impl true
|
||||||
def handle_event("validate", params, socket) do
|
def handle_event("validate", params, socket) do
|
||||||
attrs = params_to_attrs(params)
|
{valid?, attrs} = validate_attrs(params["attrs"], socket.assigns.attrs)
|
||||||
{:noreply, assign(socket, attrs)}
|
{:noreply, socket |> assign(attrs: attrs) |> assign(:valid, valid?)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("save", params, socket) do
|
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)
|
Session.set_cell_attributes(socket.assigns.session_id, socket.assigns.cell.id, attrs)
|
||||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp params_to_attrs(params) do
|
defp validate_attrs(data, prev_attrs) do
|
||||||
name = params["name"]
|
name = data["name"]
|
||||||
type = params["type"] |> String.to_existing_atom()
|
type = data["type"] |> String.to_existing_atom()
|
||||||
reactive = Map.has_key?(params, "reactive")
|
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
|
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
|
defp input_types do
|
||||||
[
|
[
|
||||||
color: "Color",
|
color: "Color",
|
||||||
|
|
|
@ -60,6 +60,13 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
||||||
source: """
|
source: """
|
||||||
IO.gets("length: ")
|
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
|
```elixir
|
||||||
IO.gets("length: ")
|
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)
|
document = Export.notebook_to_markdown(notebook)
|
||||||
|
@ -384,7 +393,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
||||||
assert expected_document == document
|
assert expected_document == document
|
||||||
end
|
end
|
||||||
|
|
||||||
test "save password as empty string" do
|
test "saves password as empty string" do
|
||||||
notebook = %{
|
notebook = %{
|
||||||
Notebook.new()
|
Notebook.new()
|
||||||
| name: "My Notebook",
|
| name: "My Notebook",
|
||||||
|
|
|
@ -38,6 +38,8 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
||||||
```elixir
|
```elixir
|
||||||
IO.gets("length: ")
|
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)
|
{notebook, []} = Import.notebook_from_markdown(markdown)
|
||||||
|
@ -92,6 +94,13 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
||||||
source: """
|
source: """
|
||||||
IO.gets("length: ")\
|
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
|
} = notebook
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -45,6 +45,28 @@ defmodule Livebook.Notebook.Cell.InputText do
|
||||||
input = %{Input.new() | type: :number, value: "-"}
|
input = %{Input.new() | type: :number, value: "-"}
|
||||||
assert Input.validate(input) == {:error, "not a valid number"}
|
assert Input.validate(input) == {:error, "not a valid number"}
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "invalidated?/2" do
|
describe "invalidated?/2" do
|
||||||
|
|
|
@ -360,6 +360,51 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
end
|
end
|
||||||
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
|
# Helpers
|
||||||
|
|
||||||
defp wait_for_session_update(session_id) do
|
defp wait_for_session_update(session_id) do
|
||||||
|
|
Loading…
Reference in a new issue