Recourse

Positionable tables

A model keeping an integer position is one a reader puts in order by hand. The column is the whole declaration; a column named otherwise is named with def self.recourse_position = :ordering. The routes say where a drop is reported:

recourses :tasks, positionable: true

A grip opens each row, dragging it moves the row, and the place it lands in is written to PATCH /tasks/5/position. The table is ordered by the column, the column is never drawn, and the search box and the sorted headings stand down while the grips are drawn, since a shortened page would report a wrong place. A listing of every row across every parent, where positions repeat, is not positioned.

Two callbacks keep the numbers running 1, 2, 3: a new row lands last among its own, and the gap closes behind one that goes. A model pointing two ways says which rows its place is counted among:

class Task < ApplicationRecord
  belongs_to :project
  belongs_to :user

  def recourse_siblings = project.tasks
end

A host keeping the order itself writes def self.recourse_position = nil, which takes the grips and both callbacks away.

A second listing

A host listing the same rows in a second order — tasks under a user, ranked — names that column on both the listing controller and the positions controller, since a drop would otherwise write the model’s own column:

module Ranking
  extend ActiveSupport::Concern

private

  def recourse_position = 'ranking'
end

class Users::TasksController < RecoursesController
  include Ranking
end

class Users::Tasks::PositionsController < Recourse::PositionsController
  include Ranking
end

Recourse::Positioning.new(relation, column).close is what such a host calls to keep its own column contiguous when a row goes.