Bump Your Deps

Update Backpex to the latest version:

defp deps do
  [
    {:backpex, "~> 0.23.0"}
  ]
end

NULL values are ordered by the database default

Before v0.23, Backpex ordered every column with ASC NULLS FIRST or DESC NULLS LAST. A regular index (including the primary key) can't serve these orders, so the database sorted the whole table on every page load of the index view, even for the default order by primary key. On large tables this is slow and writes temporary files, especially on later pages.

Backpex now uses plain ASC and DESC and leaves NULL values to the database. In PostgreSQL, NULL values now come last in ascending and first in descending order. Columns without NULL values order the same as before.

To keep the old order, set the new order_nulls option to :smallest, on the LiveResource or on a single field:

use Backpex.LiveResource,
  # ...other options
  order_nulls: :smallest
published_at: %{
  module: Backpex.Fields.DateTime,
  label: "Published at",
  order_nulls: :smallest
}

order_nulls also accepts :first and :last for NULL values in a fixed place in both directions. Every value except :default needs a matching index on large tables (for :smallest: create index(:posts, ["published_at NULLS FIRST"])). See NULL Values in the ordering guide.

Ordering by the primary_key always uses plain ASC and DESC.

If you have a custom adapter, the order criteria passed to list/4 now has a :nulls key (:default, :first, :last or :smallest).

Texts for every upload error

Before v0.23, the form of an upload field crashed with a FunctionClauseError on any upload error other than :too_large, :too_many_files and :not_accepted, for example when the browser's upload to S3 failed (:external_client_failure) or an external function returned {:error, meta, socket}. Backpex now shows a text for every error. If you translate Backpex's texts, add translations for the new upload texts in backpex.pot.

A custom upload field can give its own texts with the new optional callback Backpex.Field.upload_error/4. It's called on the field's module, so it also works for a field that delegates render_form/1 to Backpex.Fields.Upload. It gets every error, so delegate the ones your field doesn't handle to Backpex.Fields.Upload.upload_error/4:

@impl Backpex.Field
def upload_error(_field, :too_large, upload, _assigns) do
  Backpex.translate_error({"must be at most %{count} bytes", count: upload.max_file_size})
end

def upload_error(field, error, upload, assigns) do
  Backpex.Fields.Upload.upload_error(field, error, upload, assigns)
end

Fields without the callback keep the texts of Backpex.Fields.Upload, so nothing changes for them.

Backpex.HTML.Resource.pagination/1 takes a page_path function

Before v0.23, the index view decoded the whole path for the pagination links so that Backpex.HTML.Resource.pagination/1 could replace a :page placeholder in it. Decoding also undid the encoding of the search and filter values, so page links broke values containing &, #, + or %. For example, page 2 of the search R&D searched for R.

The path attribute is replaced by page_path, a function that receives a page number and returns the path to that page. If you render the component yourself, change:

<.pagination path={"/admin/posts?page=:page"} current_page={@page} total_pages={@total_pages} />

to:

<.pagination page_path={&"/admin/posts?page=#{&1}"} current_page={@page} total_pages={@total_pages} />

Build the path with an encoding function like Backpex.Router.get_path/5 or Plug.Conn.Query.encode/1 if it contains user input.

InlineCRUD deletes rows with a button

The per-row delete control of Backpex.Fields.InlineCRUD is now a button[name="change[<field>_delete][]"] that can be used with the keyboard. The hidden checkbox with the id <field>-checkbox-delete-<index> is gone, so update custom CSS, JavaScript or tests that target it. If you translate Backpex's texts, add a translation for Delete entry %{index}.

InlineCRUD child field classes apply to table cells

The rows of Backpex.Fields.InlineCRUD now share one CSS table, so the inputs and delete buttons of all rows line up. The class option of a child field lands on the table cell of that column, in the header row and in every entry row. Width classes like w-1/3 keep working. Flex classes like grow, basis-1/4 or shrink-0 have no effect on a table cell anymore, and display classes like hidden md:block override table-cell and break the row. Columns without a class no longer get flex-1; they share the remaining width.

Primary keys are encoded once in paths

Before v0.23, Backpex encoded the primary key in show links twice and turned a space into +, and the show and edit views decoded the key a second time. String primary keys with a space or a literal %XX sequence didn't find their item. Backpex now encodes every path param exactly once as a path segment (a space becomes %20, a slash %2F) and uses the value Phoenix decoded. Path params of scopes (for example scope "/:tenant") are encoded the same way.

This changes generated URLs: old bookmarks of double-encoded show links (for example /posts/a%252Fb/show for the key a/b) no longer resolve.

If your own code works around the old encoding, remove the workaround:

"Save & Continue editing" on the new view now builds the edit path with Backpex.Router.get_path/5, so the LiveResource needs an edit route for it.

Unused text "Unselect %{label}"

The remove icon in a selected MultiSelect badge no longer has an accessible name, so Backpex no longer uses the text Unselect %{label}. If you translate Backpex's texts, you can remove it from your translations.

Empty date and time values skip the format function

Before v0.23, a format function on a Backpex.Fields.Date, Backpex.Fields.DateTime or Backpex.Fields.Time field was also called with nil, so the index and show views crashed for every item with an empty value unless the function handled nil. Backpex now renders empty values as "—" without calling the function, as it does for format strings.

If your function returned a text of its own for nil (for example fn nil -> "never"; date -> ... end), that text is now replaced by "—". To keep it, move the formatting to the field's render option:

deleted_at: %{
  module: Backpex.Fields.DateTime,
  label: "Deleted At",
  render: fn assigns ->
    ~H"""
    <p>{if @value, do: Calendar.strftime(@value, "%d.%m.%Y"), else: "never"}</p>
    """
  end
}

render replaces the field's whole value template in the index and show views: format no longer applies, and the truncate class the default template adds in the index view is only there if you add it. It isn't used for index-editable fields (index_editable), which render their input instead.

Upload fields need at least one entry

Backpex.Fields.Upload accepted max_entries: 0, but every new and edit form with such a field crashed with a BadMapError. The max_entries option now has to be a positive integer. With 0, the views of the LiveResource raise a configuration error for the field. Fields of a resource action aren't validated against the config schema, so there 0 raises the ArgumentError of Phoenix.LiveView.allow_upload/3 when the action's form opens.

To show a field's files without letting users upload or remove files, set readonly: true on the field instead (see the readonly guide).