From 5f97303aa8878276e7949c43eeda5f0b6c31458e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sat, 24 Feb 2024 02:24:42 +0000 Subject: [PATCH 01/39] refactor: department index & show --- lib/atomic_web/components/avatar.ex | 10 +- .../live/department_live/department_card.ex | 29 +++ lib/atomic_web/live/department_live/index.ex | 7 + .../live/department_live/index.html.heex | 29 +-- lib/atomic_web/live/department_live/show.ex | 2 + .../live/department_live/show.html.heex | 211 ++++++++++-------- priv/repo/seeds/departments.exs | 57 +++-- 7 files changed, 214 insertions(+), 131 deletions(-) create mode 100644 lib/atomic_web/live/department_live/department_card.ex diff --git a/lib/atomic_web/components/avatar.ex b/lib/atomic_web/components/avatar.ex index 5f35ed08f..763f31742 100644 --- a/lib/atomic_web/components/avatar.ex +++ b/lib/atomic_web/components/avatar.ex @@ -6,6 +6,10 @@ defmodule AtomicWeb.Components.Avatar do attr :name, :string, required: true, doc: "The name of the entity associated with the avatar." + attr :auto_generate_initials, :boolean, + default: true, + doc: "Whether to automatically generate the initials from the name." + attr :type, :atom, values: [:user, :organization, :company], default: :user, @@ -32,7 +36,11 @@ defmodule AtomicWeb.Components.Avatar do <%= if @src do %> <% else %> - <%= extract_initials(@name) %> + <%= if @auto_generate_initials do %> + <%= extract_initials(@name) %> + <% else %> + <%= @name %> + <% end %> <% end %> """ diff --git a/lib/atomic_web/live/department_live/department_card.ex b/lib/atomic_web/live/department_live/department_card.ex new file mode 100644 index 000000000..c7adb68cd --- /dev/null +++ b/lib/atomic_web/live/department_live/department_card.ex @@ -0,0 +1,29 @@ +defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do + use AtomicWeb, :component + + import AtomicWeb.Components.Avatar + + attr :department, :map, required: true, doc: "The department to display." + attr :collaborators, :list, required: true, doc: "The list of collaborators in the department." + + def department_card(assigns) do + ~H""" +
+
+

<%= @department.name %>

+
+ <%= for person <- @collaborators |> Enum.take(4) do %> + <.avatar name={person.user.name} size={:xs} fg_color="white" bg_color="zinc-400" class="ring-1 ring-white" /> + <% end %> + <%= if length(@collaborators) > 4 do %> + <.avatar name={"+#{length(@collaborators) - 4}"} size={:xs} auto_generate_initials={false} fg_color="white" bg_color="zinc-400" class="ring-1 ring-white" /> + <% end %> +
+
+
+ String.slice(0..3)}.png"} /> +
+
+ """ + end +end diff --git a/lib/atomic_web/live/department_live/index.ex b/lib/atomic_web/live/department_live/index.ex index 7511d90c3..6ccf3f599 100644 --- a/lib/atomic_web/live/department_live/index.ex +++ b/lib/atomic_web/live/department_live/index.ex @@ -3,6 +3,7 @@ defmodule AtomicWeb.DepartmentLive.Index do import AtomicWeb.Components.Empty import AtomicWeb.Components.Button + import AtomicWeb.DepartmentLive.Components.DepartmentCard alias Atomic.Accounts alias Atomic.Departments @@ -19,6 +20,12 @@ defmodule AtomicWeb.DepartmentLive.Index do departments = Departments.list_departments_by_organization_id(organization_id, preloads: [:organization]) + |> Enum.map(fn department -> + collaborators = + department.id |> Departments.list_collaborators_by_department_id(preloads: [:user]) + + {department, collaborators} + end) {:noreply, socket diff --git a/lib/atomic_web/live/department_live/index.html.heex b/lib/atomic_web/live/department_live/index.html.heex index 0ff0243ec..651a8ca2a 100644 --- a/lib/atomic_web/live/department_live/index.html.heex +++ b/lib/atomic_web/live/department_live/index.html.heex @@ -6,37 +6,16 @@ <% end %> - -
-
-
-
-
- -
-
-
-
-
<%= if @empty? and @has_permissions? do %>
<.empty_state url={Routes.department_new_path(@socket, :new, @organization)} placeholder="department" />
<% else %> -
- <%= for department <- @departments do %> - <.link navigate={Routes.department_show_path(@socket, :show, @organization, department)} class="col-span-1 flex flex-col divide-y divide-zinc-200 rounded-lg bg-white text-center shadow"> -
-

<%= department.name %>

-
+
+ <%= for {department, collaborators} <- @departments do %> + <.link navigate={Routes.department_show_path(@socket, :show, @organization, department)}> + <.department_card department={department} collaborators={collaborators} /> <% end %>
diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index c62acd91d..f38ccc02d 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -1,6 +1,8 @@ defmodule AtomicWeb.DepartmentLive.Show do use AtomicWeb, :live_view + import AtomicWeb.Components.Avatar + alias Atomic.Accounts alias Atomic.Departments alias Atomic.Organizations diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index e87f5c474..20cdd8255 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -1,102 +1,137 @@ -
-
-
-
-
-
-

- <%= @department.name %> -

-
- <%= if ! @collaborator do %> -
+
+
+ String.slice(0..3)}.png"} /> +
+
+
+
+
+
+
+

+ <%= @department.name %> +

+ <.link navigate={Routes.organization_show_path(@socket, :show, @organization)}> +

+ @<%= @organization.name %> +

+ +
+ <%= if !@collaborator do %> -
- <% else %> - <%= if ! @collaborator.accepted do %> -
- -
<% else %> -
-
- Collaborator + <%= if ! @collaborator.accepted do %> +
+
-
+ <% else %> +
+
+ Collaborator +
+
+ <% end %> <% end %> - <% end %> +
-
- -
-
- + +
+ <%= @department.description %>
-
-
- - <%= if length(@collaborators) != 0 do %> -
-
-
-
- - - - - - - - - - - - - - <%= for collaborator <- @collaborators do %> - - - - - - - - - - <% end %> - -
NameEmailPhone NumberAcceptedRequested AtUpdated At - Edit -
<%= collaborator.user.name %><%= collaborator.user.email %>Phone number<%= AtomicWeb.Helpers.capitalize_first_letter(collaborator.accepted) %><%= display_date(collaborator.inserted_at) %> <%= display_time(collaborator.inserted_at) %><%= display_date(collaborator.updated_at) %> <%= display_time(collaborator.updated_at) %> - <.link patch={Routes.department_index_path(@socket, :edit, @organization, @department)} class="button"> - - -
+
+
+

Recent Activity

+
+ +
+

People

+
+ <%= for collaborator <- @collaborators |> Enum.take(18) do %> +
+ <.avatar name={collaborator.user.name} size={:sm} /> +
+ <% end %>
+

View all collaborators

-
- <% end %> + + +
diff --git a/priv/repo/seeds/departments.exs b/priv/repo/seeds/departments.exs index faa11ee42..60d1366d3 100644 --- a/priv/repo/seeds/departments.exs +++ b/priv/repo/seeds/departments.exs @@ -6,23 +6,28 @@ defmodule Atomic.Repo.Seeds.Departments do alias Atomic.Organizations.{Department, Organization} alias Atomic.Repo - @department_names [ - "CAOS", - "Marketing e Conteúdo", - "Relações Externas e Parcerias", - "Pedagógico", - "Recreativo", - "Financeiro", - "Administrativo", - "Comunicação", - "Tecnologia", - "Design" + @departments [ + {"CAOS", + "O CAOS (Centro de Apoio ao Open-Source) é o departamento responsável por todo o desenvolvimento de software associado ao CeSIUM, quer sejam as plataformas que servem diretamente o núcleo e os seus eventos, ou as plataformas úteis para os alunos. Durante o processo, procura expandir o conhecimento dos seus colaboradores, para além daquilo que aprendem no curso. 💻"}, + {"Marketing e Conteúdo", + "O Departamento de Marketing e Conteúdo é responsável por promover todas as atividades efetuadas pelo núcleo, manter toda a identidade visual do CeSIUM e efetuar a comunicação através das nossas redes sociais. 💡"}, + {"Recreativo", + "O Departamento Recreativo é o responsável por organizar atividades de cariz cultural e/ou lúdico, com o intuito de promover a relação entre os vários estudantes, professores e funcionários. 🤪"}, + {"Pedagógico", + "O Departamento Pedagógico é o responsável por promover a relação entre a Direção de Curso e os alunos do LEI/MEI/MIEI e, ainda, por organizar atividades que complementam a formação académica dos mesmos. ✏️"}, + {"Relações Externas e Merch", + "O Departamento de Relações Externas e Merch é responsável por todo o merchandising do núcleo e por manter e fechar parceriais com diversas entidades e negócios, das quais os sócios do CeSIUM, assim como o núcleo, possam tirar proveito. 🤝"}, + {"Financeiro", + "O Departamento Financeiro é o responsável por toda a gestão financeira do núcleo, desde a organização de contas, ao controlo de despesas e receitas. 💰"}, + {"Administrativo", + "O Departamento Administrativo é o responsável por toda a gestão administrativa do núcleo, desde a organização de documentos, ao controlo de processos e procedimentos. 📝"} ] def run do case Repo.all(Department) do [] -> seed_departments() + seed_collaborators() _ -> Mix.shell().error("Found departments, aborting seeding departments.") @@ -33,12 +38,30 @@ defmodule Atomic.Repo.Seeds.Departments do organizations = Repo.all(Organization) for organization <- organizations do - for i <- 0..Enum.random(4..(length(@department_names) - 1)) do - %{ - name: Enum.at(@department_names, i), - organization_id: organization.id - } - |> Departments.create_department() + for i <- 0..Enum.random(4..(length(@departments) - 1)) do + case @departments |> Enum.at(i) do + {name, description} -> + %{ + name: name, + description: description, + organization_id: organization.id + } + |> Departments.create_department() + end + end + end + end + + def seed_collaborators() do + for department <- Repo.all(Department) do + for user <- Repo.all(Atomic.Accounts.User) do + if Enum.random(0..6) == 1 do + %{ + department_id: department.id, + user_id: user.id + } + |> Departments.create_collaborator() + end end end end From 80d3c7e41ea2e8afd0a7e2e266e4e1e50138645b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sat, 24 Feb 2024 22:52:47 +0000 Subject: [PATCH 02/39] feat: pagination --- lib/atomic/departments.ex | 15 +++ lib/atomic/organizations/collaborator.ex | 14 +++ .../live/department_live/department_card.ex | 1 + lib/atomic_web/live/department_live/show.ex | 42 ++++++- .../live/department_live/show.html.heex | 117 ++++++------------ 5 files changed, 108 insertions(+), 81 deletions(-) diff --git a/lib/atomic/departments.ex b/lib/atomic/departments.ex index 113d4d44d..3478c659a 100644 --- a/lib/atomic/departments.ex +++ b/lib/atomic/departments.ex @@ -277,6 +277,21 @@ defmodule Atomic.Departments do Collaborator.changeset(collaborator, attrs) end + @doc """ + Returns a paginated list of collaborators. + + ## Examples + + iex> list_display_collaborators() + [%Collaborator{}, ...] + + """ + def list_display_collaborators(%{} = flop, opts \\ []) do + Collaborator + |> apply_filters(opts) + |> Flop.validate_and_run(flop, for: Collaborator) + end + @doc """ Returns the list of collaborators belonging to a department. diff --git a/lib/atomic/organizations/collaborator.ex b/lib/atomic/organizations/collaborator.ex index 5872afe29..b5fbbb6df 100644 --- a/lib/atomic/organizations/collaborator.ex +++ b/lib/atomic/organizations/collaborator.ex @@ -10,6 +10,20 @@ defmodule Atomic.Organizations.Collaborator do @required_fields ~w(user_id department_id)a @optional_fields ~w(accepted)a + @derive { + Flop.Schema, + default_limit: 7, + filterable: [:accepted], + sortable: [:collaborator_name, :inserted_at, :updated_at], + default_order: %{ + order_by: [:inserted_at], + order_directions: [:desc] + }, + join_fields: [ + collaborator_name: [binding: :user, field: :name, path: [:user, :name]] + ] + } + schema "collaborators" do belongs_to :user, User belongs_to :department, Department diff --git a/lib/atomic_web/live/department_live/department_card.ex b/lib/atomic_web/live/department_live/department_card.ex index c7adb68cd..34f0bb0e5 100644 --- a/lib/atomic_web/live/department_live/department_card.ex +++ b/lib/atomic_web/live/department_live/department_card.ex @@ -1,4 +1,5 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do + @moduledoc false use AtomicWeb, :component import AtomicWeb.Components.Avatar diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index f38ccc02d..4339221e7 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -2,6 +2,8 @@ defmodule AtomicWeb.DepartmentLive.Show do use AtomicWeb, :live_view import AtomicWeb.Components.Avatar + import AtomicWeb.Components.Table + import AtomicWeb.Components.Pagination alias Atomic.Accounts alias Atomic.Departments @@ -14,24 +16,40 @@ defmodule AtomicWeb.DepartmentLive.Show do end @impl true - def handle_params(%{"organization_id" => organization_id, "id" => id}, _, socket) do + def handle_params(%{"organization_id" => organization_id, "id" => id} = params, _, socket) do organization = Organizations.get_organization!(organization_id) department = Departments.get_department!(id) {:noreply, socket |> assign(:current_page, :departments) + |> assign(:current_view, current_view(socket, params)) |> assign(:page_title, department.name) |> assign(:organization, organization) |> assign(:department, department) + |> assign(:params, params) |> assign(:collaborator, maybe_put_collaborator(socket, department.id)) + |> assign(list_collaborators(department.id, params)) |> assign( - :collaborators, + :all_collaborators, Departments.list_collaborators_by_department_id(department.id, preloads: [:user]) ) |> assign(:has_permissions?, has_permissions?(socket, organization_id))} end + defp list_collaborators(id, params) do + case Departments.list_display_collaborators(params, + where: [department_id: id], + preloads: [:user] + ) do + {:ok, {collaborators, meta}} -> + %{collaborators: collaborators, meta: meta} + + {:error, flop} -> + %{collaborators: [], meta: flop} + end + end + @impl true def handle_event("collaborate", _, socket) do department = socket.assigns.department @@ -70,6 +88,22 @@ defmodule AtomicWeb.DepartmentLive.Show do |> push_navigate(to: Routes.user_session_path(socket, :new))} end + @impl true + def handle_event("show-collaborators", _payload, socket) do + {:noreply, + socket + |> push_patch( + to: + Routes.department_show_path( + socket, + :show, + socket.assigns.organization.id, + socket.assigns.department.id, + tab: "collaborators" + ) + )} + end + defp maybe_put_collaborator(socket, _department_id) when not socket.assigns.is_authenticated?, do: nil @@ -93,4 +127,8 @@ defmodule AtomicWeb.DepartmentLive.Show do organization_id ) end + + defp current_view(_socket, params) when is_map_key(params, "tab"), do: params["tab"] + + defp current_view(_socket, _params), do: "show" end diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index 20cdd8255..d5ab56872 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -47,90 +47,49 @@ <% end %>
- -
- <%= @department.description %> -
-
-
-

Recent Activity

+ <%= if @current_view == "show" do %> + +
+ <%= @department.description %>
- -
-

People

-
- <%= for collaborator <- @collaborators |> Enum.take(18) do %> -
- <.avatar name={collaborator.user.name} size={:sm} /> -
- <% end %> +
+
+

Recent Activity

-

View all collaborators

-
-
- +
+

People

+
+ <%= for collaborator <- @all_collaborators |> Enum.take(18) do %> +
+ <.avatar name={collaborator.user.name} size={:sm} />
-
+ <% end %>
- <% end %> +

View all collaborators

+
-
- --> + <% end %> + <%= if @current_view == "collaborators" do %> + <%= if length(@collaborators) != 0 do %> + <.table items={@collaborators} meta={@meta}> + <:col :let={collaborator} label="Name" field={:string}><%= collaborator.user.name %> + <:col :let={collaborator} label="Email" field={:string}><%= collaborator.user.email %> + <:col :let={collaborator} label="Phone number" field={:string}><%= collaborator.user.phone_number %> + <:col :let={collaborator} label="Accepted" field={:string}><%= AtomicWeb.Helpers.capitalize_first_letter(collaborator.accepted) %> + <:col :let={collaborator} label="Requested At" field={:string}><%= display_date(collaborator.inserted_at) %> <%= display_time(collaborator.inserted_at) %> + <:col :let={collaborator} label="Updated At" field={:string}><%= display_date(collaborator.updated_at) %> <%= display_time(collaborator.updated_at) %> + <:col :let={_collaborator}> + <.link patch={Routes.department_index_path(@socket, :edit, @organization, @department)} class="button"> + + + + + <.pagination items={@collaborators} meta={@meta} params={@params} class="mt-2 flex w-full items-center justify-between border-r-[1px]" /> + <% end %> + <% end %>
From a8e8250c2768c8f26eabec01fbc8b0f20687ba5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sun, 25 Feb 2024 01:32:03 +0000 Subject: [PATCH 03/39] feat: collaborators view --- lib/atomic_web/live/department_live/show.ex | 40 +++++- .../live/department_live/show.html.heex | 125 ++++++++++++------ priv/repo/seeds/departments.exs | 5 +- 3 files changed, 119 insertions(+), 51 deletions(-) diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index 4339221e7..288c6ec21 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -20,6 +20,8 @@ defmodule AtomicWeb.DepartmentLive.Show do organization = Organizations.get_organization!(organization_id) department = Departments.get_department!(id) + has_permissions = has_permissions?(socket, organization_id) + {:noreply, socket |> assign(:current_page, :departments) @@ -29,17 +31,30 @@ defmodule AtomicWeb.DepartmentLive.Show do |> assign(:department, department) |> assign(:params, params) |> assign(:collaborator, maybe_put_collaborator(socket, department.id)) - |> assign(list_collaborators(department.id, params)) + |> assign(list_collaborators(department.id, params, has_permissions)) |> assign( :all_collaborators, - Departments.list_collaborators_by_department_id(department.id, preloads: [:user]) + Departments.list_collaborators_by_department_id(department.id, + preloads: [:user], + where: [accepted: true] + ) ) - |> assign(:has_permissions?, has_permissions?(socket, organization_id))} + |> assign(:has_permissions?, has_permissions)} end - defp list_collaborators(id, params) do + defp list_collaborators(id, params, has_permissions) do + if has_permissions do + # If the user has permissions, list all collaborators + list_collaborators_paginated(params, department_id: id) + else + # If the user does not have permissions, list only accepted collaborators + list_collaborators_paginated(params, department_id: id, accepted: true) + end + end + + defp list_collaborators_paginated(params, filter) do case Departments.list_display_collaborators(params, - where: [department_id: id], + where: filter, preloads: [:user] ) do {:ok, {collaborators, meta}} -> @@ -104,6 +119,21 @@ defmodule AtomicWeb.DepartmentLive.Show do )} end + @impl true + def handle_event("show-default", _payload, socket) do + {:noreply, + socket + |> push_patch( + to: + Routes.department_show_path( + socket, + :show, + socket.assigns.organization.id, + socket.assigns.department.id + ) + )} + end + defp maybe_put_collaborator(socket, _department_id) when not socket.assigns.is_authenticated?, do: nil diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index d5ab56872..491639a20 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -1,12 +1,14 @@ -
-
+
+ +
String.slice(0..3)}.png"} />
-
+
-
+
+

