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
  • Version:

    - 0.1.0 + 0.2.0

  • diff --git a/lib/cannery_web/router.ex b/lib/cannery_web/router.ex index 347d7292..eca06d97 100644 --- a/lib/cannery_web/router.ex +++ b/lib/cannery_web/router.ex @@ -64,10 +64,11 @@ defmodule CanneryWeb.Router do live "/containers", ContainerLive.Index, :index live "/containers/new", ContainerLive.Index, :new live "/containers/:id/edit", ContainerLive.Index, :edit + live "/containers/:id/edit_tags", ContainerLive.Index, :edit_tags live "/containers/:id", ContainerLive.Show, :show live "/containers/:id/show/edit", ContainerLive.Show, :edit - live "/containers/:id/show/add_tag", ContainerLive.Show, :add_tag + live "/containers/:id/show/edit_tags", ContainerLive.Show, :edit_tags live "/ammo_groups", AmmoGroupLive.Index, :index live "/ammo_groups/new", AmmoGroupLive.Index, :new diff --git a/mix.exs b/mix.exs index 89f1471b..a59d6cb8 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Cannery.MixProject do def project do [ app: :cannery, - version: "0.1.0", + version: "0.2.0", elixir: "~> 1.12", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:gettext] ++ Mix.compilers(),