diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index cd701deb..7037c071 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -29,6 +29,8 @@ defmodule Radiator.Outline do @doc """ Returns a list of direct child nodes in correct order. """ + def order_child_nodes(nil), do: [] + def order_child_nodes(%Node{} = node) do node |> get_all_siblings() @@ -56,13 +58,6 @@ defmodule Radiator.Outline do |> List.flatten() end - defp order_nodes_by_index(index, prev_id, collection) do - case index[prev_id] do - %{uuid: uuid} = node -> order_nodes_by_index(index, uuid, [node | collection]) - _ -> Enum.reverse(collection) - end - end - @doc """ Inserts a node. @@ -111,23 +106,27 @@ defmodule Radiator.Outline do end) end - defp episode_valid?(episode_id, %Node{episode_id: episode_id}, %Node{episode_id: episode_id}), - do: true - - defp episode_valid?(episode_id, %Node{episode_id: episode_id}, nil), do: true - defp episode_valid?(episode_id, nil, %Node{episode_id: episode_id}), do: true - defp episode_valid?(_episode_id, nil, nil), do: true - defp episode_valid?(_episode_id, _parent_node, _prev_node), do: false + def indent_node(node_id) do + Repo.transaction(fn -> + case NodeRepository.get_node(node_id) do + nil -> + {:error, :not_found} - defp set_parent_id_if(attrs, nil), do: attrs - defp set_parent_id_if(attrs, %Node{uuid: uuid}), do: Map.put_new(attrs, "parent_id", uuid) + node -> + prev_node = get_prev_node(node) + do_indent_node(node, prev_node) + end + end) + |> case do + {:ok, {:error, error}} -> + {:error, error} - defp find_parent_node(%Node{parent_id: parent_id}, nil) do - NodeRepository.get_node_if(parent_id) - end + {:ok, {:ok, node_result}} -> + {:ok, node_result} - defp find_parent_node(_, parent_id) do - NodeRepository.get_node_if(parent_id) + {:error, error} -> + {:error, error} + end end @doc """ @@ -187,39 +186,6 @@ defmodule Radiator.Outline do move_node(node_id, prev_id: new_prev_id, parent_id: parent_id) end - # low level function to move a node - defp do_move_node(node, new_prev_id, new_parent_id, prev_node, parent_node) do - node_repo_result = %NodeRepoResult{node: node} - - Repo.transaction(fn -> - old_next_node = - Node - |> where_prev_node_equals(node.uuid) - |> where_parent_node_equals(get_node_id(parent_node)) - |> Repo.one() - - new_next_node = - Node - |> where_prev_node_equals(new_prev_id) - |> where_parent_node_equals(new_parent_id) - |> Repo.one() - - {:ok, node} = move_node_if(node, new_parent_id, new_prev_id) - - {:ok, _old_next_node} = - move_node_if(old_next_node, get_node_id(parent_node), get_node_id(prev_node)) - - {:ok, _new_next_node} = move_node_if(new_next_node, new_parent_id, get_node_id(node)) - - Map.merge(node_repo_result, %{ - node: node, - old_next_id: get_node_id(old_next_node), - old_prev_id: get_node_id(prev_node), - next_id: get_node_id(new_next_node) - }) - end) - end - @doc """ Updates a nodes content. @@ -453,6 +419,25 @@ defmodule Radiator.Outline do {:ok, tree} end + defp episode_valid?(episode_id, %Node{episode_id: episode_id}, %Node{episode_id: episode_id}), + do: true + + defp episode_valid?(episode_id, %Node{episode_id: episode_id}, nil), do: true + defp episode_valid?(episode_id, nil, %Node{episode_id: episode_id}), do: true + defp episode_valid?(_episode_id, nil, nil), do: true + defp episode_valid?(_episode_id, _parent_node, _prev_node), do: false + + defp set_parent_id_if(attrs, nil), do: attrs + defp set_parent_id_if(attrs, %Node{uuid: uuid}), do: Map.put_new(attrs, "parent_id", uuid) + + defp find_parent_node(%Node{parent_id: parent_id}, nil) do + NodeRepository.get_node_if(parent_id) + end + + defp find_parent_node(_, parent_id) do + NodeRepository.get_node_if(parent_id) + end + defp move_node_if(nil, _parent_node_id, _prev_node_id), do: {:ok, nil} defp move_node_if(node, parent_id, prev_id) do @@ -464,6 +449,53 @@ defmodule Radiator.Outline do |> Repo.update() end + # low level function to move a node + defp do_move_node(node, new_prev_id, new_parent_id, prev_node, parent_node) do + node_repo_result = %NodeRepoResult{node: node} + + Repo.transaction(fn -> + old_next_node = + Node + |> where_prev_node_equals(node.uuid) + |> where_parent_node_equals(get_node_id(parent_node)) + |> Repo.one() + + new_next_node = + Node + |> where_prev_node_equals(new_prev_id) + |> where_parent_node_equals(new_parent_id) + |> Repo.one() + + {:ok, node} = move_node_if(node, new_parent_id, new_prev_id) + + if !is_nil(old_next_node) do + {:ok, _old_next_node} = + move_node_if(old_next_node, old_next_node.parent_id, get_node_id(prev_node)) + end + + {:ok, _new_next_node} = move_node_if(new_next_node, new_parent_id, get_node_id(node)) + + Map.merge(node_repo_result, %{ + node: node, + old_next_id: get_node_id(old_next_node), + old_prev_id: get_node_id(prev_node), + next_id: get_node_id(new_next_node) + }) + end) + end + + defp do_indent_node(_node, nil), do: {:error, :no_prev_node} + + defp do_indent_node(node, prev_node) do + new_previous_id = + prev_node + |> order_child_nodes() + |> List.last() + |> get_node_id() + + move_node(node.uuid, prev_id: new_previous_id, parent_id: prev_node.uuid) + end + defp parent_and_prev_consistent?(_, nil), do: true defp parent_and_prev_consistent?(nil, _), do: true @@ -483,6 +515,13 @@ defmodule Radiator.Outline do node.uuid end + defp order_nodes_by_index(index, prev_id, collection) do + case index[prev_id] do + %{uuid: uuid} = node -> order_nodes_by_index(index, uuid, [node | collection]) + _ -> Enum.reverse(collection) + end + end + defp binaray_uuid_to_ecto_uuid(nil), do: nil defp binaray_uuid_to_ecto_uuid(uuid) do diff --git a/lib/radiator/outline/command.ex b/lib/radiator/outline/command.ex index db461e1a..55fecd9d 100644 --- a/lib/radiator/outline/command.ex +++ b/lib/radiator/outline/command.ex @@ -4,6 +4,7 @@ defmodule Radiator.Outline.Command do alias Radiator.Outline.Command.{ ChangeNodeContentCommand, DeleteNodeCommand, + IndentNodeCommand, InsertNodeCommand, MoveNodeCommand } @@ -24,6 +25,14 @@ defmodule Radiator.Outline.Command do } end + def build("indent_node", node_id, user_id, event_id) do + %IndentNodeCommand{ + event_id: event_id, + user_id: user_id, + node_id: node_id + } + end + def build("change_node_content", node_id, content, user_id, event_id) do %ChangeNodeContentCommand{ event_id: event_id, diff --git a/lib/radiator/outline/command/indent_node_command.ex b/lib/radiator/outline/command/indent_node_command.ex new file mode 100644 index 00000000..bfb5f545 --- /dev/null +++ b/lib/radiator/outline/command/indent_node_command.ex @@ -0,0 +1,12 @@ +defmodule Radiator.Outline.Command.IndentNodeCommand do + @moduledoc """ + Command to indent a node. + """ + @type t() :: %__MODULE__{ + event_id: binary(), + user_id: binary(), + node_id: binary() + } + + defstruct [:event_id, :user_id, :node_id] +end diff --git a/lib/radiator/outline/command_processor.ex b/lib/radiator/outline/command_processor.ex index 62a8fad4..589f2e16 100644 --- a/lib/radiator/outline/command_processor.ex +++ b/lib/radiator/outline/command_processor.ex @@ -10,6 +10,7 @@ defmodule Radiator.Outline.CommandProcessor do alias Radiator.Outline.Command.{ ChangeNodeContentCommand, DeleteNodeCommand, + IndentNodeCommand, InsertNodeCommand, MoveNodeCommand } @@ -68,6 +69,12 @@ defmodule Radiator.Outline.CommandProcessor do |> handle_move_node_result(command) end + defp process_command(%IndentNodeCommand{node_id: node_id} = command) do + node_id + |> Outline.indent_node() + |> handle_move_node_result(command) + end + defp process_command(%DeleteNodeCommand{node_id: node_id} = command) do case NodeRepository.get_node(node_id) do nil -> @@ -118,6 +125,29 @@ defmodule Radiator.Outline.CommandProcessor do node_id: node.uuid, parent_id: command.parent_id, prev_id: command.prev_id, + old_prev_id: result.old_prev_id, + old_next_id: result.old_next_id, + user_id: command.user_id, + uuid: command.event_id, + next_id: result.next_id, + episode_id: node.episode_id + } + |> EventStore.persist_event() + |> Dispatch.broadcast() + + {:ok, node} + end + + def handle_move_node_result( + {:ok, %NodeRepoResult{node: node} = result}, + %IndentNodeCommand{} = command + ) do + %NodeMovedEvent{ + node_id: node.uuid, + parent_id: result.node.parent_id, + prev_id: result.node.prev_id, + old_prev_id: result.old_prev_id, + old_next_id: result.old_next_id, user_id: command.user_id, uuid: command.event_id, next_id: result.next_id, diff --git a/lib/radiator/outline/dispatch.ex b/lib/radiator/outline/dispatch.ex index 455fa8e9..8d255aa1 100644 --- a/lib/radiator/outline/dispatch.ex +++ b/lib/radiator/outline/dispatch.ex @@ -15,6 +15,12 @@ defmodule Radiator.Outline.Dispatch do |> EventProducer.enqueue() end + def indent_node(node_id, user_id, event_id) do + "indent_node" + |> Command.build(node_id, user_id, event_id) + |> EventProducer.enqueue() + end + def move_node(node_id, user_id, event_id, parent_id: parent_id, prev_id: prev_id diff --git a/lib/radiator/outline/event.ex b/lib/radiator/outline/event.ex index fc285fdb..48da8829 100644 --- a/lib/radiator/outline/event.ex +++ b/lib/radiator/outline/event.ex @@ -49,8 +49,5 @@ defmodule Radiator.Outline.Event do def event_type(%NodeDeletedEvent{} = _event), do: "NodeDeletedEvent" def event_type(%NodeMovedEvent{} = _event), do: "NodeMovedEvent" - def episode_id(%NodeInsertedEvent{episode_id: episode_id}), do: episode_id - def episode_id(%NodeContentChangedEvent{episode_id: episode_id}), do: episode_id - def episode_id(%NodeDeletedEvent{episode_id: episode_id}), do: episode_id - def episode_id(%NodeMovedEvent{episode_id: episode_id}), do: episode_id + def episode_id(%{episode_id: episode_id}), do: episode_id end diff --git a/lib/radiator_web/live/episode_live/index.ex b/lib/radiator_web/live/episode_live/index.ex index bd51b7fc..ee22f064 100644 --- a/lib/radiator_web/live/episode_live/index.ex +++ b/lib/radiator_web/live/episode_live/index.ex @@ -61,6 +61,8 @@ defmodule RadiatorWeb.EpisodeLive.Index do end @impl true + def handle_info(%NodeMovedEvent{} = event, socket), do: proxy_event(event, socket) + def handle_info(%{uuid: <<_::binary-size(36)>> <> ":" <> id} = event, %{id: id} = socket) do socket |> stream_event(event) @@ -69,7 +71,6 @@ defmodule RadiatorWeb.EpisodeLive.Index do def handle_info(%NodeInsertedEvent{} = event, socket), do: proxy_event(event, socket) def handle_info(%NodeContentChangedEvent{} = event, socket), do: proxy_event(event, socket) - def handle_info(%NodeMovedEvent{} = event, socket), do: proxy_event(event, socket) def handle_info(%NodeDeletedEvent{} = event, socket), do: proxy_event(event, socket) defp proxy_event(event, socket) do diff --git a/lib/radiator_web/live/outline_component.ex b/lib/radiator_web/live/outline_component.ex index 40ed047e..0f2d1a9a 100644 --- a/lib/radiator_web/live/outline_component.ex +++ b/lib/radiator_web/live/outline_component.ex @@ -217,24 +217,17 @@ defmodule RadiatorWeb.OutlineComponent do ) socket - |> push_event("move_node", %{uuid: uuid, parent_id: prev_parent_id, prev_id: prev_prev_id}) end defp move_down(socket, _uuid, _prev_id) do socket - # |> push_event("move_node", %{uuid: uuid, parent_id: next_parent_id, prev_id: next_uuid}) end - defp indent(socket, uuid, prev_id) do + defp indent(socket, uuid, _prev_id) do user_id = socket.assigns.user_id - Dispatch.move_node(uuid, user_id, generate_event_id(socket.id), - parent_id: prev_id, - prev_id: nil - ) - + Dispatch.indent_node(uuid, user_id, generate_event_id(socket.id)) socket - |> push_event("move_node", %{uuid: uuid, parent_id: prev_id, prev_id: nil}) end defp outdent(socket, uuid, parent_id) do @@ -246,6 +239,5 @@ defmodule RadiatorWeb.OutlineComponent do ) socket - |> push_event("move_node", %{uuid: uuid, parent_id: nil, prev_id: parent_id}) end end diff --git a/test/radiator/outline_test.exs b/test/radiator/outline_test.exs index 06e496ff..b01eefd2 100644 --- a/test/radiator/outline_test.exs +++ b/test/radiator/outline_test.exs @@ -751,6 +751,103 @@ defmodule Radiator.OutlineTest do end end + describe "indent_node/1 - simple context" do + setup :simple_node_fixture + + # before 1 2 + # intend node 2: 1 parentof 2 + test "intended node is now child of old previous node", %{ + node_1: node_1, + node_2: node_2 + } do + assert node_2.parent_id == nil + assert node_2.prev_id == node_1.uuid + + {:ok, _} = Outline.indent_node(node_2.uuid) + + # reload nodes + node_1 = Repo.reload!(node_1) + node_2 = Repo.reload!(node_2) + + assert node_2.parent_id == node_1.uuid + assert node_2.prev_id == nil + end + + # before 1 2 + # intend node 2: 1 parentof 2 + test "returns changed nodes in result", %{ + node_1: node_1, + node_2: node_2 + } do + {:ok, result} = Outline.indent_node(node_2.uuid) + assert result.node.parent_id == node_1.uuid + assert result.old_prev_id == node_1.uuid + end + + # before 1 2 + # intend node 1: error because node 1 is the first in the list + test "intended node does not work when there is no previous node", %{ + node_1: node_1, + node_2: node_2 + } do + {:error, :no_prev_node} = Outline.indent_node(node_1.uuid) + + node_1 = Repo.reload!(node_1) + node_2 = Repo.reload!(node_2) + + assert node_1.parent_id == nil + assert node_1.prev_id == nil + assert node_2.parent_id == nil + assert node_2.prev_id == node_1.uuid + end + end + + describe "indent_node/1" do + setup :complex_node_fixture + + # 1 2 3 4 5 + test "intend node 3 moves under node 2 and does not change nested nodes", %{ + node_2: node_2, + node_3: node_3, + node_4: node_4, + nested_node_1: nested_node_1, + nested_node_2: nested_node_2 + } do + {:ok, _} = Outline.indent_node(node_3.uuid) + + # reload nodes + node_4 = Repo.reload!(node_4) + node_3 = Repo.reload!(node_3) + node_2 = Repo.reload!(node_2) + nested_node_1 = Repo.reload!(nested_node_1) + nested_node_2 = Repo.reload!(nested_node_2) + + assert node_3.prev_id == nil + assert node_3.parent_id == node_2.uuid + assert node_4.prev_id == node_2.uuid + assert nested_node_1.prev_id == nil + assert nested_node_2.prev_id == nested_node_1.uuid + assert nested_node_1.parent_id == node_3.uuid + assert nested_node_2.parent_id == node_3.uuid + end + + test "intend node 4 moves under node 3 and after existing nested nodes", %{ + node_3: node_3, + node_4: node_4, + nested_node_2: nested_node_2 + } do + {:ok, _} = Outline.indent_node(node_4.uuid) + + # reload nodes + node_4 = Repo.reload!(node_4) + node_3 = Repo.reload!(node_3) + nested_node_2 = Repo.reload!(nested_node_2) + + assert node_4.prev_id == nested_node_2.uuid + assert node_4.parent_id == node_3.uuid + end + end + describe "remove_node/1" do setup :complex_node_fixture