<%= @department.name %> @@ -17,33 +19,43 @@

- <%= if !@collaborator do %> - - <% else %> - <%= if ! @collaborator.accepted do %> -
- -
+ <%= if @current_view == "show" do %> + + <%= if !@collaborator do %> + <% else %> -
-
- Collaborator + <%= if ! @collaborator.accepted do %> +
+
-
+ <% else %> +
+
+ Collaborator +
+
+ <% end %> <% end %> + <% else %> + +
+ +
<% end %>
@@ -66,28 +78,53 @@
<% end %>
-

View all collaborators

+

View all collaborators

<% end %> <%= if @current_view == "collaborators" do %> <%= if length(@collaborators) != 0 do %> - <.table items={@collaborators} meta={@meta}> - <:col :let={collaborator} label="Name" field={:string}><%= collaborator.user.name %> - <:col :let={collaborator} label="Email" field={:string}><%= collaborator.user.email %> - <:col :let={collaborator} label="Phone number" field={:string}><%= collaborator.user.phone_number %> - <:col :let={collaborator} label="Accepted" field={:string}><%= AtomicWeb.Helpers.capitalize_first_letter(collaborator.accepted) %> - <:col :let={collaborator} label="Requested At" field={:string}><%= display_date(collaborator.inserted_at) %> <%= display_time(collaborator.inserted_at) %> - <:col :let={collaborator} label="Updated At" field={:string}><%= display_date(collaborator.updated_at) %> <%= display_time(collaborator.updated_at) %> - <:col :let={_collaborator}> - <.link patch={Routes.department_index_path(@socket, :edit, @organization, @department)} class="button"> -
+<.modal + :if={@live_action in [:edit_collaborator]} + id="edit-collaborator" + show + on_cancel={JS.patch(Routes.department_show_path(@socket, :show, @organization, @department, @params))} +> + <.live_component + scope={self} + module={AtomicWeb.CollaboratorLive.FormComponent} + id={@collaborator.id} + title={@page_title} + action={@live_action} + collaborator={@collaborator} + department={@department} + patch={"/"} + /> + \ No newline at end of file diff --git a/lib/atomic_web/live/speaker_live/index.ex b/lib/atomic_web/live/speaker_live/index.ex index e9f6ba6ca..44d975848 100644 --- a/lib/atomic_web/live/speaker_live/index.ex +++ b/lib/atomic_web/live/speaker_live/index.ex @@ -1,6 +1,8 @@ defmodule AtomicWeb.SpeakerLive.Index do use AtomicWeb, :live_view + import AtomicWeb.Components.Modal + alias Atomic.Activities alias Atomic.Activities.Speaker alias Phoenix.LiveView.JS diff --git a/lib/atomic_web/live/speaker_live/show.ex b/lib/atomic_web/live/speaker_live/show.ex index 518db2a7a..04caef12a 100644 --- a/lib/atomic_web/live/speaker_live/show.ex +++ b/lib/atomic_web/live/speaker_live/show.ex @@ -1,6 +1,8 @@ defmodule AtomicWeb.SpeakerLive.Show do use AtomicWeb, :live_view + import AtomicWeb.Components.Modal + alias Atomic.Activities alias Phoenix.LiveView.JS diff --git a/lib/atomic_web/router.ex b/lib/atomic_web/router.ex index 2f729e756..6a691714f 100644 --- a/lib/atomic_web/router.ex +++ b/lib/atomic_web/router.ex @@ -93,6 +93,7 @@ defmodule AtomicWeb.Router do pipe_through :confirm_department_association live "/new", DepartmentLive.New, :new live "/:id/edit", DepartmentLive.Index, :edit + live "/:id/collaborators/:collaborator_id/edit", DepartmentLive.Show, :edit_collaborator end scope "/partners" do diff --git a/lib/atomic_web/views/helpers.ex b/lib/atomic_web/views/helpers.ex index 318725c37..75bdce693 100644 --- a/lib/atomic_web/views/helpers.ex +++ b/lib/atomic_web/views/helpers.ex @@ -225,6 +225,31 @@ defmodule AtomicWeb.Helpers do end end + @doc """ + Return the first name of a name. + + ## Examples + + iex> extract_first_name("John Doe") + "John" + + iex> extract_first_name("John") + "John" + + iex> extract_first_name(nil) + "" + + """ + def extract_first_name(nil), do: "" + + def extract_first_name(name) do + name + |> String.split(" ") + |> Enum.filter(&String.match?(String.slice(&1, 0, 1), ~r/^\p{L}$/u)) + |> Enum.map(&String.capitalize/1) + |> hd() + end + @doc """ Slices a string if it is longer than the given length From f55271168a496428c0d9632dfa543b95682d029a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Tue, 27 Feb 2024 17:20:55 +0000 Subject: [PATCH 06/39] chore: format --- .../live/collaborator_live/form_component.ex | 85 +++++++++---------- lib/atomic_web/live/department_live/show.ex | 10 ++- .../live/department_live/show.html.heex | 20 +---- 3 files changed, 53 insertions(+), 62 deletions(-) diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index 10db5ebd3..1e06cb992 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -8,54 +8,51 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do @impl true def render(assigns) do ~H""" -
-

Collaborator

- - <%= if !@collaborator.accepted do %> -

<%= extract_first_name(@collaborator.user.name) %> has requested to be a collaborator of <%= @department.name %>.

- <% end %> - -
- <.avatar name={@collaborator.user.name} /> -
-

<%= @collaborator.user.name %>

-

@<%= @collaborator.user.slug %>

+
+

Collaborator

+ + <%= if !@collaborator.accepted do %> +

<%= extract_first_name(@collaborator.user.name) %> has requested to be a collaborator of <%= @department.name %>.

+ <% end %> + +
+ <.avatar name={@collaborator.user.name} /> +
+

<%= @collaborator.user.name %>

+

@<%= @collaborator.user.slug %>

+
+ <%= if @collaborator.accepted do %> +
+

Collaborator since <%= display_date(@collaborator.inserted_at) %>

+
+ <% else %> +
+

Not accepted

- <%= if @collaborator.accepted do %> -
-

Collaborator since <%= display_date(@collaborator.inserted_at) %>

+ <% end %> +
+ +
+ <%= if @collaborator.accepted do %> + + <% else %> +
- -
- <%= if @collaborator.accepted do %> - - <% else %> - - - <% end %> -
+ + + <% end %>
+
""" end diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index 26917e62f..02fa9bdfc 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -47,7 +47,15 @@ defmodule AtomicWeb.DepartmentLive.Show do |> assign(:has_permissions?, has_permissions) end - defp apply_action(socket, :edit_collaborator, %{"organization_id" => organization_id, "id" => department_id, "collaborator_id" => collaborator_id} = params) do + defp apply_action( + socket, + :edit_collaborator, + %{ + "organization_id" => organization_id, + "id" => department_id, + "collaborator_id" => collaborator_id + } = params + ) do organization = Organizations.get_organization!(organization_id) department = Departments.get_department!(department_id) collaborator = Departments.get_collaborator!(collaborator_id, preloads: [:user]) diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index aa33f3590..7260b9195 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -133,20 +133,6 @@
-<.modal - :if={@live_action in [:edit_collaborator]} - id="edit-collaborator" - show - on_cancel={JS.patch(Routes.department_show_path(@socket, :show, @organization, @department, @params))} -> - <.live_component - scope={self} - module={AtomicWeb.CollaboratorLive.FormComponent} - id={@collaborator.id} - title={@page_title} - action={@live_action} - collaborator={@collaborator} - department={@department} - patch={"/"} - /> - \ No newline at end of file +<.modal :if={@live_action in [:edit_collaborator]} id="edit-collaborator" show on_cancel={JS.patch(Routes.department_show_path(@socket, :show, @organization, @department, @params))}> + <.live_component scope={self} module={AtomicWeb.CollaboratorLive.FormComponent} id={@collaborator.id} title={@page_title} action={@live_action} collaborator={@collaborator} department={@department} patch="/" /> + From deee17960bb9c4f6e44184529663e5fd7987731f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Tue, 27 Feb 2024 17:50:20 +0000 Subject: [PATCH 07/39] fix: unecessary parameter --- lib/atomic_web/live/department_live/show.html.heex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index 7260b9195..675899306 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -133,6 +133,6 @@
-<.modal :if={@live_action in [:edit_collaborator]} id="edit-collaborator" show on_cancel={JS.patch(Routes.department_show_path(@socket, :show, @organization, @department, @params))}> - <.live_component scope={self} module={AtomicWeb.CollaboratorLive.FormComponent} id={@collaborator.id} title={@page_title} action={@live_action} collaborator={@collaborator} department={@department} patch="/" /> +<.modal :if={@live_action in [:edit_collaborator]} id="edit-collaborator" show on_cancel={JS.patch(Routes.department_show_path(@socket, :show, @organization, @department, Map.delete(@params, "collaborator_id")))}> + <.live_component module={AtomicWeb.CollaboratorLive.FormComponent} id={@collaborator.id} title={@page_title} action={@live_action} collaborator={@collaborator} department={@department} /> From 3ba4ccb8317515450d52fa75320587fcfc0f831e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Wed, 13 Mar 2024 16:57:46 +0000 Subject: [PATCH 08/39] fix: woop --- .../live/department_live/{ => components}/department_card.ex | 4 ++-- lib/atomic_web/live/department_live/show.html.heex | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename lib/atomic_web/live/department_live/{ => components}/department_card.ex (82%) diff --git a/lib/atomic_web/live/department_live/department_card.ex b/lib/atomic_web/live/department_live/components/department_card.ex similarity index 82% rename from lib/atomic_web/live/department_live/department_card.ex rename to lib/atomic_web/live/department_live/components/department_card.ex index 34f0bb0e5..58efa054a 100644 --- a/lib/atomic_web/live/department_live/department_card.ex +++ b/lib/atomic_web/live/department_live/components/department_card.ex @@ -14,10 +14,10 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do

<%= @department.name %>

<%= for person <- @collaborators |> Enum.take(4) do %> - <.avatar name={person.user.name} size={:xs} fg_color="white" bg_color="zinc-400" class="ring-1 ring-white" /> + <.avatar name={person.user.name} size={:xs} color={:light_gray} class="ring-1 ring-white" /> <% end %> <%= if length(@collaborators) > 4 do %> - <.avatar name={"+#{length(@collaborators) - 4}"} size={:xs} auto_generate_initials={false} fg_color="white" bg_color="zinc-400" class="ring-1 ring-white" /> + <.avatar name={"+#{length(@collaborators) - 4}"} size={:xs} auto_generate_initials={false} color={:light_gray} class="ring-1 ring-white" /> <% end %>
diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index e49df16f6..91004c5c4 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -76,7 +76,7 @@
<%= for collaborator <- @all_collaborators |> Enum.take(18) do %>
- <.avatar name={collaborator.user.name} size={:sm} /> + <.avatar name={collaborator.user.name} size={:sm} color={:light_gray} />
<% end %>
From c38485587790dd5811c131f1cb7a978d8e072497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sat, 16 Mar 2024 17:25:18 +0000 Subject: [PATCH 09/39] feat: confirm modals --- lib/atomic_web/components/button.ex | 4 +- lib/atomic_web/components/modal.ex | 2 +- .../live/collaborator_live/form_component.ex | 105 +++++++++++++----- .../live/department_live/show.html.heex | 11 +- 4 files changed, 86 insertions(+), 36 deletions(-) diff --git a/lib/atomic_web/components/button.ex b/lib/atomic_web/components/button.ex index f6b909cbe..39350908a 100644 --- a/lib/atomic_web/components/button.ex +++ b/lib/atomic_web/components/button.ex @@ -58,10 +58,10 @@ defmodule AtomicWeb.Components.Button do attr :rest, :global, include: - ~w(csrf_token disabled download form href hreflang method name navigate patch referrerpolicy rel replace target type value), + ~w(csrf_token disabled download form href hreflang method name navigate patch referrerpolicy rel replace target type value autofocus tabindex), doc: "Arbitrary HTML or phx attributes." - slot :inner_block, required: true, doc: "Slot for the body content of the page." + slot :inner_block, required: true, doc: "Slot for the content of the button." def button(assigns) do assigns diff --git a/lib/atomic_web/components/modal.ex b/lib/atomic_web/components/modal.ex index 1ccc2d21e..1545767ed 100644 --- a/lib/atomic_web/components/modal.ex +++ b/lib/atomic_web/components/modal.ex @@ -41,7 +41,7 @@ defmodule AtomicWeb.Components.Modal do
<.focus_wrap id={"#{@id}-container"} phx-click-away={JS.exec("phx-cancel", to: "##{@id}")} class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition">
-
diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index 1e06cb992..4a378e99a 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -2,8 +2,10 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do use AtomicWeb, :live_component import AtomicWeb.Components.Avatar + import AtomicWeb.Components.Badge alias Atomic.Departments + alias Phoenix.LiveView.JS @impl true def render(assigns) do @@ -15,43 +17,48 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do

<%= extract_first_name(@collaborator.user.name) %> has requested to be a collaborator of <%= @department.name %>.

<% end %> -
- <.avatar name={@collaborator.user.name} /> -
-

<%= @collaborator.user.name %>

-

@<%= @collaborator.user.slug %>

-
+
+ <.link navigate={Routes.profile_show_path(@socket, :show, @collaborator.user)} class="mt-4 flex outline-none"> + <.avatar color={:light_gray} name={@collaborator.user.name} /> +
+

<%= @collaborator.user.name %>

+

@<%= @collaborator.user.slug %>

+
+ <%= if @collaborator.accepted do %> -
+ <.badge variant={:outline} color={:success} size={:md} class="ml-auto font-normal my-5 select-none rounded-xl">

Collaborator since <%= display_date(@collaborator.inserted_at) %>

-
+ <% else %> -
+ <.badge variant={:outline} color={:warning} size={:md} class="ml-auto border-yellow-400 text-yellow-400 bg-yellow-300/5 font-normal my-5 select-none rounded-xl">

Not accepted

-
+ <% end %>
-
+
<%= if @collaborator.accepted do %> - + <.button phx-click="delete" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width> Delete <% else %> - - + <.button phx-click="deny" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width> Deny + <.button phx-click="allow" phx-target={@myself} size={:lg} icon={:check_circle} color={:white} full_width> Accept <% end %>
+ + <.modal :if={@action_modal != nil} id="action-confirm-modal" show on_cancel={JS.push("clear-action", target: @myself)}> +
+

+ <%= display_action_goal_confirm_title(@action_modal) %> +

+

+ <%= display_action_goal_confirm_description(@action_modal, @department) %> +

+
+ <.button phx-click="confirm" class="ml-2" phx-target={@myself} size={:lg} icon={:check_circle} color={if @action_modal != :delete do :white else :danger end} full_width> Confirm + <.button phx-click="clear-action" class="mr-2" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width> Cancel +
+
+
""" end @@ -59,10 +66,54 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do @impl true def update(%{collaborator: collaborator} = assigns, socket) do changeset = Departments.change_collaborator(collaborator) - {:ok, socket |> assign(assigns) + |> assign(:action_modal, nil) |> assign(:changeset, changeset)} end + + @impl true + def handle_event("clear-action", _, socket) do + {:noreply, + socket + |> assign(:action_modal, nil)} + end + + @impl true + def handle_event("allow", _, socket) do + {:noreply, + socket + |> assign(:action_modal, :confirm_invite)} + end + + @impl true + def handle_event("deny", _, socket) do + {:noreply, + socket + |> assign(:action_modal, :deny_invite)} + end + + @impl true + def handle_event("delete", _, socket) do + {:noreply, + socket + |> assign(:action_modal, :delete)} + end + + defp display_action_goal_confirm_title(action) do + case action do + :confirm_invite -> gettext("Are you sure you want to accept this request?") + :deny_invite -> gettext("Are you sure you want to deny this request?") + :delete -> gettext("Are you sure you want to remove this person from the department?") + end + end + + defp display_action_goal_confirm_description(action, department) do + case action do + :confirm_invite -> gettext("If you change your mind you can always remove this person later.") + :deny_invite -> gettext("If you deny this request, this person will not get access to the department.") + :delete -> gettext("If you remove this person, they will no longer have access to %{department_name}.", department_name: department.name) + end + end end diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index 91004c5c4..a8420128b 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -97,13 +97,12 @@ <:col :let={collaborator} label="Phone number" field={:string}><%= collaborator.user.phone_number %> <:col :let={collaborator} label="Accepted" field={:string}><%= AtomicWeb.Helpers.capitalize_first_letter(collaborator.accepted) %> <:col :let={collaborator} label="Requested At" field={:string}><%= display_date(collaborator.inserted_at) %> <%= display_time(collaborator.inserted_at) %> - <:col :let={collaborator} label="Updated At" field={:string}><%= display_date(collaborator.updated_at) %> <%= display_time(collaborator.updated_at) %> <:col :let={collaborator}> - <.link patch={Routes.department_show_path(@socket, :edit_collaborator, @organization, @department, collaborator, @params)} class="button"> - - + <%= if collaborator.accepted do %> + <.button icon={:pencil} icon_variant={:solid} color={:white} full_width patch={Routes.department_show_path(@socket, :edit_collaborator, @organization, @department, collaborator, @params)}>Edit + <% else %> + <.button icon={:envelope} icon_variant={:solid} color={:white} full_width patch={Routes.department_show_path(@socket, :edit_collaborator, @organization, @department, collaborator, @params)}>Review + <% end %>
From 367caae930da4208a6c78f5cd945fe3514b96f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sat, 16 Mar 2024 19:14:23 +0000 Subject: [PATCH 10/39] feat: remove collaborator --- lib/atomic_web/components/modal.ex | 2 +- .../live/collaborator_live/form_component.ex | 117 ++++++++++++++---- lib/atomic_web/live/department_live/show.ex | 18 +++ 3 files changed, 113 insertions(+), 24 deletions(-) diff --git a/lib/atomic_web/components/modal.ex b/lib/atomic_web/components/modal.ex index 1545767ed..bf228ff3c 100644 --- a/lib/atomic_web/components/modal.ex +++ b/lib/atomic_web/components/modal.ex @@ -41,7 +41,7 @@ defmodule AtomicWeb.Components.Modal do
<.focus_wrap id={"#{@id}-container"} phx-click-away={JS.exec("phx-cancel", to: "##{@id}")} class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition">
-
diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index 4a378e99a..119d65c4b 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -17,7 +17,7 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do

