From 5a40644cc9d91dd3dde07e1638313c7f71ad442d Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 11:01:35 +0100 Subject: [PATCH 01/21] move handle_event from quote block to top level module --- lib/backpex/live_resource.ex | 498 ++++++++++++++++++----------------- 1 file changed, 256 insertions(+), 242 deletions(-) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index 8b4a7742..abd401c4 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -1,6 +1,3 @@ -# credo:disable-for-this-file Credo.Check.Refactor.LongQuoteBlocks -# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity - defmodule Backpex.LiveResource do @moduledoc ~S''' A LiveResource makes it easy to manage existing resources in your application. It provides extensive configuration options in order to meet everyone's needs. In connection with `Backpex.Components` you can build an individual admin dashboard on top of your application in minutes. @@ -11,6 +8,9 @@ defmodule Backpex.LiveResource do ''' alias Backpex.Resource + alias Backpex.Router + use Phoenix.LiveView + import Backpex.HTML.Resource @options_schema [ adapter: [ @@ -95,6 +95,8 @@ defmodule Backpex.LiveResource do ] ] + @empty_filter_key :empty_filter + @doc """ A list of [resource_actions](Backpex.ResourceAction.html) that may be performed on the given resource. """ @@ -248,7 +250,6 @@ defmodule Backpex.LiveResource do use BackpexWeb, :html use Phoenix.LiveView, layout: @resource_opts[:layout] - import Backpex.HTML.Resource import Backpex.LiveResource import Phoenix.LiveView.Helpers import Ecto.Query @@ -256,7 +257,7 @@ defmodule Backpex.LiveResource do alias Backpex.Adapters.Ecto, as: EctoAdapter alias Backpex.Resource alias Backpex.ResourceAction - alias Backpex.Router + alias Backpex.LiveResource require Logger @@ -679,243 +680,8 @@ defmodule Backpex.LiveResource do end @impl Phoenix.LiveView - def handle_event("close-modal", _params, socket) do - socket = - socket - |> push_patch(to: socket.assigns.return_to) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("item-action", %{"action-key" => key, "item-id" => item_id}, socket) do - item = - Enum.find(socket.assigns.items, fn item -> - to_string(primary_value(item)) == to_string(item_id) - end) - - socket - |> assign(selected_items: [item]) - |> maybe_handle_item_action(key) - end - - def handle_event("item-action", %{"action-key" => key}, socket) do - maybe_handle_item_action(socket, key) - end - - defp maybe_handle_item_action(socket, key) do - key = String.to_existing_atom(key) - action = socket.assigns.item_actions[key] - items = socket.assigns.selected_items - - if has_modal?(action.module) do - open_action_confirm_modal(socket, action, key) - else - handle_item_action(socket, action, key, items) - end - end - - defp open_action_confirm_modal(socket, action, key) do - init_change = action.module.init_change(socket.assigns) - changeset_function = &action.module.changeset/3 - - metadata = Resource.build_changeset_metadata(socket.assigns) - - changeset = - init_change - |> Ecto.Changeset.change() - |> changeset_function.(%{}, metadata) - - socket = - socket - |> assign(:item_action_types, init_change) - |> assign(:changeset_function, changeset_function) - |> assign(:changeset, changeset) - |> assign(:action_to_confirm, Map.put(action, :key, key)) - - {:noreply, socket} - end - - defp handle_item_action(socket, action, key, items) do - items = Enum.filter(items, fn item -> can?(socket.assigns, key, item, __MODULE__) end) - - socket - |> assign(action_to_confirm: nil) - |> assign(selected_items: []) - |> assign(select_all: false) - |> action.module.handle(items, %{}) - end - - @impl Phoenix.LiveView - def handle_event("select-page-size", %{"select_per_page" => %{"value" => per_page}}, socket) do - %{assigns: %{query_options: query_options, params: params} = assigns} = socket - - per_page = String.to_integer(per_page) - - to = - Router.get_path( - socket, - __MODULE__, - params, - :index, - Map.merge(query_options, %{per_page: per_page}) - ) - - socket = push_patch(socket, to: to, replace: true) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("index-search", %{"index_search" => %{"value" => search_input}}, socket) do - %{assigns: %{query_options: query_options, params: params} = assigns} = socket - - to = - Router.get_path( - socket, - __MODULE__, - params, - :index, - Map.merge(query_options, %{search: search_input}) - ) - - socket = push_patch(socket, to: to, replace: true) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("change-filter", params, socket) do - query_options = socket.assigns.query_options - - empty_filter_name = Atom.to_string(@empty_filter_key) - - filters = - Map.get(query_options, :filters, %{}) - |> Map.merge(params["filters"]) - # Filter manually emptied filters and empty filter - |> Enum.filter(fn - {^empty_filter_name, _value} -> false - {_filter, ""} -> false - {_filter, %{"start" => "", "end" => ""}} -> false - _filter_params -> true - end) - - to = - Router.get_path( - socket, - __MODULE__, - socket.assigns.params, - :index, - Map.put(query_options, :filters, filters) - ) - - socket = - socket - |> assign(filters_changed: true) - |> push_patch(to: to) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("clear-filter", %{"field" => field}, socket) do - %{assigns: %{query_options: query_options, params: params, live_resource: live_resource} = assigns} = socket - - new_query_options = - Map.put( - query_options, - :filters, - Map.get(query_options, :filters, %{}) - |> Map.delete(field) - |> Backpex.LiveResource.maybe_put_empty_filter(@empty_filter_key) - ) - - to = Router.get_path(socket, __MODULE__, params, :index, new_query_options) - - socket = - push_patch(socket, to: to) - |> assign(params: Map.merge(params, new_query_options)) - |> assign(query_options: new_query_options) - |> assign(filters_changed: true) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("filter-preset-selected", %{"field" => field, "preset-index" => preset_index} = params, socket) do - query_options = socket.assigns.query_options - preset_index = String.to_integer(preset_index) - field_atom = String.to_existing_atom(field) - - get_preset_values = - socket.assigns - |> get_in([:filters, field_atom, :presets]) - |> Enum.at(preset_index) - |> Map.get(:values) - - filters = - Map.get(query_options, :filters, %{}) - |> Map.put(field, get_preset_values.()) - |> Map.drop([Atom.to_string(@empty_filter_key)]) - - to = - Router.get_path( - socket, - __MODULE__, - socket.assigns.params, - :index, - Map.put(query_options, :filters, filters) - ) - - socket = - socket - |> assign(filters_changed: true) - |> push_patch(to: to) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("update-selected-items", %{"id" => id}, socket) do - selected_items = socket.assigns.selected_items - - item = Enum.find(socket.assigns.items, fn item -> to_string(primary_value(item)) == to_string(id) end) - - updated_selected_items = - if Enum.member?(selected_items, item) do - List.delete(selected_items, item) - else - [item | selected_items] - end - - select_all = length(updated_selected_items) == length(socket.assigns.items) - - socket = - socket - |> assign(:selected_items, updated_selected_items) - |> assign(:select_all, select_all) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("toggle-item-selection", _params, socket) do - select_all = not socket.assigns.select_all - - selected_items = - if select_all do - socket.assigns.items - else - [] - end - - socket = - socket - |> assign(:select_all, select_all) - |> assign(:selected_items, selected_items) - - {:noreply, socket} + def handle_event(event, params, socket) do + LiveResource.handle_event(event, params, socket) end @impl Phoenix.LiveView @@ -1092,6 +858,7 @@ defmodule Backpex.LiveResource do end end + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity defmacro __before_compile__(_env) do quote do import Backpex.HTML.Layout @@ -1231,6 +998,253 @@ defmodule Backpex.LiveResource do end end + @impl Phoenix.LiveView + def handle_event("close-modal", _params, socket) do + socket = + socket + |> push_patch(to: socket.assigns.return_to) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("item-action", %{"action-key" => key, "item-id" => item_id}, socket) do + item = + Enum.find(socket.assigns.items, fn item -> + to_string(primary_value(item)) == to_string(item_id) + end) + + socket + |> assign(selected_items: [item]) + |> maybe_handle_item_action(key) + end + + @impl Phoenix.LiveView + def handle_event("item-action", %{"action-key" => key}, socket) do + maybe_handle_item_action(socket, key) + end + + @impl Phoenix.LiveView + def handle_event("select-page-size", %{"select_per_page" => %{"value" => per_page}}, socket) do + %{query_options: query_options, params: params} = socket.assigns + + per_page = String.to_integer(per_page) + + to = + Router.get_path( + socket, + __MODULE__, + params, + :index, + Map.merge(query_options, %{per_page: per_page}) + ) + + socket = push_patch(socket, to: to, replace: true) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("index-search", %{"index_search" => %{"value" => search_input}}, socket) do + %{query_options: query_options, params: params} = socket.assigns + + to = + Router.get_path( + socket, + __MODULE__, + params, + :index, + Map.merge(query_options, %{search: search_input}) + ) + + socket = push_patch(socket, to: to, replace: true) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("change-filter", params, socket) do + query_options = socket.assigns.query_options + + empty_filter_name = Atom.to_string(@empty_filter_key) + + filters = + Map.get(query_options, :filters, %{}) + |> Map.merge(params["filters"]) + # Filter manually emptied filters and empty filter + |> Enum.filter(fn + {^empty_filter_name, _value} -> false + {_filter, ""} -> false + {_filter, %{"start" => "", "end" => ""}} -> false + _filter_params -> true + end) + + to = + Router.get_path( + socket, + __MODULE__, + socket.assigns.params, + :index, + Map.put(query_options, :filters, filters) + ) + + socket = + socket + |> assign(filters_changed: true) + |> push_patch(to: to) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("clear-filter", %{"field" => field}, socket) do + %{query_options: query_options, params: params} = socket.assigns + + new_query_options = + Map.put( + query_options, + :filters, + Map.get(query_options, :filters, %{}) + |> Map.delete(field) + |> Backpex.LiveResource.maybe_put_empty_filter(@empty_filter_key) + ) + + to = Router.get_path(socket, __MODULE__, params, :index, new_query_options) + + socket = + push_patch(socket, to: to) + |> assign(params: Map.merge(params, new_query_options)) + |> assign(query_options: new_query_options) + |> assign(filters_changed: true) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("filter-preset-selected", %{"field" => field, "preset-index" => preset_index} = _params, socket) do + query_options = socket.assigns.query_options + preset_index = String.to_integer(preset_index) + field_atom = String.to_existing_atom(field) + + get_preset_values = + socket.assigns + |> get_in([:filters, field_atom, :presets]) + |> Enum.at(preset_index) + |> Map.get(:values) + + filters = + Map.get(query_options, :filters, %{}) + |> Map.put(field, get_preset_values.()) + |> Map.drop([Atom.to_string(@empty_filter_key)]) + + to = + Router.get_path( + socket, + __MODULE__, + socket.assigns.params, + :index, + Map.put(query_options, :filters, filters) + ) + + socket = + socket + |> assign(filters_changed: true) + |> push_patch(to: to) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("update-selected-items", %{"id" => id}, socket) do + selected_items = socket.assigns.selected_items + + item = Enum.find(socket.assigns.items, fn item -> to_string(primary_value(item)) == to_string(id) end) + + updated_selected_items = + if Enum.member?(selected_items, item) do + List.delete(selected_items, item) + else + [item | selected_items] + end + + select_all = length(updated_selected_items) == length(socket.assigns.items) + + socket = + socket + |> assign(:selected_items, updated_selected_items) + |> assign(:select_all, select_all) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("toggle-item-selection", _params, socket) do + select_all = not socket.assigns.select_all + + selected_items = + if select_all do + socket.assigns.items + else + [] + end + + socket = + socket + |> assign(:select_all, select_all) + |> assign(:selected_items, selected_items) + + {:noreply, socket} + end + + defp maybe_handle_item_action(socket, key) do + key = String.to_existing_atom(key) + action = socket.assigns.item_actions[key] + items = socket.assigns.selected_items + + if has_modal?(action.module) do + open_action_confirm_modal(socket, action, key) + else + handle_item_action(socket, action, key, items) + end + end + + defp open_action_confirm_modal(socket, action, key) do + init_change = action.module.init_change(socket.assigns) + changeset_function = &action.module.changeset/3 + + metadata = Resource.build_changeset_metadata(socket.assigns) + + changeset = + init_change + |> Ecto.Changeset.change() + |> changeset_function.(%{}, metadata) + + socket = + socket + |> assign(:item_action_types, init_change) + |> assign(:changeset_function, changeset_function) + |> assign(:changeset, changeset) + |> assign(:action_to_confirm, Map.put(action, :key, key)) + + {:noreply, socket} + end + + defp handle_item_action(socket, action, key, items) do + items = Enum.filter(items, fn item -> can?(socket.assigns, key, item, socket.assigns.live_resource) end) + + socket + |> assign(action_to_confirm: nil) + |> assign(selected_items: []) + |> assign(select_all: false) + |> action.module.handle(items, %{}) + end + + defp primary_value(item) do + item + # |> Map.get(@resource_opts[:primary_key]) + |> Map.get(:id) + end + @doc """ Subscribes to pubsub topic. """ From 0f357069df28dba4694866db46fec94b611165ca Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 11:40:39 +0100 Subject: [PATCH 02/21] move handle_info from quote block to top level module --- lib/backpex/live_resource.ex | 297 +++++++++++++++++++++-------------- 1 file changed, 179 insertions(+), 118 deletions(-) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index abd401c4..1c973513 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -7,6 +7,7 @@ defmodule Backpex.LiveResource do > When you `use Backpex.LiveResource`, the `Backpex.LiveResource` module will set `@behavior Backpex.LiveResource`. Additionally it will create a LiveView based on the given configuration in order to create fully functional index, show, new and edit views for a resource. It will also insert fallback functions that can be overridden. ''' + alias Backpex.Adapters.Ecto, as: EctoAdapter alias Backpex.Resource alias Backpex.Router use Phoenix.LiveView @@ -618,18 +619,6 @@ defmodule Backpex.LiveResource do defp maybe_put_search(query_options, params), do: query_options - def assign_items(socket) do - %{ - live_resource: live_resource, - fields: fields - } = socket.assigns - - criteria = build_criteria(socket.assigns) - items = Resource.list(fields, socket.assigns, live_resource, criteria) - - assign(socket, :items, items) - end - defp maybe_assign_metrics(socket) do %{ assigns: @@ -685,56 +674,8 @@ defmodule Backpex.LiveResource do end @impl Phoenix.LiveView - def handle_info({"backpex:" <> unquote(@resource_opts[:pubsub][:event_prefix]) <> "created", item}, socket) - when socket.assigns.live_action in [:index, :resource_action] do - {:noreply, refresh_items(socket)} - end - - @impl Phoenix.LiveView - def handle_info({"backpex:" <> unquote(@resource_opts[:pubsub][:event_prefix]) <> "deleted", item}, socket) - when socket.assigns.live_action in [:index, :resource_action] do - if Enum.filter(socket.assigns.items, &(to_string(primary_value(&1)) == to_string(primary_value(item)))) != [] do - {:noreply, refresh_items(socket)} - else - {:noreply, socket} - end - end - - @impl Phoenix.LiveView - def handle_info({"backpex:" <> unquote(@resource_opts[:pubsub][:event_prefix]) <> "updated", item}, socket) - when socket.assigns.live_action in [:index, :resource_action, :show] do - {:noreply, update_item(socket, item)} - end - - @impl Phoenix.LiveView - def handle_info({:put_assoc, {key, value} = _assoc}, socket) do - changeset = Ecto.Changeset.put_assoc(socket.assigns.changeset, key, value) - assocs = Map.get(socket.assigns, :assocs, []) |> Keyword.put(key, value) - - socket = - socket - |> assign(:assocs, assocs) - |> assign(:changeset, changeset) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_info({:put_embed, {key, value} = _assoc}, socket) do - changeset = Ecto.Changeset.put_embed(socket.assigns.changeset, key, value) - embeds = Map.get(socket.assigns, :embeds, []) |> Keyword.put(key, value) - - socket = - socket - |> assign(:embeds, embeds) - |> assign(:changeset, changeset) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_info({:update_changeset, changeset}, socket) do - {:noreply, assign(socket, :changeset, changeset)} + def handle_info(msg, socket) do + LiveResource.handle_info(msg, socket) end def get_empty_filter_key, do: @empty_filter_key @@ -744,62 +685,6 @@ defmodule Backpex.LiveResource do |> Map.get(@resource_opts[:primary_key]) end - defp update_item(socket, %{id: id} = _item) do - %{ - live_resource: live_resource, - live_action: live_action - } = socket.assigns - - fields = filtered_fields_by_action(fields(), socket.assigns, :show) - item = Resource.get(id, socket.assigns, live_resource) - - socket = - cond do - live_action in [:index, :resource_action] and item -> - items = Enum.map(socket.assigns.items, &if(primary_value(&1) == id, do: item, else: &1)) - - assign(socket, :items, items) - - live_action == :show and item -> - assign(socket, :item, item) - - true -> - socket - end - - socket - end - - defp refresh_items(socket) do - %{ - live_resource: live_resource, - schema: schema, - params: params, - fields: fields, - query_options: query_options - } = socket.assigns - - filters = Backpex.LiveResource.get_active_filters(__MODULE__, socket.assigns) - valid_filter_params = Backpex.LiveResource.get_valid_filters_from_params(params, filters, @empty_filter_key) - - count_criteria = [ - search: search_options(params, fields, schema), - filters: filter_options(valid_filter_params, filters) - ] - - item_count = Resource.count(fields, socket.assigns, live_resource, count_criteria) - %{page: page, per_page: per_page} = query_options - total_pages = calculate_total_pages(item_count, per_page) - new_query_options = Map.put(query_options, :page, validate_page(page, total_pages)) - - socket - |> assign(:item_count, item_count) - |> assign(:total_pages, total_pages) - |> assign(:query_options, new_query_options) - |> assign_items() - |> maybe_assign_metrics() - end - @impl Phoenix.LiveView def render(%{live_action: action} = assigns) when action in [:show, :show_edit] do resource_show(assigns) @@ -998,6 +883,66 @@ defmodule Backpex.LiveResource do end end + def assign_items(socket) do + %{ + live_resource: live_resource, + fields: fields + } = socket.assigns + + criteria = build_criteria(socket.assigns) + items = Resource.list(fields, socket.assigns, live_resource, criteria) + + assign(socket, :items, items) + end + + defp maybe_assign_metrics(socket) do + %{ + assigns: + %{ + repo: repo, + schema: schema, + live_action: live_action, + live_resource: live_resource, + fields: fields, + query_options: query_options, + metric_visibility: metric_visibility + } = assigns + } = socket + + filters = Backpex.LiveResource.get_active_filters(live_resource, assigns) + + metrics = + socket.assigns.live_resource.metrics() + |> Enum.map(fn {key, metric} -> + query = + EctoAdapter.list_query( + assigns, + &socket.assigns.live_resource.item_query(&1, live_action, assigns), + fields, + search: search_options(query_options, fields, schema), + filters: filter_options(query_options, filters) + ) + + case Backpex.Metric.metrics_visible?(metric_visibility, live_resource) do + true -> + data = + query + |> Ecto.Query.exclude(:select) + |> Ecto.Query.exclude(:preload) + |> Ecto.Query.exclude(:group_by) + |> metric.module.query(metric.select, repo) + + {key, Map.put(metric, :data, data)} + + _visible -> + {key, metric} + end + end) + + socket + |> assign(metrics: metrics) + end + @impl Phoenix.LiveView def handle_event("close-modal", _params, socket) do socket = @@ -1196,6 +1141,122 @@ defmodule Backpex.LiveResource do {:noreply, socket} end + @impl Phoenix.LiveView + def handle_info({:put_assoc, {key, value} = _assoc}, socket) do + changeset = Ecto.Changeset.put_assoc(socket.assigns.changeset, key, value) + assocs = Map.get(socket.assigns, :assocs, []) |> Keyword.put(key, value) + + socket = + socket + |> assign(:assocs, assocs) + |> assign(:changeset, changeset) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_info({:put_embed, {key, value} = _assoc}, socket) do + changeset = Ecto.Changeset.put_embed(socket.assigns.changeset, key, value) + embeds = Map.get(socket.assigns, :embeds, []) |> Keyword.put(key, value) + + socket = + socket + |> assign(:embeds, embeds) + |> assign(:changeset, changeset) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_info({:update_changeset, changeset}, socket) do + {:noreply, assign(socket, :changeset, changeset)} + end + + @impl Phoenix.LiveView + def handle_info({"backpex:" <> event, item}, socket) do + event_prefix = socket.assigns.live_resource.config(:pubsub)[:event_prefix] + ^event_prefix <> event_type = event + + handle_backpex_info({event_type, item}, socket) + end + + @impl Phoenix.LiveView + def handle_info(_msg, socket) do + {:noreply, socket} + end + + defp handle_backpex_info({"created", _item}, socket) when socket.assigns.live_action in [:index, :resource_action] do + {:noreply, refresh_items(socket)} + end + + defp handle_backpex_info({"deleted", item}, socket) when socket.assigns.live_action in [:index, :resource_action] do + if Enum.filter(socket.assigns.items, &(to_string(primary_value(&1)) == to_string(primary_value(item)))) != [] do + {:noreply, refresh_items(socket)} + else + {:noreply, socket} + end + end + + defp handle_backpex_info({"updated", item}, socket) + when socket.assigns.live_action in [:index, :resource_action, :show] do + {:noreply, update_item(socket, item)} + end + + defp refresh_items(socket) do + %{ + live_resource: live_resource, + schema: schema, + params: params, + fields: fields, + query_options: query_options + } = socket.assigns + + filters = Backpex.LiveResource.get_active_filters(live_resource, socket.assigns) + valid_filter_params = Backpex.LiveResource.get_valid_filters_from_params(params, filters, @empty_filter_key) + + count_criteria = [ + search: search_options(params, fields, schema), + filters: filter_options(valid_filter_params, filters) + ] + + item_count = Resource.count(fields, socket.assigns, live_resource, count_criteria) + %{page: page, per_page: per_page} = query_options + total_pages = calculate_total_pages(item_count, per_page) + new_query_options = Map.put(query_options, :page, validate_page(page, total_pages)) + + socket + |> assign(:item_count, item_count) + |> assign(:total_pages, total_pages) + |> assign(:query_options, new_query_options) + |> assign_items() + |> maybe_assign_metrics() + end + + defp update_item(socket, %{id: id} = _item) do + %{ + live_resource: live_resource, + live_action: live_action + } = socket.assigns + + item = Resource.get(id, socket.assigns, live_resource) + + socket = + cond do + live_action in [:index, :resource_action] and item -> + items = Enum.map(socket.assigns.items, &if(primary_value(&1) == id, do: item, else: &1)) + + assign(socket, :items, items) + + live_action == :show and item -> + assign(socket, :item, item) + + true -> + socket + end + + socket + end + defp maybe_handle_item_action(socket, key) do key = String.to_existing_atom(key) action = socket.assigns.item_actions[key] From 68d9872ead1567620f19e4c62f4ec15b54ee26d0 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 11:56:52 +0100 Subject: [PATCH 03/21] move mount from quote block to top level module --- lib/backpex/live_resource.ex | 146 +++++++++++++++++++---------------- 1 file changed, 81 insertions(+), 65 deletions(-) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index 1c973513..89459b0e 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -92,7 +92,8 @@ defmodule Backpex.LiveResource do default: false ], full_text_search: [ - type: :atom + type: :atom, + default: nil ] ] @@ -271,70 +272,7 @@ defmodule Backpex.LiveResource do @impl Phoenix.LiveView def mount(params, session, socket) do - subscribe_to_topic(socket, @resource_opts[:pubsub]) - - socket = - socket - |> assign(:live_resource, __MODULE__) - |> assign(:schema, @resource_opts[:adapter_config][:schema]) - |> assign(:repo, @resource_opts[:adapter_config][:repo]) - |> assign(:singular_name, singular_name()) - |> assign(:plural_name, plural_name()) - |> assign(:create_button_label, create_button_label()) - |> assign(:resource_created_message, resource_created_message()) - |> assign(:search_placeholder, search_placeholder()) - |> assign(:panels, panels()) - |> assign(:fluid?, @resource_opts[:fluid?]) - |> assign(:full_text_search, @resource_opts[:full_text_search]) - |> assign_active_fields(session) - |> assign_metrics_visibility(session) - |> assign_filters_changed_status(params) - - {:ok, socket} - end - - defp assign_active_fields(socket, session) do - fields = filtered_fields_by_action(fields(), socket.assigns, :index) - saved_fields = get_in(session, ["backpex", "column_toggle", "#{__MODULE__}"]) || %{} - - active_fields = - Enum.map(fields, fn {name, %{label: label}} -> - {name, - %{ - active: field_active?(name, saved_fields), - label: label - }} - end) - - socket - |> assign(:active_fields, active_fields) - end - - defp assign_metrics_visibility(socket, session) do - value = get_in(session, ["backpex", "metric_visibility"]) || %{} - - socket - |> assign(metric_visibility: value) - end - - defp assign_filters_changed_status(socket, params) do - %{assigns: %{live_action: live_action}} = socket - - socket - |> assign(:filters_changed, live_action == :index and params["filters_changed"] == "true") - end - - defp field_active?(name, saved_fields) do - case Map.get(saved_fields, Atom.to_string(name)) do - "true" -> - true - - "false" -> - false - - _other -> - true - end + LiveResource.mount(params, session, socket) end @impl Phoenix.LiveView @@ -883,6 +821,84 @@ defmodule Backpex.LiveResource do end end + @impl Phoenix.LiveView + def mount(params, session, socket) do + live_resource = socket.view + pubsub = live_resource.config(:pubsub) + subscribe_to_topic(socket, pubsub) + + # TODO: move these "config assigns" (and other global assigns) to where they are needed + adapter_config = live_resource.config(:adapter_config) + fluid? = live_resource.config(:fluid?) + full_text_search = live_resource.config(:full_text_search) + + socket = + socket + |> assign(:live_resource, live_resource) + |> assign(:schema, adapter_config[:schema]) + |> assign(:repo, adapter_config[:repo]) + |> assign(:singular_name, live_resource.singular_name()) + |> assign(:plural_name, live_resource.plural_name()) + |> assign(:create_button_label, live_resource.create_button_label()) + |> assign(:resource_created_message, live_resource.resource_created_message()) + |> assign(:search_placeholder, live_resource.search_placeholder()) + |> assign(:panels, live_resource.panels()) + |> assign(:fluid?, fluid?) + |> assign(:full_text_search, full_text_search) + |> assign_active_fields(session) + |> assign_metrics_visibility(session) + |> assign_filters_changed_status(params) + + {:ok, socket} + end + + defp assign_active_fields(socket, session) do + fields = + socket.assigns.live_resource.fields() + |> filtered_fields_by_action(socket.assigns, :index) + + saved_fields = get_in(session, ["backpex", "column_toggle", "#{__MODULE__}"]) || %{} + + active_fields = + Enum.map(fields, fn {name, %{label: label}} -> + {name, + %{ + active: field_active?(name, saved_fields), + label: label + }} + end) + + socket + |> assign(:active_fields, active_fields) + end + + defp assign_metrics_visibility(socket, session) do + value = get_in(session, ["backpex", "metric_visibility"]) || %{} + + socket + |> assign(metric_visibility: value) + end + + defp assign_filters_changed_status(socket, params) do + %{assigns: %{live_action: live_action}} = socket + + socket + |> assign(:filters_changed, live_action == :index and params["filters_changed"] == "true") + end + + defp field_active?(name, saved_fields) do + case Map.get(saved_fields, Atom.to_string(name)) do + "true" -> + true + + "false" -> + false + + _other -> + true + end + end + def assign_items(socket) do %{ live_resource: live_resource, From b1af85c7735507b3e9312034e0371c77f94ee1b6 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 12:39:54 +0100 Subject: [PATCH 04/21] move handle_params from quote block to top level module --- lib/backpex/adapters/ecto.ex | 4 +- lib/backpex/live_resource.ex | 652 ++++++++++++++++------------------- 2 files changed, 297 insertions(+), 359 deletions(-) diff --git a/lib/backpex/adapters/ecto.ex b/lib/backpex/adapters/ecto.ex index b9d52007..c93a1cf7 100644 --- a/lib/backpex/adapters/ecto.ex +++ b/lib/backpex/adapters/ecto.ex @@ -122,7 +122,7 @@ defmodule Backpex.Adapters.Ecto do TODO: Should be private. """ def list_query(assigns, item_query, fields, criteria \\ []) do - %{schema: schema, full_text_search: full_text_search, live_resource: live_resource} = assigns + %{schema: schema, full_text_search: full_text_search} = assigns associations = associations(fields, schema) schema @@ -132,7 +132,7 @@ defmodule Backpex.Adapters.Ecto do |> maybe_preload(associations, fields) |> maybe_merge_dynamic_fields(fields) |> apply_search(schema, full_text_search, criteria[:search]) - |> apply_filters(criteria[:filters], live_resource.get_empty_filter_key()) + |> apply_filters(criteria[:filters], Backpex.LiveResource.get_empty_filter_key()) |> apply_criteria(criteria, fields) end diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index 89459b0e..7c905249 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -7,11 +7,16 @@ defmodule Backpex.LiveResource do > When you `use Backpex.LiveResource`, the `Backpex.LiveResource` module will set `@behavior Backpex.LiveResource`. Additionally it will create a LiveView based on the given configuration in order to create fully functional index, show, new and edit views for a resource. It will also insert fallback functions that can be overridden. ''' + use Phoenix.LiveView + import Backpex.HTML.Resource alias Backpex.Adapters.Ecto, as: EctoAdapter alias Backpex.Resource + alias Backpex.ResourceAction alias Backpex.Router - use Phoenix.LiveView - import Backpex.HTML.Resource + + @empty_filter_key :empty_filter + + @permitted_order_directions ~w(asc desc)a @options_schema [ adapter: [ @@ -97,8 +102,6 @@ defmodule Backpex.LiveResource do ] ] - @empty_filter_key :empty_filter - @doc """ A list of [resource_actions](Backpex.ResourceAction.html) that may be performed on the given resource. """ @@ -256,16 +259,8 @@ defmodule Backpex.LiveResource do import Phoenix.LiveView.Helpers import Ecto.Query - alias Backpex.Adapters.Ecto, as: EctoAdapter - alias Backpex.Resource - alias Backpex.ResourceAction alias Backpex.LiveResource - require Logger - - @permitted_order_directions ~w(asc desc)a - @empty_filter_key :empty_filter - def config(key) do Keyword.fetch!(@resource_opts, key) end @@ -276,334 +271,8 @@ defmodule Backpex.LiveResource do end @impl Phoenix.LiveView - def handle_params(params, _url, socket) do - socket = - socket - |> assign(:params, params) - |> apply_item_actions(socket.assigns.live_action) - |> apply_action(socket.assigns.live_action) - - {:noreply, socket} - end - - def apply_action(socket, :index) do - socket - |> assign(:page_title, socket.assigns.plural_name) - |> apply_index() - |> assign(:item, nil) - end - - def apply_action(socket, :edit) do - %{ - live_resource: live_resource, - singular_name: singular_name - } = socket.assigns - - fields = filtered_fields_by_action(fields(), socket.assigns, :edit) - primary_value = URI.decode(socket.assigns.params["backpex_id"]) - item = Resource.get!(primary_value, socket.assigns, live_resource) - - unless can?(socket.assigns, :edit, item, __MODULE__), do: raise(Backpex.ForbiddenError) - - socket - |> assign(:fields, fields) - |> assign(:changeset_function, @resource_opts[:adapter_config][:update_changeset]) - |> assign(:page_title, Backpex.translate({"Edit %{resource}", %{resource: singular_name}})) - |> assign(:item, item) - |> assign_changeset(fields) - end - - def apply_action(socket, :show) do - %{ - live_resource: live_resource, - singular_name: singular_name - } = socket.assigns - - fields = filtered_fields_by_action(fields(), socket.assigns, :show) - primary_value = URI.decode(socket.assigns.params["backpex_id"]) - item = Resource.get!(primary_value, socket.assigns, live_resource) - - unless can?(socket.assigns, :show, item, __MODULE__), do: raise(Backpex.ForbiddenError) - - socket - |> assign(:page_title, singular_name) - |> assign(:fields, fields) - |> assign(:item, item) - |> apply_show_return_to(item) - end - - def apply_action(socket, :new) do - %{ - assigns: - %{ - schema: schema, - singular_name: singular_name, - create_button_label: create_button_label - } = assigns - } = socket - - unless can?(assigns, :new, nil, __MODULE__), do: raise(Backpex.ForbiddenError) - - fields = filtered_fields_by_action(fields(), assigns, :new) - empty_item = schema.__struct__() - - socket - |> assign(:changeset_function, @resource_opts[:adapter_config][:create_changeset]) - |> assign(:page_title, create_button_label) - |> assign(:fields, fields) - |> assign(:item, empty_item) - |> assign_changeset(fields) - end - - def apply_action(socket, :resource_action) do - id = - socket.assigns.params["backpex_id"] - |> URI.decode() - |> String.to_existing_atom() - - action = resource_actions()[id] - - unless can?(socket.assigns, id, nil, __MODULE__), do: raise(Backpex.ForbiddenError) - - socket = - socket - |> assign(:page_title, ResourceAction.name(action, :title)) - |> assign(:resource_action, action) - |> assign(:resource_action_id, id) - |> assign(:item, action.module.init_change(socket.assigns)) - |> apply_index() - |> assign(:changeset_function, &action.module.changeset/3) - |> assign_changeset(action.module.fields()) - end - - def apply_item_actions(socket, action) when action in [:index, :resource_action] do - item_actions = item_actions(default_item_actions()) - assign(socket, :item_actions, item_actions) - end - - def apply_item_actions(socket, _action), do: socket - - defp apply_index_return_to(socket) do - %{assigns: %{params: params, query_options: query_options} = assigns} = socket - - socket - |> assign( - :return_to, - Router.get_path(socket, __MODULE__, params, :index, query_options) - ) - end - - defp apply_show_return_to(socket, item) do - socket - |> assign( - :return_to, - Router.get_path(socket, __MODULE__, socket.assigns.params, :show, item) - ) - end - - defp apply_index(socket) do - %{ - live_resource: live_resource, - repo: repo, - schema: schema, - params: params - } = socket.assigns - - unless can?(socket.assigns, :index, nil, __MODULE__), do: raise(Backpex.ForbiddenError) - - fields = filtered_fields_by_action(fields(), socket.assigns, :index) - - per_page_options = @resource_opts[:per_page_options] - per_page_default = @resource_opts[:per_page_default] - init_order = @resource_opts[:init_order] - - filters = Backpex.LiveResource.get_active_filters(__MODULE__, socket.assigns) - valid_filter_params = Backpex.LiveResource.get_valid_filters_from_params(params, filters, @empty_filter_key) - - count_criteria = [ - search: search_options(params, fields, schema), - filters: filter_options(valid_filter_params, filters) - ] - - item_count = Resource.count(fields, socket.assigns, live_resource, count_criteria) - - per_page = - params - |> parse_integer("per_page", per_page_default) - |> value_in_permitted_or_default(per_page_options, per_page_default) - - total_pages = calculate_total_pages(item_count, per_page) - page = params |> parse_integer("page", 1) |> validate_page(total_pages) - - page_options = %{page: page, per_page: per_page} - - order_options = order_options_by_params(params, fields, init_order, socket.assigns, @permitted_order_directions) - - query_options = - page_options - |> Map.merge(order_options) - |> maybe_put_search(params) - |> Map.put(:filters, Map.get(valid_filter_params, "filters", %{})) - - socket - |> assign(:item_count, item_count) - |> assign(:query_options, query_options) - |> assign(:init_order, init_order) - |> assign(:total_pages, total_pages) - |> assign(:per_page_options, per_page_options) - |> assign(:filters, filters) - |> assign(:orderable_fields, orderable_fields(fields)) - |> assign(:searchable_fields, searchable_fields(fields)) - |> assign(:resource_actions, resource_actions()) - |> assign(:action_to_confirm, nil) - |> assign(:selected_items, []) - |> assign(:select_all, false) - |> assign(:fields, fields) - |> assign(:changeset_function, @resource_opts[:adapter_config][:update_changeset]) - |> maybe_redirect_to_default_filters() - |> assign_items() - |> maybe_assign_metrics() - |> apply_index_return_to() - end - - defp assign_changeset(socket, fields) do - %{ - item: item, - changeset_function: changeset_function, - live_action: live_action - } = socket.assigns - - metadata = Resource.build_changeset_metadata(socket.assigns) - changeset = changeset_function.(item, default_attrs(live_action, fields, socket.assigns), metadata) - - socket - |> assign(:changeset, changeset) - end - - defp default_attrs(:new, fields, %{schema: schema} = assigns) do - Enum.reduce(fields, %{}, fn - {name, %{default: default} = field_options} = field, attrs -> - if field_options.module.association?(field) && schema.__schema__(:association, name).cardinality == :one do - owner_key = schema.__schema__(:association, name).owner_key - - Map.put(attrs, owner_key, default.(assigns)) - else - Map.put(attrs, name, default.(assigns)) - end - - _field, attrs -> - attrs - end) - end - - defp default_attrs(:resource_action, fields, assigns) do - Enum.reduce(fields, %{}, fn - {name, %{default: default} = _field}, attrs -> - Map.put(attrs, name, default.(assigns)) - - _field, attrs -> - attrs - end) - end - - defp default_attrs(_live_action, _fields, _assigns), do: %{} - - defp maybe_redirect_to_default_filters(%{assigns: %{filters_changed: false}} = socket) do - %{ - assigns: - %{ - query_options: query_options, - params: params, - filters: filters - } = assigns - } = socket - - filters_with_defaults = - filters - |> Enum.filter(fn {_key, filter_config} -> - Map.has_key?(filter_config, :default) - end) - - # redirect to default filters if no filters are set and defaults are available - if Map.get(query_options, :filters) == %{} and Enum.count(filters_with_defaults) > 0 do - default_filter_options = - filters_with_defaults - |> Enum.map(fn {key, filter_config} -> - {key, filter_config.default} - end) - |> Enum.into(%{}, fn {key, value} -> - {Atom.to_string(key), value} - end) - - # redirect with updated query options - options = Map.put(query_options, :filters, default_filter_options) - to = Router.get_path(socket, __MODULE__, params, :index, options) - push_navigate(socket, to: to) - else - socket - end - end - - defp maybe_redirect_to_default_filters(socket) do - socket - end - - defp maybe_put_search(query_options, %{"search" => search} = _params) - when is_nil(search) or search == "", - do: query_options - - defp maybe_put_search(query_options, %{"search" => search} = _params), - do: Map.put(query_options, :search, search) - - defp maybe_put_search(query_options, params), do: query_options - - defp maybe_assign_metrics(socket) do - %{ - assigns: - %{ - repo: repo, - schema: schema, - live_action: live_action, - live_resource: live_resource, - params: params, - fields: fields, - query_options: query_options, - metric_visibility: metric_visibility - } = assigns - } = socket - - filters = Backpex.LiveResource.get_active_filters(__MODULE__, assigns) - - metrics = - metrics() - |> Enum.map(fn {key, metric} -> - query = - EctoAdapter.list_query( - assigns, - &item_query(&1, live_action, assigns), - fields, - search: search_options(query_options, fields, schema), - filters: filter_options(query_options, filters) - ) - - case Backpex.Metric.metrics_visible?(metric_visibility, live_resource) do - true -> - data = - query - |> Ecto.Query.exclude(:select) - |> Ecto.Query.exclude(:preload) - |> Ecto.Query.exclude(:group_by) - |> metric.module.query(metric.select, repo) - - {key, Map.put(metric, :data, data)} - - _visible -> - {key, metric} - end - end) - - socket - |> assign(metrics: metrics) + def handle_params(params, url, socket) do + LiveResource.handle_params(params, url, socket) end @impl Phoenix.LiveView @@ -616,8 +285,6 @@ defmodule Backpex.LiveResource do LiveResource.handle_info(msg, socket) end - def get_empty_filter_key, do: @empty_filter_key - defp primary_value(item) do item |> Map.get(@resource_opts[:primary_key]) @@ -688,11 +355,6 @@ defmodule Backpex.LiveResource do import Backpex.HTML.Resource alias Backpex.Router - @impl Phoenix.LiveView - def handle_info(_message, socket) do - {:noreply, socket} - end - @impl Backpex.LiveResource def panels, do: [] @@ -716,7 +378,7 @@ defmodule Backpex.LiveResource do @impl Backpex.LiveResource def return_to(socket, assigns, _action, _item) do - Map.get(assigns, :return_to, Router.get_path(socket, __MODULE__, %{}, :index)) + Map.get(assigns, :return_to, Router.get_path(socket, assigns.live_resource, %{}, :index)) end @impl Backpex.LiveResource @@ -857,7 +519,7 @@ defmodule Backpex.LiveResource do socket.assigns.live_resource.fields() |> filtered_fields_by_action(socket.assigns, :index) - saved_fields = get_in(session, ["backpex", "column_toggle", "#{__MODULE__}"]) || %{} + saved_fields = get_in(session, ["backpex", "column_toggle", "#{socket.assigns.live_resource}"]) || %{} active_fields = Enum.map(fields, fn {name, %{label: label}} -> @@ -959,6 +621,280 @@ defmodule Backpex.LiveResource do |> assign(metrics: metrics) end + @impl Phoenix.LiveView + def handle_params(params, _url, socket) do + socket = + socket + |> assign(:params, params) + |> apply_item_actions(socket.assigns.live_action) + |> apply_action(socket.assigns.live_action) + + {:noreply, socket} + end + + def apply_action(socket, :index) do + socket + |> assign(:page_title, socket.assigns.plural_name) + |> apply_index() + |> assign(:item, nil) + end + + def apply_action(socket, :edit) do + %{ + live_resource: live_resource, + singular_name: singular_name + } = socket.assigns + + fields = live_resource.fields |> filtered_fields_by_action(socket.assigns, :edit) + primary_value = URI.decode(socket.assigns.params["backpex_id"]) + item = Resource.get!(primary_value, socket.assigns, live_resource) + + unless can?(socket.assigns, :edit, item, live_resource), do: raise(Backpex.ForbiddenError) + + socket + |> assign(:fields, fields) + |> assign(:changeset_function, live_resource.config(:adapter_config)[:update_changeset]) + |> assign(:page_title, Backpex.translate({"Edit %{resource}", %{resource: singular_name}})) + |> assign(:item, item) + |> assign_changeset(fields) + end + + def apply_action(socket, :show) do + %{ + live_resource: live_resource, + singular_name: singular_name + } = socket.assigns + + fields = live_resource.fields() |> filtered_fields_by_action(socket.assigns, :show) + primary_value = URI.decode(socket.assigns.params["backpex_id"]) + item = Resource.get!(primary_value, socket.assigns, live_resource) + + unless can?(socket.assigns, :show, item, live_resource), do: raise(Backpex.ForbiddenError) + + socket + |> assign(:page_title, singular_name) + |> assign(:fields, fields) + |> assign(:item, item) + |> apply_show_return_to(item) + end + + def apply_action(socket, :new) do + %{ + live_resource: live_resource, + schema: schema, + create_button_label: create_button_label + } = socket.assigns + + unless can?(socket.assigns, :new, nil, live_resource), do: raise(Backpex.ForbiddenError) + + fields = live_resource.fields() |> filtered_fields_by_action(socket.assigns, :new) + empty_item = schema.__struct__() + + socket + |> assign(:changeset_function, live_resource.config(:adapter_config)[:create_changeset]) + |> assign(:page_title, create_button_label) + |> assign(:fields, fields) + |> assign(:item, empty_item) + |> assign_changeset(fields) + end + + def apply_action(socket, :resource_action) do + id = + socket.assigns.params["backpex_id"] + |> URI.decode() + |> String.to_existing_atom() + + action = socket.assigns.live_resource.resource_actions()[id] + + unless can?(socket.assigns, id, nil, socket.assigns.live_resource), do: raise(Backpex.ForbiddenError) + + socket + |> assign(:page_title, ResourceAction.name(action, :title)) + |> assign(:resource_action, action) + |> assign(:resource_action_id, id) + |> assign(:item, action.module.init_change(socket.assigns)) + |> apply_index() + |> assign(:changeset_function, &action.module.changeset/3) + |> assign_changeset(action.module.fields()) + end + + def apply_item_actions(socket, action) when action in [:index, :resource_action] do + item_actions = socket.assigns.live_resource.item_actions(default_item_actions()) + assign(socket, :item_actions, item_actions) + end + + def apply_item_actions(socket, _action), do: socket + + defp apply_index_return_to(socket) do + %{live_resource: live_resource, params: params, query_options: query_options} = socket.assigns + + socket + |> assign( + :return_to, + Router.get_path(socket, live_resource, params, :index, query_options) + ) + end + + defp apply_show_return_to(socket, item) do + %{live_resource: live_resource, params: params} = socket.assigns + + socket + |> assign(:return_to, Router.get_path(socket, live_resource, params, :show, item)) + end + + defp apply_index(socket) do + %{ + live_resource: live_resource, + schema: schema, + params: params + } = socket.assigns + + unless can?(socket.assigns, :index, nil, live_resource), do: raise(Backpex.ForbiddenError) + + fields = live_resource.fields() |> filtered_fields_by_action(socket.assigns, :index) + + per_page_options = live_resource.config(:per_page_options) + per_page_default = live_resource.config(:per_page_default) + init_order = live_resource.config(:init_order) + + filters = Backpex.LiveResource.get_active_filters(live_resource, socket.assigns) + valid_filter_params = Backpex.LiveResource.get_valid_filters_from_params(params, filters, @empty_filter_key) + + count_criteria = [ + search: search_options(params, fields, schema), + filters: filter_options(valid_filter_params, filters) + ] + + item_count = Resource.count(fields, socket.assigns, live_resource, count_criteria) + + per_page = + params + |> parse_integer("per_page", per_page_default) + |> value_in_permitted_or_default(per_page_options, per_page_default) + + total_pages = calculate_total_pages(item_count, per_page) + page = params |> parse_integer("page", 1) |> validate_page(total_pages) + + page_options = %{page: page, per_page: per_page} + + order_options = order_options_by_params(params, fields, init_order, socket.assigns, @permitted_order_directions) + + query_options = + page_options + |> Map.merge(order_options) + |> maybe_put_search(params) + |> Map.put(:filters, Map.get(valid_filter_params, "filters", %{})) + + socket + |> assign(:item_count, item_count) + |> assign(:query_options, query_options) + |> assign(:init_order, init_order) + |> assign(:total_pages, total_pages) + |> assign(:per_page_options, per_page_options) + |> assign(:filters, filters) + |> assign(:orderable_fields, orderable_fields(fields)) + |> assign(:searchable_fields, searchable_fields(fields)) + |> assign(:resource_actions, live_resource.resource_actions()) + |> assign(:action_to_confirm, nil) + |> assign(:selected_items, []) + |> assign(:select_all, false) + |> assign(:fields, fields) + |> assign(:changeset_function, live_resource.config(:adapter_config)[:update_changeset]) + |> maybe_redirect_to_default_filters() + |> assign_items() + |> maybe_assign_metrics() + |> apply_index_return_to() + end + + defp assign_changeset(socket, fields) do + %{ + item: item, + changeset_function: changeset_function, + live_action: live_action + } = socket.assigns + + metadata = Resource.build_changeset_metadata(socket.assigns) + changeset = changeset_function.(item, default_attrs(live_action, fields, socket.assigns), metadata) + + socket + |> assign(:changeset, changeset) + end + + defp default_attrs(:new, fields, %{schema: schema} = assigns) do + Enum.reduce(fields, %{}, fn + {name, %{default: default} = field_options} = field, attrs -> + if field_options.module.association?(field) && schema.__schema__(:association, name).cardinality == :one do + owner_key = schema.__schema__(:association, name).owner_key + + Map.put(attrs, owner_key, default.(assigns)) + else + Map.put(attrs, name, default.(assigns)) + end + + _field, attrs -> + attrs + end) + end + + defp default_attrs(:resource_action, fields, assigns) do + Enum.reduce(fields, %{}, fn + {name, %{default: default} = _field}, attrs -> + Map.put(attrs, name, default.(assigns)) + + _field, attrs -> + attrs + end) + end + + defp default_attrs(_live_action, _fields, _assigns), do: %{} + + defp maybe_redirect_to_default_filters(%{assigns: %{filters_changed: false}} = socket) do + %{ + live_resource: live_resource, + query_options: query_options, + params: params, + filters: filters + } = socket.assigns + + filters_with_defaults = + filters + |> Enum.filter(fn {_key, filter_config} -> + Map.has_key?(filter_config, :default) + end) + + # redirect to default filters if no filters are set and defaults are available + if Map.get(query_options, :filters) == %{} and Enum.count(filters_with_defaults) > 0 do + default_filter_options = + filters_with_defaults + |> Enum.map(fn {key, filter_config} -> + {key, filter_config.default} + end) + |> Enum.into(%{}, fn {key, value} -> + {Atom.to_string(key), value} + end) + + # redirect with updated query options + options = Map.put(query_options, :filters, default_filter_options) + to = Router.get_path(socket, live_resource, params, :index, options) + push_navigate(socket, to: to) + else + socket + end + end + + defp maybe_redirect_to_default_filters(socket) do + socket + end + + defp maybe_put_search(query_options, %{"search" => search} = _params) + when is_nil(search) or search == "", + do: query_options + + defp maybe_put_search(query_options, %{"search" => search} = _params), + do: Map.put(query_options, :search, search) + + defp maybe_put_search(query_options, _params), do: query_options + @impl Phoenix.LiveView def handle_event("close-modal", _params, socket) do socket = @@ -994,7 +930,7 @@ defmodule Backpex.LiveResource do to = Router.get_path( socket, - __MODULE__, + socket.assigns.live_resource, params, :index, Map.merge(query_options, %{per_page: per_page}) @@ -1012,7 +948,7 @@ defmodule Backpex.LiveResource do to = Router.get_path( socket, - __MODULE__, + socket.assigns.live_resource, params, :index, Map.merge(query_options, %{search: search_input}) @@ -1043,7 +979,7 @@ defmodule Backpex.LiveResource do to = Router.get_path( socket, - __MODULE__, + socket.assigns.live_resource, socket.assigns.params, :index, Map.put(query_options, :filters, filters) @@ -1059,7 +995,7 @@ defmodule Backpex.LiveResource do @impl Phoenix.LiveView def handle_event("clear-filter", %{"field" => field}, socket) do - %{query_options: query_options, params: params} = socket.assigns + %{live_resource: live_resource, query_options: query_options, params: params} = socket.assigns new_query_options = Map.put( @@ -1070,7 +1006,7 @@ defmodule Backpex.LiveResource do |> Backpex.LiveResource.maybe_put_empty_filter(@empty_filter_key) ) - to = Router.get_path(socket, __MODULE__, params, :index, new_query_options) + to = Router.get_path(socket, live_resource, params, :index, new_query_options) socket = push_patch(socket, to: to) @@ -1101,7 +1037,7 @@ defmodule Backpex.LiveResource do to = Router.get_path( socket, - __MODULE__, + socket.assigns.live_resource, socket.assigns.params, :index, Map.put(query_options, :filters, filters) @@ -1463,6 +1399,8 @@ defmodule Backpex.LiveResource do def filter_options(_no_filters_present, _filter_configs), do: %{} + def get_empty_filter_key, do: @empty_filter_key + @doc """ Checks whether a field is orderable or not. @@ -1690,7 +1628,7 @@ defmodule Backpex.LiveResource do def get_filter_options(module, query_options) do query_options |> Map.get(:filters, %{}) - |> Map.drop([Atom.to_string(module.get_empty_filter_key())]) + |> Map.drop([Atom.to_string(get_empty_filter_key())]) end @doc """ @@ -1700,7 +1638,7 @@ defmodule Backpex.LiveResource do filters = module.filters(assigns) Enum.filter(filters, fn {key, option} -> - module.get_empty_filter_key() != key and option.module.can?(assigns) + get_empty_filter_key() != key and option.module.can?(assigns) end) end From 2a43e79ea9e082272c731b592c739838452b4321 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 12:43:06 +0100 Subject: [PATCH 05/21] remove unused parameter --- lib/backpex/html/resource.ex | 2 +- lib/backpex/live_resource.ex | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/backpex/html/resource.ex b/lib/backpex/html/resource.ex index 02130f08..9170e72f 100644 --- a/lib/backpex/html/resource.ex +++ b/lib/backpex/html/resource.ex @@ -686,7 +686,7 @@ defmodule Backpex.HTML.Resource do /> <.index_filter live_resource={@live_resource} - filter_options={LiveResource.get_filter_options(@live_resource, @query_options)} + filter_options={LiveResource.get_filter_options(@query_options)} filters={LiveResource.get_active_filters(@live_resource, assigns)} /> diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index 7c905249..a82fc193 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -156,8 +156,7 @@ defmodule Backpex.LiveResource do The function has to return an `Ecto.Query`. It is recommended to build your `item_query` on top of the incoming query. Otherwise you will likely get binding errors. """ - @callback item_query(query :: Ecto.Query.t(), live_action :: atom(), assigns :: map()) :: - Ecto.Query.t() + @callback item_query(query :: Ecto.Query.t(), live_action :: atom(), assigns :: map()) :: Ecto.Query.t() @doc """ The function that can be used to add content to certain positions on Backpex views. It may also be used to overwrite content. @@ -1625,7 +1624,7 @@ defmodule Backpex.LiveResource do @doc """ Returns list of filter options from query options """ - def get_filter_options(module, query_options) do + def get_filter_options(query_options) do query_options |> Map.get(:filters, %{}) |> Map.drop([Atom.to_string(get_empty_filter_key())]) From 799e285f6769813c4591c1ef001d03d7fe72b551 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 12:52:24 +0100 Subject: [PATCH 06/21] shorten function calls --- lib/backpex/live_resource.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index a82fc193..7031d5d6 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -586,7 +586,7 @@ defmodule Backpex.LiveResource do } = assigns } = socket - filters = Backpex.LiveResource.get_active_filters(live_resource, assigns) + filters = get_active_filters(live_resource, assigns) metrics = socket.assigns.live_resource.metrics() @@ -756,8 +756,8 @@ defmodule Backpex.LiveResource do per_page_default = live_resource.config(:per_page_default) init_order = live_resource.config(:init_order) - filters = Backpex.LiveResource.get_active_filters(live_resource, socket.assigns) - valid_filter_params = Backpex.LiveResource.get_valid_filters_from_params(params, filters, @empty_filter_key) + filters = get_active_filters(live_resource, socket.assigns) + valid_filter_params = get_valid_filters_from_params(params, filters, @empty_filter_key) count_criteria = [ search: search_options(params, fields, schema), @@ -1002,7 +1002,7 @@ defmodule Backpex.LiveResource do :filters, Map.get(query_options, :filters, %{}) |> Map.delete(field) - |> Backpex.LiveResource.maybe_put_empty_filter(@empty_filter_key) + |> maybe_put_empty_filter(@empty_filter_key) ) to = Router.get_path(socket, live_resource, params, :index, new_query_options) @@ -1162,8 +1162,8 @@ defmodule Backpex.LiveResource do query_options: query_options } = socket.assigns - filters = Backpex.LiveResource.get_active_filters(live_resource, socket.assigns) - valid_filter_params = Backpex.LiveResource.get_valid_filters_from_params(params, filters, @empty_filter_key) + filters = get_active_filters(live_resource, socket.assigns) + valid_filter_params = get_valid_filters_from_params(params, filters, @empty_filter_key) count_criteria = [ search: search_options(params, fields, schema), From 2aad9baa0d708e9d6bf93d3d7d9c09739dfcd7cd Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 12:57:33 +0100 Subject: [PATCH 07/21] simplify active filters function --- lib/backpex/html/resource.ex | 2 +- lib/backpex/live_resource.ex | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/backpex/html/resource.ex b/lib/backpex/html/resource.ex index 9170e72f..005960c1 100644 --- a/lib/backpex/html/resource.ex +++ b/lib/backpex/html/resource.ex @@ -687,7 +687,7 @@ defmodule Backpex.HTML.Resource do <.index_filter live_resource={@live_resource} filter_options={LiveResource.get_filter_options(@query_options)} - filters={LiveResource.get_active_filters(@live_resource, assigns)} + filters={LiveResource.active_filters(assigns)} /> """ diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index 7031d5d6..88ffb5cd 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -586,7 +586,7 @@ defmodule Backpex.LiveResource do } = assigns } = socket - filters = get_active_filters(live_resource, assigns) + filters = active_filters(assigns) metrics = socket.assigns.live_resource.metrics() @@ -756,7 +756,7 @@ defmodule Backpex.LiveResource do per_page_default = live_resource.config(:per_page_default) init_order = live_resource.config(:init_order) - filters = get_active_filters(live_resource, socket.assigns) + filters = active_filters(socket.assigns) valid_filter_params = get_valid_filters_from_params(params, filters, @empty_filter_key) count_criteria = [ @@ -1162,7 +1162,7 @@ defmodule Backpex.LiveResource do query_options: query_options } = socket.assigns - filters = get_active_filters(live_resource, socket.assigns) + filters = active_filters(socket.assigns) valid_filter_params = get_valid_filters_from_params(params, filters, @empty_filter_key) count_criteria = [ @@ -1633,8 +1633,8 @@ defmodule Backpex.LiveResource do @doc """ Returns list of active filters. """ - def get_active_filters(module, assigns) do - filters = module.filters(assigns) + def active_filters(assigns) do + filters = assigns.live_resource.filters(assigns) Enum.filter(filters, fn {key, option} -> get_empty_filter_key() != key and option.module.can?(assigns) From 9bdd735533a69ea51401bc7c2b8282acca949e7d Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 13:10:34 +0100 Subject: [PATCH 08/21] remove obsolete Backpex.LiveResource.can?/4 helper --- lib/backpex/field.ex | 6 ++--- lib/backpex/fields/belongs_to.ex | 3 +-- lib/backpex/fields/has_many.ex | 4 +-- lib/backpex/html/resource.ex | 12 ++++----- .../resource/resource_index_table.html.heex | 2 +- lib/backpex/live_components/form_component.ex | 2 +- lib/backpex/live_resource.ex | 26 ++++++++----------- 7 files changed, 24 insertions(+), 31 deletions(-) diff --git a/lib/backpex/field.ex b/lib/backpex/field.ex index 02627f09..ae20d433 100644 --- a/lib/backpex/field.ex +++ b/lib/backpex/field.ex @@ -255,12 +255,12 @@ defmodule Backpex.Field do Handles index editable. """ def handle_index_editable(socket, value, change) do - if not Backpex.LiveResource.can?(socket.assigns, :edit, socket.assigns.item, socket.assigns.live_resource) do + %{assigns: %{item: item, live_resource: live_resource, fields: fields} = assigns} = socket + + if not live_resource.can?(assigns, :edit, item) do raise Backpex.ForbiddenError end - %{assigns: %{item: item, live_resource: live_resource, fields: fields} = assigns} = socket - opts = [ after_save_fun: fn item -> live_resource.on_item_updated(socket, item) diff --git a/lib/backpex/fields/belongs_to.ex b/lib/backpex/fields/belongs_to.ex index 1eb45c1c..f1bd8c9b 100644 --- a/lib/backpex/fields/belongs_to.ex +++ b/lib/backpex/fields/belongs_to.ex @@ -30,7 +30,6 @@ defmodule Backpex.Fields.BelongsTo do import Ecto.Query - alias Backpex.LiveResource alias Backpex.Router @impl Phoenix.LiveComponent @@ -189,7 +188,7 @@ defmodule Backpex.Fields.BelongsTo do assigns link = - if Map.has_key?(field_options, :live_resource) and LiveResource.can?(assigns, :show, value, live_resource) do + if Map.has_key?(field_options, :live_resource) and live_resource.can?(assigns, :show, value) do Router.get_path(socket, Map.get(field_options, :live_resource), params, :show, value) else nil diff --git a/lib/backpex/fields/has_many.ex b/lib/backpex/fields/has_many.ex index 4976e1de..99099c8f 100644 --- a/lib/backpex/fields/has_many.ex +++ b/lib/backpex/fields/has_many.ex @@ -39,7 +39,6 @@ defmodule Backpex.Fields.HasMany do import Ecto.Query import Backpex.HTML.Form alias Backpex.Adapters.Ecto, as: EctoAdapter - alias Backpex.LiveResource alias Backpex.Router @impl Phoenix.LiveComponent @@ -341,8 +340,7 @@ defmodule Backpex.Fields.HasMany do } = assigns link = - if link_assocs and Map.has_key?(field_options, :live_resource) and - LiveResource.can?(assigns, :show, item, live_resource) do + if link_assocs and Map.has_key?(field_options, :live_resource) and live_resource.can?(assigns, :show, item) do Router.get_path(socket, Map.get(field_options, :live_resource), params, :show, item) else nil diff --git a/lib/backpex/html/resource.ex b/lib/backpex/html/resource.ex index 005960c1..ee269378 100644 --- a/lib/backpex/html/resource.ex +++ b/lib/backpex/html/resource.ex @@ -105,7 +105,7 @@ defmodule Backpex.HTML.Resource do {_name, field_options} = field = Enum.find(fields, fn {field_name, _field_options} -> field_name == name end) readonly = - not LiveResource.can?(assigns, :edit, item, live_resource) or + not live_resource.can?(assigns, :edit, item) or Backpex.Field.readonly?(field_options, assigns) assigns = @@ -627,7 +627,7 @@ defmodule Backpex.HTML.Resource do ~H"""
<.link - :if={LiveResource.can?(assigns, :new, nil, @live_resource)} + :if={@live_resource.can?(assigns, :new, nil)} patch={Router.get_path(@socket, @live_resource, @params, :new)} > diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index 2ca721f6..0e9f51f4 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -260,44 +260,22 @@ defmodule Backpex.LiveResource do alias Backpex.LiveResource - def config(key) do - Keyword.fetch!(@resource_opts, key) - end + def config(key), do: Keyword.fetch!(@resource_opts, key) @impl Phoenix.LiveView - def mount(params, session, socket) do - LiveResource.mount(params, session, socket) - end + def mount(params, session, socket), do: LiveResource.mount(params, session, socket) @impl Phoenix.LiveView - def handle_params(params, url, socket) do - LiveResource.handle_params(params, url, socket) - end + def handle_params(params, url, socket), do: LiveResource.handle_params(params, url, socket) @impl Phoenix.LiveView - def handle_event(event, params, socket) do - LiveResource.handle_event(event, params, socket) - end + def handle_event(event, params, socket), do: LiveResource.handle_event(event, params, socket) @impl Phoenix.LiveView - def handle_info(msg, socket) do - LiveResource.handle_info(msg, socket) - end + def handle_info(msg, socket), do: LiveResource.handle_info(msg, socket) @impl Phoenix.LiveView - def render(%{live_action: action} = assigns) when action in [:show, :show_edit] do - resource_show(assigns) - end - - @impl Phoenix.LiveView - def render(%{live_action: action} = assigns) when action in [:new, :edit] do - resource_form(assigns) - end - - @impl Phoenix.LiveView - def render(assigns) do - resource_index(assigns) - end + def render(assigns), do: LiveResource.render(assigns) @impl Backpex.LiveResource def can?(_assigns, _action, _item), do: true @@ -615,6 +593,21 @@ defmodule Backpex.LiveResource do |> assign(metrics: metrics) end + @impl Phoenix.LiveView + def render(%{live_action: action} = assigns) when action in [:show, :show_edit] do + resource_show(assigns) + end + + @impl Phoenix.LiveView + def render(%{live_action: action} = assigns) when action in [:new, :edit] do + resource_form(assigns) + end + + @impl Phoenix.LiveView + def render(assigns) do + resource_index(assigns) + end + @impl Phoenix.LiveView def handle_params(params, _url, socket) do socket = From 709bff8dac65ec135d6328d79fcd1c7ffb3ab8ef Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 14:02:34 +0100 Subject: [PATCH 13/21] remove Backpex.LiveResource.schema/0 function --- lib/backpex/fields/has_many.ex | 3 ++- lib/backpex/live_resource.ex | 8 -------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/backpex/fields/has_many.ex b/lib/backpex/fields/has_many.ex index 99099c8f..41ef1757 100644 --- a/lib/backpex/fields/has_many.ex +++ b/lib/backpex/fields/has_many.ex @@ -255,7 +255,8 @@ defmodule Backpex.Fields.HasMany do {field_name, field_options} = field validate_live_resource(field_name, field_options) - schema = field_options.live_resource.schema() + # TODO: do not rely on specific adapter + schema = field_options.live_resource.config(:adapter_config)[:schema] field_name_string = to_string(field_name) new_assocs = get_new_assocs(attrs, field_name_string, schema, repo, field_options, assigns) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index 0e9f51f4..e95bb980 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -220,11 +220,6 @@ defmodule Backpex.LiveResource do """ @callback resource_created_message() :: binary() - @doc """ - Returns the schema of the live resource. - """ - @callback schema() :: module() - @doc """ Uses LiveResource in the current module to make it a LiveResource. @@ -301,9 +296,6 @@ defmodule Backpex.LiveResource do @impl Backpex.LiveResource def create_button_label, do: Backpex.translate({"New %{resource}", %{resource: singular_name()}}) - @impl Backpex.LiveResource - def schema, do: @resource_opts[:adapter_config][:schema] - @impl Backpex.LiveResource def resource_created_message, do: Backpex.translate({"New %{resource} has been created successfully.", %{resource: singular_name()}}) From f7f124b3edaa2e657ef7d296d158ac8c75878949 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Fri, 8 Nov 2024 14:27:25 +0100 Subject: [PATCH 14/21] change some internal functions to private --- lib/backpex/live_resource.ex | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index e95bb980..f1164fc4 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -611,14 +611,14 @@ defmodule Backpex.LiveResource do {:noreply, socket} end - def apply_action(socket, :index) do + defp apply_action(socket, :index) do socket |> assign(:page_title, socket.assigns.plural_name) |> apply_index() |> assign(:item, nil) end - def apply_action(socket, :edit) do + defp apply_action(socket, :edit) do %{ live_resource: live_resource, singular_name: singular_name @@ -638,7 +638,7 @@ defmodule Backpex.LiveResource do |> assign_changeset(fields) end - def apply_action(socket, :show) do + defp apply_action(socket, :show) do %{ live_resource: live_resource, singular_name: singular_name @@ -657,7 +657,7 @@ defmodule Backpex.LiveResource do |> apply_show_return_to(item) end - def apply_action(socket, :new) do + defp apply_action(socket, :new) do %{ live_resource: live_resource, schema: schema, @@ -677,7 +677,7 @@ defmodule Backpex.LiveResource do |> assign_changeset(fields) end - def apply_action(socket, :resource_action) do + defp apply_action(socket, :resource_action) do %{live_resource: live_resource} = socket.assigns id = @@ -699,12 +699,12 @@ defmodule Backpex.LiveResource do |> assign_changeset(action.module.fields()) end - def apply_item_actions(socket, action) when action in [:index, :resource_action] do + defp apply_item_actions(socket, action) when action in [:index, :resource_action] do item_actions = socket.assigns.live_resource.item_actions(default_item_actions()) assign(socket, :item_actions, item_actions) end - def apply_item_actions(socket, _action), do: socket + defp apply_item_actions(socket, _action), do: socket defp apply_index_return_to(socket) do %{live_resource: live_resource, params: params, query_options: query_options} = socket.assigns @@ -1404,10 +1404,7 @@ defmodule Backpex.LiveResource do def orderable?(field) when is_nil(field), do: false def orderable?({_name, field_options}), do: Map.get(field_options, :orderable, true) - @doc """ - TODO: make private? - """ - def build_criteria(assigns) do + defp build_criteria(assigns) do %{ schema: schema, fields: fields, @@ -1576,7 +1573,7 @@ defmodule Backpex.LiveResource do if value in permitted, do: value, else: default end - def default_item_actions do + defp default_item_actions do [ show: %{ module: Backpex.ItemActions.Show, @@ -1593,11 +1590,11 @@ defmodule Backpex.LiveResource do ] end - def maybe_put_empty_filter(%{} = filters, empty_filter_key) when filters == %{} do + defp maybe_put_empty_filter(%{} = filters, empty_filter_key) when filters == %{} do Map.put(filters, Atom.to_string(empty_filter_key), true) end - def maybe_put_empty_filter(filters, _empty_filter_key) do + defp maybe_put_empty_filter(filters, _empty_filter_key) do filters end @@ -1621,7 +1618,7 @@ defmodule Backpex.LiveResource do end) end - def get_valid_filters_from_params(%{"filters" => filters} = params, valid_filters, empty_filter_key) do + defp get_valid_filters_from_params(%{"filters" => filters} = params, valid_filters, empty_filter_key) do valid_filters = Keyword.put(valid_filters, empty_filter_key, %{}) filters = @@ -1641,7 +1638,7 @@ defmodule Backpex.LiveResource do Map.put(params, "filters", filters) end - def get_valid_filters_from_params(_params, _valid_filters, _empty_filter_key), do: %{} + defp get_valid_filters_from_params(_params, _valid_filters, _empty_filter_key), do: %{} defp maybe_to_atom(nil), do: nil defp maybe_to_atom(value), do: String.to_existing_atom(value) From 35f759952d6c9765c676dcf68802b24e418d956d Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Thu, 14 Nov 2024 10:46:20 +0100 Subject: [PATCH 15/21] Apply suggestions from code review Co-authored-by: Florian Arens <60519307+Flo0807@users.noreply.github.com> --- lib/backpex/live_resource.ex | 62 ++++++++++-------------------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index f1164fc4..a2a55619 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -514,22 +514,14 @@ defmodule Backpex.LiveResource do defp field_active?(name, saved_fields) do case Map.get(saved_fields, Atom.to_string(name)) do - "true" -> - true - - "false" -> - false - - _other -> - true + "true" -> true + "false" -> false + _other -> true end end def assign_items(socket) do - %{ - live_resource: live_resource, - fields: fields - } = socket.assigns + %{live_resource: live_resource, fields: fields} = socket.assigns criteria = build_criteria(socket.assigns) items = Resource.list(fields, socket.assigns, live_resource, criteria) @@ -586,7 +578,7 @@ defmodule Backpex.LiveResource do end @impl Phoenix.LiveView - def render(%{live_action: action} = assigns) when action in [:show, :show_edit] do + def render(%{live_action: action} = assigns) when action in [:show] do resource_show(assigns) end @@ -619,16 +611,13 @@ defmodule Backpex.LiveResource do end defp apply_action(socket, :edit) do - %{ - live_resource: live_resource, - singular_name: singular_name - } = socket.assigns + %{live_resource: live_resource, singular_name: singular_name} = socket.assigns fields = live_resource.fields |> filtered_fields_by_action(socket.assigns, :edit) primary_value = URI.decode(socket.assigns.params["backpex_id"]) item = Resource.get!(primary_value, socket.assigns, live_resource) - unless live_resource.can?(socket.assigns, :edit, item), do: raise(Backpex.ForbiddenError) + if not live_resource.can?(socket.assigns, :edit, item), do: raise(Backpex.ForbiddenError) socket |> assign(:fields, fields) @@ -639,16 +628,13 @@ defmodule Backpex.LiveResource do end defp apply_action(socket, :show) do - %{ - live_resource: live_resource, - singular_name: singular_name - } = socket.assigns + %{live_resource: live_resource, singular_name: singular_name} = socket.assigns fields = live_resource.fields() |> filtered_fields_by_action(socket.assigns, :show) primary_value = URI.decode(socket.assigns.params["backpex_id"]) item = Resource.get!(primary_value, socket.assigns, live_resource) - unless live_resource.can?(socket.assigns, :show, item), do: raise(Backpex.ForbiddenError) + if not live_resource.can?(socket.assigns, :show, item), do: raise(Backpex.ForbiddenError) socket |> assign(:page_title, singular_name) @@ -658,13 +644,9 @@ defmodule Backpex.LiveResource do end defp apply_action(socket, :new) do - %{ - live_resource: live_resource, - schema: schema, - create_button_label: create_button_label - } = socket.assigns + %{live_resource: live_resource, schema: schema, create_button_label: create_button_label} = socket.assigns - unless live_resource.can?(socket.assigns, :new, nil), do: raise(Backpex.ForbiddenError) + if not live_resource.can?(socket.assigns, :new, nil), do: raise(Backpex.ForbiddenError) fields = live_resource.fields() |> filtered_fields_by_action(socket.assigns, :new) empty_item = schema.__struct__() @@ -687,7 +669,7 @@ defmodule Backpex.LiveResource do action = live_resource.resource_actions()[id] - unless live_resource.can?(socket.assigns, id, nil), do: raise(Backpex.ForbiddenError) + if not live_resource.can?(socket.assigns, id, nil), do: raise(Backpex.ForbiddenError) socket |> assign(:page_title, ResourceAction.name(action, :title)) @@ -730,7 +712,7 @@ defmodule Backpex.LiveResource do params: params } = socket.assigns - unless live_resource.can?(socket.assigns, :index, nil), do: raise(Backpex.ForbiddenError) + if not live_resource.can?(socket.assigns, :index, nil), do: raise(Backpex.ForbiddenError) fields = live_resource.fields() |> filtered_fields_by_action(socket.assigns, :index) @@ -788,11 +770,7 @@ defmodule Backpex.LiveResource do end defp assign_changeset(socket, fields) do - %{ - item: item, - changeset_function: changeset_function, - live_action: live_action - } = socket.assigns + %{item: item, changeset_function: changeset_function, live_action: live_action} = socket.assigns metadata = Resource.build_changeset_metadata(socket.assigns) changeset = changeset_function.(item, default_attrs(live_action, fields, socket.assigns), metadata) @@ -830,12 +808,7 @@ defmodule Backpex.LiveResource do defp default_attrs(_live_action, _fields, _assigns), do: %{} defp maybe_redirect_to_default_filters(%{assigns: %{filters_changed: false}} = socket) do - %{ - live_resource: live_resource, - query_options: query_options, - params: params, - filters: filters - } = socket.assigns + %{live_resource: live_resource, query_options: query_options, params: params, filters: filters} = socket.assigns filters_with_defaults = filters @@ -1168,10 +1141,7 @@ defmodule Backpex.LiveResource do end defp update_item(socket, item) do - %{ - live_resource: live_resource, - live_action: live_action - } = socket.assigns + %{live_resource: live_resource, live_action: live_action} = socket.assigns item_primary_value = primary_value(socket, item) item = Resource.get(item_primary_value, socket.assigns, live_resource) From d078869a5a92414af8f2c58701c7731eb9b085c2 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Thu, 14 Nov 2024 11:15:14 +0100 Subject: [PATCH 16/21] use correct live_resource of association --- lib/backpex/fields/has_many.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/backpex/fields/has_many.ex b/lib/backpex/fields/has_many.ex index 3c9f1422..8a6b87d0 100644 --- a/lib/backpex/fields/has_many.ex +++ b/lib/backpex/fields/has_many.ex @@ -335,13 +335,12 @@ defmodule Backpex.Fields.HasMany do socket: socket, field_options: field_options, item: item, - live_resource: live_resource, params: params, link_assocs: link_assocs } = assigns link = - if link_assocs and Map.has_key?(field_options, :live_resource) and live_resource.can?(assigns, :show, item) do + if link_assocs and field_options.live_resource.can?(assigns, :show, item) do Router.get_path(socket, Map.get(field_options, :live_resource), params, :show, item) else nil From 778d592648af45ecca8a78005b67be4aeaf74ac0 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Thu, 14 Nov 2024 11:20:04 +0100 Subject: [PATCH 17/21] remove module attribute for empty_filter_key --- lib/backpex/adapters/ecto.ex | 2 +- lib/backpex/live_resource.ex | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/backpex/adapters/ecto.ex b/lib/backpex/adapters/ecto.ex index c93a1cf7..d02742c7 100644 --- a/lib/backpex/adapters/ecto.ex +++ b/lib/backpex/adapters/ecto.ex @@ -132,7 +132,7 @@ defmodule Backpex.Adapters.Ecto do |> maybe_preload(associations, fields) |> maybe_merge_dynamic_fields(fields) |> apply_search(schema, full_text_search, criteria[:search]) - |> apply_filters(criteria[:filters], Backpex.LiveResource.get_empty_filter_key()) + |> apply_filters(criteria[:filters], Backpex.LiveResource.empty_filter_key()) |> apply_criteria(criteria, fields) end diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index a2a55619..85bf81c4 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -14,8 +14,6 @@ defmodule Backpex.LiveResource do alias Backpex.ResourceAction alias Backpex.Router - @empty_filter_key :empty_filter - @permitted_order_directions ~w(asc desc)a @options_schema [ @@ -721,7 +719,7 @@ defmodule Backpex.LiveResource do init_order = live_resource.config(:init_order) filters = active_filters(socket.assigns) - valid_filter_params = get_valid_filters_from_params(params, filters, @empty_filter_key) + valid_filter_params = get_valid_filters_from_params(params, filters, empty_filter_key()) count_criteria = [ search: search_options(params, fields, schema), @@ -917,7 +915,7 @@ defmodule Backpex.LiveResource do def handle_event("change-filter", params, socket) do query_options = socket.assigns.query_options - empty_filter_name = Atom.to_string(@empty_filter_key) + empty_filter_name = Atom.to_string(empty_filter_key()) filters = Map.get(query_options, :filters, %{}) @@ -957,7 +955,7 @@ defmodule Backpex.LiveResource do :filters, Map.get(query_options, :filters, %{}) |> Map.delete(field) - |> maybe_put_empty_filter(@empty_filter_key) + |> maybe_put_empty_filter(empty_filter_key()) ) to = Router.get_path(socket, live_resource, params, :index, new_query_options) @@ -986,7 +984,7 @@ defmodule Backpex.LiveResource do filters = Map.get(query_options, :filters, %{}) |> Map.put(field, get_preset_values.()) - |> Map.drop([Atom.to_string(@empty_filter_key)]) + |> Map.drop([Atom.to_string(empty_filter_key())]) to = Router.get_path( @@ -1120,7 +1118,7 @@ defmodule Backpex.LiveResource do } = socket.assigns filters = active_filters(socket.assigns) - valid_filter_params = get_valid_filters_from_params(params, filters, @empty_filter_key) + valid_filter_params = get_valid_filters_from_params(params, filters, empty_filter_key()) count_criteria = [ search: search_options(params, fields, schema), @@ -1355,7 +1353,7 @@ defmodule Backpex.LiveResource do def filter_options(_no_filters_present, _filter_configs), do: %{} - def get_empty_filter_key, do: @empty_filter_key + def empty_filter_key, do: :empty_filter @doc """ Checks whether a field is orderable or not. @@ -1574,7 +1572,7 @@ defmodule Backpex.LiveResource do def get_filter_options(query_options) do query_options |> Map.get(:filters, %{}) - |> Map.drop([Atom.to_string(get_empty_filter_key())]) + |> Map.drop([Atom.to_string(empty_filter_key())]) end @doc """ @@ -1584,7 +1582,7 @@ defmodule Backpex.LiveResource do filters = assigns.live_resource.filters(assigns) Enum.filter(filters, fn {key, option} -> - get_empty_filter_key() != key and option.module.can?(assigns) + empty_filter_key() != key and option.module.can?(assigns) end) end From d6c8c1e056ed7d8726c3b8c91da7a4a26ae6aca8 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Thu, 14 Nov 2024 18:08:55 +0100 Subject: [PATCH 18/21] add upgrade guide note for changed LiveResource functions --- guides/upgrading/v0.9.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/guides/upgrading/v0.9.md b/guides/upgrading/v0.9.md index df276a24..08b270a4 100644 --- a/guides/upgrading/v0.9.md +++ b/guides/upgrading/v0.9.md @@ -12,7 +12,6 @@ Update Backpex to the latest version: end ``` - ## Refactor calls to [`Backpex.HTML.Form.field_input/1`]() We've refactored the [`Backpex.HTML.Form.field_input/1`]() component and renamed it to `Backpex.HTML.Form.input/1`. @@ -65,4 +64,9 @@ def render_form(assigns) do
""" end -``` \ No newline at end of file +``` + +## `Backpex.LiveResource` function usage + +Although the change is relatively small, if you are using public functions of the `Backpex.LiveResource` directly, +check the updated function definitions in the module documentation. From 1895d00e2bc8d8d5c0624f2596f14f27f893625c Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Thu, 14 Nov 2024 18:14:22 +0100 Subject: [PATCH 19/21] remove module attribute for permitted_order_directions --- lib/backpex/live_resource.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index 85bf81c4..d0d1d2e9 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -14,8 +14,6 @@ defmodule Backpex.LiveResource do alias Backpex.ResourceAction alias Backpex.Router - @permitted_order_directions ~w(asc desc)a - @options_schema [ adapter: [ doc: "The data layer adapter to use.", @@ -738,7 +736,7 @@ defmodule Backpex.LiveResource do page_options = %{page: page, per_page: per_page} - order_options = order_options_by_params(params, fields, init_order, socket.assigns, @permitted_order_directions) + order_options = order_options_by_params(params, fields, init_order, socket.assigns) query_options = page_options @@ -1233,7 +1231,7 @@ defmodule Backpex.LiveResource do iex> Backpex.LiveResource.order_options_by_params(%{"order_by" => "field", "order_direction" => "asc"}, [field: %{orderable: false}], %{by: :id, direction: :asc}, %{}, [:asc, :desc]) %{order_by: :id, order_direction: :asc} """ - def order_options_by_params(params, fields, init_order, assigns, permitted_order_directions) do + def order_options_by_params(params, fields, init_order, assigns) do init_order = resolve_init_order(init_order, assigns) order_by = @@ -1250,13 +1248,15 @@ defmodule Backpex.LiveResource do |> Map.get("order_direction") |> maybe_to_atom() |> value_in_permitted_or_default( - permitted_order_directions, + permitted_order_directions(), Map.get(init_order, :direction) ) %{order_by: order_by, order_direction: order_direction} end + defp permitted_order_directions(), do: ~w(asc desc)a + @doc """ Returns all orderable fields. A field is orderable by default. From 05049b8f2d2a3aeb91ad1c441f307c56372124c4 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Thu, 14 Nov 2024 18:16:16 +0100 Subject: [PATCH 20/21] fix order_options_by_params doctest --- lib/backpex/live_resource.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index d0d1d2e9..f51a8c74 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -1224,11 +1224,11 @@ defmodule Backpex.LiveResource do ## Examples - iex> Backpex.LiveResource.order_options_by_params(%{"order_by" => "field", "order_direction" => "asc"}, [field: %{}], %{by: :id, direction: :asc}, %{}, [:asc, :desc]) + iex> Backpex.LiveResource.order_options_by_params(%{"order_by" => "field", "order_direction" => "asc"}, [field: %{}], %{by: :id, direction: :asc}, %{}) %{order_by: :field, order_direction: :asc} - iex> Backpex.LiveResource.order_options_by_params(%{}, [field: %{}], %{by: :id, direction: :desc}, %{}, [:asc, :desc]) + iex> Backpex.LiveResource.order_options_by_params(%{}, [field: %{}], %{by: :id, direction: :desc}, %{}) %{order_by: :id, order_direction: :desc} - iex> Backpex.LiveResource.order_options_by_params(%{"order_by" => "field", "order_direction" => "asc"}, [field: %{orderable: false}], %{by: :id, direction: :asc}, %{}, [:asc, :desc]) + iex> Backpex.LiveResource.order_options_by_params(%{"order_by" => "field", "order_direction" => "asc"}, [field: %{orderable: false}], %{by: :id, direction: :asc}, %{}) %{order_by: :id, order_direction: :asc} """ def order_options_by_params(params, fields, init_order, assigns) do From 85a1f3d466e30410b8fc0cbbda6d721a6b1606a3 Mon Sep 17 00:00:00 2001 From: Phil-Bastian Berndt Date: Thu, 14 Nov 2024 18:19:44 +0100 Subject: [PATCH 21/21] please credo --- lib/backpex/live_resource.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index f51a8c74..cf1a2ec6 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -1255,7 +1255,7 @@ defmodule Backpex.LiveResource do %{order_by: order_by, order_direction: order_direction} end - defp permitted_order_directions(), do: ~w(asc desc)a + defp permitted_order_directions, do: ~w(asc desc)a @doc """ Returns all orderable fields. A field is orderable by default.