Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

115 allow multiple labels for one idea #126

Merged
merged 16 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.default
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ DOCKER_COMPOSE_APP_URL_PORT=4000
DOCKER_COMPOSE_POSTGRES_DB=mindwendel-dev
DOCKER_COMPOSE_POSTGRES_PASSWORD=mindwendel-user-password
DOCKER_COMPOSE_POSTGRES_PORT=5432
DOCKER_COMPOSE_POSTGRES_PORT_PUBLISHED=5432
DOCKER_COMPOSE_POSTGRES_USER=mindwendel-user
8 changes: 3 additions & 5 deletions assets/css/live/idea_live/_index_component.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// We would like to use the BEM css naming convention to avoid css collisions, see http://getbem.com/naming/

.IndexComponent__IdeaCard--labelled {
.IndexComponent__IdeaLabelBadge {
@extend .badge;
@extend .rounded-pill;
}
.IndexComponent__IdeaLabelBadge {
@extend .badge;
@extend .rounded-pill;
}

// The following placeholder selectors are taken from `../node_modules/bootstrap-icons/font/bootstrap-icons.css`.
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ services:
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-mindwendel-user}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-mindwendel-user-password}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
ports:
- "${DOCKER_COMPOSE_POSTGRES_PORT_PUBLISHED:-5432}:${DOCKER_COMPOSE_POSTGRES_PORT:-5432}"
volumes:
- postgres_data:/var/lib/postgresql/data

Expand Down
55 changes: 45 additions & 10 deletions lib/mindwendel/brainstormings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Mindwendel.Brainstormings do

alias Mindwendel.Brainstormings.Idea
alias Mindwendel.Brainstormings.IdeaLabel
alias Mindwendel.Brainstormings.IdeaIdeaLabel
alias Mindwendel.Brainstormings.Brainstorming
alias Mindwendel.Brainstormings.Like

Expand Down Expand Up @@ -66,17 +67,26 @@ defmodule Mindwendel.Brainstormings do
order_by: [desc_nulls_last: idea_count.like_count, desc: idea.inserted_at]

Repo.all(idea_query)
|> Repo.preload([:link, :likes, :label])
|> Repo.preload([:link, :likes, :label, :idea_labels])
end