<%= extract_first_name(@collaborator.user.name) %> has requested to be a collaborator of <%= @department.name %>.

<% end %> -
+
<.link navigate={Routes.profile_show_path(@socket, :show, @collaborator.user)} class="mt-4 flex outline-none"> <.avatar color={:light_gray} name={@collaborator.user.name} />
@@ -26,11 +26,11 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do
<%= if @collaborator.accepted do %> - <.badge variant={:outline} color={:success} size={:md} class="ml-auto font-normal my-5 select-none rounded-xl"> + <.badge variant={:outline} color={:success} size={:md} class="my-5 select-none rounded-xl py-1 font-normal sm:ml-auto sm:py-0">

Collaborator since <%= display_date(@collaborator.inserted_at) %>

<% else %> - <.badge variant={:outline} color={:warning} size={:md} class="ml-auto border-yellow-400 text-yellow-400 bg-yellow-300/5 font-normal my-5 select-none rounded-xl"> + <.badge variant={:outline} color={:warning} size={:md} class="bg-yellow-300/5 my-5 select-none rounded-xl border-yellow-400 py-1 font-normal text-yellow-400 sm:ml-auto sm:py-0">

Not accepted

<% end %> @@ -38,14 +38,14 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do
<%= if @collaborator.accepted do %> - <.button phx-click="delete" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width> Delete + <.button phx-click="delete" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width>Delete <% else %> - <.button phx-click="deny" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width> Deny - <.button phx-click="allow" phx-target={@myself} size={:lg} icon={:check_circle} color={:white} full_width> Accept + <.button phx-click="deny" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width>Deny + <.button phx-click="allow" phx-target={@myself} size={:lg} icon={:check_circle} color={:white} full_width>Accept <% end %>
- <.modal :if={@action_modal != nil} id="action-confirm-modal" show on_cancel={JS.push("clear-action", target: @myself)}> + <.modal :if={@action_modal} id="action-confirm-modal" show on_cancel={JS.push("clear-action", target: @myself)}>

<%= display_action_goal_confirm_title(@action_modal) %> @@ -54,8 +54,24 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do <%= display_action_goal_confirm_description(@action_modal, @department) %>

- <.button phx-click="confirm" class="ml-2" phx-target={@myself} size={:lg} icon={:check_circle} color={if @action_modal != :delete do :white else :danger end} full_width> Confirm - <.button phx-click="clear-action" class="mr-2" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width> Cancel + <.button + phx-click="confirm" + class="ml-2" + phx-target={@myself} + size={:lg} + icon={:check_circle} + color={ + if @action_modal != :delete do + :white + else + :danger + end + } + full_width + > + Confirm + + <.button phx-click="clear-action" class="mr-2" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width>Cancel

@@ -66,6 +82,7 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do @impl true def update(%{collaborator: collaborator} = assigns, socket) do changeset = Departments.change_collaborator(collaborator) + {:ok, socket |> assign(assigns) @@ -73,47 +90,101 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do |> assign(:changeset, changeset)} end + defp confirm_collaborator_request(socket) do + {:noreply, socket} + end + + defp deny_collaborator_request(socket) do + {:noreply, socket} + end + + defp delete_collaborator(socket) do + with {:ok, _} <- Departments.delete_collaborator(socket.assigns.collaborator) do + send( + self(), + {:change_collaborator, + %{status: :success, message: gettext("Collaborator removed successfully.")}} + ) + + {:noreply, socket |> assign(:action_modal, nil)} + else + _ -> + send( + self(), + {:change_collaborator, + %{ + status: :error, + message: gettext("Could not delete the collaborator. Please try again later.") + }} + ) + + {:noreply, socket |> assign(:action_modal, nil)} + end + end + + @impl true + def handle_event("confirm", _, socket) do + case socket.assigns.action_modal do + :confirm_request -> confirm_collaborator_request(socket) + :deny_request -> deny_collaborator_request(socket) + :delete_collaborator -> delete_collaborator(socket) + end + end + @impl true def handle_event("clear-action", _, socket) do {:noreply, - socket - |> assign(:action_modal, nil)} + socket + |> assign(:action_modal, nil)} end @impl true def handle_event("allow", _, socket) do {:noreply, - socket - |> assign(:action_modal, :confirm_invite)} + socket + |> assign(:action_modal, :confirm_request)} end @impl true def handle_event("deny", _, socket) do {:noreply, - socket - |> assign(:action_modal, :deny_invite)} + socket + |> assign(:action_modal, :deny_request)} end @impl true def handle_event("delete", _, socket) do {:noreply, - socket - |> assign(:action_modal, :delete)} + socket + |> assign(:action_modal, :delete_collaborator)} end defp display_action_goal_confirm_title(action) do case action do - :confirm_invite -> gettext("Are you sure you want to accept this request?") - :deny_invite -> gettext("Are you sure you want to deny this request?") - :delete -> gettext("Are you sure you want to remove this person from the department?") + :confirm_request -> + gettext("Are you sure you want to accept this request?") + + :deny_request -> + gettext("Are you sure you want to deny this request?") + + :delete_collaborator -> + gettext("Are you sure you want to remove this person from the department?") end end defp display_action_goal_confirm_description(action, department) do case action do - :confirm_invite -> gettext("If you change your mind you can always remove this person later.") - :deny_invite -> gettext("If you deny this request, this person will not get access to the department.") - :delete -> gettext("If you remove this person, they will no longer have access to %{department_name}.", department_name: department.name) + :confirm_request -> + gettext("If you change your mind you can always remove this person later.") + + :deny_request -> + gettext("If you deny this request, this person will not get access to the department.") + + :delete_collaborator -> + gettext( + "If you remove this person, they will no longer have access to %{department_name}.", + department_name: department.name + ) end end end diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index 02fa9bdfc..c824ba9a6 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -104,6 +104,24 @@ defmodule AtomicWeb.DepartmentLive.Show do end end + @impl true + def handle_info({:change_collaborator, %{status: status, message: message}}, socket) do + {:noreply, + socket + |> put_flash(status, message) + |> assign(:live_action, :show) + |> push_patch( + to: + Routes.department_show_path( + socket, + :show, + socket.assigns.organization, + socket.assigns.department, + Map.delete(socket.assigns.params, "collaborator_id") + ) + )} + end + @impl true def handle_event("collaborate", _, socket) do department = socket.assigns.department From 6f78aefcca2ac92ec3f48056a4c697745eab3041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sat, 16 Mar 2024 21:20:31 +0000 Subject: [PATCH 11/39] feat: table improvements --- assets/css/components/button.css | 2 +- lib/atomic_web/components/table.ex | 4 ++-- .../live/collaborator_live/form_component.ex | 2 +- lib/atomic_web/live/department_live/show.html.heex | 10 ++++------ 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/assets/css/components/button.css b/assets/css/components/button.css index 52123f2ba..afe6bc3ab 100644 --- a/assets/css/components/button.css +++ b/assets/css/components/button.css @@ -1,7 +1,7 @@ /* Buttons */ .atomic-button { - @apply inline-flex items-center justify-center font-medium transition duration-150 ease-in-out border rounded-md focus:outline-none; + @apply inline-flex items-center justify-center font-medium transition duration-150 ease-in-out border rounded-md select-none focus:outline-none; } /* Buttons - sizes */ diff --git a/lib/atomic_web/components/table.ex b/lib/atomic_web/components/table.ex index 9c21ca8c8..51ccc74ab 100644 --- a/lib/atomic_web/components/table.ex +++ b/lib/atomic_web/components/table.ex @@ -29,7 +29,7 @@ defmodule AtomicWeb.Components.Table do <%= for item <- @items do %> <%= for col <- @col do %> - + <%= render_slot(col, item) %> <% end %> @@ -48,7 +48,7 @@ defmodule AtomicWeb.Components.Table do assign(assigns, :direction, order_direction(assigns.meta.flop.order_directions, index)) ~H""" - + <%= if is_sortable?(@field, @meta.schema) && is_filterable?(@field, @meta.schema) && should_filter(@field, @filter) do %>
<.link patch={build_sorting_query(@field, @meta)} class="mr-2 w-full"> diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index 119d65c4b..54d685c8c 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -61,7 +61,7 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do size={:lg} icon={:check_circle} color={ - if @action_modal != :delete do + if @action_modal != :delete_collaborator do :white else :danger diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index a8420128b..246807c4a 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -53,11 +53,7 @@ <% end %> <% else %> -
- -
+ <.button icon={:arrow_left} icon_variant={:solid} color={:white} phx-click="show-default"> <% end %>
@@ -95,7 +91,9 @@ <:col :let={collaborator} label="Name" field={:string}><%= collaborator.user.name %> <:col :let={collaborator} label="Email" field={:string}><%= collaborator.user.email %> <:col :let={collaborator} label="Phone number" field={:string}><%= collaborator.user.phone_number %> - <:col :let={collaborator} label="Accepted" field={:string}><%= AtomicWeb.Helpers.capitalize_first_letter(collaborator.accepted) %> + <:col :let={collaborator} label="Accepted" field={:string}> + + <:col :let={collaborator} label="Requested At" field={:string}><%= display_date(collaborator.inserted_at) %> <%= display_time(collaborator.inserted_at) %> <:col :let={collaborator}> <%= if collaborator.accepted do %> From df087e886e2fb3c705206b9905b66ff79fb51aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sun, 17 Mar 2024 02:31:18 +0000 Subject: [PATCH 12/39] refactor: action buttons --- lib/atomic_web/components/table.ex | 2 +- .../live/department_live/show.html.heex | 73 ++++++++++--------- priv/repo/seeds/departments.exs | 6 +- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/lib/atomic_web/components/table.ex b/lib/atomic_web/components/table.ex index 51ccc74ab..d928d0210 100644 --- a/lib/atomic_web/components/table.ex +++ b/lib/atomic_web/components/table.ex @@ -48,7 +48,7 @@ defmodule AtomicWeb.Components.Table do assign(assigns, :direction, order_direction(assigns.meta.flop.order_directions, index)) ~H""" - + <%= if is_sortable?(@field, @meta.schema) && is_filterable?(@field, @meta.schema) && should_filter(@field, @filter) do %>
<.link patch={build_sorting_query(@field, @meta)} class="mr-2 w-full"> diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index 246807c4a..067a148ea 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -19,42 +19,49 @@
- <%= if @current_view == "show" do %> - - <%= if !@current_collaborator do %> - <%= if @department.collaborator_applications do %> - - <% end %> - <% else %> - <%= if ! @current_collaborator.accepted do %> -
- -
+ +
+ <%= if @current_view == "show" do %> + + <%= if !@current_collaborator do %> + <%= if @department.collaborator_applications do %> + <.button + phx-click={ + "#{if @is_authenticated? do + "collaborate" + else + "must-login" + end}" + } + color={:white} + icon={:user_plus} + icon_variant={:solid} + > + <%= gettext("Collaborate") %> + + <% end %> <% else %> -
-
- Collaborator -
-
+ <%= if ! @current_collaborator.accepted do %> + <.button color={:white} icon={:user_plus} icon_variant={:solid} aria-label={gettext("You have requested to collaborate with this department. Please wait for the department owner to accept your request.")} disabled> + <%= gettext("Collaborate") %> + + <% end %> + <% end %> + <%= if @has_permissions? || (@current_collaborator && @current_collaborator.accepted) do %> + + <.button icon={:user_group} icon_variant={:solid} color={:white} phx-click="show-collaborators"> + + <.button icon={:view_columns} icon_variant={:solid} color={:white} phx-click="show-edit"> <% end %> + + <%= if @has_permissions? do %> + <.button icon={:pencil} icon_variant={:solid} color={:white} phx-click="show-edit"> + <% end %> + <% else %> + + <.button icon={:arrow_left} icon_variant={:solid} color={:white} phx-click="show-default"> <% end %> - <% else %> - - <.button icon={:arrow_left} icon_variant={:solid} color={:white} phx-click="show-default"> - <% end %> +
<%= if @current_view == "show" do %> diff --git a/priv/repo/seeds/departments.exs b/priv/repo/seeds/departments.exs index a26353731..6c02b4774 100644 --- a/priv/repo/seeds/departments.exs +++ b/priv/repo/seeds/departments.exs @@ -44,7 +44,8 @@ defmodule Atomic.Repo.Seeds.Departments do %{ name: name, description: description, - organization_id: organization.id + organization_id: organization.id, + collaborator_applications: Enum.random([true, false]) } |> Departments.create_department() end @@ -59,8 +60,7 @@ defmodule Atomic.Repo.Seeds.Departments do %{ department_id: department.id, user_id: user.id, - accepted: Enum.random([true, false]), - collaborator_applications: Enum.random([true, false]) + accepted: Enum.random([true, false]) } |> Departments.create_collaborator() end From 0dbcb0100dbe2c4b6fd497a35c2a431eafb44358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Tue, 19 Mar 2024 00:28:46 +0000 Subject: [PATCH 13/39] feat: edit & new page --- lib/atomic_web/live/department_live/edit.ex | 25 +++++++++++- .../live/department_live/edit.html.heex | 26 ++++++++++++- .../department_live/form_component.html.heex | 39 ++++++++++++++----- .../live/department_live/index.html.heex | 2 +- lib/atomic_web/live/department_live/new.ex | 20 ---------- .../live/department_live/new.html.heex | 1 - .../live/department_live/show.html.heex | 4 +- lib/atomic_web/router.ex | 4 +- 8 files changed, 83 insertions(+), 38 deletions(-) delete mode 100644 lib/atomic_web/live/department_live/new.ex delete mode 100644 lib/atomic_web/live/department_live/new.html.heex diff --git a/lib/atomic_web/live/department_live/edit.ex b/lib/atomic_web/live/department_live/edit.ex index ec690bbca..d80196985 100644 --- a/lib/atomic_web/live/department_live/edit.ex +++ b/lib/atomic_web/live/department_live/edit.ex @@ -3,6 +3,7 @@ defmodule AtomicWeb.DepartmentLive.Edit do use AtomicWeb, :live_view alias Atomic.Departments + alias Atomic.Organizations.Department @impl true def mount(_params, _session, socket) do @@ -10,7 +11,20 @@ defmodule AtomicWeb.DepartmentLive.Edit do end @impl true - def handle_params(%{"organization_id" => organization_id, "id" => id} = _params, _, socket) do + def handle_params(params, uri, %{:assigns => %{:live_action => live_action}} = socket) do + case live_action do + :new -> + handle_params_new(params, uri, socket) + + :edit -> + handle_params_edit(params, uri, socket) + + _ -> + {:noreply, socket} + end + end + + def handle_params_edit(%{"organization_id" => organization_id, "id" => id} = _params, _, socket) do department = Departments.get_department!(id) {:noreply, @@ -20,4 +34,13 @@ defmodule AtomicWeb.DepartmentLive.Edit do |> assign(:page_title, gettext("Edit Department")) |> assign(:department, department)} end + + def handle_params_new(%{"organization_id" => organization_id} = _params, _, socket) do + {:noreply, + socket + |> assign(:organization_id, organization_id) + |> assign(:current_page, :departments) + |> assign(:page_title, gettext("Edit Department")) + |> assign(:department, %Department{organization_id: organization_id})} + end end diff --git a/lib/atomic_web/live/department_live/edit.html.heex b/lib/atomic_web/live/department_live/edit.html.heex index e72e54592..27acfe752 100644 --- a/lib/atomic_web/live/department_live/edit.html.heex +++ b/lib/atomic_web/live/department_live/edit.html.heex @@ -1 +1,25 @@ -<.live_component module={AtomicWeb.DepartmentLive.FormComponent} id={@department.id} organization={@current_organization} title={@page_title} action={@live_action} department={@department} return_to={Routes.department_show_path(@socket, :show, @organization_id, @department)} /> +<.page title={ + if @live_action in [:new] do + "New Department" + else + "Edit #{@department.name}" + end +}> +
+ <.live_component + module={AtomicWeb.DepartmentLive.FormComponent} + id="department-edit" + organization={@current_organization} + title={@page_title} + action={@live_action} + department={@department} + return_to={ + if @live_action in [:new] do + Routes.department_index_path(@socket, :index, @organization_id) + else + Routes.department_show_path(@socket, :show, @organization_id, @department) + end + } + /> +
+ diff --git a/lib/atomic_web/live/department_live/form_component.html.heex b/lib/atomic_web/live/department_live/form_component.html.heex index f68ee4172..3eef28365 100644 --- a/lib/atomic_web/live/department_live/form_component.html.heex +++ b/lib/atomic_web/live/department_live/form_component.html.heex @@ -1,17 +1,36 @@
-

