Several people, and agents, can move items in the same list at once. There are two parts: the order, which your app keeps and sends to every viewer, and, with Issho, who is moving what, shown as it happens.
Moves at the same moment
A move says where by ids ("after Roadmap"), not by an index, so a move made on a screen a moment out of date still lands where the person saw. It writes the moved rows only, so two people moving different items do not touch each other's rows.
Two drops into the same gap at the same moment may get the same key;
Narabe.Ecto.ordered/2 orders them by id, the same on every screen.
With :serialize, moves in one list run one after the other and get
different keys:
Narabe.Ecto.move(Repo, where(Card, board_id: ^board.id), move,
serialize: where(Board, id: ^board.id)
)The order on every screen
After a move, tell the other viewers. A list in a stream puts the moved
items in place with Narabe.Live.stream_move/4; a tree or a board reads
its items again:
def mount(%{"id" => id}, _session, socket) do
if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "board:#{id}")
{:ok, socket |> assign(board: Boards.get_board!(id)) |> load_board()}
end
def handle_event("move", params, socket) do
board = socket.assigns.board
result = Narabe.Ecto.move(Repo, where(Card, board_id: ^board.id), Narabe.Move.parse!(params, Card))
if match?({:ok, _}, result) do
Phoenix.PubSub.broadcast_from(MyApp.PubSub, self(), "board:#{board.id}", :changed)
end
{:reply, Narabe.Live.reply(result), load_board(socket)}
end
def handle_info(:changed, socket), do: {:noreply, load_board(socket)}Who is moving what, with Issho
With Issho in your deps, a list, a tree or a board shows the item each person is on, the items someone is dragging (in their color, with their name), and the line where they would drop them.
{:issho, "~> 0.4"}Start an Issho instance in your supervision tree and join its rooms from
the LiveView with on_mount Issho.Live (see Issho's docs for the
instance's options and its policy). Then give the list its room and the
person:
<.board
id="sprint"
columns={@columns}
cards={@cards}
on_move="move"
issho={[instance: MyApp.Issho, document: "board:#{@board.id}/cards"]}
user={@current_user}
/>The part of document before the first / is the room. user is the
person, as Issho reads them. issho takes the keys of Issho.Join; with
can: :read, the person sees the others but their own place is not
shared.
The order is not shared through Issho: it stays your app's, sent as above. Issho carries only who is moving what, and where to.
Locks
The person who lifts an item holds it until they drop it or let go. The
others cannot lift it meanwhile: it shows dimmed, with the holder's name,
and Space on it says who is moving it. lock: false in issho turns the
locks off.
A lock keeps people from moving over each other; it is not a check on the server. Two moves that reach the server both apply, in the order they come.
An agent
An agent's tool moves items as the app does. Narabe.Issho.move/5 lets
the people see it first: it takes the items' lock, goes to the first
item, picks it up, drags it to where it lands, waits a moment, runs your
function, and lets go.
def call(%{"card" => card, "column" => column}, ctx) do
{:ok, participant} = Issho.Tool.Context.join(ctx, "board:7")
move = %Narabe.Move{ids: [card], parent: column, index: 0}
Narabe.Issho.move(participant, "board:7/cards", move, fn ->
with {:ok, _} = ok <- Narabe.Ecto.move(Repo, Card, move) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "board:7", :changed)
ok
end
end)
endIt gives what the function gives. When someone else holds one of the
items, the agent does not move it: {:error, {:locked, key}}. A person
who paused or stopped the agent stops it before the move lands:
{:error, {:interrupted, by}}.
:pace is how long the agent stays at the item and at the place it
drops it (500 ms; 0 for none); travel: false puts it there at once.
Places
A person's place in a list is %{"item" => id}, or, while they drag,
%{"item" => id, "count" => n, "drag" => gap}, where the gap is
%{"parent" => id, "after" => id, "before" => id, "level" => n}.
Narabe.Issho.place/2 makes one for Issho.Motion, and
Narabe.Issho.validate/1 checks one.