# Upgrading to v0.23

## Bump Your Deps

Update Backpex to the latest version:

```elixir
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:

```elixir
use Backpex.LiveResource,
  # ...other options
  order_nulls: :smallest
```

```elixir
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](../live_resource/ordering.md#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 `c: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`:

```elixir
@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:

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

to:

```heex
<.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:

- Don't call `URI.decode/1` on `params["backpex_id"]`; it's already decoded.
- Pass the plain value to `Backpex.Router.get_path/5` and `Backpex.Router.get_path/6`; don't encode it first.

"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:

```elixir
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](../fields/readonly.md) guide).