<%= @title %>

- <.form :let={f} for={@changeset} id="department-form" phx-target={@myself} phx-change="validate" phx-submit="save"> - <%= label(f, :name) %> - <%= text_input(f, :name) %> - <%= error_tag(f, :name) %> +
+
+
+ <%= label(f, :name, class: "text-sm font-semibold") %> +

The name of the department

+
+ <%= text_input(f, :name, class: "focus:ring-primary-500 focus:border-primary-500") %> + <%= error_tag(f, :name) %> +
+ +
+
+ <%= label(f, :description, class: "text-sm font-semibold") %> +

A brief description of the department

+
+ <%= text_input(f, :description, class: "focus:ring-primary-500 focus:border-primary-500") %> + <%= error_tag(f, :description) %> +
- <%= label(f, :description) %> - <%= text_input(f, :description) %> - <%= error_tag(f, :description) %> +
+ <%= checkbox(f, :collaborator_applications, class: "my-auto focus:ring-primary-500 focus:border-primary-500 text-primary-500") %> +
+ <%= label(f, :collaborator_applications, class: "text-sm font-semibold") %> +

Allow any user to apply to be a collaborator in this department

+
+ <%= error_tag(f, :collaborator_applications) %> +
-
- <%= submit("Save", phx_disable_with: "Saving...") %> +
+ <.button size={:md} color={:white} icon={:cube}>Save Changes +
diff --git a/lib/atomic_web/live/department_live/index.html.heex b/lib/atomic_web/live/department_live/index.html.heex index 3edffa582..9866fccb8 100644 --- a/lib/atomic_web/live/department_live/index.html.heex +++ b/lib/atomic_web/live/department_live/index.html.heex @@ -1,7 +1,7 @@ <.page title="Departments"> <:actions> <%= if not @empty? and @has_permissions? do %> - <.button navigate={Routes.department_new_path(@socket, :new, @current_organization)}> + <.button patch={Routes.department_edit_path(@socket, :new, @current_organization)}> <%= gettext("New") %> <% end %> diff --git a/lib/atomic_web/live/department_live/new.ex b/lib/atomic_web/live/department_live/new.ex deleted file mode 100644 index 25c5b93f0..000000000 --- a/lib/atomic_web/live/department_live/new.ex +++ /dev/null @@ -1,20 +0,0 @@ -defmodule AtomicWeb.DepartmentLive.New do - @moduledoc false - use AtomicWeb, :live_view - - alias Atomic.Organizations.Department - - @impl true - def mount(_params, _session, socket) do - {:ok, socket} - end - - @impl true - def handle_params(%{"organization_id" => organization_id}, _, socket) do - {:noreply, - socket - |> assign(:current_page, :department) - |> assign(:page_title, gettext("New Department")) - |> assign(:department, %Department{organization_id: organization_id})} - end -end diff --git a/lib/atomic_web/live/department_live/new.html.heex b/lib/atomic_web/live/department_live/new.html.heex deleted file mode 100644 index 595297a7f..000000000 --- a/lib/atomic_web/live/department_live/new.html.heex +++ /dev/null @@ -1 +0,0 @@ -<.live_component module={AtomicWeb.DepartmentLive.FormComponent} id={:new} organization={@current_organization} title={@page_title} action={@live_action} department={@department} return_to={Routes.department_index_path(@socket, :index, @current_organization)} /> diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index 067a148ea..a6bb6848c 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -51,11 +51,11 @@ <.button icon={:user_group} icon_variant={:solid} color={:white} phx-click="show-collaborators"> - <.button icon={:view_columns} icon_variant={:solid} color={:white} phx-click="show-edit"> + <.button icon={:view_columns} icon_variant={:solid} color={:white} disabled> <% end %> <%= if @has_permissions? do %> - <.button icon={:pencil} icon_variant={:solid} color={:white} phx-click="show-edit"> + <.button icon={:pencil} icon_variant={:solid} color={:white} patch={Routes.department_edit_path(@socket, :edit, @organization, @department, @params)}> <% end %> <% else %> diff --git a/lib/atomic_web/router.ex b/lib/atomic_web/router.ex index bddbcfea8..9e319a191 100644 --- a/lib/atomic_web/router.ex +++ b/lib/atomic_web/router.ex @@ -92,8 +92,8 @@ defmodule AtomicWeb.Router do scope "/departments" do pipe_through :confirm_department_association - live "/new", DepartmentLive.New, :new - live "/:id/edit", DepartmentLive.Index, :edit + live "/new", DepartmentLive.Edit, :new + live "/:id/edit", DepartmentLive.Edit, :edit live "/:id/collaborators/:collaborator_id/edit", DepartmentLive.Show, :edit_collaborator end From 6fef032f174ee8939baebe08d8483dc96002d69f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Tue, 19 Mar 2024 11:41:43 +0000 Subject: [PATCH 14/39] refactor: single file component --- .../live/department_live/edit.html.heex | 2 +- .../live/department_live/form_component.ex | 43 +++++++++++++++++++ .../department_live/form_component.html.heex | 36 ---------------- .../live/department_live/index.html.heex | 2 +- 4 files changed, 45 insertions(+), 38 deletions(-) delete mode 100644 lib/atomic_web/live/department_live/form_component.html.heex diff --git a/lib/atomic_web/live/department_live/edit.html.heex b/lib/atomic_web/live/department_live/edit.html.heex index 27acfe752..8f83eac8f 100644 --- a/lib/atomic_web/live/department_live/edit.html.heex +++ b/lib/atomic_web/live/department_live/edit.html.heex @@ -5,7 +5,7 @@ "Edit #{@department.name}" end }> -
+
<.live_component module={AtomicWeb.DepartmentLive.FormComponent} id="department-edit" diff --git a/lib/atomic_web/live/department_live/form_component.ex b/lib/atomic_web/live/department_live/form_component.ex index 32cf1b8a5..c78201997 100644 --- a/lib/atomic_web/live/department_live/form_component.ex +++ b/lib/atomic_web/live/department_live/form_component.ex @@ -3,6 +3,48 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do alias Atomic.Departments + @impl true + def render(assigns) do + ~H""" +
+ <.form :let={f} for={@changeset} id="department-form" phx-target={@myself} phx-change="validate" phx-submit="save"> +
+
+
+ <%= label(f, :name, class: "text-sm font-semibold") %> +

The name of the department

+
+ <%= text_input(f, :name, class: "focus:ring-primary-500 focus:border-primary-500") %> + <%= error_tag(f, :name) %> +
+ +
+
+ <%= label(f, :description, class: "text-sm font-semibold") %> +

A brief description of the department

+
+ <%= text_input(f, :description, class: "focus:ring-primary-500 focus:border-primary-500") %> + <%= error_tag(f, :description) %> +
+ +
+ <%= checkbox(f, :collaborator_applications, class: "text-primary-500 my-auto focus:ring-primary-500 focus:border-primary-500") %> +
+ <%= label(f, :collaborator_applications, class: "text-sm font-semibold") %> +

Allow any user to apply to be a collaborator in this department

+
+ <%= error_tag(f, :collaborator_applications) %> +
+ +
+ <.button size={:md} color={:white} icon={:cube}>Save Changes +
+
+ +
+ """ + end + @impl true def update(%{department: department} = assigns, socket) do changeset = Departments.change_department(department) @@ -23,6 +65,7 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do {:noreply, assign(socket, :changeset, changeset)} end + @impl true def handle_event("save", %{"department" => department_params}, socket) do save_department(socket, socket.assigns.action, department_params) end diff --git a/lib/atomic_web/live/department_live/form_component.html.heex b/lib/atomic_web/live/department_live/form_component.html.heex deleted file mode 100644 index 3eef28365..000000000 --- a/lib/atomic_web/live/department_live/form_component.html.heex +++ /dev/null @@ -1,36 +0,0 @@ -
- <.form :let={f} for={@changeset} id="department-form" phx-target={@myself} phx-change="validate" phx-submit="save"> -
-
-
- <%= label(f, :name, class: "text-sm font-semibold") %> -

The name of the department

-
- <%= text_input(f, :name, class: "focus:ring-primary-500 focus:border-primary-500") %> - <%= error_tag(f, :name) %> -
- -
-
- <%= label(f, :description, class: "text-sm font-semibold") %> -

A brief description of the department

-
- <%= text_input(f, :description, class: "focus:ring-primary-500 focus:border-primary-500") %> - <%= error_tag(f, :description) %> -
- -
- <%= checkbox(f, :collaborator_applications, class: "my-auto focus:ring-primary-500 focus:border-primary-500 text-primary-500") %> -
- <%= label(f, :collaborator_applications, class: "text-sm font-semibold") %> -

Allow any user to apply to be a collaborator in this department

-
- <%= error_tag(f, :collaborator_applications) %> -
- -
- <.button size={:md} color={:white} icon={:cube}>Save Changes -
-
- -
diff --git a/lib/atomic_web/live/department_live/index.html.heex b/lib/atomic_web/live/department_live/index.html.heex index 9866fccb8..0778e8ed0 100644 --- a/lib/atomic_web/live/department_live/index.html.heex +++ b/lib/atomic_web/live/department_live/index.html.heex @@ -9,7 +9,7 @@ <%= if @empty? and @has_permissions? do %>
- <.empty_state url={Routes.department_new_path(@socket, :new, @organization)} placeholder="department" /> + <.empty_state url={Routes.department_edit_path(@socket, :new, @current_organization)} placeholder="department" />
<% else %>
From e378b7b1ee3ee88693c8956d254d0056d57df4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Tue, 19 Mar 2024 11:46:38 +0000 Subject: [PATCH 15/39] refactor: code readability --- .../live/collaborator_live/form_component.ex | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index 54d685c8c..eb0ae51c3 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -99,15 +99,16 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do end defp delete_collaborator(socket) do - with {:ok, _} <- Departments.delete_collaborator(socket.assigns.collaborator) do - send( - self(), - {:change_collaborator, - %{status: :success, message: gettext("Collaborator removed successfully.")}} - ) - - {:noreply, socket |> assign(:action_modal, nil)} - else + case Departments.delete_collaborator(socket.assigns.collaborator) do + {:ok, _} -> + send( + self(), + {:change_collaborator, + %{status: :success, message: gettext("Collaborator removed successfully.")}} + ) + + {:noreply, socket |> assign(:action_modal, nil)} + _ -> send( self(), From e6547e894c70c62a8ae6265756057947c6581b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Tue, 19 Mar 2024 16:57:11 +0000 Subject: [PATCH 16/39] feat: initial banner logic --- lib/atomic/organizations/department.ex | 2 + lib/atomic/uploaders/banner.ex | 11 ++++++ lib/atomic_web/components/image_uploader.ex | 7 +--- .../live/collaborator_live/form_component.ex | 9 ++++- .../department_banner_placeholder.ex | 39 +++++++++++++++++++ .../components/department_card.ex | 5 ++- .../live/department_live/form_component.ex | 20 ++++++++-- lib/atomic_web/live/department_live/index.ex | 6 ++- lib/atomic_web/live/department_live/show.ex | 3 ++ .../live/department_live/show.html.heex | 8 +++- .../20221000000000_create_departments.exs | 1 + 11 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 lib/atomic/uploaders/banner.ex create mode 100644 lib/atomic_web/live/department_live/components/department_banner_placeholder.ex diff --git a/lib/atomic/organizations/department.ex b/lib/atomic/organizations/department.ex index d0888a2ce..41d0cb877 100644 --- a/lib/atomic/organizations/department.ex +++ b/lib/atomic/organizations/department.ex @@ -4,6 +4,7 @@ defmodule Atomic.Organizations.Department do """ use Atomic.Schema alias Atomic.Organizations.Organization + alias Atomic.Uploaders @required_fields ~w(name organization_id)a @optional_fields ~w(description collaborator_applications)a @@ -11,6 +12,7 @@ defmodule Atomic.Organizations.Department do schema "departments" do field :name, :string field :description, :string + field :banner, Atomic.Uploaders.Banner.Type field :collaborator_applications, :boolean, default: false belongs_to :organization, Organization, on_replace: :delete_if_exists diff --git a/lib/atomic/uploaders/banner.ex b/lib/atomic/uploaders/banner.ex new file mode 100644 index 000000000..2b8e2220c --- /dev/null +++ b/lib/atomic/uploaders/banner.ex @@ -0,0 +1,11 @@ +defmodule Atomic.Uploaders.Banner do + @moduledoc """ + Uploader for department banners. + """ + use Atomic.Uploader + alias Atomic.Organizations.Department + + def storage_dir(_version, {_file, %Department{} = scope}) do + "uploads/atomic/departments/#{scope.id}/banner" + end +end diff --git a/lib/atomic_web/components/image_uploader.ex b/lib/atomic_web/components/image_uploader.ex index e843dd0a2..740220120 100644 --- a/lib/atomic_web/components/image_uploader.ex +++ b/lib/atomic_web/components/image_uploader.ex @@ -13,15 +13,12 @@ defmodule AtomicWeb.Components.ImageUploader do def render(assigns) do ~H"""
-
- - Image - +
<.live_file_input upload={@uploads.image} class="hidden" />
diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index eb0ae51c3..c3052e33b 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -1,8 +1,7 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do use AtomicWeb, :live_component - import AtomicWeb.Components.Avatar - import AtomicWeb.Components.Badge + import AtomicWeb.Components.{Avatar, Badge} alias Atomic.Departments alias Phoenix.LiveView.JS @@ -35,6 +34,12 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do <% end %>
+ <%= if !@collaborator.accepted do %> +
+ <.icon class="my-auto h-5 w-5" name={:calendar} /> +

Requested <%= Timex.from_now(@collaborator.inserted_at) %>

+
+ <% end %>
<%= if @collaborator.accepted do %> diff --git a/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex b/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex new file mode 100644 index 000000000..a86911cf1 --- /dev/null +++ b/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex @@ -0,0 +1,39 @@ +defmodule AtomicWeb.DepartmentLive.Components.DepartmentBannerPlaceholder do + @moduledoc false + use AtomicWeb, :component + + def department_banner_placeholder(assigns) do + {gradient_color_a, gradient_color_b} = generate_color(assigns.department.id) + + assigns + |> assign(:gradient_color_a, gradient_color_a) + |> assign(:gradient_color_b, gradient_color_b) + |> render_gradient() + end + + defp render_gradient(assigns) do + ~H""" +
+ """ + end + + def generate_color(uuid) when is_binary(uuid) do + # List of gradients + colors = [ + {"#000046", "#1CB5E0"}, + {"#007991", "#78ffd6"}, + {"#30E8BF", "#FF8235"}, + {"#C33764", "#1D2671"}, + {"#34e89e", "#0f3443"}, + {"#44A08D", "#093637"}, + {"#DCE35B", "#45B649"}, + {"#c0c0aa", "#1cefff"} + ] + + # Convert the UUID to an integer + index = :erlang.phash2(uuid, length(colors)) + + # Return the chosen color + Enum.at(colors, index) + end +end diff --git a/lib/atomic_web/live/department_live/components/department_card.ex b/lib/atomic_web/live/department_live/components/department_card.ex index 58efa054a..a23c5941a 100644 --- a/lib/atomic_web/live/department_live/components/department_card.ex +++ b/lib/atomic_web/live/department_live/components/department_card.ex @@ -3,6 +3,7 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do use AtomicWeb, :component import AtomicWeb.Components.Avatar + import AtomicWeb.DepartmentLive.Components.DepartmentBannerPlaceholder attr :department, :map, required: true, doc: "The department to display." attr :collaborators, :list, required: true, doc: "The list of collaborators in the department." @@ -22,7 +23,9 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do
- String.slice(0..3)}.png"} /> +
+ <.department_banner_placeholder department={@department} class="rounded-r-lg" /> +
""" diff --git a/lib/atomic_web/live/department_live/form_component.ex b/lib/atomic_web/live/department_live/form_component.ex index c78201997..b8ef6ce9d 100644 --- a/lib/atomic_web/live/department_live/form_component.ex +++ b/lib/atomic_web/live/department_live/form_component.ex @@ -3,11 +3,14 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do alias Atomic.Departments + alias AtomicWeb.Components.ImageUploader + @impl true def render(assigns) do ~H"""
<.form :let={f} for={@changeset} id="department-form" phx-target={@myself} phx-change="validate" phx-submit="save"> +

<%= gettext("General") %>

@@ -35,10 +38,20 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do
<%= error_tag(f, :collaborator_applications) %>
- -
- <.button size={:md} color={:white} icon={:cube}>Save Changes +
+

<%= gettext("Personalization") %>

+
+
+ <%= label(f, :banner, class: "text-sm font-semibold") %> +

The banner of the department

