diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..2983e289
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# 0.2.0
+- Add or remove tags from Containers list and details page
+- Show tags on containers
+- Add "Cannery" to page titles
+
+
+# 0.1.0
+- Initial release!
diff --git a/lib/cannery/containers.ex b/lib/cannery/containers.ex
index 2d2be35f..8baceba7 100644
--- a/lib/cannery/containers.ex
+++ b/lib/cannery/containers.ex
@@ -19,8 +19,16 @@ defmodule Cannery.Containers do
"""
@spec list_containers(User.t()) :: [Container.t()]
- def list_containers(%User{id: user_id}),
- do: Repo.all(from c in Container, where: c.user_id == ^user_id, order_by: c.name)
+ def list_containers(%User{id: user_id}) do
+ Repo.all(
+ from c in Container,
+ left_join: t in assoc(c, :tags),
+ left_join: ag in assoc(c, :ammo_groups),
+ where: c.user_id == ^user_id,
+ order_by: c.name,
+ preload: [tags: t, ammo_groups: ag]
+ )
+ end
@doc """
Gets a single container.
@@ -37,8 +45,17 @@ defmodule Cannery.Containers do
"""
@spec get_container!(Container.id(), User.t()) :: Container.t()
- def get_container!(id, %User{id: user_id}),
- do: Repo.one!(from c in Container, where: c.id == ^id and c.user_id == ^user_id)
+ def get_container!(id, %User{id: user_id}) do
+ Repo.one!(
+ from c in Container,
+ left_join: t in assoc(c, :tags),
+ left_join: ag in assoc(c, :ammo_groups),
+ where: c.user_id == ^user_id,
+ where: c.id == ^id,
+ order_by: c.name,
+ preload: [tags: t, ammo_groups: ag]
+ )
+ end
@doc """
Creates a container.
@@ -189,4 +206,17 @@ defmodule Cannery.Containers do
if count == 0, do: raise("could not delete container tag"), else: count
end
+
+ @doc """
+ Returns number of rounds in container. If data is already preloaded, then
+ there will be no db hit.
+ """
+ @spec get_container_rounds!(Container.t()) :: non_neg_integer()
+ def get_container_rounds!(%Container{} = container) do
+ container
+ |> Repo.preload(:ammo_groups)
+ |> Map.get(:ammo_groups)
+ |> Enum.map(fn %{count: count} -> count end)
+ |> Enum.sum()
+ end
end
diff --git a/lib/cannery_web/components/container_card.ex b/lib/cannery_web/components/container_card.ex
index 2f73f550..ce9bfd27 100644
--- a/lib/cannery_web/components/container_card.ex
+++ b/lib/cannery_web/components/container_card.ex
@@ -4,17 +4,21 @@ defmodule CanneryWeb.Components.ContainerCard do
"""
use CanneryWeb, :component
+ import CanneryWeb.Components.TagCard
+ alias Cannery.{Repo, Containers}
alias CanneryWeb.Endpoint
- def container_card(assigns) do
+ def container_card(%{container: container} = assigns) do
+ assigns = assigns |> Map.put(:container, container |> Repo.preload([:tags, :ammo_groups]))
+
~H"""
-
+
<%= live_redirect to: Routes.container_show_path(Endpoint, :show, @container),
class: "link" do %>
@@ -40,6 +44,25 @@ defmodule CanneryWeb.Components.ContainerCard do
<%= @container.location %>
<% end %>
+
+ <%= if @container.ammo_groups do %>
+
+ <%= gettext("Rounds:") %>
+ <%= @container |> Containers.get_container_rounds!() %>
+
+ <% end %>
+
+
+ <%= unless @container.tags |> Enum.empty?() do %>
+ <%= for tag <- @container.tags do %>
+ <.simple_tag_card tag={tag} />
+ <% end %>
+ <% end %>
+
+ <%= if assigns |> Map.has_key?(:tag_actions) do %>
+ <%= render_slot(@tag_actions) %>
+ <% end %>
+
<%= if assigns |> Map.has_key?(:inner_block) do %>
diff --git a/lib/cannery_web/components/tag_card.ex b/lib/cannery_web/components/tag_card.ex
index 15a393d9..e5a18a3e 100644
--- a/lib/cannery_web/components/tag_card.ex
+++ b/lib/cannery_web/components/tag_card.ex
@@ -13,15 +13,20 @@ defmodule CanneryWeb.Components.TagCard do
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
transition-all duration-300 ease-in-out"
>
-
- <%= @tag.name %>
-
-
+ <.simple_tag_card tag={@tag} />
<%= render_slot(@inner_block) %>
"""
end
+
+ def simple_tag_card(assigns) do
+ ~H"""
+
+ <%= @tag.name %>
+
+ """
+ end
end
diff --git a/lib/cannery_web/live/container_live/add_tag_component.ex b/lib/cannery_web/live/container_live/add_tag_component.ex
deleted file mode 100644
index e6be4088..00000000
--- a/lib/cannery_web/live/container_live/add_tag_component.ex
+++ /dev/null
@@ -1,51 +0,0 @@
-defmodule CanneryWeb.ContainerLive.AddTagComponent do
- @moduledoc """
- Livecomponent that can add a tag to a Container
- """
-
- use CanneryWeb, :live_component
- alias Cannery.{Accounts.User, Containers, Containers.Container, Tags, Tags.Tag}
- alias Phoenix.LiveView.Socket
-
- @impl true
- @spec update(
- %{:container => Container.t(), :current_user => User.t(), optional(any) => any},
- Socket.t()
- ) :: {:ok, Socket.t()}
- def update(%{container: _container, current_user: current_user} = assigns, socket) do
- {:ok, socket |> assign(assigns) |> assign(:tags, Tags.list_tags(current_user))}
- end
-
- @impl true
- def handle_event(
- "save",
- %{"tag" => %{"tag_id" => tag_id}},
- %{
- assigns: %{
- tags: tags,
- container: container,
- current_user: current_user,
- return_to: return_to
- }
- } = socket
- ) do
- socket =
- case tags |> Enum.find(fn %{id: id} -> tag_id == id end) do
- nil ->
- prompt = dgettext("errors", "Tag could not be added")
- socket |> put_flash(:error, prompt)
-
- %{name: tag_name} = tag ->
- _container_tag = Containers.add_tag!(container, tag, current_user)
- prompt = dgettext("prompts", "%{name} added successfully", name: tag_name)
- socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
- end
-
- {:noreply, socket}
- end
-
- @spec tag_options([Tag.t()]) :: [{String.t(), Tag.id()}]
- defp tag_options(tags) do
- tags |> Enum.map(fn %{id: id, name: name} -> {name, id} end)
- end
-end
diff --git a/lib/cannery_web/live/container_live/add_tag_component.html.heex b/lib/cannery_web/live/container_live/add_tag_component.html.heex
deleted file mode 100644
index 1eb27b4e..00000000
--- a/lib/cannery_web/live/container_live/add_tag_component.html.heex
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
- <%= @title %>
-
-
- <.form
- let={f}
- for={:tag}
- id="add-tag-to-container-form"
- class="flex flex-col sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
- phx-target={@myself}
- phx-submit="save"
- >
- <%= select(f, :tag_id, tag_options(@tags), class: "text-center col-span-2 input input-primary") %>
- <%= error_tag(f, :tag_id, "col-span-3 text-center") %>
-
- <%= submit(dgettext("actions", "Add"),
- class: "mx-auto btn btn-primary",
- phx_disable_with: dgettext("prompts", "Adding...")
- ) %>
-
-
diff --git a/lib/cannery_web/live/container_live/edit_tags_component.ex b/lib/cannery_web/live/container_live/edit_tags_component.ex
new file mode 100644
index 00000000..af871ad2
--- /dev/null
+++ b/lib/cannery_web/live/container_live/edit_tags_component.ex
@@ -0,0 +1,73 @@
+defmodule CanneryWeb.ContainerLive.EditTagsComponent do
+ @moduledoc """
+ Livecomponent that can add or remove a tag to a Container
+ """
+
+ use CanneryWeb, :live_component
+ alias Cannery.{Accounts.User, Containers, Containers.Container, Tags, Tags.Tag, Repo}
+ alias Phoenix.LiveView.Socket
+
+ @impl true
+ @spec update(
+ %{:container => Container.t(), :current_user => User.t(), optional(any) => any},
+ Socket.t()
+ ) :: {:ok, Socket.t()}
+ def update(%{container: container, current_user: current_user} = assigns, socket) do
+ tags = Tags.list_tags(current_user)
+ container = container |> Repo.preload(:tags)
+ {:ok, socket |> assign(assigns) |> assign(tags: tags, container: container)}
+ end
+
+ @impl true
+ def handle_event(
+ "save",
+ %{"tag" => %{"tag_id" => tag_id}},
+ %{assigns: %{tags: tags, container: container, current_user: current_user}} = socket
+ ) do
+ socket =
+ case tags |> Enum.find(fn %{id: id} -> tag_id == id end) do
+ nil ->
+ prompt = dgettext("errors", "Tag could not be added")
+ socket |> put_flash(:error, prompt)
+
+ %{name: tag_name} = tag ->
+ _container_tag = Containers.add_tag!(container, tag, current_user)
+ container = container |> Repo.preload(:tags, force: true)
+ prompt = dgettext("prompts", "%{name} added successfully", name: tag_name)
+ socket |> put_flash(:info, prompt) |> assign(container: container)
+ end
+
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event(
+ "delete",
+ %{"tag-id" => tag_id},
+ %{assigns: %{tags: tags, container: container, current_user: current_user}} = socket
+ ) do
+ socket =
+ case tags |> Enum.find(fn %{id: id} -> tag_id == id end) do
+ nil ->
+ prompt = dgettext("errors", "Tag could not be removed")
+ socket |> put_flash(:error, prompt)
+
+ %{name: tag_name} = tag ->
+ _container_tag = Containers.remove_tag!(container, tag, current_user)
+ container = container |> Repo.preload(:tags, force: true)
+ prompt = dgettext("prompts", "%{name} removed successfully", name: tag_name)
+ socket |> put_flash(:info, prompt) |> assign(container: container)
+ end
+
+ {:noreply, socket}
+ end
+
+ @spec tag_options([Tag.t()], Container.t()) :: [{String.t(), Tag.id()}]
+ defp tag_options(tags, %Container{tags: container_tags}) do
+ container_tags_map = container_tags |> Enum.map(fn %{id: id} -> id end) |> MapSet.new()
+
+ tags
+ |> Enum.reject(fn %{id: id} -> container_tags_map |> MapSet.member?(id) end)
+ |> Enum.map(fn %{id: id, name: name} -> {name, id} end)
+ end
+end
diff --git a/lib/cannery_web/live/container_live/edit_tags_component.html.heex b/lib/cannery_web/live/container_live/edit_tags_component.html.heex
new file mode 100644
index 00000000..a287d29f
--- /dev/null
+++ b/lib/cannery_web/live/container_live/edit_tags_component.html.heex
@@ -0,0 +1,56 @@
+
+
+ <%= @title %>
+
+
+
+ <%= for tag <- @container.tags do %>
+ <%= link to: "#",
+ class: "mx-2 my-1 px-4 py-2 rounded-lg title text-xl",
+ style: "color: #{tag.text_color}; background-color: #{tag.bg_color}",
+ phx_click: "delete",
+ phx_value_tag_id: tag.id,
+ phx_target: @myself,
+ data: [
+ confirm:
+ dgettext(
+ "prompts",
+ "Are you sure you want to remove the %{tag_name} tag from %{container_name}?",
+ tag_name: tag.name,
+ container_name: @container.name
+ )
+ ] do %>
+ <%= tag.name %>
+
+ <% end %>
+ <% end %>
+
+ <%= if @container.tags |> Enum.empty?() do %>
+
+ <%= gettext("No tags") %>
+ <%= display_emoji("😔") %>
+
+ <% end %>
+
+
+ <%= unless tag_options(@tags, @container) |> Enum.empty?() do %>
+
+
+ <.form
+ let={f}
+ for={:tag}
+ id="add-tag-to-container-form"
+ class="flex flex-col sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
+ phx-target={@myself}
+ phx-submit="save"
+ >
+ <%= select(f, :tag_id, tag_options(@tags, @container), class: "text-center col-span-2 input input-primary") %>
+ <%= error_tag(f, :tag_id, "col-span-3 text-center") %>
+
+ <%= submit(dgettext("actions", "Add"),
+ class: "mx-auto btn btn-primary",
+ phx_disable_with: dgettext("prompts", "Adding...")
+ ) %>
+
+ <% end %>
+
diff --git a/lib/cannery_web/live/container_live/index.ex b/lib/cannery_web/live/container_live/index.ex
index 2521ca2c..fdd4b54a 100644
--- a/lib/cannery_web/live/container_live/index.ex
+++ b/lib/cannery_web/live/container_live/index.ex
@@ -5,13 +5,13 @@ defmodule CanneryWeb.ContainerLive.Index do
use CanneryWeb, :live_view
import CanneryWeb.Components.ContainerCard
- alias Cannery.{Containers, Containers.Container}
+ alias Cannery.{Containers, Containers.Container, Repo}
alias CanneryWeb.Endpoint
alias Ecto.Changeset
@impl true
def mount(_params, session, socket) do
- {:ok, socket |> assign_defaults(session) |> display_containers()}
+ {:ok, socket |> assign_defaults(session)}
end
@impl true
@@ -20,9 +20,13 @@ defmodule CanneryWeb.ContainerLive.Index do
end
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
+ %{name: container_name} =
+ container =
+ Containers.get_container!(id, current_user)
+ |> Repo.preload([:tags, :ammo_groups], force: true)
+
socket
- |> assign(:page_title, gettext("Edit Container"))
- |> assign(:container, Containers.get_container!(id, current_user))
+ |> assign(page_title: gettext("Edit %{name}", name: container_name), container: container)
end
defp apply_action(socket, :new, _params) do
@@ -30,7 +34,19 @@ defmodule CanneryWeb.ContainerLive.Index do
end
defp apply_action(socket, :index, _params) do
- socket |> assign(:page_title, gettext("Listing Containers")) |> assign(:container, nil)
+ socket
+ |> assign(:page_title, gettext("Listing Containers"))
+ |> assign(:container, nil)
+ |> display_containers()
+ end
+
+ defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit_tags, %{"id" => id}) do
+ %{name: container_name} =
+ container =
+ Containers.get_container!(id, current_user) |> Repo.preload([:tags, :ammo_groups])
+
+ page_title = gettext("Edit %{name} tags", name: container_name)
+ socket |> assign(page_title: page_title, container: container)
end
@impl true
@@ -70,6 +86,9 @@ defmodule CanneryWeb.ContainerLive.Index do
end
defp display_containers(%{assigns: %{current_user: current_user}} = socket) do
- socket |> assign(containers: Containers.list_containers(current_user))
+ containers =
+ Containers.list_containers(current_user) |> Repo.preload([:tags, :ammo_groups], force: true)
+
+ socket |> assign(containers: containers)
end
end
diff --git a/lib/cannery_web/live/container_live/index.html.heex b/lib/cannery_web/live/container_live/index.html.heex
index 8ecb4559..e9835ba2 100644
--- a/lib/cannery_web/live/container_live/index.html.heex
+++ b/lib/cannery_web/live/container_live/index.html.heex
@@ -23,6 +23,15 @@
<%= for container <- @containers do %>
<.container_card container={container}>
+ <:tag_actions>
+
+ <%= live_patch to: Routes.container_index_path(Endpoint, :edit_tags, container),
+ class: "text-primary-600 link" do %>
+
+ <% end %>
+
+
+
<%= live_patch to: Routes.container_index_path(Endpoint, :edit, container),
class: "text-primary-600 link",
data: [qa: "edit-#{container.id}"] do %>
@@ -58,3 +67,16 @@
/>
<% end %>
+
+<%= if @live_action == :edit_tags do %>
+ <.modal return_to={Routes.container_index_path(Endpoint, :index)}>
+ <.live_component
+ module={CanneryWeb.ContainerLive.EditTagsComponent}
+ id={@container.id}
+ title={@page_title}
+ action={@live_action}
+ container={@container}
+ current_user={@current_user}
+ />
+
+<% end %>
diff --git a/lib/cannery_web/live/container_live/show.ex b/lib/cannery_web/live/container_live/show.ex
index 9a1b9b31..4e0393dd 100644
--- a/lib/cannery_web/live/container_live/show.ex
+++ b/lib/cannery_web/live/container_live/show.ex
@@ -19,10 +19,9 @@ defmodule CanneryWeb.ContainerLive.Show do
def handle_params(
%{"id" => id},
_,
- %{assigns: %{current_user: current_user, live_action: live_action}} = socket
+ %{assigns: %{current_user: current_user}} = socket
) do
- {:noreply,
- socket |> assign(page_title: page_title(live_action)) |> render_container(id, current_user)}
+ {:noreply, socket |> render_container(id, current_user)}
end
@impl true
@@ -85,16 +84,19 @@ defmodule CanneryWeb.ContainerLive.Show do
{:noreply, socket}
end
- defp page_title(:show), do: gettext("Show Container")
- defp page_title(:edit), do: gettext("Edit Container")
- defp page_title(:add_tag), do: gettext("Add Tag to Container")
-
@spec render_container(Socket.t(), Container.id(), User.t()) :: Socket.t()
- defp render_container(socket, id, current_user) do
- container =
+ defp render_container(%{assigns: %{live_action: live_action}} = socket, id, current_user) do
+ %{name: container_name} = container =
Containers.get_container!(id, current_user)
|> Repo.preload([:ammo_groups, :tags], force: true)
- socket |> assign(container: container)
+ page_title =
+ case live_action do
+ :show -> gettext("Show %{name}", name: container_name)
+ :edit -> gettext("Edit %{name}", name: container_name)
+ :edit_tags -> gettext("Edit %{name} tags", name: container_name)
+ end
+
+ socket |> assign(container: container, page_title: page_title)
end
end
diff --git a/lib/cannery_web/live/container_live/show.html.heex b/lib/cannery_web/live/container_live/show.html.heex
index 450d22f0..e4a9046d 100644
--- a/lib/cannery_web/live/container_live/show.html.heex
+++ b/lib/cannery_web/live/container_live/show.html.heex
@@ -51,34 +51,23 @@
<%= live_patch(dgettext("actions", "Why not add one?"),
- to: Routes.container_show_path(Endpoint, :add_tag, @container),
+ to: Routes.container_show_path(Endpoint, :edit_tags, @container),
class: "btn btn-primary"
) %>
<% else %>
-
- <%= gettext("Tags") %>
-
+
+ <%= for tag <- @container.tags do %>
+ <.simple_tag_card tag={tag} />
+ <% end %>
- <%= for tag <- @container.tags do %>
- <.tag_card tag={tag}>
- <%= link to: "#",
- class: "text-primary-600 link",
- phx_click: "delete_tag",
- phx_value_tag_id: tag.id,
- data: [
- confirm:
- dgettext(
- "prompts",
- "Are you sure you want to remove the %{tag_name} tag from %{container_name}?",
- tag_name: tag.name,
- container_name: @container.name
- )
- ] do %>
-
+
+ <%= live_patch to: Routes.container_show_path(Endpoint, :edit_tags, @container),
+ class: "text-primary-600 link" do %>
+
<% end %>
-
- <% end %>
+
+
<% end %>
@@ -87,9 +76,11 @@
<%= if @container.ammo_groups |> Enum.empty?() do %>
<%= gettext("No ammo groups in this container") %>
<% else %>
- <%= for ammo_group <- @container.ammo_groups do %>
- <.ammo_group_card ammo_group={ammo_group} />
- <% end %>
+
+ <%= for ammo_group <- @container.ammo_groups do %>
+ <.ammo_group_card ammo_group={ammo_group} />
+ <% end %>
+
<% end %>
@@ -108,10 +99,10 @@
<% end %>
-<%= if @live_action == :add_tag do %>
+<%= if @live_action == :edit_tags do %>
<.modal return_to={Routes.container_show_path(Endpoint, :show, @container)}>
<.live_component
- module={CanneryWeb.ContainerLive.AddTagComponent}
+ module={CanneryWeb.ContainerLive.EditTagsComponent}
id={@container.id}
title={@page_title}
action={@live_action}
diff --git a/lib/cannery_web/live/home_live.ex b/lib/cannery_web/live/home_live.ex
index 0de9d6d0..ba8bdc4e 100644
--- a/lib/cannery_web/live/home_live.ex
+++ b/lib/cannery_web/live/home_live.ex
@@ -124,7 +124,7 @@ defmodule CanneryWeb.HomeLive do