Skip to content

Commit

Permalink
dispatch merge_prev & merge_next (#603)
Browse files Browse the repository at this point in the history
* dispatch merge

* add commands for merge

* supress users own content changes

* add command processing

* clean liveview handle_events

* cleanup js merge_prev & merge_next actions

* increase credo happyness

* implement Outline.merge_next_node and process MergeNextCommands

* implement Outline.merge_prev_node and process MergePrevCommands

---------

Co-authored-by: Martin Wöginger <[email protected]>
  • Loading branch information
sorax and electronicbites authored Jan 5, 2025
1 parent 9c60f18 commit 89798a9
Show file tree
Hide file tree
Showing 11 changed files with 460 additions and 97 deletions.
12 changes: 4 additions & 8 deletions assets/js/hooks/events/listener/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,16 @@ export function processEvent(action, event) {
break;

case "merge_prev":
const prevNode = getPrevNode(node);
if (cursorAtStart && prevNode) {
if (cursorAtStart) {
event.preventDefault();
this.pushEventTo(this.el, "merge_prev", { uuid, content });
focusNode(prevNode);
this.pushEventTo(this.el, "merge_prev", { uuid });
}
break;

case "merge_next":
const nextNode = getNextNode(node);
if (cursorAtEnd && nextNode) {
if (cursorAtEnd) {
event.preventDefault();
this.pushEventTo(this.el, "merge_next", { uuid, content });
focusNode(nextNode);
this.pushEventTo(this.el, "merge_next", { uuid });
}
break;

Expand Down
93 changes: 93 additions & 0 deletions lib/radiator/outline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule Radiator.Outline do
@moduledoc """
The Outline context.
"""
import Ecto.Query

alias Radiator.Outline.Node
alias Radiator.Outline.NodeRepoResult
Expand Down Expand Up @@ -346,6 +347,98 @@ defmodule Radiator.Outline do
{first, last}
end

def merge_prev_node(%Node{prev_id: nil}), do: {:error, :no_prev_node}

def merge_prev_node(%Node{} = node) do
merge_prev_node(node, NodeRepository.get_prev_node(node))
end

def merge_prev_node(node_id), do: merge_prev_node(NodeRepository.get_node!(node_id))

def merge_prev_node(
%Node{uuid: node_id} = node,
%Node{uuid: prev_node_id, content: content_of_prev_node} = prev_node
) do
Ecto.Multi.new()
|> Ecto.Multi.update_all(
:update_childrens,
fn _ ->
from(n in Node, where: n.parent_id == ^node_id, update: [set: [parent_id: ^prev_node_id]])
end,
[]
)
|> Ecto.Multi.update(:update_content, fn _changes ->
Node.update_content_changeset(prev_node, %{content: content_of_prev_node <> node.content})
end)
|> Ecto.Multi.run(:delete_node, fn _, _ ->
%NodeRepoResult{node: deleted_node, next: updated_next_node} = remove_node(node)
{:ok, %{deleted_node: deleted_node, updated_next_node: updated_next_node}}
end)
|> Repo.transaction()
|> case do
{:ok, result} ->
updated_prev_node = result.update_content

{:ok,
%NodeRepoResult{
node: result.update_content,
old_next: result.delete_node.deleted_node,
next: result.delete_node.updated_next_node,
children: NodeRepository.get_all_siblings(updated_prev_node),
outline_node_container_id: updated_prev_node.outline_node_container_id
}}

{:error, _tag, error, _others} ->
{:error, error}
end
end

def merge_next_node(%Node{} = node) do
merge_next_node(node, NodeRepository.get_next_node(node))
end

def merge_next_node(node_id), do: merge_next_node(NodeRepository.get_node!(node_id))

def merge_next_node(%Node{} = _node, nil), do: {:error, :no_next_node}

def merge_next_node(
%Node{uuid: node_id} = node,
%Node{uuid: next_node_id, content: content_of_next_node} = next_node
) do
Ecto.Multi.new()
|> Ecto.Multi.update_all(
:update_childrens,
fn _ ->
from(n in Node, where: n.parent_id == ^next_node_id, update: [set: [parent_id: ^node_id]])
end,
[]
)
|> Ecto.Multi.update(:update_content, fn _changes ->
Node.update_content_changeset(node, %{content: node.content <> content_of_next_node})
end)
|> Ecto.Multi.run(:delete_next_node, fn _, _ ->
%NodeRepoResult{node: deleted_node, next: updated_next_node} = remove_node(next_node)
{:ok, %{deleted_node: deleted_node, updated_next_node: updated_next_node}}
end)
|> Repo.transaction()
|> case do
{:ok, result} ->
node = result.update_content

{:ok,
%NodeRepoResult{
node: result.update_content,
old_next: result.delete_next_node.deleted_node,
next: result.delete_next_node.updated_next_node,
children: NodeRepository.get_all_siblings(node),
outline_node_container_id: node.outline_node_container_id
}}

{:error, _tag, error, _others} ->
{:error, error}
end
end

@doc """
Removes a node from the tree and deletes it from the repository.
Recursivly deletes all children if there are some.
Expand Down
18 changes: 18 additions & 0 deletions lib/radiator/outline/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ defmodule Radiator.Outline.Command do
DeleteNodeCommand,
IndentNodeCommand,
InsertNodeCommand,
MergeNextNodeCommand,
MergePrevNodeCommand,
MoveDownCommand,
MoveNodeCommand,
MoveUpCommand,
Expand Down Expand Up @@ -71,6 +73,22 @@ defmodule Radiator.Outline.Command do
}
end

def build("merge_prev", node_id, user_id, event_id) do
%MergePrevNodeCommand{
event_id: event_id,
user_id: user_id,
node_id: node_id
}
end

def build("merge_next", node_id, user_id, event_id) do
%MergeNextNodeCommand{
event_id: event_id,
user_id: user_id,
node_id: node_id
}
end

def build("split_node", node_id, selection, user_id, event_id) do
%SplitNodeCommand{
event_id: event_id,
Expand Down
12 changes: 12 additions & 0 deletions lib/radiator/outline/command/merge_next_node_command.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Radiator.Outline.Command.MergeNextNodeCommand do
@moduledoc """
Command to merge a node with the next node.
"""
@type t() :: %__MODULE__{
event_id: binary(),
user_id: binary(),
node_id: binary()
}

defstruct [:event_id, :user_id, :node_id]
end
12 changes: 12 additions & 0 deletions lib/radiator/outline/command/merge_prev_node_command.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Radiator.Outline.Command.MergePrevNodeCommand do
@moduledoc """
Command to merge a node with the previous node.
"""
@type t() :: %__MODULE__{
event_id: binary(),
user_id: binary(),
node_id: binary()
}

defstruct [:event_id, :user_id, :node_id]
end
78 changes: 72 additions & 6 deletions lib/radiator/outline/command_processor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ defmodule Radiator.Outline.CommandProcessor do
DeleteNodeCommand,
IndentNodeCommand,
InsertNodeCommand,
MergeNextNodeCommand,
MergePrevNodeCommand,
MoveDownCommand,
MoveNodeCommand,
MoveUpCommand,
Expand Down Expand Up @@ -157,6 +159,67 @@ defmodule Radiator.Outline.CommandProcessor do
handle_change_node_content_result({:ok, node}, command)
end

defp process_command(%MergePrevNodeCommand{node_id: node_id} = command) do
case Outline.merge_prev_node(node_id) do
{:ok, %NodeRepoResult{} = result} ->
handle_merge_result(result, command)

{:error, error} ->
Logger.error("Merge next node failed. #{inspect(error)}")
end
end

defp process_command(%MergeNextNodeCommand{node_id: node_id} = command) do
case Outline.merge_next_node(node_id) do
{:ok, %NodeRepoResult{} = result} ->
handle_merge_result(result, command)

{:error, error} ->
Logger.error("Merge next node failed. #{inspect(error)}")
end
end

def handle_merge_result(
%NodeRepoResult{
node: node,
old_next: deleted_node,
next: next,
children: children,
outline_node_container_id: outline_node_container_id
},
command
) do
%NodeDeletedEvent{
node: deleted_node,
outline_node_container_id: outline_node_container_id,
event_id: command.event_id,
user_id: command.user_id,
children: [],
next: next
}
|> persist_and_broadcast_event()

%NodeContentChangedEvent{
node_id: node.uuid,
content: node.content,
user_id: command.user_id,
event_id: Ecto.UUID.generate(),
outline_node_container_id: node.outline_node_container_id
}
|> persist_and_broadcast_event()

Enum.each(children, fn child ->
%NodeMovedEvent{
node: child,
user_id: command.user_id,
event_id: Ecto.UUID.generate(),
children: [],
outline_node_container_id: child.outline_node_container_id
}
|> persist_and_broadcast_event()
end)
end

defp handle_insert_node_result(
{:ok,
%NodeRepoResult{
Expand All @@ -173,8 +236,7 @@ defmodule Radiator.Outline.CommandProcessor do
next: next,
outline_node_container_id: outline_node_container_id
}
|> EventStore.persist_event()
|> Dispatch.broadcast()
|> persist_and_broadcast_event()

{:ok, node}
end
Expand All @@ -199,8 +261,7 @@ defmodule Radiator.Outline.CommandProcessor do
children: result.children,
outline_node_container_id: result.outline_node_container_id
}
|> EventStore.persist_event()
|> Dispatch.broadcast()
|> persist_and_broadcast_event()

{:ok, node}
end
Expand All @@ -218,8 +279,7 @@ defmodule Radiator.Outline.CommandProcessor do
event_id: command.event_id,
outline_node_container_id: node.outline_node_container_id
}
|> EventStore.persist_event()
|> Dispatch.broadcast()
|> persist_and_broadcast_event()

{:ok, node}
end
Expand All @@ -242,4 +302,10 @@ defmodule Radiator.Outline.CommandProcessor do

Map.put(payload, "outline_node_container_id", episode.outline_node_container_id)
end

defp persist_and_broadcast_event(event) do
event
|> EventStore.persist_event()
|> Dispatch.broadcast()
end
end
12 changes: 12 additions & 0 deletions lib/radiator/outline/dispatch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ defmodule Radiator.Outline.Dispatch do
|> CommandQueue.enqueue()
end

def merge_prev(node_id, user_id, event_id) do
"merge_prev"
|> Command.build(node_id, user_id, event_id)
|> CommandQueue.enqueue()
end

def merge_next(node_id, user_id, event_id) do
"merge_next"
|> Command.build(node_id, user_id, event_id)
|> CommandQueue.enqueue()
end

def indent_node(node_id, user_id, event_id) do
"indent_node"
|> Command.build(node_id, user_id, event_id)
Expand Down
1 change: 1 addition & 0 deletions lib/radiator/outline/node_repository.ex
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ defmodule Radiator.Outline.NodeRepository do
end

@doc """
TODO wrong name: sibling is "geschwister" in german
Returns all direct child nodes of a given node.
## Examples
iex> get_all_siblings(%Node{})
Expand Down
42 changes: 12 additions & 30 deletions lib/radiator_web/live/components/outline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ defmodule RadiatorWeb.Components.Outline do
|> reply(:ok)
end

def update(
%{event: %NodeContentChangedEvent{event_id: <<_::binary-size(36)>> <> ":" <> id}},
%{id: id} = socket
),
do: socket |> reply(:ok)

def update(%{event: %NodeContentChangedEvent{node_id: node_id, content: content}}, socket) do
socket
|> push_event("set_content", %{uuid: node_id, content: content})
Expand Down Expand Up @@ -146,41 +152,17 @@ defmodule RadiatorWeb.Components.Outline do
|> reply(:noreply)
end

def handle_event("merge_prev", %{"uuid" => uuid, "content" => content}, socket) do
prev_node = Outline.get_node_above(uuid)

if prev_node do
user_id = socket.assigns.user_id

Dispatch.change_node_content(
prev_node.uuid,
"#{prev_node.content}#{content}",
user_id,
generate_event_id(socket.id)
)

Dispatch.delete_node(uuid, user_id, generate_event_id(socket.id))
end
def handle_event("merge_prev", %{"uuid" => uuid}, socket) do
user_id = socket.assigns.user_id
Dispatch.merge_prev(uuid, user_id, generate_event_id(socket.id))

socket
|> reply(:noreply)
end

def handle_event("merge_next", %{"uuid" => uuid, "content" => content}, socket) do
next_node = Outline.get_node_below(uuid)

if next_node do
user_id = socket.assigns.user_id

Dispatch.change_node_content(
uuid,
"#{content}#{next_node.content}",
user_id,
generate_event_id(socket.id)
)

Dispatch.delete_node(next_node.uuid, user_id, generate_event_id(socket.id))
end
def handle_event("merge_next", %{"uuid" => uuid}, socket) do
user_id = socket.assigns.user_id
Dispatch.merge_next(uuid, user_id, generate_event_id(socket.id))

socket
|> reply(:noreply)
Expand Down
Loading

0 comments on commit 89798a9

Please sign in to comment.