+
+ <.live_component module={ImageUploader} id="uploader" uploads={@uploads} target={@myself} /> +
+
+ +
+ <.button size={:md} color={:white} icon={:cube}>Save Changes
@@ -51,6 +64,7 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do {:ok, socket + |> allow_upload(:image, accept: Atomic.Uploader.extensions_whitelist(), max_entries: 1) |> assign(assigns) |> assign(:changeset, changeset)} end diff --git a/lib/atomic_web/live/department_live/index.ex b/lib/atomic_web/live/department_live/index.ex index 6ccf3f599..2372c3485 100644 --- a/lib/atomic_web/live/department_live/index.ex +++ b/lib/atomic_web/live/department_live/index.ex @@ -22,7 +22,11 @@ defmodule AtomicWeb.DepartmentLive.Index do Departments.list_departments_by_organization_id(organization_id, preloads: [:organization]) |> Enum.map(fn department -> collaborators = - department.id |> Departments.list_collaborators_by_department_id(preloads: [:user]) + department.id + |> Departments.list_collaborators_by_department_id( + preloads: [:user], + where: [accepted: true] + ) {department, collaborators} end) diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index c824ba9a6..eb1caf424 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -5,6 +5,7 @@ defmodule AtomicWeb.DepartmentLive.Show do import AtomicWeb.Components.Table import AtomicWeb.Components.Pagination import AtomicWeb.Components.Modal + import AtomicWeb.DepartmentLive.Components.DepartmentBannerPlaceholder alias Atomic.Accounts alias Atomic.Departments @@ -178,6 +179,8 @@ defmodule AtomicWeb.DepartmentLive.Show do @impl true def handle_event("show-default", _payload, socket) do + IO.puts("TESTE VOLTAR") + {:noreply, socket |> push_patch( diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index a6bb6848c..581e880a9 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -1,7 +1,7 @@
- String.slice(0..3)}.png"} /> + <.department_banner_placeholder department={@department} class="object-cover" />
@@ -82,6 +82,11 @@ <.avatar name={collaborator.user.name} size={:sm} color={:light_gray} />
<% end %> + + <%= if length(@all_collaborators) == 0 do %> + <.icon class="mx-32 text-gray-900" name={:users} /> +

<%= gettext("This department doesn't have any collaborators yet!") %>

+ <% end %>

View all collaborators

@@ -101,7 +106,6 @@ <:col :let={collaborator} label="Accepted" field={:string}> - <:col :let={collaborator} label="Requested At" field={:string}><%= display_date(collaborator.inserted_at) %> <%= display_time(collaborator.inserted_at) %> <:col :let={collaborator}> <%= if collaborator.accepted do %> <.button icon={:pencil} icon_variant={:solid} color={:white} full_width patch={Routes.department_show_path(@socket, :edit_collaborator, @organization, @department, collaborator, @params)}>Edit diff --git a/priv/repo/migrations/20221000000000_create_departments.exs b/priv/repo/migrations/20221000000000_create_departments.exs index 83e73fd59..fe94c7607 100644 --- a/priv/repo/migrations/20221000000000_create_departments.exs +++ b/priv/repo/migrations/20221000000000_create_departments.exs @@ -6,6 +6,7 @@ defmodule Atomic.Repo.Migrations.CreateDepartments do add :id, :binary_id, primary_key: true add :name, :string, null: false add :description, :text + add :banner, :string add :collaborator_applications, :boolean, default: false, null: false add :organization_id, references(:organizations, on_delete: :delete_all, type: :binary_id), From 41c639d44a4d5de46b7d8f132fbd046ae5d5e1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Tue, 19 Mar 2024 19:19:43 +0000 Subject: [PATCH 17/39] refactor: ui changes --- lib/atomic_web/components/modal.ex | 1 - .../live/collaborator_live/form_component.ex | 4 +- .../department_banner_placeholder.ex | 3 +- .../components/department_card.ex | 14 ++-- lib/atomic_web/live/department_live/show.ex | 33 --------- .../live/department_live/show.html.heex | 72 ++++++++++++++----- 6 files changed, 66 insertions(+), 61 deletions(-) diff --git a/lib/atomic_web/components/modal.ex b/lib/atomic_web/components/modal.ex index bf228ff3c..8990f8c3f 100644 --- a/lib/atomic_web/components/modal.ex +++ b/lib/atomic_web/components/modal.ex @@ -65,7 +65,6 @@ defmodule AtomicWeb.Components.Modal do ) |> show("##{id}-container") |> JS.add_class("overflow-hidden", to: "body") - |> JS.focus_first(to: "##{id}-content") end def hide_modal(js \\ %JS{}, id) do diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index c3052e33b..70b65cf6d 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -58,7 +58,8 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do

<%= display_action_goal_confirm_description(@action_modal, @department) %>

-
+
+ <.button phx-click="clear-action" class="mr-2" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width>Cancel <.button phx-click="confirm" class="ml-2" @@ -76,7 +77,6 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do > Confirm - <.button phx-click="clear-action" class="mr-2" phx-target={@myself} size={:lg} icon={:x_circle} color={:white} full_width>Cancel
diff --git a/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex b/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex index a86911cf1..f80294e1f 100644 --- a/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex +++ b/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex @@ -27,7 +27,8 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentBannerPlaceholder do {"#34e89e", "#0f3443"}, {"#44A08D", "#093637"}, {"#DCE35B", "#45B649"}, - {"#c0c0aa", "#1cefff"} + {"#c0c0aa", "#1cefff"}, + {"#ee0979", "#ff6a00"} ] # Convert the UUID to an integer diff --git a/lib/atomic_web/live/department_live/components/department_card.ex b/lib/atomic_web/live/department_live/components/department_card.ex index a23c5941a..b22c3683b 100644 --- a/lib/atomic_web/live/department_live/components/department_card.ex +++ b/lib/atomic_web/live/department_live/components/department_card.ex @@ -10,10 +10,13 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do def department_card(assigns) do ~H""" -
-
+
+
+ <.department_banner_placeholder department={@department} class="rounded-t-lg" /> +
+

<%= @department.name %>

-
+
<%= for person <- @collaborators |> Enum.take(4) do %> <.avatar name={person.user.name} size={:xs} color={:light_gray} class="ring-1 ring-white" /> <% end %> @@ -22,11 +25,6 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do <% end %>
-
-
- <.department_banner_placeholder department={@department} class="rounded-r-lg" /> -
-
""" end diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index eb1caf424..830d483ff 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -161,39 +161,6 @@ defmodule AtomicWeb.DepartmentLive.Show do |> push_navigate(to: Routes.user_session_path(socket, :new))} end - @impl true - def handle_event("show-collaborators", _payload, socket) do - {:noreply, - socket - |> push_patch( - to: - Routes.department_show_path( - socket, - :show, - socket.assigns.organization.id, - socket.assigns.department.id, - tab: "collaborators" - ) - )} - end - - @impl true - def handle_event("show-default", _payload, socket) do - IO.puts("TESTE VOLTAR") - - {:noreply, - socket - |> push_patch( - to: - Routes.department_show_path( - socket, - :show, - socket.assigns.organization.id, - socket.assigns.department.id - ) - )} - end - defp maybe_put_collaborator(socket, _department_id) when not socket.assigns.is_authenticated?, do: nil diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index 581e880a9..d6cd126d3 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -1,12 +1,12 @@
-
+
<.department_banner_placeholder department={@department} class="object-cover" />
-
+
@@ -49,7 +49,21 @@ <% end %> <%= if @has_permissions? || (@current_collaborator && @current_collaborator.accepted) do %> - <.button icon={:user_group} icon_variant={:solid} color={:white} phx-click="show-collaborators"> + <.button + icon={:user_group} + icon_variant={:solid} + color={:white} + patch={ + Routes.department_show_path( + @socket, + :show, + @organization.id, + @department.id, + tab: "collaborators" + ) + } + > + <.button icon={:view_columns} icon_variant={:solid} color={:white} disabled> <% end %> @@ -59,7 +73,20 @@ <% end %> <% else %> - <.button icon={:arrow_left} icon_variant={:solid} color={:white} phx-click="show-default"> + <.button + icon={:arrow_left} + icon_variant={:solid} + color={:white} + patch={ + Routes.department_show_path( + @socket, + :show, + @organization.id, + @department.id + ) + } + > + <% end %>
@@ -69,14 +96,14 @@
<%= @department.description %>
-
+
-

Recent Activity

+

Recent Activity

-

People

-
+

People

+
<%= for collaborator <- @all_collaborators |> Enum.take(18) do %>
<.avatar name={collaborator.user.name} size={:sm} color={:light_gray} /> @@ -85,10 +112,23 @@ <%= if length(@all_collaborators) == 0 do %> <.icon class="mx-32 text-gray-900" name={:users} /> -

<%= gettext("This department doesn't have any collaborators yet!") %>

+

<%= gettext("This department doesn't have any collaborators yet!") %>

<% end %>
-

View all collaborators

+ <.link + patch={ + Routes.department_show_path( + @socket, + :show, + @organization.id, + @department.id, + tab: "collaborators" + ) + } + class="text-orange-500 hover:cursor-pointer hover:underline" + > + View all collaborators +
<% end %> @@ -97,14 +137,14 @@ <%= if @has_permissions? do %>
-

Collaborators

+

Collaborators

<.table items={@collaborators} meta={@meta} filter={[]}> <:col :let={collaborator} label="Name" field={:string}><%= collaborator.user.name %> <:col :let={collaborator} label="Email" field={:string}><%= collaborator.user.email %> <:col :let={collaborator} label="Phone number" field={:string}><%= collaborator.user.phone_number %> <:col :let={collaborator} label="Accepted" field={:string}> - + <:col :let={collaborator}> <%= if collaborator.accepted do %> @@ -115,17 +155,17 @@
- <.pagination items={@collaborators} meta={@meta} params={@params} class="pt-2 flex w-full items-center justify-between border border-t-0" /> + <.pagination items={@collaborators} meta={@meta} params={@params} class="flex w-full items-center justify-between border border-t-0 pt-2" />
<% else %>
-

Collaborators

+

Collaborators

<%= for collaborator <- @collaborators do %> -
+
<.avatar name={collaborator.user.name} /> -
+

<%= collaborator.user.name %>

@<%= collaborator.user.slug %>

From 233d3489fcf560597646302f0102b2709c7d913c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Tue, 19 Mar 2024 22:34:32 +0000 Subject: [PATCH 18/39] feat: add icon to new department button --- lib/atomic/organizations/department.ex | 1 - lib/atomic_web/live/department_live/index.html.heex | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/atomic/organizations/department.ex b/lib/atomic/organizations/department.ex index 41d0cb877..24d763352 100644 --- a/lib/atomic/organizations/department.ex +++ b/lib/atomic/organizations/department.ex @@ -4,7 +4,6 @@ defmodule Atomic.Organizations.Department do """ use Atomic.Schema alias Atomic.Organizations.Organization - alias Atomic.Uploaders @required_fields ~w(name organization_id)a @optional_fields ~w(description collaborator_applications)a diff --git a/lib/atomic_web/live/department_live/index.html.heex b/lib/atomic_web/live/department_live/index.html.heex index 0778e8ed0..30c532038 100644 --- a/lib/atomic_web/live/department_live/index.html.heex +++ b/lib/atomic_web/live/department_live/index.html.heex @@ -1,7 +1,7 @@ <.page title="Departments"> <:actions> <%= if not @empty? and @has_permissions? do %> - <.button patch={Routes.department_edit_path(@socket, :new, @current_organization)}> + <.button patch={Routes.department_edit_path(@socket, :new, @current_organization)} icon={:plus}> <%= gettext("New") %> <% end %> From 347dcdd8a39523115a6b94be9bc6ea0c4d1cb7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Thu, 21 Mar 2024 16:07:00 +0000 Subject: [PATCH 19/39] feat: accept or deny collaborator requests --- lib/atomic/departments.ex | 18 ++++++ lib/atomic/organizations/collaborator.ex | 3 +- .../live/collaborator_live/form_component.ex | 62 ++++++++++++++++--- .../20230880102641_create_collaborators.exs | 1 + priv/repo/seeds/departments.exs | 10 ++- 5 files changed, 82 insertions(+), 12 deletions(-) diff --git a/lib/atomic/departments.ex b/lib/atomic/departments.ex index 2cd162d11..c22c71525 100644 --- a/lib/atomic/departments.ex +++ b/lib/atomic/departments.ex @@ -252,6 +252,24 @@ defmodule Atomic.Departments do |> Repo.update() end + @doc """ + Accepts a collaborator. + + ## Examples + + iex> accept_collaborator(collaborator) + {:ok, %Collaborator{}} + + iex> accept_collaborator(collaborator) + {:error, %Ecto.Changeset{}} + + """ + def accept_collaborator(%Collaborator{} = collaborator) do + collaborator + |> Collaborator.changeset(%{accepted: true, accepted_at: NaiveDateTime.utc_now()}) + |> Repo.update() + end + @doc """ Deletes a collaborator. diff --git a/lib/atomic/organizations/collaborator.ex b/lib/atomic/organizations/collaborator.ex index b5fbbb6df..8da1b88cc 100644 --- a/lib/atomic/organizations/collaborator.ex +++ b/lib/atomic/organizations/collaborator.ex @@ -8,7 +8,7 @@ defmodule Atomic.Organizations.Collaborator do alias Atomic.Organizations.Department @required_fields ~w(user_id department_id)a - @optional_fields ~w(accepted)a + @optional_fields ~w(accepted accepted_at)a @derive { Flop.Schema, @@ -29,6 +29,7 @@ defmodule Atomic.Organizations.Collaborator do belongs_to :department, Department field :accepted, :boolean, default: false + field :accepted_at, :naive_datetime timestamps() end diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index 70b65cf6d..d215545ca 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -26,7 +26,7 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do <%= if @collaborator.accepted do %> <.badge variant={:outline} color={:success} size={:md} class="my-5 select-none rounded-xl py-1 font-normal sm:ml-auto sm:py-0"> -

Collaborator since <%= display_date(@collaborator.inserted_at) %>

+

Collaborator since <%= display_date(@collaborator.accepted_at) %>

<% else %> <.badge variant={:outline} color={:warning} size={:md} class="bg-yellow-300/5 my-5 select-none rounded-xl border-yellow-400 py-1 font-normal text-yellow-400 sm:ml-auto sm:py-0"> @@ -67,10 +67,10 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do size={:lg} icon={:check_circle} color={ - if @action_modal != :delete_collaborator do - :white - else + if @action_modal in [:delete_collaborator, :deny_request] do :danger + else + :success end } full_width @@ -95,12 +95,29 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do |> assign(:changeset, changeset)} end - defp confirm_collaborator_request(socket) do - {:noreply, socket} - end - defp deny_collaborator_request(socket) do - {:noreply, socket} + case Departments.delete_collaborator(socket.assigns.collaborator) do + {:ok, _} -> + send( + self(), + {:change_collaborator, + %{status: :success, message: gettext("Collaborator request denied.")}} + ) + + {:noreply, socket |> assign(:action_modal, nil)} + + _ -> + send( + self(), + {:change_collaborator, + %{ + status: :error, + message: gettext("Could not deny the collaborator request. Please try again later.") + }} + ) + + {:noreply, socket |> assign(:action_modal, nil)} + end end defp delete_collaborator(socket) do @@ -128,10 +145,35 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do end end + defp accept_collaborator_request(socket) do + case Departments.accept_collaborator(socket.assigns.collaborator) do + {:ok, _} -> + send( + self(), + {:change_collaborator, + %{status: :success, message: gettext("Collaborator accepted successfully.")}} + ) + + {:noreply, socket |> assign(:action_modal, nil)} + + _ -> + send( + self(), + {:change_collaborator, + %{ + status: :error, + message: gettext("Could not accept the collaborator. Please try again later.") + }} + ) + + {:noreply, socket |> assign(:action_modal, nil)} + end + end + @impl true def handle_event("confirm", _, socket) do case socket.assigns.action_modal do - :confirm_request -> confirm_collaborator_request(socket) + :confirm_request -> accept_collaborator_request(socket) :deny_request -> deny_collaborator_request(socket) :delete_collaborator -> delete_collaborator(socket) end diff --git a/priv/repo/migrations/20230880102641_create_collaborators.exs b/priv/repo/migrations/20230880102641_create_collaborators.exs index 2b0410d0b..561eaabb6 100644 --- a/priv/repo/migrations/20230880102641_create_collaborators.exs +++ b/priv/repo/migrations/20230880102641_create_collaborators.exs @@ -5,6 +5,7 @@ defmodule Atomic.Repo.Migrations.CreateCollaborators do create table(:collaborators, primary_key: false) do add :id, :binary_id, primary_key: true add :accepted, :boolean, default: false + add :accepted_at, :naive_datetime add :user_id, references(:users, on_delete: :nothing, type: :binary_id), null: false diff --git a/priv/repo/seeds/departments.exs b/priv/repo/seeds/departments.exs index 6c02b4774..cbab532b6 100644 --- a/priv/repo/seeds/departments.exs +++ b/priv/repo/seeds/departments.exs @@ -56,11 +56,19 @@ defmodule Atomic.Repo.Seeds.Departments do def seed_collaborators do for department <- Repo.all(Department) do for user <- Repo.all(Atomic.Accounts.User) do + is_accepted = Enum.random([true, false]) + if Enum.random(0..6) == 1 do %{ department_id: department.id, user_id: user.id, - accepted: Enum.random([true, false]) + accepted: is_accepted, + accepted_at: + if is_accepted do + NaiveDateTime.utc_now() + else + nil + end } |> Departments.create_collaborator() end From 577309141b83c58c4902697ac667b31dba2a4bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Thu, 21 Mar 2024 22:04:49 +0000 Subject: [PATCH 20/39] refactor: use pattern matching for department form --- lib/atomic_web/live/department_live/edit.ex | 26 +++++++++------------ 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/atomic_web/live/department_live/edit.ex b/lib/atomic_web/live/department_live/edit.ex index d80196985..ed6f80516 100644 --- a/lib/atomic_web/live/department_live/edit.ex +++ b/lib/atomic_web/live/department_live/edit.ex @@ -11,20 +11,11 @@ defmodule AtomicWeb.DepartmentLive.Edit do end @impl true - def handle_params(params, uri, %{:assigns => %{:live_action => live_action}} = socket) do - case live_action do - :new -> - handle_params_new(params, uri, socket) - - :edit -> - handle_params_edit(params, uri, socket) - - _ -> - {:noreply, socket} - end - end - - def handle_params_edit(%{"organization_id" => organization_id, "id" => id} = _params, _, socket) do + def handle_params( + %{"organization_id" => organization_id, "id" => id} = _params, + _, + %{:assigns => %{:live_action => :edit}} = socket + ) do department = Departments.get_department!(id) {:noreply, @@ -35,7 +26,12 @@ defmodule AtomicWeb.DepartmentLive.Edit do |> assign(:department, department)} end - def handle_params_new(%{"organization_id" => organization_id} = _params, _, socket) do + @impl true + def handle_params( + %{"organization_id" => organization_id} = _params, + _, + %{:assigns => %{:live_action => :new}} = socket + ) do {:noreply, socket |> assign(:organization_id, organization_id) From aa71f1451e92263c7bd1480367a178c34ffc04af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sun, 24 Mar 2024 15:10:06 +0000 Subject: [PATCH 21/39] refactor: use new forms components --- .../live/department_live/form_component.ex | 34 ++++--------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/lib/atomic_web/live/department_live/form_component.ex b/lib/atomic_web/live/department_live/form_component.ex index b8ef6ce9d..90d761ec4 100644 --- a/lib/atomic_web/live/department_live/form_component.ex +++ b/lib/atomic_web/live/department_live/form_component.ex @@ -2,42 +2,20 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do use AtomicWeb, :live_component alias Atomic.Departments - alias AtomicWeb.Components.ImageUploader + import AtomicWeb.Components.Forms + @impl true def render(assigns) do ~H"""
<.form :let={f} for={@changeset} id="department-form" phx-target={@myself} phx-change="validate" phx-submit="save">