def sort_ideas_by_labels(brainstorming_id) do
Repo.all(
from idea in Idea,
left_join: l in assoc(idea, :label),
where: idea.brainstorming_id == ^brainstorming_id,
order_by: [asc_nulls_last: l.position_order, desc: idea.inserted_at]
from(
idea in Idea,
left_join: l in assoc(idea, :idea_labels),
where: idea.brainstorming_id == ^brainstorming_id,
preload: [
:link,
:likes,
:idea_labels
],
order_by: [
asc_nulls_last: l.position_order,
desc: idea.inserted_at
]
)
|> Repo.preload([:link, :likes, :label])
|> Repo.all()
|> Enum.uniq()
end

@doc """
Expand All @@ -93,7 +103,7 @@ defmodule Mindwendel.Brainstormings do
** (Ecto.NoResultsError)

"""
def get_idea!(id), do: Repo.get!(Idea, id) |> Repo.preload([:label])
def get_idea!(id), do: Repo.get!(Idea, id) |> Repo.preload([:label, :idea_labels])

@doc """
Count likes for an idea.
Expand Down Expand Up @@ -175,6 +185,31 @@ defmodule Mindwendel.Brainstormings do
|> broadcast(:idea_updated)
end

def add_idea_label_to_idea(%Idea{} = idea, %IdeaLabel{} = idea_label) do
idea = Repo.preload(idea, :idea_labels)

idea_labels =
(idea.idea_labels ++ [idea_label])
|> Enum.map(&Ecto.Changeset.change/1)

idea
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:idea_labels, idea_labels)
|> Repo.update()
|> broadcast(:idea_updated)
end

def remove_idea_label_from_idea(%Idea{} = idea, %IdeaLabel{} = idea_label) do
from(idea_idea_label in IdeaIdeaLabel,
where:
idea_idea_label.idea_id == ^idea.id and
idea_idea_label.idea_label_id == ^idea_label.id
)
|> Repo.delete_all()

{:ok, get_idea!(idea.id)} |> broadcast(:idea_updated)
end

@doc """
Deletes a idea.

Expand Down Expand Up @@ -202,7 +237,7 @@ defmodule Mindwendel.Brainstormings do

"""
def change_idea(%Idea{} = idea, attrs \\ %{}) do
Repo.preload(idea, :link) |> Idea.changeset(attrs)
Repo.preload(idea, [:link, :idea_labels]) |> Idea.changeset(attrs)
end

@doc """
Expand Down Expand Up @@ -241,7 +276,7 @@ defmodule Mindwendel.Brainstormings do
|> Repo.preload([
:users,
labels: from(idea_label in IdeaLabel, order_by: idea_label.position_order),
ideas: [:link, :likes, :label]
ideas: [:link, :likes, :label, :idea_labels]
])
end

Expand Down
8 changes: 7 additions & 1 deletion lib/mindwendel/brainstormings/idea.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Mindwendel.Brainstormings.Idea do
import Ecto.Changeset
alias Mindwendel.Brainstormings.Brainstorming
alias Mindwendel.Brainstormings.IdeaLabel
alias Mindwendel.Brainstormings.IdeaIdeaLabel
alias Mindwendel.Brainstormings.Like
alias Mindwendel.Attachments.Link
alias Mindwendel.UrlPreview
Expand All @@ -20,12 +21,13 @@ defmodule Mindwendel.Brainstormings.Idea do
has_many :likes, Like
belongs_to :brainstorming, Brainstorming, foreign_key: :brainstorming_id, type: :binary_id
belongs_to :label, IdeaLabel, foreign_key: :label_id, type: :binary_id, on_replace: :nilify
many_to_many :idea_labels, IdeaLabel, join_through: IdeaIdeaLabel, on_replace: :delete

timestamps()
end

@doc false
def changeset(idea, attrs) do
def changeset(idea, attrs \\ %{}) do
idea
|> cast(attrs, [:username, :body, :brainstorming_id, :deprecated_label, :label_id, :user_id])
|> validate_required([:username, :body, :brainstorming_id])
Expand All @@ -37,6 +39,10 @@ defmodule Mindwendel.Brainstormings.Idea do
change(idea) |> put_assoc(:label, label)
end

def changeset_update_labels(idea, idea_labels) do
change(idea) |> put_assoc(:idea_labels, idea_labels)
end

def build_link(idea) do
idea |> check_for_link_in_body
end
Expand Down
26 changes: 26 additions & 0 deletions lib/mindwendel/brainstormings/idea_idea_label.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Mindwendel.Brainstormings.IdeaIdeaLabel do
gerardo-navarro marked this conversation as resolved.
Show resolved Hide resolved
use Ecto.Schema

import Ecto.Changeset
alias Mindwendel.Brainstormings.Idea
alias Mindwendel.Brainstormings.IdeaLabel

@primary_key false
schema "idea_idea_labels" do
belongs_to :idea, Idea, type: :binary_id, primary_key: true
belongs_to :idea_label, IdeaLabel, type: :binary_id, primary_key: true

timestamps()
end

@doc false
def changeset(idea_idea_label, attrs \\ %{}) do
idea_idea_label
|> cast(attrs, [:idea_id, :idea_label_id])
|> cast_assoc(:idea, required: true)
|> cast_assoc(:idea_label, required: true)
|> unique_constraint([:idea_id, :idea_label_id],
name: :idea_idea_labels_idea_id_idea_label_id_index
)
end
end
7 changes: 5 additions & 2 deletions lib/mindwendel/brainstormings/idea_label.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Mindwendel.Brainstormings.IdeaLabel do
import Ecto.Changeset
alias Mindwendel.Brainstormings.Brainstorming
alias Mindwendel.Brainstormings.Idea
alias Mindwendel.Brainstormings.IdeaIdeaLabel

schema "idea_labels" do
field :name, :string
Expand All @@ -14,15 +15,17 @@ defmodule Mindwendel.Brainstormings.IdeaLabel do

belongs_to :brainstorming, Brainstorming, foreign_key: :brainstorming_id, type: :binary_id

has_many :ideas, Idea, foreign_key: :label_id
many_to_many :ideas, Idea, join_through: "idea_idea_labels", on_replace: :delete
has_many :idea_idea_labels, IdeaIdeaLabel, on_replace: :delete

timestamps()
end

def changeset(idea_label, params \\ %{})

def changeset(idea_label, %{delete: true}) do
%{Ecto.Changeset.change(idea_label, delete: true) | action: :delete}
|> no_assoc_constraint(:ideas, message: "idea label associated with idea")
|> no_assoc_constraint(:idea_idea_labels, message: "idea label associated with idea")
end

def changeset(idea_label, params) do
Expand Down
24 changes: 14 additions & 10 deletions lib/mindwendel_web/live/admin/brainstorming_live/edit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ defmodule MindwendelWeb.Admin.BrainstormingLive.Edit do

brainstorming =
Brainstormings.get_brainstorming_by!(%{admin_url_id: id})
|> Repo.preload(labels: from(idea_label in IdeaLabel, order_by: idea_label.position_order))
|> Repo.preload(
labels:
from(idea_label in IdeaLabel,
order_by: idea_label.position_order,
preload: [:idea_idea_labels]
)
)

changeset = Brainstormings.change_brainstorming(brainstorming, %{})

Expand Down Expand Up @@ -113,16 +119,14 @@ defmodule MindwendelWeb.Admin.BrainstormingLive.Edit do
brainstorming = socket.assigns.brainstorming

brainstorming_labels =
Enum.map(
brainstorming.labels,
fn label ->
if label.id == idea_label_id do
%{label | delete: true}
else
label
end
brainstorming.labels
|> Enum.map(fn label ->
if label.id == idea_label_id do
%{label | delete: true}
else
label
end
)
end)
|> Enum.map(&Map.from_struct/1)

case Brainstormings.update_brainstorming(brainstorming, %{labels: brainstorming_labels}) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@
<%= inputs_for f, :labels, fn p -> %>
<div class="input-group has-validation mb-3">
<%= color_input p, :color, class: "form-control form-control-color #{if p.errors[:color], do: "is-invalid"} #{if p.source.changes[:name] || p.source.changes[:color], do: "border-success"}", title: gettext("Choose the label color") %>
<%= text_input p, :name, class: "form-control #{if p.errors[:name] || p.errors[:ideas] || f.errors[:labels], do: "is-invalid"} #{if p.source.changes[:name] || p.source.changes[:color], do: "is-valid"}", placeholder: gettext("Type the label name"), phx_debounce: 500 %>
<%= text_input p, :name, class: "form-control #{if p.errors[:name] || p.errors[:idea_idea_labels] || f.errors[:labels], do: "is-invalid"} #{if p.source.changes[:name] || p.source.changes[:color], do: "is-valid"}", placeholder: gettext("Type the label name"), phx_debounce: 500 %>
<button class="btn btn-outline-secondary" type="button" phx-click="remove_idea_label" value="<%= input_value(p, :id) %>"><%= gettext("Remove idea label") %></button>
<%= error_tag p, :color %>
<%= error_tag p, :name %>
<%= if message = p.errors[:ideas] do %>
<span class="invalid-feedback" phx_feedback_for="<%= input_id(p, :name)%>"><%= translate_error(message) %></span>
<%= if message = p.errors[:idea_idea_labels] do %>
<span class="is-invalid"></span>
<span class="invalid-feedback" phx_feedback_for="<%= input_id(p, :name)%>"><%= translate_error(message) %></span>
<% end %>
<%= if message = f.errors[:labels] do %>
<span class="invalid-feedback" phx_feedback_for="<%= input_id(f, :labels)%>"><%= translate_error(message) %></span>
Expand Down
41 changes: 34 additions & 7 deletions lib/mindwendel_web/live/idea_live/index_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,43 @@ defmodule MindwendelWeb.IdeaLive.IndexComponent do
{:noreply, socket}
end

def handle_event("update_label", %{"id" => id, "label-id" => label_id}, socket) do
idea_label = Brainstormings.get_idea_label(label_id)
def handle_event(
"add_idea_label_to_idea",
%{
"idea-id" => idea_id,
"idea-label-id" => idea_label_id
},
socket
) do
idea = Brainstormings.get_idea!(idea_id)
idea_label = Brainstormings.get_idea_label(idea_label_id)

Brainstormings.get_idea!(id)
|> Brainstormings.update_idea_label(idea_label)
case(Brainstormings.add_idea_label_to_idea(idea, idea_label)) do
{:ok, idea} ->
{:noreply, socket}

{:noreply, socket}
{:error, changeset} ->
{:noreply, socket}
end
end

def handle_event("update_label", %{"id" => id}, socket) do
handle_event("update_label", %{"id" => id, "label-id" => nil}, socket)
def handle_event(
"remove_idea_label_from_idea",
%{
"idea-id" => idea_id,
"idea-label-id" => idea_label_id
},
socket
) do
idea = Brainstormings.get_idea!(idea_id)
idea_label = Brainstormings.get_idea_label(idea_label_id)

case(Brainstormings.remove_idea_label_from_idea(idea, idea_label)) do
{:ok, idea} ->
{:noreply, socket}

{:error, changeset} ->
{:noreply, socket}
end
end
end
26 changes: 11 additions & 15 deletions lib/mindwendel_web/live/idea_live/index_component.html.leex
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,18 @@
<div id="ideas" class="row mb-5">
<%= for idea <- @ideas do %>
<div class="col-md-6">
<%= unless idea.label do %>
<div class="card mt-3 shadow-sm p-2 rounded IndexComponent__IdeaCard" data-testid="<%= idea.id %>">
<% else %>
<div class="card mt-3 shadow-sm p-2 rounded IndexComponent__IdeaCard--labelled" data-testid="<%= idea.id %>" style="border-color: <%= idea.label.color %>">
<% end %>
<div class="card mt-3 shadow-sm p-2 rounded IndexComponent__IdeaCard" data-testid="<%= idea.id %>">
<div class="card-body-mindwendel-idea">
<%= if idea.label do %>
<span class="IndexComponent__IdeaLabelBadge" style="background-color: <%= idea.label.color %>;"><%= idea.label.name %></span>
<% end %>

<%= if idea.user_id == @current_user.id do %>
<%= link to: "#", class: "float-end ms-3 mb-3", phx_click: "delete", phx_target: @myself, phx_value_id: idea.id, title: 'Delete', data: [confirm: gettext("Are you sure you want to delete this idea?")] do %>
<i class="bi bi-x text-secondary"></i>
<% end %>
<% end %>

<%= for idea_label <- Enum.sort_by(idea.idea_labels, &(&1.position_order)) do %>
<span class="IndexComponent__IdeaLabelBadge mb-3" data-testid="<%= idea_label.id %>" style="background-color: <%= idea_label.color %>;"><%= idea_label.name %></span>
<% end %>

<%= unless idea.link do %>
<%= text_to_html(idea.body) %>
<% end %>
Expand Down Expand Up @@ -55,14 +51,14 @@
<% end %>
</div>
<div class="IndexComponent__IdeaLabelSection">
<%= for label <- @brainstorming.labels do %>
<%= unless idea.label && idea.label.id == label.id do %>
<%= link to: "#", class: "text-decoration-none me-1", data_testid: label.id, title: "label #{label.name}", phx_click: "update_label", phx_target: @myself, phx_value_id: idea.id, phx_value_label_id: label.id do %>
<i class="IndexComponent__IdeaLabel" data-testid="<%= label.id %>" style="color: <%= label.color %>;" ></i>
<%= for brainstorming_idea_label <- @brainstorming.labels do %>
<%= unless Enum.find(idea.idea_labels, fn idea_label -> idea_label.id == brainstorming_idea_label.id end) do %>
<%= link to: "#", class: "text-decoration-none me-1", data_testid: brainstorming_idea_label.id, title: "Label #{brainstorming_idea_label.name}", phx_click: "add_idea_label_to_idea", phx_target: @myself, phx_value_idea_id: idea.id, phx_value_idea_label_id: brainstorming_idea_label.id do %>
<i class="IndexComponent__IdeaLabel" data-testid="<%= brainstorming_idea_label.id %>" style="color: <%= brainstorming_idea_label.color %>;" ></i>
<% end %>
<% else %>
<%= link to: "#", class: "text-decoration-none me-1", data_testid: label.id, title: "label #{label.name}", phx_click: "update_label", phx_target: @myself, phx_value_id: idea.id, phx_value_label_id: nil do %>
<i class="IndexComponent__IdeaLabel--active" data-testid="<%= label.id %>" style="color: <%= label.color %>;" ></i>
<%= link to: "#", class: "text-decoration-none me-1", data_testid: brainstorming_idea_label.id, title: "Label #{brainstorming_idea_label.name}", phx_click: "remove_idea_label_from_idea", phx_target: @myself, phx_value_idea_id: idea.id, phx_value_idea_label_id: brainstorming_idea_label.id do %>
<i class="IndexComponent__IdeaLabel--active" data-testid="<%= brainstorming_idea_label.id %>" style="color: <%= brainstorming_idea_label.color %>;" ></i>
<% end %>
<% end %>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule Mindwendel.MixProject do
aliases: aliases(),
deps: deps(),
# This was necessary when executing `mix test` and was thrown by priv/repo/data_migrations/migrate_idea_labels.exs .
# The follwoing line avoids a warning in the test, see https://elixirforum.com/t/the-inspect-protocol-has-already-been-consolidated-for-ecto-schema-with-redacted-field/34992/14
# The following line avoids a warning in the test, see https://elixirforum.com/t/the-inspect-protocol-has-already-been-consolidated-for-ecto-schema-with-redacted-field/34992/14
# Apparently, it should have been resolved in the latest version of phoenix. But, we will see.
consolidate_protocols: Mix.env() != :test
]
Expand Down
Loading