diff --git a/lib/cannery/accounts/email_worker.ex b/lib/cannery/accounts/email_worker.ex index bee0b4b5..d8e52a0c 100644 --- a/lib/cannery/accounts/email_worker.ex +++ b/lib/cannery/accounts/email_worker.ex @@ -4,7 +4,7 @@ defmodule Cannery.EmailWorker do """ use Oban.Worker, queue: :mailers - alias Cannery.{Accounts, Mailer, Email} + alias Cannery.{Accounts, Email, Mailer} @impl Oban.Worker def perform(%Oban.Job{args: %{"email" => email, "user_id" => user_id, "attrs" => attrs}}) do diff --git a/lib/cannery/containers.ex b/lib/cannery/containers.ex index ae22543d..08827e31 100644 --- a/lib/cannery/containers.ex +++ b/lib/cannery/containers.ex @@ -184,8 +184,7 @@ defmodule Cannery.Containers do Repo.delete_all( from ct in ContainerTag, where: ct.container_id == ^container_id, - where: ct.tag_id == ^tag_id, - where: ct.user_id == ^user_id + where: ct.tag_id == ^tag_id ) if count == 0, do: raise("could not delete container tag"), else: count diff --git a/lib/cannery/containers/container.ex b/lib/cannery/containers/container.ex index 8b3f0c51..5d4fe3a7 100644 --- a/lib/cannery/containers/container.ex +++ b/lib/cannery/containers/container.ex @@ -6,7 +6,8 @@ defmodule Cannery.Containers.Container do use Ecto.Schema import Ecto.Changeset alias Ecto.{Changeset, UUID} - alias Cannery.{Accounts.User, Ammo.AmmoGroup, Containers.Container} + alias Cannery.Containers.{Container, ContainerTag} + alias Cannery.{Accounts.User, Ammo.AmmoGroup, Tags.Tag} @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id @@ -19,6 +20,7 @@ defmodule Cannery.Containers.Container do belongs_to :user, User has_many :ammo_groups, AmmoGroup + many_to_many :tags, Tag, join_through: ContainerTag timestamps() end @@ -32,6 +34,7 @@ defmodule Cannery.Containers.Container do user: User.t(), user_id: User.id(), ammo_groups: [AmmoGroup.t()] | nil, + tags: [Tag.t()] | nil, inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } diff --git a/lib/cannery/tags.ex b/lib/cannery/tags.ex index a99ba800..4b6a9c0d 100644 --- a/lib/cannery/tags.ex +++ b/lib/cannery/tags.ex @@ -4,6 +4,7 @@ defmodule Cannery.Tags do """ import Ecto.Query, warn: false + import CanneryWeb.Gettext alias Cannery.{Accounts.User, Repo, Tags.Tag} alias Ecto.Changeset @@ -22,6 +23,27 @@ defmodule Cannery.Tags do @doc """ Gets a single tag. + ## Examples + + iex> get_tag(123, %User{id: 123}) + {:ok, %Tag{}} + + iex> get_tag(456, %User{id: 123}) + {:error, "tag not found"} + + """ + @spec get_tag(Tag.id(), User.t()) :: {:ok, Tag.t()} | {:error, String.t()} + def get_tag(id, %User{id: user_id}) do + Repo.one(from t in Tag, where: t.id == ^id and t.user_id == ^user_id) + |> case do + nil -> {:error, dgettext("errors", "Tag not found")} + tag -> {:ok, tag} + end + end + + @doc """ + Gets a single tag. + Raises `Ecto.NoResultsError` if the Tag does not exist. ## Examples diff --git a/lib/cannery_web/live/container_live/add_tag_component.ex b/lib/cannery_web/live/container_live/add_tag_component.ex new file mode 100644 index 00000000..6cd43cf6 --- /dev/null +++ b/lib/cannery_web/live/container_live/add_tag_component.ex @@ -0,0 +1,79 @@ +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 + + @impl true + def render(assigns) do + ~H""" +
+

+ <%= @title %> +

