Masu.Action (Masu v0.7.0)

Copy Markdown View Source

Actions of your own, on a grid: on the rows selected (:action), on a row (:row_action), and items of its menus, in a column's header menu (:header_action) or in the menu of a cell (:cell_action).

<.data_grid id="orders" source={Order} repo={Repo} selectable cell_menu scope={@current_scope}>
  <:col field={:customer} />
  <:col field={:total} type={:decimal} />

  <:action label="Archive" on_action={&OrdersGrid.archive/1} />
  <:action :if={@admin?} label="Delete" on_action={&OrdersGrid.delete/1}
    confirm="Delete {count} orders?" danger />

  <:row_action label="Open" navigate={&~p"/orders/#{&1}"} />
  <:row_action label="Send again" on_action={&OrdersGrid.resend/1}
    disabled={&(&1.status == "draft")} />

  <:header_action label="Explain" on_action={&explain/1} />
  <:cell_action label="Open the customer" event="open_customer" columns={["customer"]} />
  <:cell_action label="Copy a link" js={JS.dispatch("app:copy-link")} />
  <:header_action :let={action} label="Column stats" types={[:integer, :decimal]}>
    <.stats column={action.column} />
  </:header_action>
</.data_grid>

Where they are

  • :action - a button of the bar above the grid while rows are selected ("4 selected · Archive · Delete · Clear"); it needs selectable;
  • :row_action - an item of the menu at the end of each row (its button, a right click, or the context-menu key or Shift+F10 on the row), and of the menu of its cells;
  • :header_action - an item of a column's header menu;
  • :cell_action - an item of the menu of a cell (a right click, or the context-menu key or Shift+F10 on the active cell).

What an action does

  • on_action - a function of the action's context, called in the LiveView's process (as on_select is), which gives its result (below);
  • event - a name: the LiveView gets {Masu.Action, name, context} in its handle_info/2, to change its own assigns (open a modal, navigate);
  • navigate and patch (:row_action) - a function of the row that gives a path: the item is a link, which a middle click opens in a tab and a right click copies;
  • js (:header_action, :cell_action) - a Phoenix.LiveView.JS command, run in the browser on the grid's element; the element gets a masu:action event first, whose detail is the context as the browser has it;
  • content (:header_action, :cell_action) - a panel of your own, which the server renders with the context (:let) under the header or the cell, until the person closes it (Escape, or a click away).

The context

A map:

  • grid - the id of the grid;
  • scope - the grid's scope (@current_scope);
  • ref - the action's reference, for :async and Masu.reply/2;
  • for the rows selected: selected, as Masu.Live.selected/1 gives it ({:ids, ids}, or {:query, query} when the person selected every row of an Ecto source's query: an action on a million rows is one Repo.update_all/3), and count;
  • for a row: id, row, the row as the source reads it now, and editors, the people (of the grid's Issho room) whose editor is open on a cell of the row;
  • for a header or a cell: column, the key of the column;
  • for a cell: id, row, value, the cell's value; read on the server, not taken from the browser;
  • for a cell in a selected range: range, %{ids: ids, columns: keys}, the rows (up to 10,000) and the columns of the range; the ids are the browser's, which an action checks as it checks any params.

In the browser (js), the context has the column's key, the row's id, and the value as the browser shows it.

The result

on_action gives one of:

  • :ok (or any other value) - done;
  • {:ok, message} - done, and the grid says message (to a screen reader too);
  • {:error, message} - it failed: the grid says so;
  • :async - it goes on in another process (an Oban job, a task), which says how it went with Masu.reply(context.ref, result), and how far it is with Masu.progress/3. The grid shows it running meanwhile.

After an action on the rows selected, or on a row, that is done (or replied :ok), the grid reads its rows again, and the selection is cleared (keep_selection keeps it). A failed action keeps it.

An action of a LiveView's own (to change its assigns, run it with start_async/3) sends itself a message, and replies when it is done:

<:action label="Archive" on_action={&send(self(), {:archive, &1})} ... />

def handle_info({:archive, context}, socket) do
  socket =
    start_async(socket, :archive, fn ->
      Orders.archive(context.scope, context.selected)
      Masu.reply(context.ref, {:ok, "#{context.count} orders archived"})
    end)

  {:noreply, socket}
end

…with on_action giving :async: &(send(self(), {:archive, &1}) && :async).

Which actions a grid offers

:if leaves an action out altogether (:if={@admin?}). columns (keys) and types limit the columns of a header or cell item, and show, a function of the Masu.Column, too. A row action's show is a function of the row: the row has the item or not; its disabled, a function of the row (or a boolean), shows it unavailable.

confirm asks first: "Delete {count} orders?" ({count} is the rows selected); danger shows the action as one that destroys.

The server runs only the actions that the grid offers, for the column and for the row: one left out by :if, hidden or disabled for the row (read again from the source), or with no selection, is refused.

Summary

Types

What an action's on_action gives.

t()

Types

kind()

@type kind() :: :header | :cell | :selection | :row

result()

@type result() :: :ok | {:ok, String.t()} | {:error, String.t()} | :async

What an action's on_action gives.

t()

@type t() :: %Masu.Action{
  columns: [String.t()] | nil,
  confirm: String.t() | nil,
  danger: boolean(),
  disabled: (term() -> boolean()) | boolean(),
  event: String.t() | nil,
  index: non_neg_integer(),
  js: String.t() | nil,
  keep_selection: boolean(),
  kind: kind(),
  label: String.t(),
  navigate: (term() -> String.t()) | nil,
  on_action: (map() -> term()) | nil,
  patch: (term() -> String.t()) | nil,
  show: (term() -> boolean()) | nil,
  slot: map() | nil,
  types: [Masu.Column.type()] | nil
}