defmodule ExAdmin.Index do @moduledoc """ Override the default index page for an ExAdmin resource By default, ExAdmin renders the index table without any additional configuration. It renders each column in the model, except the id, inserted_at, and updated_at columns. ## Default Table Type ExAdmin displays a selection checkbox column on the left with a batch action control that enables when a checkbox is selected. To customize the index page, use the `index` macro. For example, the following will show on the id an name fields, as well place a selection column and batch actions row on the page: defmodule MyProject.ExAdmin.MyModel do use ExAdmin.Register register_resource MyProject.MyModel do index do selectable_column() column :id column :name actions # display the default actions column end end end ### Image fields For image fields, use the `image: true` option. For example: index do column :name column :image, [image: true, height: 100], &(ExAdminDemo.Image.url({&1.image, &1}, :thumb)) end ### Custom columns Columns can be customized with column/2 where the second argument is an anonymous function called with model. Here are a couple examples: index do column :id column :name, fn(category) -> Phoenix.HTML.Tag.content_tag :span, category.name, "data-id": category.id, class: "category" end column "Created", fn(category) -> category.created_at end end ### Override the Actions column The Actions column can be customized by adding `column "Actions", fn(x) -> ...` column "Actions", fn(r) -> safe_concat link_to("Restore", "/admin/backuprestores/restore/#\{r.id}", "data-method": :put, "data-confirm": "You are about to restore #\{r.file_name}. Are you sure?", class: "member_link restore-link"), link_to("Delete", "/admin/backuprestores/#\{r.id}", "data-method": :delete, "data-confirm": "Are you sure you want to delete this?", class: "member_link") end ### Associations By default, ExAdmin will attempt to render a belongs_to association with a select control, using name field in the association. If you would like to render an association with another field name, or would like to use more than one field, use the :field option. column :account, fields: [:username] ### Change the column label Use the :label option to override the column name: column :name, label: "Custom Name" ## As Grid By providing option `as: :grid` to the `index` macro, a grid index page is rendered. ### For Example: index as: :grid, default: true do cell fn(p) -> markup do div do a href: admin_resource_path(p, :show) do img(src: ExAdminDemo.Image.url({p.image_file_name, p}, :thumb), height: 100) end end a truncate(p.title), href: admin_resource_path(p, :show) end end end """ require Logger require Integer import ExAdmin.Utils import ExAdmin.Helpers import ExAdmin.Gettext import Kernel, except: [div: 2, to_string: 1] use Xain # alias ExAdmin.Schema @doc false defmacro __using__(_) do quote do import unquote(__MODULE__) end end @default_actions [:show, :edit, :delete] @doc """ The index macro is used to customize the index page of a resource. """ defmacro index(opts \\ [], do: contents) do quote location: :keep do import ExAdmin.CSV, only: [csv: 1, csv: 2] import ExAdmin.Register import ExAdmin.Index def index_view(var!(conn), page, scope_counts) do import ExAdmin.Form, except: [actions: 1] import ExAdmin.Register, except: [actions: 1] import ExAdmin.ViewHelpers var!(columns, ExAdmin.Show) = [] var!(selectable_column, ExAdmin.Index) = nil var!(actions, ExAdmin.Index) = nil var!(cell, ExAdmin.Index) = nil opts = unquote(opts) unquote(contents) selectable = case Macro.expand(var!(selectable_column, ExAdmin.Index), __ENV__) do nil -> false other -> other end actions = ExAdmin.Index.get_index_actions(var!(conn).assigns.defn, var!(actions, ExAdmin.Index)) opts = Enum.into(opts, %{}) |> Map.put(:column_list, var!(columns, ExAdmin.Show) |> Enum.reverse) |> Map.put(:selectable_column, selectable) |> Map.put(:actions, actions) markup safe: true do ExAdmin.Index.render_index_pages(var!(conn), page, scope_counts, var!(cell, ExAdmin.Index), opts) end end end end @doc false def get_index_actions(defn, actions) do actions = case actions do [] -> @default_actions nil -> @default_actions false -> [] list -> list end actions -- (@default_actions -- defn.actions) end @doc """ Define which actions will be displayed in the index view. ## Examples actions actions [:new, :delete] """ defmacro actions(opts \\ []) do if opts != nil and opts != false and (opts -- @default_actions) != [] do raise ArgumentError, "Only #{inspect @default_actions} are allowed!" end quote do var!(actions, ExAdmin.Index) = unquote(opts) end end @doc """ Define a grid cell for grid view. ## Example index as: :grid, default: true, columns: 6 do import Kernel, except: [div: 2] cell fn(p) -> div ".box" do div ".box-body" do a href: admin_resource_path(p, :show) do img(src: ExAdminDemo.Image.url({p.image_file_name, p}, :thumb), height: 100) end end div ".box-footer" do a truncate(p.title), href: admin_resource_path(p, :show) end end end end """ defmacro cell(fun) do quote do var!(cell, ExAdmin.Index) = unquote(fun) end end @doc """ Add a column of selection check boxes Allows users to select individual rows on the index page. Selecting columns activates the batch actions button. """ defmacro selectable_column do quote do var!(selectable_column, ExAdmin.Index) = true end end @doc false def default_index_view(conn, page, scope_counts) do case conn.assigns.defn do nil -> throw :invalid_route %{__struct__: _} = defn -> columns = case defn.index_filters do [] -> [] [false] -> [] [_] -> ExAdmin.Filter.fields(conn.assigns.defn) |> Keyword.keys end |> case do [] -> defn.resource_model.__schema__(:fields) |> Enum.filter(&(not &1 in [:inserted_at, :updated_at])) other -> other end |> Enum.map(&({translate_field(defn, &1), %{}})) columns = if :id in defn.resource_model.__schema__(:fields) and Enum.any?(columns, (&(elem(&1, 0) == :id))) do Keyword.put columns, :id, %{link: true} else columns end opts = %{} |> Map.put(:column_list, columns) |> Map.put(:selectable_column, true) |> Map.put(:actions, get_index_actions(defn, [])) markup safe: true do ExAdmin.Index.render_index_pages(var!(conn), page, scope_counts, nil, opts) end end end defp get_resource_fields([]), do: [] defp get_resource_fields([resource | _]), do: resource.__struct__.__schema__(:fields) @doc false def render_index_pages(conn, page, scope_counts, cell, page_opts) do # require IEx # IEx.pry name = resource_model(conn) |> titleize |> Inflex.pluralize defn = conn.assigns.defn label = get_resource_label(conn) |> Inflex.pluralize batch_actions = (not false in defn.batch_actions) and :delete in page_opts[:actions] opts = %{ columns: Map.get(page_opts, :columns, 3), column_list: Map.get(page_opts, :column_list), count: page.total_entries, name: name, order: ExQueb.get_sort_order(conn.params["order"]), href: admin_resource_path(conn, :index) <> "?order=", defn: defn, batch_actions: batch_actions, scopes: defn.scopes, label: label, resource_model: conn.params["resource"], page: page, cell: cell, scope_counts: scope_counts, opts: page_opts, resources: page.entries, selectable_column: page_opts[:selectable_column], actions: page_opts[:actions] } _render_index_page(conn, opts, page_opts) end defp _render_index_page(conn, opts, %{as: :grid}) do Module.concat(conn.assigns.theme, Index).wrap_index_grid fn -> Module.concat(conn.assigns.theme, Index).batch_action_form conn, false, opts[:scopes], opts[:resource_model], opts[:scope_counts], fn -> if opts[:count] == 0 do Module.concat(conn.assigns.theme, Index).blank_slate_page(conn, opts) else Module.concat(conn.assigns.theme, Index).paginated_collection_grid(conn, opts) end end end end defp _render_index_page(conn, opts, page_opts) do page = opts[:page] actions = opts[:actions] opts = Map.put(opts, :fields, get_resource_fields page.entries) columns = page_opts[:column_list] custom_actions_column? = Enum.any? columns, &((elem &1, 0) == "Actions") columns = if custom_actions_column? || Enum.empty?(actions) do columns else columns ++ [{"Actions", %{fun: fn(resource) -> build_index_links(conn, resource, actions) end, label: ExAdmin.Gettext.gettext("Actions") }}] end opts = Map.put opts, :column_list, columns Module.concat(conn.assigns.theme, Index).wrap_index_grid fn -> Module.concat(conn.assigns.theme, Index).batch_action_form conn, opts[:batch_actions], opts[:scopes], opts[:resource_model], opts[:scope_counts], fn -> if opts[:count] == 0 do Module.concat(conn.assigns.theme, Index).blank_slate_page(conn, opts) else Module.concat(conn.assigns.theme, Index).paginated_collection_table(conn, opts) end end end end @doc """ Build the scope link. """ def build_scope_href(href, nil), do: href def build_scope_href(href, scope) do String.replace(href, "?", "?scope=#{scope}&") end @doc """ Build the order link. """ def build_order_href(href, {name, sort}), do: href <> "#{name}_#{sort}" def build_order_href(href, _), do: href @doc """ Build the filter link. """ def build_filter_href(href, nil), do: href def build_filter_href(href, q) do q |> Map.to_list |> Enum.reduce(href, fn({name, value}, acc) -> acc <> "&q%5B" <> name <> "%5D=" <> value end) end @doc false def download_links(conn, opts) do div ".download_links " <> (gettext "Download:") <> " " do a "CSV", href: build_csv_href(conn, opts) end end @doc false def build_csv_href(conn, opts) do admin_resource_path(conn, :csv) <> "?order=" |> build_scope_href(conn.params["scope"]) |> build_order_href(opts[:order]) |> build_filter_href(conn.params["q"]) end @doc false def parameterize(name, seperator \\ "_") def parameterize(atom, seperator) when is_atom(atom) do Atom.to_string(atom) |> parameterize(seperator) end def parameterize(string, seperator) do Inflex.parameterize(string, seperator) end @doc false def build_index_links(conn, resource, actions) do resource_model = resource.__struct__ links = case actions do nil -> [] other -> other end list = get_authorized_links(conn, links, resource_model) |> Enum.reverse labels = conn.assigns.defn.action_labels Module.concat(conn.assigns.theme, Index).handle_action_links(list, resource, labels) end @doc false def get_authorized_links(conn, links, resource_model) do Enum.reduce links, [], fn(item, acc) -> if ExAdmin.Utils.authorized_action?(conn, item, resource_model), do: [item | acc], else: acc end end end