Skip to content

Commit

Permalink
split_node
Browse files Browse the repository at this point in the history
  • Loading branch information
sorax authored and electronicbites committed Nov 23, 2024
1 parent 03eb328 commit 90407e6
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 38 deletions.
12 changes: 4 additions & 8 deletions assets/js/hooks/events/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ export function keydown(event: KeyboardEvent) {
const selection = window.getSelection();
const range = selection?.getRangeAt(0);
const start = range!.startOffset;
const end = range!.endOffset;
const stop = range!.endOffset;

const cursorAtStart = start == 0 && end == 0;
const cursorAtEnd = start == content?.length && end == content?.length;
const cursorAtStart = start == 0 && stop == 0;
const cursorAtEnd = start == content?.length && stop == content?.length;

if (event.key == "Tab") {
event.preventDefault();
Expand All @@ -47,11 +47,7 @@ export function keydown(event: KeyboardEvent) {
if (event.key == "Enter" && !event.shiftKey) {
event.preventDefault();

this.pushEventTo(this.el.phxHookId, "new", {
uuid,
content,
selection: { start, end },
});
this.pushEventTo(this.el.phxHookId, "new", { uuid, start, stop });
}

if (event.key == "Backspace" && cursorAtStart) {
Expand Down
55 changes: 55 additions & 0 deletions lib/radiator/outline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,61 @@ defmodule Radiator.Outline do
end
end

@doc """
Splits a node at the given position. The content of the node will be split,
the original node will be updated with the content before the split and a new
node will be created with the content after the split.
The split is a selection with a start and stop index. What is in between will be
deleted.
"""
def split_node(_node_id, {start, stop}) when start > stop do
{:error, :invalid_range}
end

def split_node(node_id, {start, stop}) do
# missing transaction!!
case NodeRepository.get_node(node_id) do
nil ->
{:error, :not_found}

node ->
{orig_node_content, new_node_content} = multisplit(node.content, start, stop)

{:ok, updated_node} =
node
|> Node.update_content_changeset(%{content: orig_node_content})
|> Repo.update()

node_attrs = %{
"content" => new_node_content,
"episode_id" => node.episode_id,
"parent_id" => node.parent_id,
"prev_id" => node.uuid
}

{:ok, %NodeRepoResult{node: new_node, next: old_next_node}} =
insert_node(node_attrs)

{:ok,
%NodeRepoResult{
node: updated_node,
next: get_node_result_info(new_node),
episode_id: updated_node.episode_id,
old_next: get_node_result_info(old_next_node)
}}
end
end

defp multisplit(nil, _start, _stop), do: {"", ""}

defp multisplit(string, start, stop) do
{first, _} = String.split_at(string, start)
{_, last} = String.split_at(string, stop)

{first, last}
end

@doc """
Removes a node from the tree and deletes it from the repository.
Recursivly deletes all children if there are some.
Expand Down
12 changes: 11 additions & 1 deletion lib/radiator/outline/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ defmodule Radiator.Outline.Command do
MoveDownCommand,
MoveNodeCommand,
MoveUpCommand,
OutdentNodeCommand
OutdentNodeCommand,
SplitNodeCommand
}

@move_commands [
Expand Down Expand Up @@ -70,6 +71,15 @@ defmodule Radiator.Outline.Command do
}
end

def build("split_node", node_id, selection, user_id, event_id) do
%SplitNodeCommand{
event_id: event_id,
user_id: user_id,
node_id: node_id,
selection: selection
}
end

def build("change_node_content", node_id, content, user_id, event_id) do
%ChangeNodeContentCommand{
event_id: event_id,
Expand Down
13 changes: 13 additions & 0 deletions lib/radiator/outline/command/split_node_command.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule Radiator.Outline.Command.SplitNodeCommand do
@moduledoc """
Command to split a node in two parts.
"""
@type t() :: %__MODULE__{
event_id: binary(),
user_id: binary(),
node_id: binary(),
selection: tuple()
}

defstruct [:event_id, :user_id, :node_id, :selection]
end
26 changes: 24 additions & 2 deletions lib/radiator/outline/command_processor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ defmodule Radiator.Outline.CommandProcessor do
MoveDownCommand,
MoveNodeCommand,
MoveUpCommand,
OutdentNodeCommand
OutdentNodeCommand,
SplitNodeCommand
}

alias Radiator.Outline.Dispatch
Expand Down Expand Up @@ -123,6 +124,27 @@ defmodule Radiator.Outline.CommandProcessor do
:ok
end

defp process_command(
%SplitNodeCommand{
node_id: node_id,
selection: selection
} = command
) do
{:ok, %NodeRepoResult{node: node, next: next, episode_id: episode_id, old_next: old_next}} =
node_id
|> Outline.split_node(selection)

# broadcast two events
handle_insert_node_result(
{:ok, %NodeRepoResult{node: next, next: old_next, episode_id: episode_id}},
command
)

# for the second event, we need to generate a new event_id
command = Map.put(command, :event_id, Ecto.UUID.generate())
handle_change_node_content_result({:ok, node}, command)
end

defp handle_insert_node_result(
{:ok, %NodeRepoResult{node: node, next: next, episode_id: episode_id}},
command
Expand Down Expand Up @@ -171,7 +193,7 @@ defmodule Radiator.Outline.CommandProcessor do
:error
end

def handle_change_node_content_result({:ok, node}, %ChangeNodeContentCommand{} = command) do
def handle_change_node_content_result({:ok, node}, command) do
%NodeContentChangedEvent{
node_id: node.uuid,
content: node.content,
Expand Down
6 changes: 6 additions & 0 deletions lib/radiator/outline/dispatch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ defmodule Radiator.Outline.Dispatch do
|> CommandQueue.enqueue()
end

def split_node(node_id, selection, user_id, event_id) do
"split_node"
|> Command.build(node_id, selection, 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
2 changes: 2 additions & 0 deletions lib/radiator/outline/node_repository.ex
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@ defmodule Radiator.Outline.NodeRepository do
)
SELECT * FROM node_tree;
"""
def get_node_tree(nil), do: {:error, "episode_id is nil"}

def get_node_tree(episode_id) do
node_tree_initial_query =
Node
Expand Down
36 changes: 10 additions & 26 deletions lib/radiator_web/live/outline_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,21 @@ defmodule RadiatorWeb.OutlineComponent do
alias RadiatorWeb.OutlineComponents

@impl true
def update(%{event: %NodeInsertedEvent{event_id: event_id, node: node, next: nil}}, socket) do
def update(%{event: %NodeInsertedEvent{event_id: event_id, node: node}}, socket) do
socket
|> stream_insert(:nodes, to_change_form(node, %{}))
|> focus_self(node.uuid, event_id)
|> reply(:ok)
end

def update(%{event: %NodeInsertedEvent{event_id: event_id, node: node, next: next}}, socket) do
socket
|> stream_insert(:nodes, to_change_form(node, %{}))
|> push_event("move_nodes", %{nodes: [next]})
|> focus_self(node.uuid, event_id)
|> reply(:ok)
end
# TODO: beim zusammenziehen von nodes ist es wichtig, dass wir
# (also der verursacher) auch den neuen content erhalten

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

def update(%{event: %NodeContentChangedEvent{node_id: node_id, content: content}}, socket) do
socket
Expand Down Expand Up @@ -152,20 +147,9 @@ defmodule RadiatorWeb.OutlineComponent do
|> reply(:noreply)
end

def handle_event(
"new",
%{"uuid" => uuid, "content" => content, "selection" => selection},
socket
) do
{first, _} = String.split_at(content, selection["start"])
{_, last} = String.split_at(content, selection["end"])

def handle_event("new", %{"uuid" => uuid, "start" => start, "stop" => stop}, socket) do
user_id = socket.assigns.user_id
Dispatch.change_node_content(uuid, first, user_id, generate_event_id(socket.id))

episode_id = socket.assigns.episode_id
params = %{"prev_id" => uuid, "content" => last, "episode_id" => episode_id}
Dispatch.insert_node(params, user_id, generate_event_id(socket.id))
Dispatch.split_node(uuid, {start, stop}, user_id, generate_event_id(socket.id))

socket
|> reply(:noreply)
Expand Down
18 changes: 18 additions & 0 deletions test/radiator/outline_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,24 @@ defmodule Radiator.OutlineTest do
end
end

describe "split_node/2" do
setup :simple_node_fixture

test "returns updated node", %{
node_1: node_1
} do
start = 2

{:ok, %Radiator.Outline.NodeRepoResult{node: node}} =
Outline.split_node(node_1.uuid, {2, 3})

assert node.uuid == node_1.uuid
assert node.episode_id == node_1.episode_id
{new_content, _} = String.split_at(node_1.content, start)
assert node.content == new_content
end
end

describe "indent_node/1 - simple context" do
setup :simple_node_fixture

Expand Down
3 changes: 2 additions & 1 deletion test/radiator_web/live/outline_live_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ defmodule RadiatorWeb.OutlineLiveTest do
params = %{
"uuid" => uuid,
"content" => "node_1",
"selection" => %{"start" => 2, "end" => 2}
"start" => 2,
"stop" => 2
}

assert live
Expand Down

0 comments on commit 90407e6

Please sign in to comment.