relative_positioning.rb 5.0 KB
Newer Older
1 2
# frozen_string_literal: true

3 4 5
module RelativePositioning
  extend ActiveSupport::Concern

6
  MIN_POSITION = 0
7
  START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2
8
  MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
9
  IDEAL_DISTANCE = 500
10

11 12 13 14
  included do
    after_save :save_positionable_neighbours
  end

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
  class_methods do
    def move_to_end(objects)
      parent_ids = objects.map(&:parent_ids).flatten.uniq
      max_relative_position = in_parents(parent_ids).maximum(:relative_position) || START_POSITION
      objects = objects.reject(&:relative_position)

      self.transaction do
        objects.each do |object|
          relative_position = position_between(max_relative_position, MAX_POSITION)
          object.relative_position = relative_position
          max_relative_position = relative_position
          object.save
        end
      end
    end

    # This method takes two integer values (positions) and
    # calculates the position between them. The range is huge as
    # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time
    # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number
    def position_between(pos_before, pos_after)
      pos_before ||= MIN_POSITION
      pos_after ||= MAX_POSITION

      pos_before, pos_after = [pos_before, pos_after].sort

      halfway = (pos_after + pos_before) / 2
      distance_to_halfway = pos_after - halfway

      if distance_to_halfway < IDEAL_DISTANCE
        halfway
      else
        if pos_before == MIN_POSITION
          pos_after - IDEAL_DISTANCE
        elsif pos_after == MAX_POSITION
          pos_before + IDEAL_DISTANCE
        else
          halfway
        end
      end
    end
  end

58 59
  def min_relative_position
    self.class.in_parents(parent_ids).minimum(:relative_position)
F
Felipe Artur 已提交
60 61
  end

62
  def max_relative_position
63
    self.class.in_parents(parent_ids).maximum(:relative_position)
64 65
  end

66 67 68 69
  def prev_relative_position
    prev_pos = nil

    if self.relative_position
70
      prev_pos = self.class
71
        .in_parents(parent_ids)
72 73
        .where('relative_position < ?', self.relative_position)
        .maximum(:relative_position)
74 75
    end

76
    prev_pos
77 78 79 80 81 82
  end

  def next_relative_position
    next_pos = nil

    if self.relative_position
83
      next_pos = self.class
84
        .in_parents(parent_ids)
85 86
        .where('relative_position > ?', self.relative_position)
        .minimum(:relative_position)
87 88
    end

89
    next_pos
90 91
  end

92
  def move_between(before, after)
93 94
    return move_after(before) unless after
    return move_before(after) unless before
95

96 97 98 99
    # If there is no place to insert an issue we need to create one by moving the before issue closer
    # to its predecessor. This process will recursively move all the predecessors until we have a place
    if (after.relative_position - before.relative_position) < 2
      before.move_before
100
      @positionable_neighbours = [before] # rubocop:disable Gitlab/ModuleWithInstanceVariables
101 102
    end

103
    self.relative_position = self.class.position_between(before.relative_position, after.relative_position)
104 105 106
  end

  def move_after(before = self)
107
    pos_before = before.relative_position
108
    pos_after = before.next_relative_position
109

110
    if before.shift_after?
111
      issue_to_move = self.class.in_parents(parent_ids).find_by!(relative_position: pos_after)
112
      issue_to_move.move_after
113
      @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
114

115
      pos_after = issue_to_move.relative_position
116 117
    end

118
    self.relative_position = self.class.position_between(pos_before, pos_after)
119 120
  end

121 122 123 124 125
  def move_before(after = self)
    pos_after = after.relative_position
    pos_before = after.prev_relative_position

    if after.shift_before?
126
      issue_to_move = self.class.in_parents(parent_ids).find_by!(relative_position: pos_before)
127
      issue_to_move.move_before
128
      @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
129 130 131 132

      pos_before = issue_to_move.relative_position
    end

133
    self.relative_position = self.class.position_between(pos_before, pos_after)
134 135
  end

136
  def move_to_end
137
    self.relative_position = self.class.position_between(max_relative_position || START_POSITION, MAX_POSITION)
138 139
  end

140
  def move_to_start
141
    self.relative_position = self.class.position_between(min_relative_position || START_POSITION, MIN_POSITION)
142 143
  end

144 145 146 147 148 149 150 151 152 153 154 155
  # Indicates if there is an issue that should be shifted to free the place
  def shift_after?
    next_pos = next_relative_position
    next_pos && (next_pos - relative_position) == 1
  end

  # Indicates if there is an issue that should be shifted to free the place
  def shift_before?
    prev_pos = prev_relative_position
    prev_pos && (relative_position - prev_pos) == 1
  end

156 157
  private

158
  # rubocop:disable Gitlab/ModuleWithInstanceVariables
159 160 161 162 163 164 165 166
  def save_positionable_neighbours
    return unless @positionable_neighbours

    status = @positionable_neighbours.all?(&:save)
    @positionable_neighbours = nil

    status
  end
167
  # rubocop:enable Gitlab/ModuleWithInstanceVariables
168
end