defmodule ExAdmin.Register do @moduledoc """ Allows registering a resource or a page to be displayed with ExAdmin. For each model you wanted rendered by ExAdmin, use the `register_resource` call. For each general page (like a dashboard), use the `register_page` call. To allow ExAdmin to manage the resource with defaults, do not place any additional code in the block of `register_resource`. ## Examples Register the Survey.Answer model with all defaults. defmodule Survey.ExAdmin.Answer do use ExAdmin.Register register_resource Survey.Answer do end end ## Commands available in the register_resource do block * `menu` - Customize the properties of the menu item * `index` - Customize the index page * `show` - Customize the show page * `form` - Customize the form page * `query` - Customize the `Ecto` queries for each page * `options` - Change various options for a resource * `member_action` - Add a custom action for id based requests * `filter` - Disable/Customize the filter pages * `controller` - Override the default controller * `action_items` - Define which actions are available for a resource * `batch_actions` - Customize the batch_actions shown on the index page * `csv` - Customize the csv export file * `collection_action` - Add a custom action for collection based requests * `clear_action_items!` - Remove the action item buttons * `action_item` - Defines custom action items * `changesets` - Defines custom changeset functions """ if File.dir?("/tmp") do @filename "/tmp/ex_admin_registered" else @filename System.tmp_dir <> "/ex_admin_registered" end import ExAdmin.Utils import ExAdmin.DslUtils defmacro __using__(_) do quote do use ExAdmin.Index, except: [actions: 1] use ExAdmin.Show use ExAdmin.Form, except: [actions: 1] use ExAdmin.CSV import unquote(__MODULE__) import Phoenix.HTML.Tag import Ecto.Query, only: [from: 2] import Xain, except: [input: 1, input: 2, input: 3, menu: 1, form: 2] import ExAdmin.ViewHelpers Module.register_attribute __MODULE__, :member_actions, accumulate: true, persist: true Module.register_attribute __MODULE__, :collection_actions, accumulate: true, persist: true end end File.rm @filename File.touch @filename @doc """ Register an Ecto model. Once registered, ExAdmin adds the resource to the administration pages. If no additional code is added to the do block, the resource will be rendered with defaults, including: * A paginated index page listing all columns in the model's database table * A details page (show) listing fields and simple associations * New and edit pages * A menu item * A CSV export link on the index page # Default Association Rendering ExAdmin will render an association using the following algorithm in the following order: * Look for a `:name` field in the association * Look for a display_name/1 function in the Admin Resource Module * Look for a display_name/1 function in the Model's Module * Use the 2nd field in the Model's schema """ defmacro register_resource(mod, [do: block]) do quote location: :keep do import ExAdmin.ViewHelpers import ExAdmin.Utils require Logger @all_options [:edit, :show, :new, :delete] Module.register_attribute __MODULE__, :query, accumulate: false, persist: true Module.register_attribute __MODULE__, :index_filters, accumulate: true, persist: true Module.register_attribute __MODULE__, :batch_actions, accumulate: true, persist: true Module.register_attribute __MODULE__, :selectable_column, accumulate: false, persist: true Module.register_attribute(__MODULE__, :form_items, accumulate: true, persist: true) Module.register_attribute(__MODULE__, :controller_plugs, accumulate: true, persist: true) Module.register_attribute(__MODULE__, :sidebars, accumulate: true, persist: true) Module.register_attribute(__MODULE__, :scopes, accumulate: true, persist: true) Module.register_attribute __MODULE__, :actions, accumulate: true, persist: true Enum.each @all_options, &(Module.put_attribute __MODULE__, :actions, &1) module = unquote(mod) Module.put_attribute(__MODULE__, :module, module) Module.put_attribute(__MODULE__, :query, nil) Module.put_attribute(__MODULE__, :selectable_column, nil) Module.put_attribute(__MODULE__, :changesets, []) @name_column Module.get_attribute(__MODULE__, :name_column) || apply(ExAdmin.Helpers, :get_name_field, [module]) alias unquote(mod) import Ecto.Query def config do apply __MODULE__, :__struct__, [] end unquote(block) query_opts = case Module.get_attribute(__MODULE__, :query) do nil -> list = module.__schema__(:associations) |> Enum.map(&(ExAdmin.Register.build_query_association module, &1)) |> Enum.filter(&(not is_nil(&1))) query = %{all: [preload: list]} Module.put_attribute __MODULE__, :query, query query other -> other end controller = case Module.get_attribute(__MODULE__, :controller) do nil -> controller_mod = String.to_atom("#{module}Controller") Module.put_attribute(__MODULE__, :controller, controller_mod) other -> Logger.warn "Should not get here - controller: #{inspect other}" end menu_opts = case Module.get_attribute(__MODULE__, :menu) do false -> %{none: true} nil -> %{ priority: 10, label: (base_name(module) |> Inflex.pluralize)} other -> Enum.into other, %{} end controller_route = (base_name(module) |> Inflex.underscore |> Inflex.pluralize) controller_route = case Module.get_attribute(__MODULE__, :options) do nil -> controller_route options -> Keyword.get(options, :controller_route, controller_route) end plugs = case Module.get_attribute(__MODULE__, :controller_plugs) do nil -> [] list -> Enum.reverse list end sidebars = case Module.get_attribute(__MODULE__, :sidebars) do nil -> [] list -> Enum.reverse list end scopes = case Module.get_attribute(__MODULE__, :scopes) do nil -> [] list -> Enum.reverse list end controller_filters = (Module.get_attribute(__MODULE__, :controller_filters) || []) |> ExAdmin.Helpers.group_reduce_by_reverse action_labels = ExAdmin.Register.get_action_labels(Module.get_attribute(__MODULE__, :actions)) actions = ExAdmin.Register.get_action_items(Module.get_attribute(__MODULE__, :actions), @all_options) |> ExAdmin.Register.custom_action_actions(Module.get_attribute(__MODULE__, :member_actions), module, :member_actions) |> ExAdmin.Register.custom_action_actions(Module.get_attribute(__MODULE__, :collection_actions), module, :collection_actions) defstruct controller: @controller, controller_methods: Module.get_attribute(__MODULE__, :controller_methods), title_actions: &ExAdmin.default_resource_title_actions/2, type: :resource, resource_model: module, resource_name: resource_name(module), query_opts: query_opts, controller_route: controller_route, menu: menu_opts, actions: actions, action_labels: action_labels, member_actions: Module.get_attribute(__MODULE__, :member_actions), collection_actions: Module.get_attribute(__MODULE__, :collection_actions), controller_filters: controller_filters, index_filters: Module.get_attribute(__MODULE__, :index_filters), selectable_column: Module.get_attribute(__MODULE__, :selectable_column), position_column: Module.get_attribute(__MODULE__, :position_column), name_column: @name_column, batch_actions: Module.get_attribute(__MODULE__, :batch_actions), changesets: Module.get_attribute(__MODULE__, :changesets), plugs: plugs, sidebars: sidebars, scopes: scopes def run_query(repo, defn, action, id \\ nil) do %__MODULE__{} |> Map.get(:resource_model) |> ExAdmin.Query.run_query(repo, defn, action, id, @query) end def run_query_counts(repo, defn, action, id \\ nil) do %__MODULE__{} |> Map.get(:resource_model) |> ExAdmin.Query.run_query_counts(repo, defn, action, id, @query) end def build_admin_search_query(keywords) do cond do function_exported?(@module, :admin_search_query, 1) -> apply(@module, :admin_search_query, [keywords]) function_exported?(__MODULE__, :admin_search_query, 1) -> apply(__MODULE__, :admin_search_query, [keywords]) true -> suggest_admin_search_query(keywords) end end defp suggest_admin_search_query(keywords) do field = @name_column query = from r in @module, order_by: ^field case keywords do nil -> query "" -> query keywords -> from r in query, where: ilike(field(r, ^field), ^("%#{keywords}%")) end end def changeset_fn(defn, action) do Keyword.get(defn.changesets, action, &defn.resource_model.changeset/2) end def plugs(), do: @controller_plugs File.write!(unquote(@filename), "#{__MODULE__}\n", [:append]) end end @doc false def get_action_labels(nil), do: [] def get_action_labels([opts|_]) when is_list(opts) do opts[:labels] || [] end def get_action_labels(_), do: [] @doc false def get_action_items(nil, _), do: [] def get_action_items(actions, all_options) when is_list(actions) do {atoms, keywords} = List.flatten(actions) |> Enum.reduce({[], []}, fn atom, {acca, acck} when is_atom(atom) -> {[atom | acca], acck} kw, {acca, acck} -> {acca, [kw | acck]} end) atoms = Enum.reverse atoms keywords = Enum.reverse Keyword.drop(keywords, [:labels]) cond do keywords[:only] && keywords[:except] -> raise "options :only and :except cannot be used together" keywords[:only] -> Keyword.delete(keywords, :only) ++ keywords[:only] keywords[:except] -> Keyword.delete(keywords, :except) ++ all_options -- keywords[:except] true -> keywords ++ atoms end end def custom_action_actions(actions, custom_actions, module, type) do custom_actions |> Enum.reduce(actions, fn {name, opts}, acc -> fun = quote do name = unquote(name) human_name = case unquote(opts)[:opts][:label] do nil -> humanize name label -> label end module = unquote(module) type = unquote(type) if type == :member_actions do fn id -> resource = struct(module.__struct__, id: id) url = ExAdmin.Utils.admin_resource_path(resource, :member, [name]) ExAdmin.ViewHelpers.action_item_link human_name, href: url, "data-method": :put end else fn id -> resource = module url = ExAdmin.Utils.admin_resource_path(resource, :collection, [name]) ExAdmin.ViewHelpers.action_item_link human_name, href: url end end end action = if type == :member_actions, do: :show, else: :index [{action, fun} | acc] end) end @doc """ Override the controller for a resource. Allows custom actions, filters, and plugs for the controller. Commands in the controller block include: * `define_method` - Create a controller action with the body of the action * `before_filter` - Add a before_filter to the controller * `after_filter` - Add an after callback to the controller * `redirect_to` - Redirects to another page * `plug` - Add a plug to the controller """ defmacro controller([do: block]) do quote do Module.register_attribute(__MODULE__, :controller_methods, accumulate: false, persist: true) Module.register_attribute(__MODULE__, :controller_filters, accumulate: true, persist: true) Module.put_attribute(__MODULE__, :controller_methods, []) unquote(block) end end defmacro controller(controller_mod) do quote do Module.put_attribute __MODULE__, :controller, unquote(controller_mod) end end @doc """ Override an action on a controller. Allows the customization of controller actions. ## Examples Override the index action to redirect to the root page. controller do define_method(:index) do redirect_to "/" end end """ defmacro define_method(name, [do: block]) do quote do methods = Module.get_attribute(__MODULE__, :controller_methods) Module.put_attribute(__MODULE__, :controller_methods, [{unquote(name), []} | methods]) unquote(block) end end @doc """ Add a before_filter to a controller. The before filter is executed before the controller action(s) are executed. Normally, the function should return the conn struct. However, if you want to modify the params, then return the tuple `{conn, new_parms}`. ## Examples The following example illustrates how to add a sync action that will be run before the index page is loaded. controller do before_filter :sync, only: [:index] def sync(conn, _) do BackupRestore.sync conn end end controller do before_filter :no_change, except: [:create, :modify] def no_change(conn, params) do {conn, put_in(params, [:setting, :no_mod], true)} end end """ defmacro before_filter(name, opts \\ []) do quote location: :keep do Module.put_attribute(__MODULE__, :controller_filters, {:before_filter, {unquote(name), unquote(opts)}}) end end @doc """ Add an after filter to a controller. The after filter is executed after the controller action(s) are executed and before the page is rendered/redirected. In the case of `update` and `create`, it is only called on success. Normally, the function should return the conn struct. However, you can also return a `{conn, params, resource}` to modify the params and resource. ## Examples controller do after_filter :do_after, only: [:create, :update] def do_after(conn, params, resource, :create) do user = Repo.all(User) |> hd resource = Product.changeset(resource, %{user_id: user.id}) |> Repo.update! {Plug.Conn.assign(conn, :product, resource), params, resource} end def do_after(conn, _params, _resource, :update) do Plug.Conn.assign(conn, :answer, 42) end end """ defmacro after_filter(name, opts \\ []) do quote location: :keep do Module.put_attribute(__MODULE__, :controller_filters, {:after_filter, {unquote(name), unquote(opts)}}) end end @doc """ Override the changeset function for `update` and `create` actions. By default, `changeset/2` for the resource will be used. ## Examples The following example illustrates how to add a sync action that will be run before the index page is loaded. changesets create: &__MODULE__.create_changeset/2, update: &__MODULE__.update_changeset/2 def create_changeset(model, params) do Ecto.Changeset.cast(model, params, ~w(name password), ~w(age)) end def update_changeset(model, params) do Ecto.Changeset.cast(model, params, ~w(name), ~w(age password)) end """ defmacro changesets(opts) do quote location: :keep do Module.put_attribute(__MODULE__, :changesets, unquote(opts)) end end @doc """ Redirect to a given path. Use this command in a controller block to redirect to another page. """ defmacro redirect_to(path) do quote do [{name, opts} | tail] = Module.get_attribute(__MODULE__, :controller_methods) new_opts = [{:redirect_to, unquote(path)} | opts] Module.put_attribute(__MODULE__, :controller_methods, [{name, new_opts} | tail]) end end @doc """ Add a plug to the controller. Add custom plugs to a controller. ## Example controller do plug :my_plug, the_answer: 42 end """ defmacro plug(name, opts \\ []) do quote do Module.put_attribute(__MODULE__, :controller_plugs, {unquote(name), unquote(opts)}) end end @doc """ Register a static page. Use `register_page` to create a static page, like a dashboard, or welcome page to the admin interface. See the default dashboard page for an example. """ defmacro register_page(name, [do: block]) do quote location: :keep do import ExAdmin.Register, except: [column: 1] use ExAdmin.Page Module.register_attribute __MODULE__, :query, accumulate: false, persist: true Module.register_attribute __MODULE__, :index_filters, accumulate: true, persist: true Module.register_attribute __MODULE__, :batch_actions, accumulate: true, persist: true Module.register_attribute __MODULE__, :selectable_column, accumulate: false, persist: true Module.register_attribute __MODULE__, :form_items, accumulate: true, persist: true Module.register_attribute(__MODULE__, :sidebars, accumulate: true, persist: true) Module.put_attribute __MODULE__, :controller_plugs, nil page_name = unquote(name) unquote(block) # query_opts = Module.get_attribute(__MODULE__, :query) menu_opts = case Module.get_attribute(__MODULE__, :menu) do false -> %{none: true} nil -> %{label: page_name, priority: 99} other -> Enum.into other, %{} end controller_methods = Module.get_attribute(__MODULE__, :controller_methods) page_name = Kernel.to_string(page_name) plugs = case Module.get_attribute(__MODULE__, :controller_plugs) do nil -> [] list -> Enum.reverse list end sidebars = case Module.get_attribute(__MODULE__, :sidebars) do nil -> [] list -> Enum.reverse list end defstruct controller: Module.concat(Application.get_env(:ex_admin_runtime, :project), AdminController), controller_methods: Module.get_attribute(__MODULE__, :controller_methods), type: :page, page_name: page_name, title_actions: &ExAdmin.default_page_title_actions/2, controller_route: (page_name |> Inflex.parameterize("_")), menu: menu_opts, member_actions: Module.get_attribute(__MODULE__, :member_actions), collection_actions: Module.get_attribute(__MODULE__, :collection_actions), controller_filters: Module.get_attribute(__MODULE__, :controller_filters), index_filters: [false], # selectable_column: Module.get_attribute(__MODULE__, :selectable_column), batch_actions: Module.get_attribute(__MODULE__, :batch_actions), plugs: plugs, sidebars: sidebars, scopes: [] def plugs(), do: @controller_plugs File.write!(unquote(@filename), "#{__MODULE__}\n", [:append]) end end @doc """ Add a sidebar to the page. The available options are: * `:only` - Filters the list of actions for the filter. * `:except` - Filters out actions in the except atom or list. ## Examples sidebar "ExAdmin Demo", only: [:index, :show] do Phoenix.View.render ExAdminDemo.AdminView, "sidebar_links.html", [] end sidebar :Orders, only: :show do attributes_table_for resource do row "title", fn(_) -> { resource.title } end row "author", fn(_) -> { resource.author } end end end # customize the panel sidebar "Expert Administration", box_attributes: ".box.box-warning", header_attributes: ".box-header.with-border.text-yellow" do Phoenix.View.render MyApp.AdminView, "sidebar_warning.html", [] end """ defmacro sidebar(name, opts \\ [], [do: block]) do contents = quote do unquote(block) end quote location: :keep, bind_quoted: [name: escape(name), opts: escape(opts), contents: escape(contents)] do fun_name = "side_bar_#{name}" |> String.replace(" ", "_") |> String.to_atom def unquote(fun_name)(var!(conn), var!(resource)) do _ = var!(conn) _ = var!(resource) unquote(contents) end Module.put_attribute __MODULE__, :sidebars, {name, opts, {__MODULE__, fun_name}} end end @doc """ Scope the index page. ## Examples scope :all, default: true scope :available, fn(q) -> now = Ecto.Date.utc where(q, [p], p.available_on <= ^now) end scope :drafts, fn(q) -> now = Ecto.Date.utc where(q, [p], p.available_on > ^now) end scope :featured_products, [], fn(q) -> where(q, [p], p.featured == true) end scope :featured """ defmacro scope(name) do quote location: :keep do Module.put_attribute __MODULE__, :scopes, {unquote(name), []} end end defmacro scope(name, opts_or_fun) do quote location: :keep do opts_or_fun = unquote(opts_or_fun) if is_function(opts_or_fun) do scope unquote(name), [], unquote(opts_or_fun) else Module.put_attribute __MODULE__, :scopes, {unquote(name), opts_or_fun} end end end defmacro scope(name, opts, fun) do contents = quote do unquote(fun) end quote location: :keep, bind_quoted: [name: escape(name), opts: escape(opts), contents: escape(contents)] do fun_name = "scope_#{name}" |> String.replace(" ", "_") |> String.to_atom def unquote(fun_name)(var!(resource)) do unquote(contents).(var!(resource)) end opts = [{:fun, {__MODULE__, fun_name}} | opts] Module.put_attribute __MODULE__, :scopes, {name, opts} end end @doc """ Customize the resource admin page by setting options for the page. The available actions are: * TBD """ defmacro options(opts) do quote do Module.put_attribute __MODULE__, :options, unquote(opts) end end @doc """ Customize the menu of a page. The available options are: * `:priority` - Sets the position of the menu, with 0 being the left most menu item * `:label` - The name used in the menu * `:if` - Only display the menu item if the condition returns non false/nil * `:url` - The custom URL used in the menu link ## Examples The following example adds a custom label, sets the priority, and is only displayed if the current user is a superadmin. menu label: "Backup & Restore", priority: 14, if: &__MODULE__.is_superadmin/1 This example disables the menu item: menu false """ defmacro menu(opts) do quote do Module.put_attribute __MODULE__, :menu, unquote(opts) end end @doc """ Add query options to the Ecto queries. For the most part, use `query` to setup preload options. Query customization can be done for all pages, or individually specified. ## Examples Load the belongs_to :category, has_many :phone_numbers, and the has_many :groups for all pages for the resource. query do %{ all: [preload: [:category, :phone_numbers, :groups]], } end Load the has_many :contacts association, as well as the has_many :phone_numbers of the contact query do %{show: [preload: [contacts: [:phone_numbers]]] } end A more complicated example that defines a default preload, with a more specific preload for the show page. query do %{ all: [preload: [:group]], show: [preload: [:group, messages: [receiver: [:category, :phone_numbers]]]] } end Change the index page default sort order to ascending. query do %{index: [default_sort_order: :asc]} end Change the index page default sort field and order. query do %{index: [default_sort: [asc: :name]]} end Change the index page default sort field. query do %{index: [default_sort_field: :name]} end """ defmacro query(do: qry) do quote do Module.put_attribute __MODULE__, :query, unquote(qry) end end @doc """ Add a column to a table. Can be used on the index page, or in the table attributes on the show page. A number of options are valid: * `label` - Change the name of the column heading * `fields` - Add the fields to be included in an association * `link` - Set to true to add a link to an association * `fn/1` - An anonymous function to be called to render the field * `collection` - Add the collection for a belongs_to association """ defmacro column(name, opts \\ [], fun \\ nil) do quote do opts = ExAdmin.DslUtils.fun_to_opts unquote(opts), unquote(fun) var!(columns, ExAdmin.Show) = [{unquote(name), (opts)} | var!(columns, ExAdmin.Show)] end end @doc """ Drag&drop control for sortable tables. `fa_icon_name` is one of [Font Awesome icons](https://fortawesome.github.io/Font-Awesome/icons/), default - ["bars"](http://fortawesome.github.io/Font-Awesome/icon/bars/) """ defmacro sort_handle_column(fa_icon_name \\ "bars") do quote do column "", [], fn(_) -> i "", class: "fa fa-#{unquote(fa_icon_name)} handle", "aria-hidden": "true" end end end @doc """ Add a row to the attributes table on the show page. See `column/3` for a list of options. """ defmacro row(name, opts \\ [], fun \\ nil) do quote do opts = ExAdmin.DslUtils.fun_to_opts unquote(opts), unquote(fun) var!(rows, ExAdmin.Show) = [{unquote(name), opts} | var!(rows, ExAdmin.Show)] end end @doc """ Add a link to a path """ defmacro link_to(name, path, opts \\ quote(do: [])) do quote do opts = Keyword.merge [to: unquote(path)], unquote(opts) Phoenix.HTML.Link.link "#{unquote(name)}", opts end end @doc false # Note: `actions/2` has been depreciated. Please use `action_items/1` instead defmacro actions(:all, opts \\ quote(do: [])) do require Logger Logger.warn "actions/2 has been depreciated. Please use action_items/1 instead" quote do opts = unquote(opts) Module.put_attribute __MODULE__, :actions, unquote(opts) end end @doc """ Define which actions will be displayed. Action labels could be overriden with `labels` option. ## Examples action_items except: [:new, :delete, :edit] action_items only: [:new] action_items labels: [delete: "Revoke"] Notes: * this replaces the deprecated `actions/2` macro * `action_items` macro will not remove any custom actions defined by the `action_item` macro. """ defmacro action_items(opts \\ nil) do quote do opts = unquote(opts) Module.put_attribute __MODULE__, :actions, unquote(opts) end end @doc """ Add an id based action and show page link. Member actions are those actions that act on an individual record in the database. ## Examples The following example illustrates how to add a restore action to a backup and restore page. member_action :restore, &__MODULE__.restore_action/2 ... def restore_action(conn, params) do case BackupRestore.restore Repo.get(BackupRestore, params[:id]) do {:ok, filename} -> Controller.put_flash(conn, :notice, "Restore \#{filename} complete.") {:error, message} -> Controller.put_flash(conn, :error, "Restore Failed: \#{message}.") end |> Controller.redirect(to: ExAdmin.Utils.admin_resource_path(conn, :index)) end The above example adds the following: * a custom `restore` action to the controller, accessible by the route /admin/:resource/:id/member/restore * a "Restore" action link to the show page ## Options * an optional label: "Button Label" """ defmacro member_action(name, fun, opts \\ []) do quote do Module.put_attribute __MODULE__, :member_actions, {unquote(name), [fun: unquote(fun), opts: unquote(opts)]} end end @doc """ Add a action that acts on a collection and adds a link to the index page. ## Examples The following example shows how to add a backup action on the index page. collection_action :backup, &__MODULE__.backup_action/2, label: "Backup Database!" def backup_action(conn, _params) do Repo.insert %BackupRestore{} Controller.put_flash(conn, :notice, "Backup complete.") |> Controller.redirect(to: ExAdmin.Utils.admin_resource_path(conn, :index)) end The above example adds the following: * a custom `backup` action to the controller, accessible by the route /admin/:resource/collection/backup * a "Backup Database!" action link to the show page ## Options * an optional label: "Button Label" (shown above) """ defmacro collection_action(name, fun, opts \\ []) do quote do Module.put_attribute __MODULE__, :collection_actions, {unquote(name), [fun: unquote(fun), opts: unquote(opts)]} end end @doc """ Clear the default [:edit, :show, :new, :delete] action items. Can be used alone, or followed with `action_item` to add custom actions. """ defmacro clear_action_items! do quote do Module.delete_attribute __MODULE__, :actions Module.register_attribute __MODULE__, :actions, accumulate: true, persist: true end end @doc """ Add a custom action button to the page. ## Examples The following example demonstrates how to add a custom button to your index page, with no other action buttons due to the `clear_action_items!` call. clear_action_items! action_item :index, fn -> action_item_link "Something Special", href: "/my/custom/route" end An example of adding a link to the show page action_item :show, fn id -> action_item_link "Show Link", href: "/custom/link", "data-method": :put, id: id end """ defmacro action_item(opts, fun) do fun = Macro.escape(fun, unquote: true) quote do Module.put_attribute __MODULE__, :actions, {unquote(opts), unquote(fun)} end end @doc """ Customize the filter pages on the right side of the index page. ## Examples Disable the filter view: filter false Only show index columns and filters for the specified fields: filter [:name, :email, :inserted_at] filter [:name, :email, :inserted_at, labels: [email: "EMail Address"]] filter only: [:name, :email, :inserted_at], label: [email: "EMail Address"] filter except: [:encrypted_password], labels: [name: "Full Name"] Note: Restricting fields with the `filter` macro also removes the field columns from the default index table. """ defmacro filter(disable) when disable in [nil, false] do quote do Module.put_attribute __MODULE__, :index_filters, false end end defmacro filter(fields) when is_list(fields) do quote do Module.put_attribute __MODULE__, :index_filters, unquote(fields) end end defmacro filter(field, opts \\ quote(do: [])) do quote do Module.put_attribute __MODULE__, :index_filters, {unquote(field), unquote(opts)} end end @doc """ Disable the batch_actions button the index page. ## Examples batch_actions false """ defmacro batch_actions(false) do quote do Module.put_attribute __MODULE__, :batch_actions, false end end @doc false def build_query_association(module, field) do case module.__schema__(:association, field) do %Ecto.Association.BelongsTo{cardinality: :one} -> field %Ecto.Association.Has{cardinality: :many} -> check_preload field, :preload_many _ -> nil end end defp check_preload(field, key) do if Application.get_env :ex_admin_runtime, key, true do field else nil end end end