<%= gettext("General") %>

-
-
-
- <%= label(f, :name, class: "text-sm font-semibold") %> -

The name of the department

-
- <%= text_input(f, :name, class: "focus:ring-primary-500 focus:border-primary-500") %> - <%= error_tag(f, :name) %> -
- -
-
- <%= label(f, :description, class: "text-sm font-semibold") %> -

A brief description of the department

-
- <%= text_input(f, :description, class: "focus:ring-primary-500 focus:border-primary-500") %> - <%= error_tag(f, :description) %> -
- -
- <%= checkbox(f, :collaborator_applications, class: "text-primary-500 my-auto focus:ring-primary-500 focus:border-primary-500") %> -
- <%= label(f, :collaborator_applications, class: "text-sm font-semibold") %> -

Allow any user to apply to be a collaborator in this department

-
- <%= error_tag(f, :collaborator_applications) %> -
+
+ <.field type="text" help_text="The name of the department" field={f[:name]} placeholder="Name" required /> + <.field type="textarea" help_text="A brief description of the department" field={f[:description]} placeholder="Description" /> + <.field type="checkbox" help_text="Allow any user to apply to be a collaborator in this department" field={f[:collaborator_applications]} />

<%= gettext("Personalization") %>

From 2c8a19ab738a94c1cc5d2c37d7d91b3767689b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Wed, 27 Mar 2024 00:05:55 +0000 Subject: [PATCH 22/39] feat: empty states --- lib/atomic_web/live/department_live/show.html.heex | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index d6cd126d3..b01593865 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -99,6 +99,12 @@

Recent Activity

+ + <.link class="flex flex-col items-center justify-center mt-4" navigate={Routes.organization_show_path(@socket, :show, @organization)}> + <.avatar class="mb-4 p-1" type={:organization} color={:white} size={:lg} name={@organization.name} src={Uploaders.Logo.url({@organization.logo, @organization}, :original)} /> +

<%= gettext("This department doesn't have any recent activity.") %>

+

<%= gettext("In the meantime, check out %{organization_name}.", organization_name: @organization.name) %>

