Skip to content
This repository has been archived by the owner on Oct 10, 2023. It is now read-only.

Commit

Permalink
Merge pull request #37 from elixirfocus/issue-27-maintain-edit-state
Browse files Browse the repository at this point in the history
Refactor LiveComponents to allow UI to maintain an active edit state during changes. [Fixes #27]
zorn authored Nov 15, 2021
2 parents ce2a3bf + 641396a commit 28bdc7f
Showing 5 changed files with 105 additions and 58 deletions.
28 changes: 26 additions & 2 deletions lib/retro_taxi/boards.ex
Original file line number Diff line number Diff line change
@@ -108,6 +108,13 @@ defmodule RetroTaxi.Boards do
|> broadcast(:board_phase_updated, board.id)
end

@spec list_columns(list(Column.id())) :: list(Column.t())
def list_columns(list_of_ids) when is_list(list_of_ids) do
from(c in Column, where: c.id in ^list_of_ids)
|> Repo.all()
end

@spec list_columns(Board.id(), keyword()) :: list(Column.t())
def list_columns(board_id, preloads \\ []) do
Repo.all(
from c in Column, where: c.board_id == ^board_id, order_by: c.sort_order, preload: ^preloads
@@ -176,14 +183,31 @@ defmodule RetroTaxi.Boards do
|> validate_required([:author_id, :content, :column_id, :sort_order])
end

@spec list_topic_cards(list(TopicCard.id())) :: list(TopicCard.t())
def list_topic_cards(list_of_ids) when is_list(list_of_ids) do
from(tc in TopicCard, where: tc.id in ^list_of_ids)
|> Repo.all()
end

@doc """
Returns a list of `RetroTaxis.Boards.TopicCard` entities for the given column id.
"""
@spec list_topic_cards(column_id: Column.id()) :: list(TopicCard.t())
def list_topic_cards(column_id: column_id) do
@spec list_topic_cards(Column.id()) :: list(TopicCard.t())
def list_topic_cards(column_id) do
Repo.all(query_topic_cards_for_column_id(column_id))
end

@spec list_topic_card_ids(Column.id()) :: list(TopicCard.t())
def list_topic_card_ids(column_id) do
query =
from tc in TopicCard,
where: tc.column_id == ^column_id,
order_by: tc.sort_order,
select: tc.id

Repo.all(query)
end

@doc """
Returns the count of `RetroTaxis.Boards.TopicCard` entities for the given column id.
"""
62 changes: 27 additions & 35 deletions lib/retro_taxi_web/live/board_live.ex
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ defmodule RetroTaxiWeb.BoardLive do
def mount(:not_mounted_at_router, session, socket) do
board = Boards.get_board!(session["board_id"], [:facilitator])
current_user = session["current_user"]
columns = Boards.list_columns(board.id, [:topic_cards])
columns = Boards.list_columns(board.id)

if connected?(socket), do: Boards.subscribe(board.id)

@@ -44,51 +44,43 @@ defmodule RetroTaxiWeb.BoardLive do
{:ok, socket}
end

def handle_info({:board_phase_updated, board}, socket)
when socket.assigns.board.id == board.id do
def handle_info({:board_phase_updated, board}, socket) do
{:noreply,
update(socket, :board, fn current_board ->
%{current_board | phase: board.phase}
end)}
end

def handle_info({:topic_card_created, topic_card}, socket) do
# FIXME: For now we are using a generic `boards` topic name so we'll need to
# filter here to make sure we only react to topic cards that are present on
# this board.
# FIXME: I think this was address recently and this filter can be removed.

column_ids = Enum.map(socket.assigns.columns, & &1.id)

if topic_card.column_id in column_ids do
# If a topic card of this board, reload the board.

# I feel like I need to maybe update a collection of columns on the
# liveview and have the columns be preloaded with the topic cards so I can
# properly kick the liveview change tracking with this call.
{:noreply,
update(socket, :columns, fn _current_columns ->
Boards.list_columns(socket.assigns.board.id, [:topic_cards])
end)}
else
{:noreply, socket}
end
send_update(RetroTaxiWeb.ColumnComponent,
id: topic_card.column_id,
board_phase: socket.assigns.board.phase,
column: Enum.find(socket.assigns.columns, fn c -> c.id == topic_card.column_id end),
current_user: socket.assigns.current_user
)

{:noreply, socket}
end

def handle_info({:topic_card_updated, _topic_card}, socket) do
# FIXME: Could and maybe should cherry pick the deletion instead of such a harsh reload.
{:noreply,
update(socket, :columns, fn _current_columns ->
Boards.list_columns(socket.assigns.board.id, [:topic_cards])
end)}
def handle_info({:topic_card_updated, topic_card}, socket) do
send_update(RetroTaxiWeb.TopicCardShowComponent,
id: topic_card.id,
board_phase: socket.assigns.board.phase,
current_user_id: socket.assigns.current_user.id
)

{:noreply, socket}
end

def handle_info({:topic_card_deleted, _topic_card}, socket) do
# FIXME: Could and maybe should cherry pick the deletion instead of such a harsh reload.
{:noreply,
update(socket, :columns, fn _current_columns ->
Boards.list_columns(socket.assigns.board.id, [:topic_cards])
end)}
def handle_info({:topic_card_deleted, topic_card}, socket) do
send_update(RetroTaxiWeb.ColumnComponent,
id: topic_card.column_id,
board_phase: socket.assigns.board.phase,
column: Enum.find(socket.assigns.columns, fn c -> c.id == topic_card.column_id end),
current_user: socket.assigns.current_user
)

{:noreply, socket}
end

@impl true
26 changes: 12 additions & 14 deletions lib/retro_taxi_web/live/components/column_component.ex
Original file line number Diff line number Diff line change
@@ -5,13 +5,15 @@ defmodule RetroTaxiWeb.ColumnComponent do
alias RetroTaxi.Boards.TopicCard
alias RetroTaxi.Users.User

def mount(socket) do
{:ok, assign(socket, show_compose_form: false, compose_changeset: nil)}
end

def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign(show_compose_form: false)
|> assign(topic_cards: Boards.list_topic_cards(column_id: assigns.column.id))
|> assign(compose_changeset: nil)
|> assign(topic_card_ids: Boards.list_topic_card_ids(assigns.id))

{:ok, socket}
end
@@ -28,21 +30,19 @@ defmodule RetroTaxiWeb.ColumnComponent do
def handle_event("add-topic", %{"topic_card" => %{"content" => content}}, socket) do
%User{id: author_id} = socket.assigns.current_user

{:ok, topic_card} =
# The `create_topic_card/1` function will broadcast the needed PubSub event
# to notify all LiveView clients looking at this board that `topic_card_ids`
# need to be updated.
{:ok, _topic_card} =
Boards.create_topic_card(%{
author_id: author_id,
content: content,
column_id: socket.assigns.column.id
})

# FIXME: We need to be more explicit about sort order here but need to
# understand how everyone will see the cards during the compose phase before
# we lay down too many rules.
new_list = socket.assigns.topic_cards ++ [topic_card]

# Hide the compose form.
socket =
socket
|> assign(topic_cards: new_list)
|> assign(show_compose_form: false)
|> assign(compose_changeset: nil)

@@ -92,12 +92,10 @@ defmodule RetroTaxiWeb.ColumnComponent do
</button>
<% end %>
<%= for topic_card <- @topic_cards do %>
<%= live_component @socket, RetroTaxiWeb.TopicCardShowComponent, id: topic_card.id, topic_card: topic_card, board_phase: @board_phase, can_edit: topic_card.author_id == @current_user.id %>
<%= for topic_card_id <- @topic_card_ids do %>
<%= live_component @socket, RetroTaxiWeb.TopicCardShowComponent, id: topic_card_id, board_phase: @board_phase, current_user_id: @current_user.id %>
<% end %>
<%# live_component @socket, RetroTaxiWeb.CreateTopicCardFormComponent %>
</div>
"""
end
45 changes: 39 additions & 6 deletions lib/retro_taxi_web/live/components/topic_card_show_component.ex
Original file line number Diff line number Diff line change
@@ -2,14 +2,47 @@ defmodule RetroTaxiWeb.TopicCardShowComponent do
use RetroTaxiWeb, :live_component

alias RetroTaxi.Boards
alias RetroTaxi.Boards.TopicCard

def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> assign(is_editing: false)
def mount(socket) do
{:ok, assign(socket, is_editing: false)}
end

def preload(list_of_assigns) do
# This component has a requirement to know if the topic card it will render
# should be presented as editable to the current user. However to know this
# editable value we need the `topic_card` entity, which for optimizations
# reasons is loaded in this `preload/1` function.
#
# Since the `current_user_id` will be the same value for all assigns in the
# `list_of_assigns` we just grab it from the first item in the list. Later
# will compare it to the author of the loaded `topic_card` to figure out if
# the component should be presented as editable.
current_user_id = List.first(list_of_assigns).current_user_id

# Instead of having each component load its own topic card, we load them
# all here as a single database call.
topic_cards_index_map = topic_cards_index_map(list_of_assigns)

Enum.map(list_of_assigns, fn assigns ->
# Using the id from the assigns, get the appropriate topic card.
topic_card = topic_cards_index_map[assigns.id]

# Add that topic card to the assigns, along with the proper `can_edit` value.
assigns
|> Map.put(:topic_card, topic_card)
|> Map.put(:can_edit, topic_card.author_id == current_user_id)
end)
end

{:ok, socket}
# Loads and returns an index-keyed map of topic cards.
@spec topic_cards_index_map(list()) :: %{TopicCard.id() => TopicCard.t()}
defp topic_cards_index_map(list_of_assigns) do
list_of_assigns
|> Enum.map(& &1.id)
|> Boards.list_topic_cards()
|> Enum.map(fn tc -> {tc.id, tc} end)
|> Map.new()
end

def handle_event("start-editing", _, socket) do
2 changes: 1 addition & 1 deletion test/retro_taxi/boards_test.exs
Original file line number Diff line number Diff line change
@@ -138,7 +138,7 @@ defmodule RetroTaxi.BoardsTest do
%TopicCard{content: content_4}
] = insert_list(4, :topic_card, column_id: column_id)

fetched_topic_cards = Boards.list_topic_cards(column_id: column_id)
fetched_topic_cards = Boards.list_topic_cards(column_id)

assert length(fetched_topic_cards) == 4
assert Enum.find(fetched_topic_cards, &match?(%TopicCard{content: ^content_1}, &1))

0 comments on commit 28bdc7f

Please sign in to comment.