From 2e7105938c5a1373e30002a10db86cac663319e8 Mon Sep 17 00:00:00 2001 From: endoooo Date: Sun, 5 Nov 2023 01:54:44 -0300 Subject: [PATCH] refactor: refactored `LantternWeb.AssessmentPointsFilterViewOverlayComponent` adjusted filter view overlay implementation, based on `mix phx.gen.live` generators pattern. - added "/dashboard/filter_view/new" and "/dashboard/filter_view/:id/edit" routes to handle overlay display - moved `<.slide_over>` component to dashboard live view, reducing the scope of the live component to form only --- lib/lanttern/explorer.ex | 5 +- .../filter_view_form_component.ex | 136 +++++++++++++ lib/lanttern_web/live/dashboard_live/index.ex | 105 ++++------ .../live/dashboard_live/index.html.heex | 43 ++-- ...nt_points_filter_view_overlay_component.ex | 186 ------------------ lib/lanttern_web/router.ex | 10 +- test/lanttern/explorer_test.exs | 12 +- .../lanttern_web/live/dashboard_live_test.exs | 4 +- 8 files changed, 224 insertions(+), 277 deletions(-) create mode 100644 lib/lanttern_web/live/dashboard_live/filter_view_form_component.ex delete mode 100644 lib/lanttern_web/live_components/assessment_points_filter_view_overlay_component.ex diff --git a/lib/lanttern/explorer.ex b/lib/lanttern/explorer.ex index 9ec76ba5..0ab7b8a8 100644 --- a/lib/lanttern/explorer.ex +++ b/lib/lanttern/explorer.ex @@ -66,7 +66,7 @@ defmodule Lanttern.Explorer do end @doc """ - Creates a assessment_points_filter_view. + Creates an assessment_points_filter_view. ## Examples @@ -78,7 +78,8 @@ defmodule Lanttern.Explorer do """ def create_assessment_points_filter_view(attrs \\ %{}) do - %AssessmentPointsFilterView{} + # add classes and subjects to force return with preloaded classes/subjects + %AssessmentPointsFilterView{classes: [], subjects: []} |> AssessmentPointsFilterView.changeset(attrs) |> Repo.insert() end diff --git a/lib/lanttern_web/live/dashboard_live/filter_view_form_component.ex b/lib/lanttern_web/live/dashboard_live/filter_view_form_component.ex new file mode 100644 index 00000000..219a339f --- /dev/null +++ b/lib/lanttern_web/live/dashboard_live/filter_view_form_component.ex @@ -0,0 +1,136 @@ +defmodule LantternWeb.DashboardLive.FilterViewFormComponent do + @moduledoc """ + Assessment points filter view form component. + + This form is used inside a `<.slide_over>` component, + where the "submit" button is rendered. + """ + + use LantternWeb, :live_component + alias Lanttern.Explorer + alias Lanttern.Explorer.AssessmentPointsFilterView + + def render(assigns) do + ~H""" +
+ <.form + id="assessment-points-filter-view-form" + for={@form} + phx-change="validate" + phx-submit="save" + phx-target={@myself} + > + <.error_block :if={@form.source.action in [:insert, :update]} class="mb-6"> + Oops, something went wrong! Please check the errors below. + + <.input field={@form[:id]} type="hidden" /> + <.input field={@form[:profile_id]} type="hidden" /> + <.input field={@form[:name]} label="Filter view name" phx-debounce="1500" class="mb-6" /> +
+
+ Classes +
+ <.check_field + :for={opt <- @classes} + id={"class-#{opt.id}"} + field={@form[:classes_ids]} + opt={opt} + /> +
+
+
+ Subjects +
+ <.check_field + :for={opt <- @subjects} + id={"subject-#{opt.id}"} + field={@form[:subjects_ids]} + opt={opt} + /> +
+
+
+ +
+ """ + end + + # lifecycle + + def mount(socket) do + classes = Lanttern.Schools.list_classes() + subjects = Lanttern.Taxonomy.list_subjects() + + socket = + socket + |> assign(:classes, classes) + |> assign(:subjects, subjects) + |> assign(:action, "create") + + {:ok, socket} + end + + def update(%{filter_view: filter_view} = assigns, socket) do + changeset = + filter_view + |> Map.put(:classes_ids, Enum.map(filter_view.classes, &"#{&1.id}")) + |> Map.put(:subjects_ids, Enum.map(filter_view.subjects, &"#{&1.id}")) + |> Explorer.change_assessment_points_filter_view() + + socket = + socket + |> assign(assigns) + |> assign(:form, to_form(changeset)) + |> assign(:filter_view, filter_view) + + {:ok, socket} + end + + def update(assigns, socket), + do: {:ok, assign(socket, assigns)} + + # event handlers + + def handle_event("validate", %{"assessment_points_filter_view" => params}, socket) do + form = + %AssessmentPointsFilterView{} + |> Explorer.change_assessment_points_filter_view(params) + |> Map.put(:action, :validate) + |> to_form() + + {:noreply, assign(socket, form: form)} + end + + def handle_event("save", %{"assessment_points_filter_view" => params}, socket), + do: save_filter_view(socket, socket.assigns.action, params) + + defp save_filter_view(socket, :new_filter_view, params) do + case Explorer.create_assessment_points_filter_view(params) do + {:ok, assessment_points_filter_view} -> + notify_parent({:created, assessment_points_filter_view}) + {:noreply, socket} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp save_filter_view(socket, :edit_filter_view, params) do + # force classes_ids and subjects_ids inclusion to remove filters if needed + params = + params + |> Map.put_new("classes_ids", []) + |> Map.put_new("subjects_ids", []) + + case Explorer.update_assessment_points_filter_view(socket.assigns.filter_view, params) do + {:ok, assessment_points_filter_view} -> + notify_parent({:updated, assessment_points_filter_view}) + {:noreply, socket} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) +end diff --git a/lib/lanttern_web/live/dashboard_live/index.ex b/lib/lanttern_web/live/dashboard_live/index.ex index 2217d08a..c706e167 100644 --- a/lib/lanttern_web/live/dashboard_live/index.ex +++ b/lib/lanttern_web/live/dashboard_live/index.ex @@ -1,15 +1,11 @@ defmodule LantternWeb.DashboardLive.Index do @moduledoc """ - ### PubSub subscription topics - - - "dashboard:profile_id" on `handle_params` - - Expected broadcasted messages in `handle_info/2` documentation. + Dashboard live view """ use LantternWeb, :live_view - alias Phoenix.PubSub alias Lanttern.Explorer + alias Lanttern.Explorer.AssessmentPointsFilterView # view components @@ -42,8 +38,7 @@ defmodule LantternWeb.DashboardLive.Index do <:menu_items> <.menu_button_item id={"edit-filter-view-#{@id}"} - phx-click="edit_filter_view" - phx-value-id={@filter_view.id} + phx-click={JS.patch(~p"/dashboard/filter_view/#{@filter_view.id}/edit")} > Edit @@ -95,19 +90,17 @@ defmodule LantternWeb.DashboardLive.Index do def mount(_params, _session, socket) do profile_id = socket.assigns.current_user.current_profile_id - if connected?(socket) do - PubSub.subscribe( - Lanttern.PubSub, - "dashboard:#{profile_id}" + filter_views = + Explorer.list_assessment_points_filter_views( + profile_id: profile_id, + preloads: [:subjects, :classes] ) - end - filter_views = list_filter_views(profile_id) filter_view_count = length(filter_views) socket = socket - |> stream(:assessment_points_filter_views, filter_views) + |> stream(:filter_views, filter_views) |> assign(:filter_view_count, filter_view_count) |> assign(:current_filter_view_id, nil) |> assign(:show_filter_view_overlay, false) @@ -115,21 +108,33 @@ defmodule LantternWeb.DashboardLive.Index do {:ok, socket} end - # event handlers - - def handle_event("add_filter_view", _params, socket) do - {:noreply, assign(socket, :show_filter_view_overlay, true)} + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} end - def handle_event("edit_filter_view", %{"id" => filter_view_id} = _params, socket) do - socket = - socket - |> assign(:current_filter_view_id, String.to_integer(filter_view_id)) - |> assign(:show_filter_view_overlay, true) + defp apply_action(socket, :edit_filter_view, %{"id" => id}) do + socket + |> assign(:filter_view_overlay_title, "Edit assessment points filter view") + |> assign( + :filter_view, + Explorer.get_assessment_points_filter_view!(id, preloads: [:subjects, :classes]) + ) + end - {:noreply, socket} + defp apply_action(socket, :new_filter_view, _params) do + socket + |> assign(:filter_view_overlay_title, "Create assessment points filter view") + |> assign(:filter_view, %AssessmentPointsFilterView{ + profile_id: socket.assigns.current_user.current_profile_id, + classes: [], + subjects: [] + }) end + defp apply_action(socket, :index, _params), do: socket + + # event handlers + def handle_event("delete_filter_view", %{"id" => filter_view_id} = _params, socket) do assessment_points_filter_view = Explorer.get_assessment_points_filter_view!(filter_view_id) @@ -137,7 +142,7 @@ defmodule LantternWeb.DashboardLive.Index do {:ok, _} -> socket = socket - |> stream_delete(:assessment_points_filter_views, assessment_points_filter_view) + |> stream_delete(:filter_views, assessment_points_filter_view) |> update(:filter_view_count, fn count -> count - 1 end) {:noreply, socket} @@ -158,66 +163,30 @@ defmodule LantternWeb.DashboardLive.Index do # info handlers - @doc """ - Handles sent or broadcasted messages from children Live Components. - - ## Clauses - - #### Assessment points filter view create success - - Broadcasted to `"dashboard:profile_id"` from `LantternWeb.AssessmentPointsFilterViewOverlayComponent`. - - handle_info({:assessment_points_filter_view_created, assessment_points_filter_view}, socket) - - #### Assessment points filter view update success - - Broadcasted to `"dashboard:profile_id"` from `LantternWeb.AssessmentPointsFilterViewOverlayComponent`. - - handle_info({:assessment_points_filter_view_updated, assessment_points_filter_view}, socket) - - """ - def handle_info( - {:assessment_points_filter_view_created, _assessment_points_filter_view}, + {LantternWeb.DashboardLive.FilterViewFormComponent, {:created, filter_view}}, socket ) do socket = socket - |> stream( - :assessment_points_filter_views, - list_filter_views(socket.assigns.current_user.current_profile_id), - reset: true - ) + |> stream_insert(:filter_views, filter_view) |> put_flash(:info, "Assessment points filter view created.") |> update(:filter_view_count, fn count -> count + 1 end) - |> assign(:show_filter_view_overlay, false) + |> push_patch(to: ~p"/dashboard") {:noreply, socket} end def handle_info( - {:assessment_points_filter_view_updated, assessment_points_filter_view}, + {LantternWeb.DashboardLive.FilterViewFormComponent, {:updated, filter_view}}, socket ) do socket = socket - |> stream_insert( - :assessment_points_filter_views, - assessment_points_filter_view - ) + |> stream_insert(:filter_views, filter_view) |> put_flash(:info, "Assessment points filter view updated.") - |> assign(:current_filter_view_id, false) - |> assign(:show_filter_view_overlay, false) + |> push_patch(to: ~p"/dashboard") {:noreply, socket} end - - # helpers - - defp list_filter_views(profile_id) do - Explorer.list_assessment_points_filter_views( - profile_id: profile_id, - preloads: [:subjects, :classes] - ) - end end diff --git a/lib/lanttern_web/live/dashboard_live/index.html.heex b/lib/lanttern_web/live/dashboard_live/index.html.heex index c6c51d38..23790d7e 100644 --- a/lib/lanttern_web/live/dashboard_live/index.html.heex +++ b/lib/lanttern_web/live/dashboard_live/index.html.heex @@ -24,13 +24,13 @@ filter views - + <%= if @filter_view_count > 0 do %>
<.filter_view_card - :for={{dom_id, filter_view} <- @streams.assessment_points_filter_views} + :for={{dom_id, filter_view} <- @streams.filter_views} id={dom_id} filter_view={filter_view} /> @@ -48,12 +48,29 @@ <.empty_state>You don't have any filter view yet <% end %>
-<.live_component - module={LantternWeb.AssessmentPointsFilterViewOverlayComponent} - id="create-assessment-points-filter-view-overlay" - current_user={@current_user} - show={@show_filter_view_overlay} - topic={"dashboard:#{@current_user.current_profile_id}"} - on_cancel={JS.push("cancel_filter_view")} - filter_view_id={@current_filter_view_id} -/> +<.slide_over + :if={@live_action in [:new_filter_view, :edit_filter_view]} + id="assessment-points-filter-view-overlay" + show={true} + on_cancel={JS.patch(~p"/dashboard")} +> + <:title><%= @filter_view_overlay_title %> + <.live_component + module={LantternWeb.DashboardLive.FilterViewFormComponent} + id={@filter_view.id || :new} + action={@live_action} + filter_view={@filter_view} + /> + <:actions> + <.button + type="button" + theme="ghost" + phx-click={JS.exec("data-cancel", to: "#assessment-points-filter-view-overlay")} + > + Cancel + + <.button type="submit" form="assessment-points-filter-view-form" phx-disable-with="Saving..."> + Save + + + diff --git a/lib/lanttern_web/live_components/assessment_points_filter_view_overlay_component.ex b/lib/lanttern_web/live_components/assessment_points_filter_view_overlay_component.ex deleted file mode 100644 index cfca9ee2..00000000 --- a/lib/lanttern_web/live_components/assessment_points_filter_view_overlay_component.ex +++ /dev/null @@ -1,186 +0,0 @@ -defmodule LantternWeb.AssessmentPointsFilterViewOverlayComponent do - @moduledoc """ - ### PubSub: expected broadcast messages - - All messages should be broadcast to topic in assigns, following `{:key, msg}` pattern. - - - `:assessment_points_filter_view_created` - - ### Expected external assigns: - - attr :id, :string, required: true - attr :current_user, User, required: true - attr :show, :boolean, required: true - attr :on_cancel, JS, default: %JS{} - attr :filter_view_id, :integer, doc: "For updating views" - attr :topic, :string - """ - - use LantternWeb, :live_component - alias Phoenix.PubSub - alias Lanttern.Explorer - alias Lanttern.Explorer.AssessmentPointsFilterView - - def render(assigns) do - ~H""" -
- <.slide_over :if={@show} id={@id} show={true} on_cancel={Map.get(assigns, :on_cancel, %JS{})}> - <:title> - <%= if @action == "create", do: "Create", else: "Update" %> assessment points filter view - - <.form - id="assessment-points-filter-view-form" - for={@form} - phx-change="validate" - phx-submit={@action} - phx-target={@myself} - > - <.error_block :if={@form.source.action in [:insert, :update]} class="mb-6"> - Oops, something went wrong! Please check the errors below. - - <.input field={@form[:id]} type="hidden" /> - <.input field={@form[:profile_id]} type="hidden" /> - <.input field={@form[:name]} label="Filter view name" phx-debounce="1500" class="mb-6" /> -
-
- Classes -
- <.check_field - :for={opt <- @classes} - id={"class-#{opt.id}"} - field={@form[:classes_ids]} - opt={opt} - /> -
-
-
- Subjects -
- <.check_field - :for={opt <- @subjects} - id={"subject-#{opt.id}"} - field={@form[:subjects_ids]} - opt={opt} - /> -
-
-
- - <:actions> - <.button type="button" theme="ghost" phx-click={JS.exec("data-cancel", to: "##{@id}")}> - Cancel - - <.button - type="submit" - form="assessment-points-filter-view-form" - phx-disable-with="Saving..." - > - Save - - - -
- """ - end - - # lifecycle - - def mount(socket) do - classes = Lanttern.Schools.list_classes() - subjects = Lanttern.Taxonomy.list_subjects() - - socket = - socket - |> assign(:classes, classes) - |> assign(:subjects, subjects) - |> assign(:action, "create") - - {:ok, socket} - end - - def update(%{show: true, filter_view_id: id} = assigns, socket) when is_integer(id) do - filter_view = Explorer.get_assessment_points_filter_view!(id, preloads: [:classes, :subjects]) - - changeset = - filter_view - |> Map.put(:classes_ids, Enum.map(filter_view.classes, &"#{&1.id}")) - |> Map.put(:subjects_ids, Enum.map(filter_view.subjects, &"#{&1.id}")) - |> Explorer.change_assessment_points_filter_view() - - socket = - socket - |> assign(assigns) - |> assign(:form, to_form(changeset)) - |> assign(:action, "update") - |> assign(:filter_view, filter_view) - - {:ok, socket} - end - - def update(%{show: true} = assigns, socket) do - changeset = - %AssessmentPointsFilterView{profile_id: assigns.current_user.current_profile.id} - |> Explorer.change_assessment_points_filter_view() - - socket = - socket - |> assign(assigns) - |> assign(:form, to_form(changeset)) - |> assign(:action, "create") - - {:ok, socket} - end - - def update(assigns, socket), - do: {:ok, assign(socket, assigns)} - - # event handlers - - def handle_event("validate", %{"assessment_points_filter_view" => params}, socket) do - form = - %AssessmentPointsFilterView{} - |> Explorer.change_assessment_points_filter_view(params) - |> Map.put(:action, :validate) - |> to_form() - - {:noreply, assign(socket, form: form)} - end - - def handle_event("create", %{"assessment_points_filter_view" => params}, socket) do - case Explorer.create_assessment_points_filter_view(params) do - {:ok, assessment_points_filter_view} -> - msg = {:assessment_points_filter_view_created, assessment_points_filter_view} - - if socket.assigns.topic do - PubSub.broadcast(Lanttern.PubSub, socket.assigns.topic, msg) - end - - {:noreply, socket} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, form: to_form(changeset))} - end - end - - def handle_event("update", %{"assessment_points_filter_view" => params}, socket) do - # force classes_ids and subjects_ids inclusion to remove filters if needed - params = - params - |> Map.put_new("classes_ids", []) - |> Map.put_new("subjects_ids", []) - - case Explorer.update_assessment_points_filter_view(socket.assigns.filter_view, params) do - {:ok, assessment_points_filter_view} -> - msg = {:assessment_points_filter_view_updated, assessment_points_filter_view} - - if socket.assigns.topic do - PubSub.broadcast(Lanttern.PubSub, socket.assigns.topic, msg) - end - - {:noreply, socket} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, form: to_form(changeset))} - end - end -end diff --git a/lib/lanttern_web/router.ex b/lib/lanttern_web/router.ex index 5e709e0f..5d4291a9 100644 --- a/lib/lanttern_web/router.ex +++ b/lib/lanttern_web/router.ex @@ -41,7 +41,15 @@ defmodule LantternWeb.Router do {LantternWeb.UserAuth, :ensure_authenticated}, {LantternWeb.Path, :put_path_in_socket} ] do - live "/dashboard", DashboardLive.Index + live "/dashboard", DashboardLive.Index, :index + + live "/dashboard/filter_view/new", + DashboardLive.Index, + :new_filter_view + + live "/dashboard/filter_view/:id/edit", + DashboardLive.Index, + :edit_filter_view live "/assessment_points", AssessmentPointLive.Explorer live "/assessment_points/:id", AssessmentPointLive.Show diff --git a/test/lanttern/explorer_test.exs b/test/lanttern/explorer_test.exs index 01bf8252..6b97a2e5 100644 --- a/test/lanttern/explorer_test.exs +++ b/test/lanttern/explorer_test.exs @@ -14,7 +14,8 @@ defmodule Lanttern.ExplorerTest do test "list_assessment_points_filter_views/1 returns all assessment_points_filter_views" do assessment_points_filter_view = assessment_points_filter_view_fixture() - assert Explorer.list_assessment_points_filter_views() == [assessment_points_filter_view] + [expected] = Explorer.list_assessment_points_filter_views() + assert expected.id == assessment_points_filter_view.id end test "list_assessment_points_filter_views/1 with preloads returns all assessment_points_filter_views with preloaded data" do @@ -44,8 +45,8 @@ defmodule Lanttern.ExplorerTest do test "get_assessment_points_filter_view!/2 returns the assessment_points_filter_view with given id" do assessment_points_filter_view = assessment_points_filter_view_fixture() - assert Explorer.get_assessment_points_filter_view!(assessment_points_filter_view.id) == - assessment_points_filter_view + expected = Explorer.get_assessment_points_filter_view!(assessment_points_filter_view.id) + assert expected.id == assessment_points_filter_view.id end test "get_assessment_points_filter_view!/2 with preloads returns the assessment_points_filter_view with given id with preloaded data" do @@ -126,8 +127,9 @@ defmodule Lanttern.ExplorerTest do @invalid_attrs ) - assert assessment_points_filter_view == - Explorer.get_assessment_points_filter_view!(assessment_points_filter_view.id) + expected = Explorer.get_assessment_points_filter_view!(assessment_points_filter_view.id) + assert expected.id == assessment_points_filter_view.id + assert expected.name == assessment_points_filter_view.name end test "delete_assessment_points_filter_view/1 deletes the assessment_points_filter_view" do diff --git a/test/lanttern_web/live/dashboard_live_test.exs b/test/lanttern_web/live/dashboard_live_test.exs index 5aa3f793..e311ba5b 100644 --- a/test/lanttern_web/live/dashboard_live_test.exs +++ b/test/lanttern_web/live/dashboard_live_test.exs @@ -64,7 +64,7 @@ defmodule LantternWeb.DashboardLiveTest do # open create modal view - |> element("button", "Create new view") + |> element("a", "Create new view") |> render_click() # submit form @@ -88,7 +88,7 @@ defmodule LantternWeb.DashboardLiveTest do # "click" remove view - |> element("button#remove-filter-view-assessment_points_filter_views-#{filter_view.id}") + |> element("button#remove-filter-view-filter_views-#{filter_view.id}") |> render_click() # assert view is removed