+
@@ -116,6 +122,7 @@ <% end %>
<.link + :if={length(@all_collaborators) != 0} patch={ Routes.department_show_path( @socket, From 234cf4002cda5144a6ea186a346062d97c767799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Wed, 27 Mar 2024 00:21:24 +0000 Subject: [PATCH 23/39] fix: stupid bug --- lib/atomic_web/components/forms.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/atomic_web/components/forms.ex b/lib/atomic_web/components/forms.ex index ea357107d..2b224f0f9 100644 --- a/lib/atomic_web/components/forms.ex +++ b/lib/atomic_web/components/forms.ex @@ -153,9 +153,7 @@ defmodule AtomicWeb.Components.Forms do <%= @label %> - + <.field_error :for={msg <- @errors}><%= msg %> <.field_help_text help_text={@help_text} /> From 62c0d71df80eb12ffca0d680bf96f71b81595a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Wed, 10 Apr 2024 16:47:55 +0100 Subject: [PATCH 24/39] feat: archive and delete actions --- lib/atomic/departments.ex | 36 +++++++++ lib/atomic/organizations/department.ex | 3 +- .../components/department_card.ex | 11 ++- lib/atomic_web/live/department_live/edit.ex | 80 +++++++++++++++++++ .../live/department_live/edit.html.heex | 47 +++++++++++ lib/atomic_web/live/department_live/index.ex | 49 ++++++++---- lib/atomic_web/live/department_live/show.ex | 36 +++++---- .../20221000000000_create_departments.exs | 1 + .../20230880102641_create_collaborators.exs | 2 +- 9 files changed, 230 insertions(+), 35 deletions(-) diff --git a/lib/atomic/departments.ex b/lib/atomic/departments.ex index a317b58dc..dab79413e 100644 --- a/lib/atomic/departments.ex +++ b/lib/atomic/departments.ex @@ -127,6 +127,42 @@ defmodule Atomic.Departments do Repo.delete(department) end + @doc """ + Archives a department. + + ## Examples + + iex> archive_department(department) + {:ok, %Department{}} + + iex> archive_department(department) + {:error, %Ecto.Changeset{}} + + """ + def archive_department(%Department{} = department) do + department + |> Department.changeset(%{archived: true}) + |> Repo.update() + end + + @doc """ + Unarchives a department. + + ## Examples + + iex> unarchive_department(department) + {:ok, %Department{}} + + iex> unarchive_department(department) + {:error, %Ecto.Changeset{}} + + """ + def unarchive_department(%Department{} = department) do + department + |> Department.changeset(%{archived: false}) + |> Repo.update() + end + @doc """ Returns an `%Ecto.Changeset{}` for tracking department changes. diff --git a/lib/atomic/organizations/department.ex b/lib/atomic/organizations/department.ex index 24d763352..6ead84b7e 100644 --- a/lib/atomic/organizations/department.ex +++ b/lib/atomic/organizations/department.ex @@ -6,13 +6,14 @@ defmodule Atomic.Organizations.Department do alias Atomic.Organizations.Organization @required_fields ~w(name organization_id)a - @optional_fields ~w(description collaborator_applications)a + @optional_fields ~w(description collaborator_applications archived)a schema "departments" do field :name, :string field :description, :string field :banner, Atomic.Uploaders.Banner.Type field :collaborator_applications, :boolean, default: false + field :archived, :boolean, default: false belongs_to :organization, Organization, on_replace: :delete_if_exists diff --git a/lib/atomic_web/live/department_live/components/department_card.ex b/lib/atomic_web/live/department_live/components/department_card.ex index b22c3683b..662151c76 100644 --- a/lib/atomic_web/live/department_live/components/department_card.ex +++ b/lib/atomic_web/live/department_live/components/department_card.ex @@ -2,7 +2,7 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do @moduledoc false use AtomicWeb, :component - import AtomicWeb.Components.Avatar + import AtomicWeb.Components.{Avatar, Badge} import AtomicWeb.DepartmentLive.Components.DepartmentBannerPlaceholder attr :department, :map, required: true, doc: "The department to display." @@ -10,12 +10,17 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do def department_card(assigns) do ~H""" -
+
<.department_banner_placeholder department={@department} class="rounded-t-lg" />
-

<%= @department.name %>

+
+

<%= @department.name %>

+ <.badge :if={@department.archived} variant={:outline} color={:warning} size={:md} class="bg-yellow-300/5 select-none rounded-xl border-yellow-400 py-1 font-normal text-yellow-400 sm:ml-auto sm:py-0"> +

<%= gettext("Archived") %>

+ +
<%= for person <- @collaborators |> Enum.take(4) do %> <.avatar name={person.user.name} size={:xs} color={:light_gray} class="ring-1 ring-white" /> diff --git a/lib/atomic_web/live/department_live/edit.ex b/lib/atomic_web/live/department_live/edit.ex index ed6f80516..84ed25264 100644 --- a/lib/atomic_web/live/department_live/edit.ex +++ b/lib/atomic_web/live/department_live/edit.ex @@ -4,6 +4,7 @@ defmodule AtomicWeb.DepartmentLive.Edit do alias Atomic.Departments alias Atomic.Organizations.Department + alias Phoenix.LiveView.JS @impl true def mount(_params, _session, socket) do @@ -21,6 +22,7 @@ defmodule AtomicWeb.DepartmentLive.Edit do {:noreply, socket |> assign(:organization_id, organization_id) + |> assign(:action, nil) |> assign(:current_page, :departments) |> assign(:page_title, gettext("Edit Department")) |> assign(:department, department)} @@ -35,8 +37,86 @@ defmodule AtomicWeb.DepartmentLive.Edit do {:noreply, socket |> assign(:organization_id, organization_id) + |> assign(:action, nil) |> assign(:current_page, :departments) |> assign(:page_title, gettext("Edit Department")) |> assign(:department, %Department{organization_id: organization_id})} end + + @impl true + def handle_event("set-action", %{"action" => action}, socket) do + {:noreply, assign(socket, :action, action |> String.to_atom())} + end + + @impl true + def handle_event("clear-action", _params, socket) do + {:noreply, assign(socket, :action, nil)} + end + + @impl true + def handle_event("confirm-action", _params, %{assigns: %{action: :archive}} = socket) do + Departments.archive_department(socket.assigns.department) + + {:noreply, + socket + |> push_navigate( + to: Routes.department_index_path(socket, :index, socket.assigns.organization_id) + ) + |> put_flash(:info, gettext("Department archived successfully"))} + end + + @impl true + def handle_event("confirm-action", _params, %{assigns: %{action: :unarchive}} = socket) do + Departments.unarchive_department(socket.assigns.department) + + {:noreply, + socket + |> push_navigate( + to: Routes.department_index_path(socket, :index, socket.assigns.organization_id) + ) + |> put_flash(:info, gettext("Department unarchived successfully"))} + end + + @impl true + def handle_event("confirm-action", _params, %{assigns: %{action: :delete}} = socket) do + Departments.delete_department(socket.assigns.department) + + {:noreply, + socket + |> push_navigate( + to: Routes.department_index_path(socket, :index, socket.assigns.organization_id) + ) + |> put_flash(:info, gettext("Department deleted successfully"))} + end + + defp display_action_goal_confirm_title(action) do + case action do + :archive -> + gettext("Are you sure you want to archive this department?") + + :unarchive -> + gettext("Are you sure you want to unarchive this department?") + + :delete -> + gettext("Are you sure you want do delete this department?") + end + end + + defp display_action_goal_confirm_description(action, department) do + case action do + :archive -> + gettext("You can always change you mind later and make it public again.") + + :unarchive -> + gettext( + "This will make it so that any person can view this department and their collaborators." + ) + + :delete -> + gettext( + "This will permanently delete %{department_name}, this action is not reversible.", + department_name: department.name + ) + end + end end diff --git a/lib/atomic_web/live/department_live/edit.html.heex b/lib/atomic_web/live/department_live/edit.html.heex index 8f83eac8f..53e08f29a 100644 --- a/lib/atomic_web/live/department_live/edit.html.heex +++ b/lib/atomic_web/live/department_live/edit.html.heex @@ -5,6 +5,24 @@ "Edit #{@department.name}" end }> + <:actions> + <%= if @live_action in [:edit] do %> +
+ <%= if !@department.archived do %> + <.button color={:white} icon={:archive_box} phx-click="set-action" phx-value-action={:archive}> + <%= gettext("Archive") %> + + <% else %> + <.button color={:white} icon={:archive_box_x_mark} phx-click="set-action" phx-value-action={:unarchive}> + <%= gettext("Unarchive") %> + + <.button color={:white} icon={:trash} phx-click="set-action" phx-value-action={:delete}> + <%= gettext("Delete") %> + + <% end %> +
+ <% end %> +
<.live_component module={AtomicWeb.DepartmentLive.FormComponent} @@ -23,3 +41,32 @@ />
+<.modal :if={@action} id="action-modal" show on_cancel={JS.push("clear-action")}> +
+

+ <%= display_action_goal_confirm_title(@action) %> +

+

+ <%= display_action_goal_confirm_description(@action, @department) %> +

+
+ <.button phx-click="clear-action" class="mr-2" size={:lg} icon={:x_circle} color={:white} full_width>Cancel + <.button + phx-click="confirm-action" + class="mr-2" + size={:lg} + icon={:check_circle} + color={ + if @action == :delete do + :danger + else + :white + end + } + full_width + > + Confirm + +
+
+ diff --git a/lib/atomic_web/live/department_live/index.ex b/lib/atomic_web/live/department_live/index.ex index 2372c3485..7097f4d5b 100644 --- a/lib/atomic_web/live/department_live/index.ex +++ b/lib/atomic_web/live/department_live/index.ex @@ -17,19 +17,8 @@ defmodule AtomicWeb.DepartmentLive.Index do @impl true def handle_params(%{"organization_id" => organization_id}, _, socket) do organization = Organizations.get_organization!(organization_id) - - departments = - Departments.list_departments_by_organization_id(organization_id, preloads: [:organization]) - |> Enum.map(fn department -> - collaborators = - department.id - |> Departments.list_collaborators_by_department_id( - preloads: [:user], - where: [accepted: true] - ) - - {department, collaborators} - end) + has_permissions = has_permissions?(socket, organization_id) + departments = get_department_and_collaborators(organization_id, has_permissions) {:noreply, socket @@ -38,7 +27,7 @@ defmodule AtomicWeb.DepartmentLive.Index do |> assign(:organization, organization) |> assign(:departments, departments) |> assign(:empty?, Enum.empty?(departments)) - |> assign(:has_permissions?, has_permissions?(socket, organization_id))} + |> assign(:has_permissions?, has_permissions)} end defp has_permissions?(socket, organization_id) do @@ -48,4 +37,36 @@ defmodule AtomicWeb.DepartmentLive.Index do organization_id ) end + + defp get_department_and_collaborators(organization_id, false) do + Departments.list_departments_by_organization_id(organization_id, + preloads: [:organization], + where: [archived: false] + ) + |> Enum.map(fn department -> + collaborators = + department.id + |> Departments.list_collaborators_by_department_id( + preloads: [:user], + where: [accepted: true] + ) + + {department, collaborators} + end) + end + + defp get_department_and_collaborators(organization_id, true) do + Departments.list_departments_by_organization_id(organization_id, preloads: [:organization]) + |> Enum.sort_by(& &1.archived) + |> Enum.map(fn department -> + collaborators = + department.id + |> Departments.list_collaborators_by_department_id( + preloads: [:user], + where: [accepted: true] + ) + + {department, collaborators} + end) + end end diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index 830d483ff..c8df04eb0 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -29,23 +29,27 @@ defmodule AtomicWeb.DepartmentLive.Show do has_permissions = has_permissions?(socket, organization_id) - socket - |> assign(:current_page, :departments) - |> assign(:current_view, current_view(socket, params)) - |> assign(:page_title, department.name) - |> assign(:organization, organization) - |> assign(:department, department) - |> assign(:params, params) - |> assign(:current_collaborator, maybe_put_collaborator(socket, department.id)) - |> assign(list_collaborators(department.id, params, has_permissions)) - |> assign( - :all_collaborators, - Departments.list_collaborators_by_department_id(department.id, - preloads: [:user], - where: [accepted: true] + if department.archived && !has_permissions do + raise Ecto.NoResultsError, queryable: Atomic.Organizations.Department + else + socket + |> assign(:current_page, :departments) + |> assign(:current_view, current_view(socket, params)) + |> assign(:page_title, department.name) + |> assign(:organization, organization) + |> assign(:department, department) + |> assign(:params, params) + |> assign(:current_collaborator, maybe_put_collaborator(socket, department.id)) + |> assign(list_collaborators(department.id, params, has_permissions)) + |> assign( + :all_collaborators, + Departments.list_collaborators_by_department_id(department.id, + preloads: [:user], + where: [accepted: true] + ) ) - ) - |> assign(:has_permissions?, has_permissions) + |> assign(:has_permissions?, has_permissions) + end end defp apply_action( diff --git a/priv/repo/migrations/20221000000000_create_departments.exs b/priv/repo/migrations/20221000000000_create_departments.exs index fe94c7607..641873234 100644 --- a/priv/repo/migrations/20221000000000_create_departments.exs +++ b/priv/repo/migrations/20221000000000_create_departments.exs @@ -8,6 +8,7 @@ defmodule Atomic.Repo.Migrations.CreateDepartments do add :description, :text add :banner, :string add :collaborator_applications, :boolean, default: false, null: false + add :archived, :boolean, default: false, null: false add :organization_id, references(:organizations, on_delete: :delete_all, type: :binary_id), null: false diff --git a/priv/repo/migrations/20230880102641_create_collaborators.exs b/priv/repo/migrations/20230880102641_create_collaborators.exs index 561eaabb6..e6446ccb5 100644 --- a/priv/repo/migrations/20230880102641_create_collaborators.exs +++ b/priv/repo/migrations/20230880102641_create_collaborators.exs @@ -9,7 +9,7 @@ defmodule Atomic.Repo.Migrations.CreateCollaborators do add :user_id, references(:users, on_delete: :nothing, type: :binary_id), null: false - add :department_id, references(:departments, on_delete: :nothing, type: :binary_id), + add :department_id, references(:departments, on_delete: :delete_all, type: :binary_id), null: false timestamps() From f3fc19711cff45447d58acb31286fad2eec21e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Thu, 11 Apr 2024 17:04:38 +0100 Subject: [PATCH 25/39] fix: department banner upload --- lib/atomic/departments.ex | 24 ++++++++++++-- lib/atomic/organizations/department.ex | 5 +++ .../components/department_card.ex | 6 +++- .../live/department_live/form_component.ex | 31 ++++++++++++++++--- .../live/department_live/show.html.heex | 6 +++- 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/lib/atomic/departments.ex b/lib/atomic/departments.ex index dab79413e..a82c9dd5c 100644 --- a/lib/atomic/departments.ex +++ b/lib/atomic/departments.ex @@ -87,10 +87,11 @@ defmodule Atomic.Departments do {:error, %Ecto.Changeset{}} """ - def create_department(attrs \\ %{}) do + def create_department(attrs \\ %{}, after_save \\ &{:ok, &1}) do %Department{} |> Department.changeset(attrs) |> Repo.insert() + |> after_save(after_save) end @doc """ @@ -105,10 +106,11 @@ defmodule Atomic.Departments do {:error, %Ecto.Changeset{}} """ - def update_department(%Department{} = department, attrs) do + def update_department(%Department{} = department, attrs, after_save \\ &{:ok, &1}) do department |> Department.changeset(attrs) |> Repo.update() + |> after_save(after_save) end @doc """ @@ -368,4 +370,22 @@ defmodule Atomic.Departments do |> where([c], c.department_id == ^id) |> Repo.all() end + + @doc """ + Updates a department banner. + + ## Examples + + iex> update_department_banner(department, %{field: new_value}) + {:ok, %Department{}} + + iex> update_department_banner(department, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_department_banner(%Department{} = department, attrs) do + department + |> Department.banner_changeset(attrs) + |> Repo.update() + end end diff --git a/lib/atomic/organizations/department.ex b/lib/atomic/organizations/department.ex index 6ead84b7e..831e7d012 100644 --- a/lib/atomic/organizations/department.ex +++ b/lib/atomic/organizations/department.ex @@ -25,4 +25,9 @@ defmodule Atomic.Organizations.Department do |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) end + + def banner_changeset(department, attrs) do + department + |> cast_attachments(attrs, [:banner]) + end end diff --git a/lib/atomic_web/live/department_live/components/department_card.ex b/lib/atomic_web/live/department_live/components/department_card.ex index 662151c76..8779cd3dd 100644 --- a/lib/atomic_web/live/department_live/components/department_card.ex +++ b/lib/atomic_web/live/department_live/components/department_card.ex @@ -12,7 +12,11 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do ~H"""
- <.department_banner_placeholder department={@department} class="rounded-t-lg" /> + <%= if @department.banner do %> + + <% else %> + <.department_banner_placeholder department={@department} class="rounded-t-lg" /> + <% end %>
diff --git a/lib/atomic_web/live/department_live/form_component.ex b/lib/atomic_web/live/department_live/form_component.ex index 90d761ec4..cf422e5ac 100644 --- a/lib/atomic_web/live/department_live/form_component.ex +++ b/lib/atomic_web/live/department_live/form_component.ex @@ -20,8 +20,8 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do

<%= gettext("Personalization") %>

- <%= label(f, :banner, class: "text-sm font-semibold") %> -

The banner of the department

+ <%= label(f, :banner, class: "department-form_description") %> +

The banner of the department

<.live_component module={ImageUploader} id="uploader" uploads={@uploads} target={@myself} /> @@ -63,7 +63,11 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do end defp save_department(socket, :edit, department_params) do - case Departments.update_department(socket.assigns.department, department_params) do + case Departments.update_department( + socket.assigns.department, + department_params, + &consume_image_data(socket, &1) + ) do {:ok, _department} -> {:noreply, socket @@ -79,7 +83,7 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do department_params = Map.put(department_params, "organization_id", socket.assigns.organization.id) - case Departments.create_department(department_params) do + case Departments.create_department(department_params, &consume_image_data(socket, &1)) do {:ok, _department} -> {:noreply, socket @@ -90,4 +94,23 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do {:noreply, assign(socket, changeset: changeset)} end end + + defp consume_image_data(socket, department) do + consume_uploaded_entries(socket, :image, fn %{path: path}, entry -> + Departments.update_department_banner(department, %{ + "banner" => %Plug.Upload{ + content_type: entry.client_type, + filename: entry.client_name, + path: path + } + }) + end) + |> case do + [{:ok, department}] -> + {:ok, department} + + _errors -> + {:ok, department} + end + end end diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index b01593865..7399c6626 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -1,7 +1,11 @@
- <.department_banner_placeholder department={@department} class="object-cover" /> + <%= if @department.banner do %> + + <% else %> + <.department_banner_placeholder department={@department} class="object-cover" /> + <% end %>
From be1945e31d20edf2968506dbe16be3134674c503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Thu, 11 Apr 2024 23:34:54 +0100 Subject: [PATCH 26/39] fix: department show on mobile --- lib/atomic_web/live/department_live/show.ex | 5 +- .../live/department_live/show.html.heex | 66 ++++++++++++++++++- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index c8df04eb0..7f2a931f2 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -1,10 +1,7 @@ defmodule AtomicWeb.DepartmentLive.Show do use AtomicWeb, :live_view - import AtomicWeb.Components.Avatar - import AtomicWeb.Components.Table - import AtomicWeb.Components.Pagination - import AtomicWeb.Components.Modal + import AtomicWeb.Components.{Avatar, Dropdown, Table, Pagination, Modal} import AtomicWeb.DepartmentLive.Components.DepartmentBannerPlaceholder alias Atomic.Accounts diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index 7399c6626..0a61b59be 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -24,7 +24,67 @@
-
+ +
+ <%= if @current_view == "show" do %> + + <%= if !@current_collaborator do %> + <%= if @department.collaborator_applications do %> + <.button + phx-click={ + "#{if @is_authenticated? do + "collaborate" + else + "must-login" + end}" + } + color={:white} + icon={:user_plus} + icon_variant={:solid} + > + <%= gettext("Collaborate") %> + + <% end %> + <% else %> + <%= if ! @current_collaborator.accepted do %> + <.button color={:white} icon={:user_plus} icon_variant={:solid} aria-label={gettext("You have requested to collaborate with this department. Please wait for the department owner to accept your request.")} disabled> + <%= gettext("Collaborate") %> + + <% end %> + <% end %> + <.dropdown + id="actions" + items={ + [ + %{ + name: gettext("Collaborators"), + link: + Routes.department_show_path( + @socket, + :show, + @organization.id, + @department.id, + tab: "collaborators" + ), + icon: :user_group + }, + %{name: gettext("Kanban"), link: "", icon: :view_columns} + ] ++ + if @has_permissions? || (@current_collaborator && @current_collaborator.accepted) do + [%{name: gettext("Edit"), link: Routes.department_edit_path(@socket, :edit, @organization, @department, @params), icon: :pencil}] + else + [] + end + } + > + <:wrapper> + <.button icon_variant={:solid} color={:white} icon={:ellipsis_horizontal}> + + + <% end %> +
+ +
-
+

Recent Activity

<.link class="flex flex-col items-center justify-center mt-4" navigate={Routes.organization_show_path(@socket, :show, @organization)}> @@ -111,7 +171,7 @@
-
+ -
+
    <%= for person <- @collaborators |> Enum.take(4) do %> - <.avatar name={person.user.name} size={:xs} color={:light_gray} class="ring-1 ring-white" /> +
  • + <.avatar name={person.user.name} size={:xs} color={:light_gray} class="ring-1 ring-white" /> +
  • <% end %> <%= if length(@collaborators) > 4 do %> - <.avatar name={"+#{length(@collaborators) - 4}"} size={:xs} auto_generate_initials={false} color={:light_gray} class="ring-1 ring-white" /> +
  • + <.avatar name={"+#{length(@collaborators) - 4}"} size={:xs} auto_generate_initials={false} color={:light_gray} class="ring-1 ring-white" /> +
  • <% end %> -
+
""" diff --git a/lib/atomic_web/live/department_live/edit.html.heex b/lib/atomic_web/live/department_live/edit.html.heex index df8456b54..383119840 100644 --- a/lib/atomic_web/live/department_live/edit.html.heex +++ b/lib/atomic_web/live/department_live/edit.html.heex @@ -1,8 +1,8 @@ <.page title={ if @live_action in [:new] do - "New Department" + gettext("New Department") else - "Edit #{@department.name}" + gettext("Edit %{department_name}", department_name: @department.name) end }> <:actions> @@ -53,7 +53,7 @@ <%= display_action_goal_confirm_description(@action, @department) %>

- <.button phx-click="clear-action" class="mr-2" size={:lg} icon={:x_circle} color={:white} full_width>Cancel + <.button phx-click="clear-action" class="mr-2" size={:lg} icon={:x_circle} color={:white} full_width><%= gettext("Cancel") %> <.button phx-click="confirm-action" class="mr-2" @@ -68,7 +68,7 @@ } full_width > - Confirm + <%= gettext("Confirm") %>
diff --git a/lib/atomic_web/live/department_live/form_component.ex b/lib/atomic_web/live/department_live/form_component.ex index cf422e5ac..1a0c4da65 100644 --- a/lib/atomic_web/live/department_live/form_component.ex +++ b/lib/atomic_web/live/department_live/form_component.ex @@ -13,15 +13,15 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do <.form :let={f} for={@changeset} id="department-form" phx-target={@myself} phx-change="validate" phx-submit="save">

<%= gettext("General") %>

- <.field type="text" help_text="The name of the department" field={f[:name]} placeholder="Name" required /> - <.field type="textarea" help_text="A brief description of the department" field={f[:description]} placeholder="Description" /> - <.field type="checkbox" help_text="Allow any user to apply to be a collaborator in this department" field={f[:collaborator_applications]} /> + <.field type="text" help_text={gettext("The name of the department")} field={f[:name]} placeholder="Name" required /> + <.field type="textarea" help_text={gettext("A brief description of the department")} field={f[:description]} placeholder="Description" /> + <.field type="checkbox" help_text={gettext("Allow any user to apply to be a collaborator in this department")} field={f[:collaborator_applications]} />

<%= gettext("Personalization") %>

<%= label(f, :banner, class: "department-form_description") %> -

The banner of the department

+

<%= gettext("The banner of the department") %>

<.live_component module={ImageUploader} id="uploader" uploads={@uploads} target={@myself} /> @@ -29,7 +29,7 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do
- <.button size={:md} color={:white} icon={:cube}>Save Changes + <.button size={:md} color={:white} icon={:cube}><%= gettext("Save Changes") %>
diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index f264225f6..2bbeaacaa 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -159,7 +159,7 @@
-

Recent Activity

+

<%= gettext("Recent Activity") %>

<.link class="flex flex-col items-center justify-center mt-4" navigate={Routes.organization_show_path(@socket, :show, @organization)}> <.avatar class="mb-4 p-1" type={:organization} color={:white} size={:lg} name={@organization.name} src={Uploaders.Logo.url({@organization.logo, @organization}, :original)} /> @@ -169,7 +169,7 @@
- <.button size={:md} color={:white} icon={:cube}><%= gettext("Save Changes") %> + <.button size={:md} color={:white} icon={:cube} type="submit"><%= gettext("Save Changes") %>
From dac435b8dc0cac490e13f699d9d8ae536c48ba90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Mon, 15 Apr 2024 16:17:44 +0100 Subject: [PATCH 31/39] refactor: modal parent notify logic --- .../live/collaborator_live/form_component.ex | 68 ++++++------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index 13aca4ab1..164f58381 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -100,78 +100,50 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do defp deny_collaborator_request(socket) do case Departments.delete_collaborator(socket.assigns.collaborator) do {:ok, _} -> - send( - self(), - {:change_collaborator, - %{status: :success, message: gettext("Collaborator request denied.")}} - ) - - {:noreply, socket |> assign(:action_modal, nil)} + notify_result(socket, :success, gettext("Collaborator request denied.")) _ -> - send( - self(), - {:change_collaborator, - %{ - status: :error, - message: gettext("Could not deny the collaborator request. Please try again later.") - }} + notify_result( + socket, + :error, + gettext("Could not deny the collaborator request. Please try again later.") ) - - {:noreply, socket |> assign(:action_modal, nil)} end end defp delete_collaborator(socket) do case Departments.delete_collaborator(socket.assigns.collaborator) do {:ok, _} -> - send( - self(), - {:change_collaborator, - %{status: :success, message: gettext("Collaborator removed successfully.")}} - ) - - {:noreply, socket |> assign(:action_modal, nil)} + notify_result(socket, :success, gettext("Collaborator removed successfully.")) _ -> - send( - self(), - {:change_collaborator, - %{ - status: :error, - message: gettext("Could not delete the collaborator. Please try again later.") - }} + notify_result( + socket, + :error, + gettext("Could not delete the collaborator. Please try again later.") ) - - {:noreply, socket |> assign(:action_modal, nil)} end end defp accept_collaborator_request(socket) do case Departments.accept_collaborator(socket.assigns.collaborator) do {:ok, _} -> - send( - self(), - {:change_collaborator, - %{status: :success, message: gettext("Collaborator accepted successfully.")}} - ) - - {:noreply, socket |> assign(:action_modal, nil)} + notify_result(socket, :success, gettext("Collaborator accepted successfully.")) _ -> - send( - self(), - {:change_collaborator, - %{ - status: :error, - message: gettext("Could not accept the collaborator. Please try again later.") - }} + notify_result( + socket, + :error, + gettext("Could not accept the collaborator. Please try again later.") ) - - {:noreply, socket |> assign(:action_modal, nil)} end end + defp notify_result(socket, status, message) do + send(self(), {:change_collaborator, %{status: status, message: message}}) + {:noreply, assign(socket, :action_modal, nil)} + end + @impl true def handle_event("confirm", _, socket) do case socket.assigns.action_modal do From d9fd1b5eeac7cbce7f813155ac0fbd554c54ea0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Mon, 15 Apr 2024 17:15:09 +0100 Subject: [PATCH 32/39] =?UTF-8?q?BREAKING=20CHANGE:=20=F0=9F=91=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/atomic/organizations/collaborator.ex | 4 ++-- lib/atomic_web/live/department_live/show.html.heex | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/atomic/organizations/collaborator.ex b/lib/atomic/organizations/collaborator.ex index 8da1b88cc..704b23ee1 100644 --- a/lib/atomic/organizations/collaborator.ex +++ b/lib/atomic/organizations/collaborator.ex @@ -7,8 +7,8 @@ defmodule Atomic.Organizations.Collaborator do alias Atomic.Accounts.User alias Atomic.Organizations.Department - @required_fields ~w(user_id department_id)a - @optional_fields ~w(accepted accepted_at)a + @required_fields ~w(user_id department_id accepted)a + @optional_fields ~w(accepted_at)a @derive { Flop.Schema, diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index 2bbeaacaa..270d1b02a 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -11,10 +11,10 @@
-
+
-

+

<%= @department.name %>

<.link navigate={Routes.organization_show_path(@socket, :show, @organization)}> @@ -41,15 +41,13 @@ color={:white} icon={:user_plus} icon_variant={:solid} + title={gettext("Collaborate")} > - <%= gettext("Collaborate") %> <% end %> <% else %> <%= if ! @current_collaborator.accepted do %> - <.button color={:white} icon={:user_plus} icon_variant={:solid} aria-label={gettext("You have requested to collaborate with this department. Please wait for the department owner to accept your request.")} disabled> - <%= gettext("Collaborate") %> - + <.button color={:white} icon={:user_plus} icon_variant={:solid} aria-label={gettext("You have requested to collaborate with this department. Please wait for the department owner to accept your request.")} disabled> <% end %> <% end %> <.dropdown From cb4115c75f178e55a1ddb424a18012f265741673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Mon, 15 Apr 2024 17:49:59 +0100 Subject: [PATCH 33/39] feat: banner resolution recommendation --- lib/atomic_web/live/department_live/form_component.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/atomic_web/live/department_live/form_component.ex b/lib/atomic_web/live/department_live/form_component.ex index 6a8ce6a91..144cbc5ba 100644 --- a/lib/atomic_web/live/department_live/form_component.ex +++ b/lib/atomic_web/live/department_live/form_component.ex @@ -21,7 +21,7 @@ defmodule AtomicWeb.DepartmentLive.FormComponent do
<%= label(f, :banner, class: "department-form_description") %> -

<%= gettext("The banner of the department") %>

+

<%= gettext("The banner of the department (2055×256px for best display)") %>

<.live_component module={ImageUploader} id="uploader" uploads={@uploads} target={@myself} /> From e2d291b347b9a3198c9bb9535ad5adc2bdfec9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Mon, 15 Apr 2024 18:22:35 +0100 Subject: [PATCH 34/39] feat: gradient component --- lib/atomic_web/components/gradient.ex | 54 +++++++++++++++++++ .../department_banner_placeholder.ex | 40 -------------- .../components/department_card.ex | 5 +- lib/atomic_web/live/department_live/index.ex | 3 +- lib/atomic_web/live/department_live/show.ex | 3 +- .../live/department_live/show.html.heex | 2 +- storybook/components/gradient.story.exs | 29 ++++++++++ 7 files changed, 88 insertions(+), 48 deletions(-) create mode 100644 lib/atomic_web/components/gradient.ex delete mode 100644 lib/atomic_web/live/department_live/components/department_banner_placeholder.ex create mode 100644 storybook/components/gradient.story.exs diff --git a/lib/atomic_web/components/gradient.ex b/lib/atomic_web/components/gradient.ex new file mode 100644 index 000000000..fdec377fc --- /dev/null +++ b/lib/atomic_web/components/gradient.ex @@ -0,0 +1,54 @@ +defmodule AtomicWeb.Components.Gradient do + @moduledoc """ + Generates a random gradient background or a predictable gradient background based on a seed that can be of any data type. + """ + use Phoenix.Component + + # List of gradients + @colors [ + {"#000046", "#1CB5E0"}, + {"#007991", "#78ffd6"}, + {"#30E8BF", "#FF8235"}, + {"#C33764", "#1D2671"}, + {"#34e89e", "#0f3443"}, + {"#44A08D", "#093637"}, + {"#DCE35B", "#45B649"}, + {"#c0c0aa", "#1cefff"}, + {"#ee0979", "#ff6a00"} + ] + + attr :class, :string, default: "", doc: "Additional classes to apply to the component." + attr :seed, :any, required: false, doc: "For predictable gradients." + + def gradient(assigns) do + {gradient_color_a, gradient_color_b} = + if Map.has_key?(assigns, :id) do + generate_color(assigns.id) + else + generate_color() + end + + assigns + |> assign(:gradient_color_a, gradient_color_a) + |> assign(:gradient_color_b, gradient_color_b) + |> render_gradient() + end + + defp render_gradient(assigns) do + ~H""" +
+ """ + end + + defp generate_color(seed) when is_binary(seed) do + # Convert the argument into an integer + index = :erlang.phash2(seed, length(@colors)) + + # Return the chosen color + Enum.at(@colors, index) + end + + defp generate_color() do + Enum.random(@colors) + end +end diff --git a/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex b/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex deleted file mode 100644 index f80294e1f..000000000 --- a/lib/atomic_web/live/department_live/components/department_banner_placeholder.ex +++ /dev/null @@ -1,40 +0,0 @@ -defmodule AtomicWeb.DepartmentLive.Components.DepartmentBannerPlaceholder do - @moduledoc false - use AtomicWeb, :component - - def department_banner_placeholder(assigns) do - {gradient_color_a, gradient_color_b} = generate_color(assigns.department.id) - - assigns - |> assign(:gradient_color_a, gradient_color_a) - |> assign(:gradient_color_b, gradient_color_b) - |> render_gradient() - end - - defp render_gradient(assigns) do - ~H""" -
- """ - end - - def generate_color(uuid) when is_binary(uuid) do - # List of gradients - colors = [ - {"#000046", "#1CB5E0"}, - {"#007991", "#78ffd6"}, - {"#30E8BF", "#FF8235"}, - {"#C33764", "#1D2671"}, - {"#34e89e", "#0f3443"}, - {"#44A08D", "#093637"}, - {"#DCE35B", "#45B649"}, - {"#c0c0aa", "#1cefff"}, - {"#ee0979", "#ff6a00"} - ] - - # Convert the UUID to an integer - index = :erlang.phash2(uuid, length(colors)) - - # Return the chosen color - Enum.at(colors, index) - end -end diff --git a/lib/atomic_web/live/department_live/components/department_card.ex b/lib/atomic_web/live/department_live/components/department_card.ex index ed2934976..f67f711f7 100644 --- a/lib/atomic_web/live/department_live/components/department_card.ex +++ b/lib/atomic_web/live/department_live/components/department_card.ex @@ -2,8 +2,7 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do @moduledoc false use AtomicWeb, :component - import AtomicWeb.Components.{Avatar, Badge} - import AtomicWeb.DepartmentLive.Components.DepartmentBannerPlaceholder + import AtomicWeb.Components.{Avatar, Badge, Gradient} attr :department, :map, required: true, doc: "The department to display." attr :collaborators, :list, required: true, doc: "The list of collaborators in the department." @@ -15,7 +14,7 @@ defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do <%= if @department.banner do %> <% else %> - <.department_banner_placeholder department={@department} class="rounded-t-lg" /> + <.gradient seed={@department.id} class="rounded-t-lg" /> <% end %>
diff --git a/lib/atomic_web/live/department_live/index.ex b/lib/atomic_web/live/department_live/index.ex index 7097f4d5b..a5396b57a 100644 --- a/lib/atomic_web/live/department_live/index.ex +++ b/lib/atomic_web/live/department_live/index.ex @@ -1,8 +1,7 @@ defmodule AtomicWeb.DepartmentLive.Index do use AtomicWeb, :live_view - import AtomicWeb.Components.Empty - import AtomicWeb.Components.Button + import AtomicWeb.Components.{Button, Empty} import AtomicWeb.DepartmentLive.Components.DepartmentCard alias Atomic.Accounts diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index 7f2a931f2..8ace79463 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -1,8 +1,7 @@ defmodule AtomicWeb.DepartmentLive.Show do use AtomicWeb, :live_view - import AtomicWeb.Components.{Avatar, Dropdown, Table, Pagination, Modal} - import AtomicWeb.DepartmentLive.Components.DepartmentBannerPlaceholder + import AtomicWeb.Components.{Avatar, Dropdown, Gradient, Table, Pagination, Modal} alias Atomic.Accounts alias Atomic.Departments diff --git a/lib/atomic_web/live/department_live/show.html.heex b/lib/atomic_web/live/department_live/show.html.heex index 270d1b02a..1802faa60 100644 --- a/lib/atomic_web/live/department_live/show.html.heex +++ b/lib/atomic_web/live/department_live/show.html.heex @@ -4,7 +4,7 @@ <%= if @department.banner do %> <% else %> - <.department_banner_placeholder department={@department} class="object-cover" /> + <.gradient seed={@department.id} class="object-cover" /> <% end %>
diff --git a/storybook/components/gradient.story.exs b/storybook/components/gradient.story.exs new file mode 100644 index 000000000..df2e86958 --- /dev/null +++ b/storybook/components/gradient.story.exs @@ -0,0 +1,29 @@ +defmodule AtomicWeb.Storybook.Components.Gradient do + use PhoenixStorybook.Story, :component + + alias AtomicWeb.Components.Gradient + + def function, do: &Gradient.gradient/1 + + def template do + """ +
+ <.lsb-variation/> +
+ """ + end + + def variations do + [ + %Variation{ + id: :random + }, + %Variation{ + id: :predictable, + attributes: %{ + seed: "CAOS" + } + } + ] + end +end From 5e27395bba5cef95c1c3128a5feda29ec1654dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Mon, 15 Apr 2024 18:24:58 +0100 Subject: [PATCH 35/39] ci: lint --- lib/atomic_web/components/gradient.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/atomic_web/components/gradient.ex b/lib/atomic_web/components/gradient.ex index fdec377fc..3dc2bbeb5 100644 --- a/lib/atomic_web/components/gradient.ex +++ b/lib/atomic_web/components/gradient.ex @@ -48,7 +48,7 @@ defmodule AtomicWeb.Components.Gradient do Enum.at(@colors, index) end - defp generate_color() do + defp generate_color do Enum.random(@colors) end end From 226f0792e33921f132591ce7e8af9ea6b721bcba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Mon, 15 Apr 2024 18:33:39 +0100 Subject: [PATCH 36/39] fix: stupid --- lib/atomic_web/components/gradient.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/atomic_web/components/gradient.ex b/lib/atomic_web/components/gradient.ex index 3dc2bbeb5..bb0cb1e42 100644 --- a/lib/atomic_web/components/gradient.ex +++ b/lib/atomic_web/components/gradient.ex @@ -22,8 +22,8 @@ defmodule AtomicWeb.Components.Gradient do def gradient(assigns) do {gradient_color_a, gradient_color_b} = - if Map.has_key?(assigns, :id) do - generate_color(assigns.id) + if Map.has_key?(assigns, :seed) do + generate_color(assigns.seed) else generate_color() end From 66dd16ccf50f9ff6fe286c72afd4b85c6d7295f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Wed, 17 Apr 2024 22:04:44 +0100 Subject: [PATCH 37/39] feat: add collaborator accepted email --- lib/atomic/departments.ex | 36 +++++++++++++++++++ lib/atomic_web/emails/department_emails.ex | 36 +++++++++++++++++++ .../live/collaborator_live/form_component.ex | 2 +- .../email/collaborator_accepted.html.eex | 18 ++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 lib/atomic_web/emails/department_emails.ex create mode 100644 lib/atomic_web/templates/email/collaborator_accepted.html.eex diff --git a/lib/atomic/departments.ex b/lib/atomic/departments.ex index a82c9dd5c..95dc75eec 100644 --- a/lib/atomic/departments.ex +++ b/lib/atomic/departments.ex @@ -5,6 +5,7 @@ defmodule Atomic.Departments do use Atomic.Context alias Atomic.Organizations.{Collaborator, Department} + alias AtomicWeb.DepartmentEmails @doc """ Returns the list of departments. @@ -388,4 +389,39 @@ defmodule Atomic.Departments do |> Department.banner_changeset(attrs) |> Repo.update() end + + @doc """ + Accept collaborator request and send email. + + ## Examples + + iex> accept_collaborator_request(collaborator) + {:ok, %Collaborator{}} + + iex> accept_collaborator_request(collaborator) + {:error, %Ecto.Changeset{}} + + """ + def accept_collaborator_request(%Collaborator{} = collaborator) do + collaborator + |> Repo.preload(department: [:organization]) + |> accept_collaborator() + |> case do + {:ok, collaborator} -> + DepartmentEmails.send_collaborator_accepted_email( + collaborator, + collaborator.department, + AtomicWeb.Router.Helpers.department_show_path( + AtomicWeb.Endpoint, + :show, + collaborator.department.organization, + collaborator.department + ), + to: collaborator.user.email + ) + + error -> + error + end + end end diff --git a/lib/atomic_web/emails/department_emails.ex b/lib/atomic_web/emails/department_emails.ex new file mode 100644 index 000000000..fc4c92069 --- /dev/null +++ b/lib/atomic_web/emails/department_emails.ex @@ -0,0 +1,36 @@ +defmodule AtomicWeb.DepartmentEmails do + @moduledoc """ + A module to build department related emails. + """ + use Phoenix.Swoosh, view: AtomicWeb.EmailView + + alias Atomic.Mailer + + @doc """ + Sends an email to the collaborator when their application to join the department is accepted. + + ## Examples + + iex> send_collaborator_request_email(collaborator, department, department_show_url, to: email) + {:ok, email} + + iex> send_collaborator_request_email(collaborator, department, department_show_url, to: email) + {:error, reason} + """ + def send_collaborator_accepted_email(collaborator, department, department_show_url, to: email) do + base_email(to: email) + |> subject("[Atomic] You are now a collaborator of #{department.name}") + |> assign(:collaborator, collaborator) + |> assign(:department, department) + |> assign(:department_url, department_show_url) + |> render_body("collaborator_accepted.html") + |> Mailer.deliver() + end + + defp base_email(to: email) do + new() + |> from({"Atomic", "noreply@atomic.cesium.pt"}) + |> to(email) + |> reply_to("caos@cesium.di.uminho.pt") + end +end diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex index 164f58381..09082e9a9 100644 --- a/lib/atomic_web/live/collaborator_live/form_component.ex +++ b/lib/atomic_web/live/collaborator_live/form_component.ex @@ -126,7 +126,7 @@ defmodule AtomicWeb.CollaboratorLive.FormComponent do end defp accept_collaborator_request(socket) do - case Departments.accept_collaborator(socket.assigns.collaborator) do + case Departments.accept_collaborator_request(socket.assigns.collaborator) do {:ok, _} -> notify_result(socket, :success, gettext("Collaborator accepted successfully.")) diff --git a/lib/atomic_web/templates/email/collaborator_accepted.html.eex b/lib/atomic_web/templates/email/collaborator_accepted.html.eex new file mode 100644 index 000000000..e56cfa7cb --- /dev/null +++ b/lib/atomic_web/templates/email/collaborator_accepted.html.eex @@ -0,0 +1,18 @@ + + + + + + + +
+

Hello <%= @collaborator.user.name %>,

+ +

Your application to be a collaborator of <%= @department.name %> has been accepted!

+ +

Now you can access the department's projects and tasks >here.

+ +

This email has been automatically generated. Please, do not reply.

+
+ + From 4184f1baed79c3e9eded02199e511e839f83d997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Thu, 18 Apr 2024 01:23:01 +0100 Subject: [PATCH 38/39] feat: notify department admins on new collaborator request --- lib/atomic/accounts/user.ex | 7 ++- lib/atomic/departments.ex | 58 ++++++++++++++++++- lib/atomic_web/emails/department_emails.ex | 27 +++++++++ lib/atomic_web/live/department_live/edit.ex | 2 +- lib/atomic_web/live/department_live/show.ex | 2 +- .../email/collaborator_request.html.eex | 18 ++++++ 6 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 lib/atomic_web/templates/email/collaborator_request.html.eex diff --git a/lib/atomic/accounts/user.ex b/lib/atomic/accounts/user.ex index ca67ec77a..248dd3775 100644 --- a/lib/atomic/accounts/user.ex +++ b/lib/atomic/accounts/user.ex @@ -6,7 +6,7 @@ defmodule Atomic.Accounts.User do alias Atomic.Accounts.Course alias Atomic.Activities.ActivityEnrollment - alias Atomic.Organizations.{Membership, Organization} + alias Atomic.Organizations.{Collaborator, Membership, Organization} alias Atomic.Uploaders.ProfilePicture @required_fields ~w(email password)a @@ -23,14 +23,17 @@ defmodule Atomic.Accounts.User do field :password, :string, virtual: true, redact: true field :hashed_password, :string, redact: true field :confirmed_at, :naive_datetime - field :phone_number, :string field :profile_picture, ProfilePicture.Type field :role, Ecto.Enum, values: @roles, default: :student + belongs_to :course, Course belongs_to :current_organization, Organization has_many :activity_enrollments, ActivityEnrollment + has_many :memberships, Membership + has_many :collaborators, Collaborator + many_to_many :organizations, Organization, join_through: Membership timestamps() diff --git a/lib/atomic/departments.ex b/lib/atomic/departments.ex index 95dc75eec..2c621b9cd 100644 --- a/lib/atomic/departments.ex +++ b/lib/atomic/departments.ex @@ -4,8 +4,10 @@ defmodule Atomic.Departments do """ use Atomic.Context + alias Atomic.Accounts.User alias Atomic.Organizations.{Collaborator, Department} alias AtomicWeb.DepartmentEmails + alias AtomicWeb.Router.Helpers @doc """ Returns the list of departments. @@ -390,6 +392,60 @@ defmodule Atomic.Departments do |> Repo.update() end + @doc """ + Get all admins of an organization that are collaborators of a department. + + ## Examples + + iex> get_admin_collaborators(department) + [%User{}, ...] + + """ + def get_admin_collaborators(%Department{} = department) do + User + |> join(:inner, [u], c in assoc(u, :collaborators)) + |> where([u, c], c.department_id == ^department.id and c.accepted == true) + |> join(:inner, [u, c], m in assoc(u, :memberships)) + |> where([u, c, m], m.organization_id == ^department.organization_id and m.role == :admin) + |> Repo.all() + end + + @doc """ + Request collaborator access and send email. + + ## Examples + + iex> request_collaborator_access(user, department) + {:ok, %Collaborator{}} + + iex> request_collaborator_access(user, department) + {:error, %Ecto.Changeset{}} + + """ + def request_collaborator_access(%User{} = user, %Department{} = department) do + case create_collaborator(%{department_id: department.id, user_id: user.id}) do + {:ok, %Collaborator{} = collaborator} -> + DepartmentEmails.send_collaborator_request_email( + collaborator |> Repo.preload(:user), + department, + Helpers.department_show_path( + AtomicWeb.Endpoint, + :edit_collaborator, + department.organization_id, + department, + collaborator, + tab: "collaborators" + ), + to: get_admin_collaborators(department) |> Enum.map(& &1.email) + ) + + {:ok, collaborator} + + error -> + error + end + end + @doc """ Accept collaborator request and send email. @@ -411,7 +467,7 @@ defmodule Atomic.Departments do DepartmentEmails.send_collaborator_accepted_email( collaborator, collaborator.department, - AtomicWeb.Router.Helpers.department_show_path( + Helpers.department_show_path( AtomicWeb.Endpoint, :show, collaborator.department.organization, diff --git a/lib/atomic_web/emails/department_emails.ex b/lib/atomic_web/emails/department_emails.ex index fc4c92069..7bbf4eb10 100644 --- a/lib/atomic_web/emails/department_emails.ex +++ b/lib/atomic_web/emails/department_emails.ex @@ -27,6 +27,33 @@ defmodule AtomicWeb.DepartmentEmails do |> Mailer.deliver() end + @doc """ + Sends an email to the department admins when a new collaborator requests to join the department. + + ## Examples + + iex> send_collaborator_request_email(collaborator, department, collaborator_review_url, + ...> to: emails + ...> ) + {:ok, email} + + iex> send_collaborator_request_email(collaborator, department, collaborator_review_url, + ...> to: emails + ...> ) + {:error, reason} + """ + def send_collaborator_request_email(collaborator, department, collaborator_review_url, + to: emails + ) do + base_email(to: emails) + |> subject("[Atomic] New collaborator request for #{department.name}") + |> assign(:collaborator, collaborator) + |> assign(:department, department) + |> assign(:collaborator_review_url, collaborator_review_url) + |> render_body("collaborator_request.html") + |> Mailer.deliver() + end + defp base_email(to: email) do new() |> from({"Atomic", "noreply@atomic.cesium.pt"}) diff --git a/lib/atomic_web/live/department_live/edit.ex b/lib/atomic_web/live/department_live/edit.ex index 84ed25264..d3dc6634c 100644 --- a/lib/atomic_web/live/department_live/edit.ex +++ b/lib/atomic_web/live/department_live/edit.ex @@ -39,7 +39,7 @@ defmodule AtomicWeb.DepartmentLive.Edit do |> assign(:organization_id, organization_id) |> assign(:action, nil) |> assign(:current_page, :departments) - |> assign(:page_title, gettext("Edit Department")) + |> assign(:page_title, gettext("New Department")) |> assign(:department, %Department{organization_id: organization_id})} end diff --git a/lib/atomic_web/live/department_live/show.ex b/lib/atomic_web/live/department_live/show.ex index 8ace79463..85f88883c 100644 --- a/lib/atomic_web/live/department_live/show.ex +++ b/lib/atomic_web/live/department_live/show.ex @@ -128,7 +128,7 @@ defmodule AtomicWeb.DepartmentLive.Show do department = socket.assigns.department user = socket.assigns.current_user - case Departments.create_collaborator(%{department_id: department.id, user_id: user.id}) do + case Departments.request_collaborator_access(user, department) do {:ok, %Collaborator{} = collaborator} -> {:noreply, socket diff --git a/lib/atomic_web/templates/email/collaborator_request.html.eex b/lib/atomic_web/templates/email/collaborator_request.html.eex new file mode 100644 index 000000000..eece55acc --- /dev/null +++ b/lib/atomic_web/templates/email/collaborator_request.html.eex @@ -0,0 +1,18 @@ + + + + + + + +
+

Hello,

+ +

<%= @collaborator.user.name %> has requested to be a collaborator of <%= @department.name %>!

+ +

This request can be reviewed >here.

+ +

This email has been automatically generated. Please, do not reply.

+
+ + From 41631d0f9e5b1510c5f6dbe54558babac9a8d903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Thu, 18 Apr 2024 01:44:08 +0100 Subject: [PATCH 39/39] fix: mario --- lib/atomic/departments.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/atomic/departments.ex b/lib/atomic/departments.ex index 2c621b9cd..1abf710d0 100644 --- a/lib/atomic/departments.ex +++ b/lib/atomic/departments.ex @@ -406,7 +406,10 @@ defmodule Atomic.Departments do |> join(:inner, [u], c in assoc(u, :collaborators)) |> where([u, c], c.department_id == ^department.id and c.accepted == true) |> join(:inner, [u, c], m in assoc(u, :memberships)) - |> where([u, c, m], m.organization_id == ^department.organization_id and m.role == :admin) + |> where( + [u, c, m], + m.organization_id == ^department.organization_id and m.role in [:admin, :owner] + ) |> Repo.all() end