How to reorder items in a list in Ash framework

Hi everyone,

I have a list of items with a position attribute that needs to be updated whenever an item’s position changes (e.g., after a drag-and-drop event in the UI). I found an example of how this can be done in Ecto here, but I’m wondering if there’s an “Ash way” to achieve this.

Has anyone implemented something similar in Ash? Any guidance would be appreciated!

Thanks!

You could use an action like this:

# sets the position and reorders
update :move_position do
  accept [:position]

  change YourResource.Changes.RepositionAll
end

update :decrement_position do
  change atomic_update(:position, position - 1)
end

update :increment_position do
  change atomic_update(:position, position - 1)
end

Some adaptation of the code you linked:

defmodule YourResource.Changes.RepositionAll do
  use Ash.Resource.Change
  require Ash.Query
  import Ash.Expr

  def change(changeset, opts, _) do
    changeset
    |> Ash.Changeset.before_action(changeset, fn changeset -> 
      requested_position = Ash.Changeset.get_attribute(changeset, :position)

      max_position =
        resource
        # put in whatever grouping you're sorting these within
        |> Ash.Query.filter(group_id == ^Ash.Changeset.get_attribute(changeset, :group_id))
        |> Ash.count!()

     new_position = min(requested_position, max_position)

      id = Ash.Changeset.get_attribute(changeset, :id)
     
      with %Ash.BulkResult{status: :succes} <- shift_down(id, new_position),
              %Ash.BulkResult{status: :succes} <- shift_up(id, new_position) do
         changeset
      else
         %Ash.BulkResult{errors: errors} -> Ash.Changeset.add_error(changeset, errors)
      end
    end)
  end

  def shift_down(resource, id, new_position) do
    resource
    |> Ash.Query.filter(position > ^old_position(id) and position <= new_position)
    |> Ash.Query.bulk_update!(:decrement_position)
  end

  def shift_up(id, new_position) do
    resource
    |> Ash.Query.filter(position < ^old_position(id) and position > new_position)
    |> Ash.Query.bulk_update!(:decrement_position)
  end

  def old_position(id) do
    expr(fragment("SELECT position FROM table WHERE id = ?", ^id))
  end
end

One small note:

old_position = from(og in type, where: og.id == ^struct.id, select: og.position)

stating the above is currently a limitation in Ash expressions, that will be addressed soon: Support resources as aggregate targets in expressions · Issue #939 · ash-project/ash · GitHub

So for that reason, I’ve done it as a SQL fragment, which is not ideal.

I didn’t run any of the above code, but it should be a reasonable place to get started :slight_smile:

4 Likes

Works! Thank you