+ + <.form + let={f} + for={:tag} + id="add-tag-to-container-form" + class="grid grid-cols-3 justify-center items-center space-x-2" + 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...") + ) %> + +
+ """ + 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/show.ex b/lib/cannery_web/live/container_live/show.ex index 66f74148..628bf362 100644 --- a/lib/cannery_web/live/container_live/show.ex +++ b/lib/cannery_web/live/container_live/show.ex @@ -4,9 +4,11 @@ defmodule CanneryWeb.ContainerLive.Show do """ use CanneryWeb, :live_view - import CanneryWeb.Components.AmmoGroupCard - alias Cannery.{Containers, Repo} + import CanneryWeb.Components.{AmmoGroupCard, TagCard} + alias Cannery.{Containers, Repo, Tags} + alias CanneryWeb.Endpoint alias Ecto.Changeset + alias Phoenix.LiveView.Socket @impl true def mount(_params, session, socket) do @@ -19,19 +21,39 @@ defmodule CanneryWeb.ContainerLive.Show do _, %{assigns: %{current_user: current_user, live_action: live_action}} = socket ) do + {:noreply, + socket |> assign(page_title: page_title(live_action)) |> render_container(id, current_user)} + end + + @impl true + def handle_event( + "delete_tag", + %{"tag-id" => tag_id}, + %{assigns: %{container: container, current_user: current_user}} = socket + ) do socket = - socket - |> assign( - page_title: page_title(live_action), - container: Containers.get_container!(id, current_user) |> Repo.preload(:ammo_groups) - ) + case Tags.get_tag(tag_id, current_user) do + {:ok, tag} -> + _count = Containers.remove_tag!(container, tag, current_user) + + prompt = + dgettext("prompts", "%{tag_name} has been removed from %{container_name}", + tag_name: tag.name, + container_name: container.name + ) + + socket |> put_flash(:info, prompt) |> render_container(container.id, current_user) + + {:error, error_string} -> + socket |> put_flash(:error, error_string) + end {:noreply, socket} end @impl true def handle_event( - "delete", + "delete_container", _, %{assigns: %{container: container, current_user: current_user}} = socket ) do @@ -65,4 +87,14 @@ defmodule CanneryWeb.ContainerLive.Show do 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 = + Containers.get_container!(id, current_user) + |> Repo.preload([:ammo_groups, :tags], force: true) + + socket |> assign(container: container) + 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 f2f2987e..4449db0f 100644 --- a/lib/cannery_web/live/container_live/show.html.heex +++ b/lib/cannery_web/live/container_live/show.html.heex @@ -30,7 +30,7 @@ <%= link to: "#", class: "text-primary-500 link", - phx_click: "delete", + phx_click: "delete_container", data: [ confirm: dgettext("prompts", "Are you sure you want to delete %{name}?", name: @container.name) @@ -39,7 +39,46 @@ <% end %> -
+
+ + <%= if @container.tags |> Enum.empty?() do %> +
+

+ <%= gettext("No tags for this container") %> 😔 +

+ + <%= live_patch(dgettext("actions", "Why not add one?"), + to: Routes.container_show_path(Endpoint, :add_tag, @container), + class: "btn btn-primary" + ) %> +
+ <% else %> +

+ <%= gettext("Tags") %> +

+ + <%= for tag <- @container.tags do %> + <.tag_card tag={tag}> + <%= link to: "#", + class: "text-primary-500 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 %> + + <% end %> + + <% end %> + <% end %> + +

<%= if @container.ammo_groups |> Enum.empty?() do %> @@ -50,14 +89,26 @@ <% end %> <% end %>

- - <%= if @live_action in [:edit] do %> - <%= live_modal(CanneryWeb.ContainerLive.FormComponent, - id: @container.id, - title: @page_title, - action: @live_action, - container: @container, - return_to: Routes.container_show_path(@socket, :show, @container) - ) %> - <% end %> + +<%= if @live_action in [:edit] do %> + <%= live_modal(CanneryWeb.ContainerLive.FormComponent, + id: @container.id, + title: @page_title, + action: @live_action, + container: @container, + return_to: Routes.container_show_path(Endpoint, :show, @container), + current_user: @current_user + ) %> +<% end %> + +<%= if @live_action == :add_tag do %> + <%= live_modal(CanneryWeb.ContainerLive.AddTagComponent, + id: @container.id, + title: @page_title, + action: @live_action, + container: @container, + return_to: Routes.container_show_path(Endpoint, :show, @container), + current_user: @current_user + ) %> +<% end %> diff --git a/lib/cannery_web/router.ex b/lib/cannery_web/router.ex index f56014e3..f1f4dc7f 100644 --- a/lib/cannery_web/router.ex +++ b/lib/cannery_web/router.ex @@ -67,6 +67,7 @@ defmodule CanneryWeb.Router do 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 "/ammo_groups", AmmoGroupLive.Index, :index live "/ammo_groups/new", AmmoGroupLive.Index, :new diff --git a/priv/gettext/actions.pot b/priv/gettext/actions.pot index 0283e4d5..a476a759 100644 --- a/priv/gettext/actions.pot +++ b/priv/gettext/actions.pot @@ -143,3 +143,13 @@ msgstr "" #: lib/cannery_web/live/ammo_group_live/index.html.heex:56 msgid "View" msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/live/container_live/show.html.heex:50 +msgid "Why not add one?" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/live/container_live/add_tag_component.ex:66 +msgid "Add" +msgstr "" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index ab8916ba..4c94dc2b 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -160,7 +160,7 @@ msgstr "" #, elixir-format, ex-autogen #: lib/cannery_web/live/container_live/index.ex:23 -#: lib/cannery_web/live/container_live/show.ex:67 +#: lib/cannery_web/live/container_live/show.ex:89 msgid "Edit Container" msgstr "" @@ -347,7 +347,7 @@ msgid "No ammo for this type" msgstr "" #, elixir-format, ex-autogen -#: lib/cannery_web/live/container_live/show.html.heex:46 +#: lib/cannery_web/live/container_live/show.html.heex:85 msgid "No ammo groups in this container" msgstr "" @@ -456,7 +456,7 @@ msgid "Show Ammo type" msgstr "" #, elixir-format, ex-autogen -#: lib/cannery_web/live/container_live/show.ex:66 +#: lib/cannery_web/live/container_live/show.ex:88 msgid "Show Container" msgstr "" @@ -482,6 +482,7 @@ msgstr "" #, elixir-format, ex-autogen #: lib/cannery_web/components/topbar.ex:35 +#: lib/cannery_web/live/container_live/show.html.heex:57 msgid "Tags" msgstr "" @@ -546,3 +547,13 @@ msgstr "" #: lib/cannery_web/live/home_live.ex:67 msgid "Your data stays with you, period" msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/live/container_live/show.ex:90 +msgid "Add Tag to Container" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/live/container_live/show.html.heex:47 +msgid "No tags for this container" +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index 37bba7b2..53548367 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -17,7 +17,7 @@ msgstr "" #, elixir-format, ex-autogen #: lib/cannery_web/live/container_live/index.ex:54 -#: lib/cannery_web/live/container_live/show.ex:52 +#: lib/cannery_web/live/container_live/show.ex:74 msgid "Could not delete %{name}: %{error}" msgstr "" @@ -130,3 +130,13 @@ msgstr "" #: lib/cannery/accounts/user.ex:82 msgid "must have the @ sign and no spaces" msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery/tags.ex:39 +msgid "Tag not found" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/live/container_live/add_tag_component.ex:35 +msgid "Tag could not be added" +msgstr "" diff --git a/priv/gettext/prompts.pot b/priv/gettext/prompts.pot index ace431f1..7bc6dc35 100644 --- a/priv/gettext/prompts.pot +++ b/priv/gettext/prompts.pot @@ -39,7 +39,7 @@ msgstr "" #, elixir-format, ex-autogen #: lib/cannery_web/live/container_live/index.ex:47 -#: lib/cannery_web/live/container_live/show.ex:42 +#: lib/cannery_web/live/container_live/show.ex:64 msgid "%{name} has been deleted" msgstr "" @@ -177,3 +177,23 @@ msgstr "" #: lib/cannery_web/controllers/user_settings_controller.ex:78 msgid "Your account has been deleted" msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/live/container_live/show.html.heex:68 +msgid "Are you sure you want to remove the %{tag_name} tag from %{container_name}?" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/live/container_live/add_tag_component.ex:40 +msgid "%{name} added successfully" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/live/container_live/show.ex:40 +msgid "%{tag_name} has been removed from %{container_name}" +msgstr "" + +#, elixir-format, ex-autogen +#: lib/cannery_web/live/container_live/add_tag_component.ex:68 +msgid "Adding..." +msgstr ""