Commit c4a80d88 authored by Jarka Košanová's avatar Jarka Košanová

Add support for relative ordering between classes

- add support for ordering of objects
from 2 different classes
parent 134974f5
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MoveEpicIssuesAfterEpics < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 1_000
disable_ddl_transaction!
class Epic < ActiveRecord::Base
self.table_name = 'epics'
end
class EpicIssue < ActiveRecord::Base
self.table_name = 'epic_issues'
include ::EachBatch
end
def up
maximum_epic_position = Epic.maximum(:relative_position)
return unless maximum_epic_position
max_position = Gitlab::Database::MAX_INT_VALUE
delta = ((maximum_epic_position - max_position) / 2.0).abs.ceil
EpicIssue.where('relative_position < ?', maximum_epic_position).each_batch(of: BATCH_SIZE) do |batch, _|
batch.update_all("relative_position = relative_position + #{delta}")
end
end
def down
# no need
end
end
# frozen_string_literal: true
module EpicTreeSorting
extend ActiveSupport::Concern
include FromUnion
include RelativePositioning
class_methods do
def relative_positioning_query_base(object)
from_union([
EpicIssue.select("id, relative_position, epic_id, 'epic_issue' as object_type").in_epic(object.parent_ids),
Epic.select("id, relative_position, parent_id as epic_id, 'epic' as object_type").where(parent_id: object.parent_ids)
])
end
def relative_positioning_parent_column
:epic_id
end
end
included do
def move_sequence(start_pos, end_pos, delta)
items_to_update = scoped_items
.where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
items_to_update.group_by { |item| item.object_type }.each do |type, group_items|
items = type.camelcase.constantize.where(id: group_items.map(&:id))
items = items.where.not(id: self.id) if type == self.class.underscore
items.update_all("relative_position = relative_position + #{delta}")
end
end
end
end
...@@ -12,9 +12,9 @@ module EE ...@@ -12,9 +12,9 @@ module EE
include Referable include Referable
include Awardable include Awardable
include LabelEventable include LabelEventable
include RelativePositioning
include UsageStatistics include UsageStatistics
include FromUnion include FromUnion
include EpicTreeSorting
enum state_id: { enum state_id: {
opened: ::Epic.available_states[:opened], opened: ::Epic.available_states[:opened],
...@@ -177,14 +177,6 @@ module EE ...@@ -177,14 +177,6 @@ module EE
::Group ::Group
end end
def relative_positioning_query_base(epic)
in_parents(epic.parent_ids)
end
def relative_positioning_parent_column
:parent_id
end
# Return the deepest relation level for an epic. # Return the deepest relation level for an epic.
# Example 1: # Example 1:
# epic1 - parent: nil # epic1 - parent: nil
......
# frozen_string_literal: true # frozen_string_literal: true
class EpicIssue < ApplicationRecord class EpicIssue < ApplicationRecord
include RelativePositioning include EpicTreeSorting
validates :epic, :issue, presence: true validates :epic, :issue, presence: true
validates :issue, uniqueness: true validates :issue, uniqueness: true
...@@ -12,12 +12,4 @@ class EpicIssue < ApplicationRecord ...@@ -12,12 +12,4 @@ class EpicIssue < ApplicationRecord
alias_attribute :parent_ids, :epic_id alias_attribute :parent_ids, :epic_id
scope :in_epic, ->(epic_id) { where(epic_id: epic_id) } scope :in_epic, ->(epic_id) { where(epic_id: epic_id) }
def self.relative_positioning_query_base(epic_issue)
in_epic(epic_issue.parent_ids)
end
def self.relative_positioning_parent_column
:epic_id
end
end end
...@@ -11,40 +11,39 @@ module Epics ...@@ -11,40 +11,39 @@ module Epics
end end
def execute def execute
klass = case moving_object
when EpicIssue
EpicIssues::UpdateService
when Epic
EpicLinks::UpdateService
end
return error('Only epics and epic_issues are supported.') unless klass
error_message = validate_objects error_message = validate_objects
return error(error_message) if error_message.present? return error(error_message) if error_message.present?
klass.new(moving_object, current_user, moving_params).execute move!
success
end end
private private
def moving_params def move!
key = case params[:relative_position].to_sym moving_object.move_between(before_object, after_object)
when :after moving_object.save!(touch: false)
:move_after_id end
when :before
:move_before_id
end
{}.tap { |p| p[key] = adjacent_reference.id } def before_object
return unless params[:relative_position].to_sym == :before
adjacent_reference
end
def after_object
return unless params[:relative_position].to_sym == :after
adjacent_reference
end end
# for now we support only ordering within the same type
# Follow-up issue: https://gitlab.com/gitlab-org/gitlab/issues/13633
def validate_objects def validate_objects
unless moving_object.is_a?(EpicIssue) || moving_object.is_a?(Epic)
return 'Only epics and epic_issues are supported.'
end
return 'You don\'t have permissions to move the objects.' unless authorized? return 'You don\'t have permissions to move the objects.' unless authorized?
return 'Provided objects are not the same type.' if moving_object.class != adjacent_reference.class
end end
def authorized? def authorized?
......
# frozen_string_literal: true
require 'spec_helper'
describe EpicTreeSorting do
let(:group) { create(:group) }
let(:base_epic) { create(:epic, group: group) }
let!(:epic_issue1) { create(:epic_issue, epic: base_epic, relative_position: 10) }
let!(:epic_issue2) { create(:epic_issue, epic: base_epic, relative_position: 500) }
let!(:epic_issue3) { create(:epic_issue, epic: base_epic, relative_position: 1002) }
let!(:epic_issue4) { create(:epic_issue, epic: base_epic, relative_position: 1500) }
let!(:epic1) { create(:epic, parent: base_epic, group: group, relative_position: 100) }
let!(:epic2) { create(:epic, parent: base_epic, group: group, relative_position: 1000) }
let!(:epic3) { create(:epic, parent: base_epic, group: group, relative_position: 1001) }
context '#move_after' do
it 'moves an epic' do
epic1.move_after(epic_issue2)
expect(epic1.relative_position).to be_between(epic_issue2.reload.relative_position, epic2.reload.relative_position).exclusive
end
it 'moves an epic_issue' do
epic_issue2.move_after(epic2)
expect(epic_issue2.relative_position).to be_between(epic2.reload.relative_position, epic3.reload.relative_position).exclusive
end
end
context '#move_before' do
it 'moves an epic' do
epic2.move_before(epic_issue2)
expect(epic2.relative_position).to be_between(epic_issue1.reload.relative_position, epic_issue2.reload.relative_position).exclusive
end
it 'moves an epic_issue' do
epic_issue3.move_before(epic2)
expect(epic_issue3.relative_position).to be_between(epic_issue2.reload.relative_position, epic2.reload.relative_position).exclusive
end
end
context '#move_between' do
it 'moves an epic' do
epic1.move_between(epic_issue1, epic_issue2)
expect(epic1.relative_position).to be_between(epic_issue1.reload.relative_position, epic_issue2.reload.relative_position).exclusive
end
it 'moves an epic_issue' do
epic_issue3.move_between(epic1, epic_issue2)
expect(epic_issue3.relative_position).to be_between(epic1.reload.relative_position, epic_issue2.reload.relative_position).exclusive
end
end
end
...@@ -10,4 +10,31 @@ describe EpicIssue do ...@@ -10,4 +10,31 @@ describe EpicIssue do
let(:default_params) { { epic: epic } } let(:default_params) { { epic: epic } }
end end
end end
context 'relative positioning with 2 classes' do
let(:group) { create(:group) }
let(:base_epic) { create(:epic, group: group) }
let!(:epic_issue1) { create(:epic_issue, epic: base_epic, relative_position: 10) }
let!(:epic_issue2) { create(:epic_issue, epic: base_epic, relative_position: 50) }
let!(:epic_issue3) { create(:epic_issue, epic: base_epic, relative_position: 1500) }
let!(:epic1) { create(:epic, parent: base_epic, group: group, relative_position: 1000) }
let!(:epic2) { create(:epic, parent: base_epic, group: group, relative_position: 2000) }
context '#move_after' do
it 'moves the epic after an epic_issue' do
epic1.move_after(epic_issue3)
expect(epic1.relative_position).to be > epic_issue3.relative_position
end
end
context '#move_between' do
it 'moves the epic between epic_issues' do
epic1.move_between(epic_issue1, epic_issue2)
expect(epic1.relative_position).to be > epic_issue1.relative_position
expect(epic1.relative_position).to be < epic_issue2.relative_position
end
end
end
end end
...@@ -12,8 +12,8 @@ describe Epics::TreeReorderService do ...@@ -12,8 +12,8 @@ describe Epics::TreeReorderService do
let(:issue2) { create(:issue, project: project) } let(:issue2) { create(:issue, project: project) }
let(:epic1) { create(:epic, group: group, parent: epic, relative_position: 10) } let(:epic1) { create(:epic, group: group, parent: epic, relative_position: 10) }
let(:epic2) { create(:epic, group: group, parent: epic, relative_position: 20) } let(:epic2) { create(:epic, group: group, parent: epic, relative_position: 20) }
let(:epic_issue1) { create(:epic_issue, epic: epic, issue: issue1, relative_position: 10) } let(:epic_issue1) { create(:epic_issue, epic: epic, issue: issue1, relative_position: 30) }
let(:epic_issue2) { create(:epic_issue, epic: epic, issue: issue2, relative_position: 20) } let(:epic_issue2) { create(:epic_issue, epic: epic, issue: issue2, relative_position: 40) }
let(:relative_position) { :after } let(:relative_position) { :after }
let!(:tree_object_1) { epic1 } let!(:tree_object_1) { epic1 }
...@@ -32,10 +32,8 @@ describe Epics::TreeReorderService do ...@@ -32,10 +32,8 @@ describe Epics::TreeReorderService do
shared_examples 'error for the tree update' do |expected_error| shared_examples 'error for the tree update' do |expected_error|
it 'does not change relative_positions' do it 'does not change relative_positions' do
subject expect { subject }.not_to change { tree_object_1.relative_position }
expect { subject }.not_to change { tree_object_2.relative_position }
expect(tree_object_1.relative_position).to eq(10)
expect(tree_object_2.relative_position).to eq(20)
end end
it 'returns error status' do it 'returns error status' do
...@@ -69,12 +67,16 @@ describe Epics::TreeReorderService do ...@@ -69,12 +67,16 @@ describe Epics::TreeReorderService do
let!(:tree_object_1) { epic_issue1 } let!(:tree_object_1) { epic_issue1 }
let!(:tree_object_2) { epic_issue2 } let!(:tree_object_2) { epic_issue2 }
# for now we support only ordering within the same type
# Follow-up issue: https://gitlab.com/gitlab-org/gitlab/issues/13633
context 'when object being moved is not the same type as the switched object' do context 'when object being moved is not the same type as the switched object' do
let!(:tree_object_3) { epic1 }
let!(:tree_object_4) { epic2 }
let(:adjacent_reference_id) { GitlabSchema.id_from_object(epic2) } let(:adjacent_reference_id) { GitlabSchema.id_from_object(epic2) }
it_behaves_like 'error for the tree update', 'Provided objects are not the same type.' it 'reorders the objects' do
subject
expect(epic2.reload.relative_position).to be > tree_object_2.reload.relative_position
end
end end
context 'when no object to switch is provided' do context 'when no object to switch is provided' do
...@@ -117,14 +119,6 @@ describe Epics::TreeReorderService do ...@@ -117,14 +119,6 @@ describe Epics::TreeReorderService do
let!(:tree_object_1) { epic1 } let!(:tree_object_1) { epic1 }
let!(:tree_object_2) { epic2 } let!(:tree_object_2) { epic2 }
# for now we support only ordering within the same type
# Follow-up issue: https://gitlab.com/gitlab-org/gitlab/issues/13633
context 'when object being moved is not the same type as the switched object' do
let(:adjacent_reference_id) { GitlabSchema.id_from_object(epic_issue2) }
it_behaves_like 'error for the tree update', 'Provided objects are not the same type.'
end
context 'when the reordered epics are not subepics of the base epic' do context 'when the reordered epics are not subepics of the base epic' do
let(:another_group) { create(:group) } let(:another_group) { create(:group) }
let(:another_epic) { create(:epic, group: another_group) } let(:another_epic) { create(:epic, group: another_group) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment