Add button for inserting a branching section (#2205)

This commit is contained in:
Jonatan Kłosko 2023-09-15 20:25:41 +07:00 committed by GitHub
parent 4d42202c83
commit 8633c9a357
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 191 additions and 43 deletions

View file

@ -341,6 +341,14 @@ defmodule Livebook.Session do
GenServer.cast(pid, {:insert_section_into, self(), section_id, index}) GenServer.cast(pid, {:insert_section_into, self(), section_id, index})
end end
@doc """
Sends branching section insertion request to the server.
"""
@spec insert_branching_section_into(pid(), Section.id(), non_neg_integer()) :: :ok
def insert_branching_section_into(pid, section_id, index) do
GenServer.cast(pid, {:insert_branching_section_into, self(), section_id, index})
end
@doc """ @doc """
Sends parent update request to the server. Sends parent update request to the server.
""" """
@ -1066,6 +1074,13 @@ defmodule Livebook.Session do
{:noreply, handle_operation(state, operation)} {:noreply, handle_operation(state, operation)}
end end
def handle_cast({:insert_branching_section_into, client_pid, section_id, index}, state) do
client_id = client_id(state, client_pid)
# Include new id in the operation, so it's reproducible
operation = {:insert_branching_section_into, client_id, section_id, index, Utils.random_id()}
{:noreply, handle_operation(state, operation)}
end
def handle_cast({:set_section_parent, client_pid, section_id, parent_id}, state) do def handle_cast({:set_section_parent, client_pid, section_id, parent_id}, state) do
client_id = client_id(state, client_pid) client_id = client_id(state, client_pid)
# Include new id in the operation, so it's reproducible # Include new id in the operation, so it's reproducible

View file

@ -177,6 +177,7 @@ defmodule Livebook.Session.Data do
{:set_notebook_attributes, client_id(), map()} {:set_notebook_attributes, client_id(), map()}
| {:insert_section, client_id(), index(), Section.id()} | {:insert_section, client_id(), index(), Section.id()}
| {:insert_section_into, client_id(), Section.id(), index(), Section.id()} | {:insert_section_into, client_id(), Section.id(), index(), Section.id()}
| {:insert_branching_section_into, client_id(), Section.id(), index(), Section.id()}
| {:set_section_parent, client_id(), Section.id(), parent_id :: Section.id()} | {:set_section_parent, client_id(), Section.id(), parent_id :: Section.id()}
| {:unset_section_parent, client_id(), Section.id()} | {:unset_section_parent, client_id(), Section.id()}
| {:insert_cell, client_id(), Section.id(), index(), Cell.type(), Cell.id(), map()} | {:insert_cell, client_id(), Section.id(), index(), Cell.type(), Cell.id(), map()}
@ -402,6 +403,19 @@ defmodule Livebook.Session.Data do
end end
end end
def apply_operation(data, {:insert_branching_section_into, _client_id, section_id, index, id}) do
with {:ok, _section} <- Notebook.fetch_section(data.notebook, section_id) do
section = %{Section.new() | id: id}
data
|> with_actions()
|> insert_section_into(section_id, index, section)
|> set_default_section_parent(section)
|> set_dirty()
|> wrap_ok()
end
end
def apply_operation(data, {:set_section_parent, _client_id, section_id, parent_id}) do def apply_operation(data, {:set_section_parent, _client_id, section_id, parent_id}) do
with {:ok, section} <- Notebook.fetch_section(data.notebook, section_id), with {:ok, section} <- Notebook.fetch_section(data.notebook, section_id),
{:ok, parent_section} <- Notebook.fetch_section(data.notebook, parent_id), {:ok, parent_section} <- Notebook.fetch_section(data.notebook, parent_id),
@ -1037,6 +1051,15 @@ defmodule Livebook.Session.Data do
) )
end end
def set_default_section_parent({data, _actions} = data_actions, section) do
parent =
data.notebook
|> Notebook.valid_parents_for(section.id)
|> List.last()
set_section_parent(data_actions, section, parent)
end
defp insert_cell({data, _} = data_actions, section_id, index, cell) do defp insert_cell({data, _} = data_actions, section_id, index, cell) do
data_actions data_actions
|> set!( |> set!(

View file

@ -1079,6 +1079,19 @@ defmodule LivebookWeb.SessionLive do
{:noreply, socket} {:noreply, socket}
end end
def handle_event("insert_branching_section_below", params, socket) do
with {:ok, section, index} <-
section_with_next_index(
socket.private.data.notebook,
params["section_id"],
params["cell_id"]
) do
Session.insert_branching_section_into(socket.assigns.session.pid, section.id, index)
end
{:noreply, socket}
end
def handle_event( def handle_event(
"set_section_parent", "set_section_parent",
%{"section_id" => section_id, "parent_id" => parent_id}, %{"section_id" => section_id, "parent_id" => parent_id},

View file

@ -96,6 +96,17 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
<span>Section</span> <span>Section</span>
</button> </button>
</.menu_item> </.menu_item>
<.menu_item>
<button
role="menuitem"
phx-click="insert_branching_section_below"
phx-value-section_id={@section_id}
phx-value-cell_id={@cell_id}
>
<.remix_icon icon="git-branch-line" />
<span>Branching section</span>
</button>
</.menu_item>
<div class="flex items-center mt-4 mb-1 px-5 text-xs text-gray-400 font-light"> <div class="flex items-center mt-4 mb-1 px-5 text-xs text-gray-400 font-light">
MARKDOWN MARKDOWN
</div> </div>

View file

@ -60,44 +60,24 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
<.remix_icon icon="link" class="text-xl" /> <.remix_icon icon="link" class="text-xl" />
</a> </a>
</span> </span>
<.menu <.branching_menu
:if={@section_view.valid_parents != [] and not @section_view.has_children?} section_view={@section_view}
id={"section-#{@section_view.id}-branch-menu"} scope="actions"
position={:bottom_right}
disabled={cannot_branch_out_reason(@section_view) != nil}
> >
<:toggle> <span
<span class="tooltip top" data-tooltip="Branch out from"> class="tooltip top"
<button class="icon-button" aria-label="branch out from other section"> data-tooltip={cannot_branch_out_reason(@section_view) || "Branch out from"}
<.remix_icon icon="git-branch-line" class="text-xl flip-horizontally" /> >
</button> <button
</span> class={["icon-button", cannot_branch_out_reason(@section_view) && "disabled"]}
</:toggle> aria-label="branch out from other section"
<.menu_item :for={parent <- @section_view.valid_parents}> >
<%= if @section_view.parent && @section_view.parent.id == parent.id do %> <.remix_icon icon="git-branch-line" class="text-xl flip-horizontally" />
<button </button>
class="text-gray-900" </span>
phx-click="unset_section_parent" </.branching_menu>
phx-value-section_id={@section_view.id}
>
<.remix_icon icon="arrow-right-s-line" />
<span><%= parent.name %></span>
</button>
<% else %>
<button
class="text-gray-500"
phx-click="set_section_parent"
phx-value-section_id={@section_view.id}
phx-value-parent_id={parent.id}
>
<.remix_icon
:if={@section_view.parent && @section_view.parent.id}
icon="arrow-right-s-line"
class="invisible"
/>
<span><%= parent.name %></span>
</button>
<% end %>
</.menu_item>
</.menu>
<span class="tooltip top" data-tooltip="Move up"> <span class="tooltip top" data-tooltip="Move up">
<button <button
class="icon-button" class="icon-button"
@ -121,8 +101,8 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
</button> </button>
</span> </span>
<span {if @section_view.has_children?, <span {if @section_view.has_children?,
do: [class: "tooltip left", data_tooltip: "Cannot delete this section because\nother sections branch from it"], do: [class: "tooltip left", "data-tooltip": "Cannot delete this section because\nother sections branch from it"],
else: [class: "tooltip top", data_tooltip: "Delete"]}> else: [class: "tooltip top", "data-tooltip": "Delete"]}>
<button <button
class={["icon-button", @section_view.has_children? && "disabled"]} class={["icon-button", @section_view.has_children? && "disabled"]}
aria-label="delete section" aria-label="delete section"
@ -136,10 +116,10 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
</div> </div>
<h3 <h3
:if={@section_view.parent} :if={@section_view.parent}
class="mt-1 flex items-end space-x-1 text-sm font-semibold text-gray-800" class="mt-1 flex items-end space-x-1 font-semibold text-gray-800"
data-el-section-subheadline data-el-section-subheadline
> >
<span <div
class="tooltip bottom" class="tooltip bottom"
data-tooltip="This section branches out from the main flow data-tooltip="This section branches out from the main flow
and can be evaluated in parallel" and can be evaluated in parallel"
@ -148,8 +128,12 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
icon="git-branch-line" icon="git-branch-line"
class="text-lg font-normal flip-horizontally leading-none" class="text-lg font-normal flip-horizontally leading-none"
/> />
</span> </div>
<span class="leading-none">from <%= @section_view.parent.name %></span> <.branching_menu section_view={@section_view} scope="subheading" position={:bottom_left}>
<div class="text-sm leading-none cursor-pointer">
from <%= @section_view.parent.name %>
</div>
</.branching_menu>
</h3> </h3>
<h3 <h3
@ -202,4 +186,61 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
</section> </section>
""" """
end end
attr :section_view, :map, required: true
attr :scope, :string, required: true
attr :position, :atom, required: true
attr :disabled, :boolean, default: false
slot :inner_block, required: true
defp branching_menu(assigns) do
~H"""
<.menu
id={"section-#{@section_view.id}-branch-menu-#{@scope}"}
position={@position}
disabled={@disabled}
>
<:toggle>
<%= render_slot(@inner_block) %>
</:toggle>
<%= if @section_view.parent do %>
<.menu_item>
<button
class="text-gray-500"
phx-click="unset_section_parent"
phx-value-section_id={@section_view.id}
>
<.remix_icon icon="close-line" />
<span>Clear</span>
</button>
</.menu_item>
<div class="my-1 border-t border-gray-200"></div>
<% end %>
<.menu_item :for={parent <- @section_view.valid_parents}>
<button
class="text-gray-500"
phx-click="set_section_parent"
phx-value-section_id={@section_view.id}
phx-value-parent_id={parent.id}
>
<.remix_icon
:if={@section_view.parent}
icon="arrow-right-s-line"
class={[(@section_view.parent && @section_view.parent.id == parent.id) || "invisible"]}
/>
<span><%= parent.name %></span>
</button>
</.menu_item>
</.menu>
"""
end
defp cannot_branch_out_reason(%{valid_parents: []}),
do: "No section to branch out from"
defp cannot_branch_out_reason(%{has_children?: true}),
do: "Cannot branch out this section because\nother sections branch from it"
defp cannot_branch_out_reason(_section_view), do: nil
end end

View file

@ -184,6 +184,51 @@ defmodule Livebook.Session.DataTest do
end end
end end
describe "apply_operation/2 given :insert_branching_section_into" do
test "returns an error given invalid section id" do
data = Data.new()
operation = {:insert_branching_section_into, @cid, "nonexistent", 0, "s1"}
assert :error = Data.apply_operation(data, operation)
end
test "sets the insertion section as parent if it is not branching" do
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"}
])
operation = {:insert_branching_section_into, @cid, "s1", 0, "s2"}
assert {:ok,
%{
notebook: %{
sections: [%{id: "s1"}, %{id: "s2", parent_id: "s1"}]
},
section_infos: %{"s2" => _}
}, []} = Data.apply_operation(data, operation)
end
test "sets the closest regular section as parent" do
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
{:insert_section, @cid, 1, "s2"},
{:insert_section, @cid, 2, "s3"},
{:set_section_parent, @cid, "s3", "s1"}
])
operation = {:insert_branching_section_into, @cid, "s3", 0, "s4"}
assert {:ok,
%{
notebook: %{
sections: [%{id: "s1"}, %{id: "s2"}, %{id: "s3"}, %{id: "s4", parent_id: "s2"}]
},
section_infos: %{"s4" => _}
}, []} = Data.apply_operation(data, operation)
end
end
describe "apply_operation/2 given :set_section_parent" do describe "apply_operation/2 given :set_section_parent" do
test "returns an error given invalid section id" do test "returns an error given invalid section id" do
data = data =