Commit 79b43ac2 authored by Valery Sizov's avatar Valery Sizov

Porting IssueBoardOrdering feature from CE

parent 9f908cfc
......@@ -56,11 +56,6 @@ import boardCard from './board_card';
});
}
},
computed: {
orderedIssues () {
return _.sortBy(this.issues, 'priority');
},
},
methods: {
listHeight () {
return this.$refs.list.getBoundingClientRect().height;
......@@ -92,9 +87,9 @@ import boardCard from './board_card';
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues',
sort: false,
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
......@@ -111,6 +106,13 @@ import boardCard from './board_card';
e.item.remove();
});
},
onUpdate: (e) => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
},
onMove(e) {
return !e.related.classList.contains('board-list-count');
}
});
this.sortable = Sortable.create(this.$refs.list, options);
......
......@@ -15,6 +15,7 @@ class ListIssue {
this.labels = [];
this.selected = false;
this.assignee = false;
this.position = obj.relative_position || Infinity;
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee);
......@@ -27,10 +28,6 @@ class ListIssue {
obj.labels.forEach((label) => {
this.labels.push(new ListLabel(label));
});
this.priority = this.labels.reduce((max, label) => {
return (label.priority < max) ? label.priority : max;
}, Infinity);
}
addLabel (label) {
......
......@@ -110,9 +110,20 @@ class List {
}
addIssue (issue, listFrom, newIndex) {
let moveBeforeIid = null;
let moveAfterIid = null;
if (!this.findIssue(issue.id)) {
if (newIndex !== undefined) {
this.issues.splice(newIndex, 0, issue);
if (this.issues[newIndex - 1]) {
moveBeforeIid = this.issues[newIndex - 1].id;
}
if (this.issues[newIndex + 1]) {
moveAfterIid = this.issues[newIndex + 1].id;
}
} else {
this.issues.push(issue);
}
......@@ -123,13 +134,21 @@ class List {
if (listFrom) {
this.issuesSize += 1;
this.updateIssueLabel(issue, listFrom);
this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid);
}
}
}
updateIssueLabel(issue, listFrom) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) {
this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue);
gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid);
}
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
.then(() => {
listFrom.getIssues(false);
});
......
......@@ -64,10 +64,12 @@ class BoardService {
return this.issues.get(data);
}
moveIssue (id, from_list_id, to_list_id) {
moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) {
return this.issue.update({ id }, {
from_list_id,
to_list_id
to_list_id,
move_before_iid,
move_after_iid,
});
}
......
......@@ -109,6 +109,12 @@
listFrom.removeIssue(issue);
}
},
moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
findList (key, val, type = 'label') {
return this.state.lists.filter((list) => {
const byType = type ? list['type'] === type : true;
......
......@@ -43,7 +43,14 @@
return event;
}
function getTraget(target) {
function isLast(target) {
var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
var children = el.children;
return children.length - 1 === target.index;
}
function getTarget(target) {
var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
var children = el.children;
......@@ -75,12 +82,22 @@
function simulateDrag(options, callback) {
options.to.el = options.to.el || options.from.el;
var fromEl = getTraget(options.from);
var toEl = getTraget(options.to);
var fromEl = getTarget(options.from);
var toEl = getTarget(options.to);
var firstEl = getTarget({
el: options.to.el,
index: 'first'
});
var lastEl = getTarget({
el: options.to.el,
index: 'last'
});
var scrollable = options.scrollable;
var fromRect = getRect(fromEl);
var toRect = getRect(toEl);
var firstRect = getRect(firstEl);
var lastRect = getRect(lastEl);
var startTime = new Date().getTime();
var duration = options.duration || 1000;
......@@ -88,6 +105,12 @@
options.ontap && options.ontap();
window.SIMULATE_DRAG_ACTIVE = 1;
if (options.to.index === 0) {
toRect.cy = firstRect.y;
} else if (isLast(options.to)) {
toRect.cy = lastRect.y + lastRect.h + 50;
}
var dragInterval = setInterval(function loop() {
var progress = (new Date().getTime() - startTime) / duration;
var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
......
......@@ -8,6 +8,7 @@ module Projects
def index
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
make_sure_position_is_set(issues)
render json: {
issues: serialize_as_json(issues),
......@@ -38,6 +39,12 @@ module Projects
private
def make_sure_position_is_set(issues)
issues.each do |issue|
issue.move_to_end && issue.save unless issue.relative_position
end
end
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: project.id)
......@@ -63,7 +70,7 @@ module Projects
end
def move_params
params.permit(:board_id, :id, :from_list_id, :to_list_id)
params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid)
end
def issue_params
......@@ -73,7 +80,7 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
only: [:id, :iid, :title, :confidential, :due_date],
only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
......
module RelativePositioning
extend ActiveSupport::Concern
MIN_POSITION = 0
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
included do
after_save :save_positionable_neighbours
end
def min_relative_position
self.class.in_projects(project.id).minimum(:relative_position)
end
def max_relative_position
self.class.in_projects(project.id).maximum(:relative_position)
end
def prev_relative_position
prev_pos = nil
if self.relative_position
prev_pos = self.class.
in_projects(project.id).
where('relative_position < ?', self.relative_position).
maximum(:relative_position)
end
prev_pos || MIN_POSITION
end
def next_relative_position
next_pos = nil
if self.relative_position
next_pos = self.class.
in_projects(project.id).
where('relative_position > ?', self.relative_position).
minimum(:relative_position)
end
next_pos || MAX_POSITION
end
def move_between(before, after)
return move_after(before) unless after
return move_before(after) unless before
pos_before = before.relative_position
pos_after = after.relative_position
if pos_after && (pos_before == pos_after)
self.relative_position = pos_before
before.move_before(self)
after.move_after(self)
@positionable_neighbours = [before, after]
else
self.relative_position = position_between(pos_before, pos_after)
end
end
def move_before(after)
self.relative_position = position_between(after.prev_relative_position, after.relative_position)
end
def move_after(before)
self.relative_position = position_between(before.relative_position, before.next_relative_position)
end
def move_to_end
self.relative_position = position_between(max_relative_position, MAX_POSITION)
end
private
# This method takes two integer values (positions) and
# calculates some random position between them. The range is huge as
# the maximum integer value is 2147483647. Ideally, the calculated value would be
# exactly between those terminating values, but this will introduce possibility of a race condition
# so two or more issues can get the same value, we want to avoid that and we also want to avoid
# using a lock here. If we have two issues with distance more than one thousand, we are OK.
# Given the huge range of possible values that integer can fit we shoud never face a problem.
def position_between(pos_before, pos_after)
pos_before ||= MIN_POSITION
pos_after ||= MAX_POSITION
pos_before, pos_after = [pos_before, pos_after].sort
rand(pos_before.next..pos_after.pred)
end
def save_positionable_neighbours
return unless @positionable_neighbours
status = @positionable_neighbours.all?(&:save)
@positionable_neighbours = nil
status
end
end
......@@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base
include Sortable
include Spammable
include FasterCacheKeys
include RelativePositioning
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
......
......@@ -5,7 +5,7 @@ module Boards
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list?
issues = with_list_label(issues) if movable_list?
issues
issues.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'))
end
private
......@@ -26,7 +26,6 @@ module Boards
def filter_params
set_default_scope
set_default_sort
set_project
set_state
......@@ -37,10 +36,6 @@ module Boards
params[:scope] = 'all'
end
def set_default_sort
params[:sort] = 'priority'
end
def set_project
params[:project_id] = project.id
end
......
......@@ -3,7 +3,7 @@ module Boards
class MoveService < BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false unless valid_move?
return false if issue_params.empty?
update_service.execute(issue)
end
......@@ -14,7 +14,7 @@ module Boards
@board ||= project.boards.find(params[:board_id])
end
def valid_move?
def move_between_lists?
moving_from_list.present? && moving_to_list.present? &&
moving_from_list != moving_to_list
end
......@@ -32,11 +32,19 @@ module Boards
end
def issue_params
{
add_label_ids: add_label_ids,
remove_label_ids: remove_label_ids,
state_event: issue_state
}
attrs = {}
if move_between_lists?
attrs.merge!(
add_label_ids: add_label_ids,
remove_label_ids: remove_label_ids,
state_event: issue_state,
)
end
attrs[:move_between_iids] = move_between_iids if move_between_iids
attrs
end
def issue_state
......@@ -58,6 +66,12 @@ module Boards
Array(label_ids).compact
end
def move_between_iids
return unless params[:move_after_iid] || params[:move_before_iid]
[params[:move_after_iid], params[:move_before_iid]]
end
end
end
end
......@@ -211,7 +211,7 @@ class IssuableBaseService < BaseService
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
if params.present?
if issuable.changed? || params.present?
issuable.assign_attributes(params.merge(updated_by: current_user))
before_update(issuable)
......
......@@ -13,6 +13,7 @@ module Issues
def before_create(issue)
spam_check(issue, current_user)
issue.move_to_end
end
def after_create(issuable)
......
......@@ -3,8 +3,8 @@ module Issues
include SpamCheckService
def execute(issue)
handle_move_between_iids(issue)
filter_spam_check_params
update(issue)
end
......@@ -37,11 +37,13 @@ module Issues
end
added_labels = issue.labels - old_labels
if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user)
end
added_mentions = issue.mentioned_users - old_mentioned_users
if added_mentions.present?
notification_service.new_mentions_in_issue(issue, added_mentions, current_user)
end
......@@ -55,8 +57,24 @@ module Issues
Issues::CloseService
end
def handle_move_between_iids(issue)
return unless params[:move_between_iids]
after_iid, before_iid = params.delete(:move_between_iids)
issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid
issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid
issue.move_between(issue_before, issue_after)
end
private
def get_issue_if_allowed(project, iid)
issue = project.issues.find_by(iid: iid)
issue if can?(current_user, :update_issue, issue)
end
def create_confidentiality_note(issue)
SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
end
......
......@@ -8,7 +8,7 @@
"v-show" => "!loading",
":data-board" => "list.id",
":class" => '{ "is-smaller": showIssueForm }' }
%board-card{ "v-for" => "(issue, index) in orderedIssues",
%board-card{ "v-for" => "(issue, index) in issues",
"ref" => "issue",
":index" => "index",
":list" => "list",
......@@ -17,7 +17,8 @@
":root-path" => "rootPath",
":disabled" => "disabled",
":key" => "issue.id" }
%li.board-list-count.text-center{ "v-if" => "showCount" }
%li.board-list-count.text-center{ "v-if" => "showCount",
"data-issue-id" => "-1" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddRelativePositionToIssues < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
def up
add_column :issues, :relative_position, :integer
add_concurrent_index :issues, :relative_position
end
def down
remove_column :issues, :relative_position
remove_index :issues, :relative_position if index_exists? :issues, :relative_position
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170217151947) do
ActiveRecord::Schema.define(version: 20170308015651) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -29,10 +29,11 @@ ActiveRecord::Schema.define(version: 20170217151947) do
create_table "appearances", force: :cascade do |t|
t.string "title"
t.text "description"
t.string "header_logo"
t.string "logo"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "updated_by"
t.datetime "created_at"
t.datetime "updated_at"
t.string "header_logo"
t.text "description_html"
end
......@@ -46,6 +47,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.datetime "updated_at"
t.string "home_page_url"
t.integer "default_branch_protection", default: 2
t.text "help_text"
t.text "restricted_visibility_levels"
t.boolean "version_check_enabled", default: true
t.integer "max_attachment_size", default: 10, null: false
......@@ -61,6 +63,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.boolean "shared_runners_enabled", default: true, null: false
t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token"
t.integer "max_pages_size", default: 100, null: false
t.boolean "require_two_factor_authentication", default: false
t.integer "two_factor_grace_period", default: 48
t.boolean "metrics_enabled", default: false
......@@ -88,10 +91,13 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.integer "container_registry_token_expire_delay", default: 5
t.text "after_sign_up_text"
t.boolean "user_default_external", default: false, null: false
t.boolean "elasticsearch_indexing", default: false, null: false
t.boolean "elasticsearch_search", default: false, null: false
t.string "repository_storages", default: "default"
t.string "enabled_git_access_protocol"
t.boolean "domain_blacklist_enabled", default: false
t.text "domain_blacklist"
t.boolean "usage_ping_enabled", default: true, null: false
t.boolean "koding_enabled"
t.string "koding_url"
t.text "sign_in_text_html"
......@@ -109,11 +115,51 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.boolean "html_emails_enabled", default: true
t.string "plantuml_url"
t.boolean "plantuml_enabled"
t.integer "max_pages_size", default: 100, null: false
t.integer "shared_runners_minutes", default: 0, null: false
t.integer "repository_size_limit", limit: 8, default: 0
t.integer "terminal_max_session_time", default: 0, null: false
t.string "default_artifacts_expire_in", default: '0', null: false
t.integer "minimum_mirror_sync_time", default: 15, null: false
t.string "default_artifacts_expire_in", default: "0", null: false
t.integer "unique_ips_limit_per_user"
t.integer "unique_ips_limit_time_window"
t.boolean "unique_ips_limit_enabled", default: false, null: false
t.string "elasticsearch_url", default: "http://localhost:9200"
t.boolean "elasticsearch_aws", default: false, null: false
t.string "elasticsearch_aws_region", default: "us-east-1"
t.string "elasticsearch_aws_access_key"
t.string "elasticsearch_aws_secret_access_key"
t.integer "geo_status_timeout", default: 10
end
create_table "approvals", force: :cascade do |t|
t.integer "merge_request_id", null: false
t.integer "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "approver_groups", force: :cascade do |t|
t.integer "target_id", null: false
t.string "target_type", null: false
t.integer "group_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "approver_groups", ["group_id"], name: "index_approver_groups_on_group_id", using: :btree
add_index "approver_groups", ["target_id", "target_type"], name: "index_approver_groups_on_target_id_and_target_type", using: :btree
create_table "approvers", force: :cascade do |t|
t.integer "target_id", null: false
t.string "target_type"
t.integer "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "approvers", ["target_id", "target_type"], name: "index_approvers_on_target_id_and_target_type", using: :btree
add_index "approvers", ["user_id"], name: "index_approvers_on_user_id", using: :btree
create_table "audit_events", force: :cascade do |t|
t.integer "author_id", null: false
t.string "type", null: false
......@@ -142,8 +188,11 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.integer "project_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name", default: "Development", null: false
t.integer "milestone_id"
end
add_index "boards", ["milestone_id"], name: "index_boards_on_milestone_id", using: :btree
add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
create_table "broadcast_messages", force: :cascade do |t|
......@@ -172,6 +221,16 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree
add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree
create_table "chat_teams", force: :cascade do |t|
t.integer "namespace_id", null: false
t.string "team_id"
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
create_table "ci_application_settings", force: :cascade do |t|
t.boolean "all_broken_builds"
t.boolean "add_pusher"
......@@ -377,6 +436,8 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "gl_project_id"
t.integer "owner_id"
t.string "description"
end
add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
......@@ -472,16 +533,54 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
create_table "geo_nodes", force: :cascade do |t|
t.string "schema"
t.string "host"
t.integer "port"
t.string "relative_url_root"
t.boolean "primary"
t.integer "geo_node_key_id"
t.integer "oauth_application_id"
t.integer "system_hook_id"
t.string "access_key"
t.string "encrypted_secret_access_key"
t.string "encrypted_secret_access_key_iv"
t.boolean "enabled", default: true, null: false
end
add_index "geo_nodes", ["access_key"], name: "index_geo_nodes_on_access_key", using: :btree
add_index "geo_nodes", ["host"], name: "index_geo_nodes_on_host", using: :btree
add_index "geo_nodes", ["primary"], name: "index_geo_nodes_on_primary", using: :btree
create_table "historical_data", force: :cascade do |t|
t.date "date", null: false
t.integer "active_user_count"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "identities", force: :cascade do |t|
t.string "extern_uid"
t.string "provider"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "secondary_extern_uid"
end
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
create_table "index_statuses", force: :cascade do |t|
t.integer "project_id", null: false
t.datetime "indexed_at"
t.text "note"
t.string "last_commit"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "index_statuses", ["project_id"], name: "index_index_statuses_on_project_id", unique: true, using: :btree
create_table "issue_metrics", force: :cascade do |t|
t.integer "issue_id", null: false
t.datetime "first_mentioned_in_commit_at"
......@@ -507,6 +606,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
t.integer "weight"
t.boolean "confidential", default: false
t.datetime "deleted_at"
t.date "due_date"
......@@ -515,6 +615,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.text "title_html"
t.text "description_html"
t.integer "time_estimate"
t.integer "relative_position"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
......@@ -526,6 +627,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
......@@ -581,9 +683,18 @@ ActiveRecord::Schema.define(version: 20170217151947) do
end
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
create_table "ldap_group_links", force: :cascade do |t|
t.string "cn", null: false
t.integer "group_access", null: false
t.integer "group_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "provider"
end
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
......@@ -604,6 +715,12 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
create_table "licenses", force: :cascade do |t|
t.text "data", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "lists", force: :cascade do |t|
t.integer "board_id", null: false
t.integer "label_id"
......@@ -631,6 +748,8 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.datetime "invite_accepted_at"
t.datetime "requested_at"
t.date "expires_at"
t.boolean "ldap", default: false, null: false
t.boolean "override", default: false, null: false
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
......@@ -693,11 +812,14 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.integer "merge_user_id"
t.string "merge_commit_sha"
t.datetime "deleted_at"
t.integer "approvals_before_merge"
t.string "rebase_commit_sha"
t.string "in_progress_merge_commit_sha"
t.integer "lock_version"
t.text "title_html"
t.text "description_html"
t.integer "time_estimate"
t.boolean "squash", default: false, null: false
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......@@ -743,6 +865,14 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespace_statistics", force: :cascade do |t|
t.integer "namespace_id", null: false
t.integer "shared_runners_seconds", default: 0, null: false
t.datetime "shared_runners_seconds_last_reset"
end
add_index "namespace_statistics", ["namespace_id"], name: "index_namespace_statistics_on_namespace_id", unique: true, using: :btree
create_table "namespaces", force: :cascade do |t|
t.string "name", null: false
t.string "path", null: false
......@@ -752,17 +882,27 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.string "type"
t.string "description", default: "", null: false
t.string "avatar"
t.boolean "membership_lock", default: false
t.boolean "share_with_group_lock", default: false
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: false, null: false
t.string "ldap_sync_status", default: "ready", null: false
t.string "ldap_sync_error"
t.datetime "ldap_sync_last_update_at"
t.datetime "ldap_sync_last_successful_update_at"
t.datetime "ldap_sync_last_sync_at"
t.datetime "deleted_at"
t.boolean "lfs_enabled"
t.text "description_html"
t.boolean "lfs_enabled"
t.integer "parent_id"
t.integer "shared_runners_minutes_limit"
t.integer "repository_size_limit", limit: 8
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree
add_index "namespaces", ["ldap_sync_last_successful_update_at"], name: "index_namespaces_on_ldap_sync_last_successful_update_at", using: :btree
add_index "namespaces", ["ldap_sync_last_update_at"], name: "index_namespaces_on_ldap_sync_last_update_at", using: :btree
add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
......@@ -874,6 +1014,18 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
create_table "path_locks", force: :cascade do |t|
t.string "path", null: false
t.integer "project_id"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "path_locks", ["path"], name: "index_path_locks_on_path", using: :btree
add_index "path_locks", ["project_id"], name: "index_path_locks_on_project_id", using: :btree
add_index "path_locks", ["user_id"], name: "index_path_locks_on_user_id", using: :btree
create_table "personal_access_tokens", force: :cascade do |t|
t.integer "user_id", null: false
t.string "token", null: false
......@@ -938,6 +1090,8 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.integer "repository_size", limit: 8, default: 0, null: false
t.integer "lfs_objects_size", limit: 8, default: 0, null: false
t.integer "build_artifacts_size", limit: 8, default: 0, null: false
t.integer "shared_runners_seconds", limit: 8, default: 0, null: false
t.datetime "shared_runners_seconds_last_reset"
end
add_index "project_statistics", ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree
......@@ -957,9 +1111,19 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.boolean "archived", default: false, null: false
t.string "avatar"
t.string "import_status"
t.text "merge_requests_template"
t.integer "star_count", default: 0, null: false
t.boolean "merge_requests_rebase_enabled", default: false
t.string "import_type"
t.string "import_source"
t.integer "approvals_before_merge", default: 0, null: false
t.boolean "reset_approvals_on_push", default: true
t.boolean "merge_requests_ff_only_enabled", default: false
t.text "issues_template"
t.boolean "mirror", default: false, null: false
t.datetime "mirror_last_update_at"
t.datetime "mirror_last_successful_update_at"
t.integer "mirror_user_id"
t.text "import_error"
t.integer "ci_id"
t.boolean "shared_runners_enabled", default: true, null: false
......@@ -967,6 +1131,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.string "build_coverage_regex"
t.boolean "build_allow_git_fetch", default: true, null: false
t.integer "build_timeout", default: 3600, null: false
t.boolean "mirror_trigger_builds", default: false, null: false
t.boolean "pending_delete", default: false
t.boolean "public_builds", default: true, null: false
t.boolean "last_repository_check_failed"
......@@ -975,11 +1140,14 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.boolean "only_allow_merge_if_pipeline_succeeds", default: false, null: false
t.boolean "has_external_issue_tracker"
t.string "repository_storage", default: "default", null: false
t.boolean "repository_read_only"
t.boolean "request_access_enabled", default: false, null: false
t.boolean "has_external_wiki"
t.boolean "lfs_enabled"
t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.integer "repository_size_limit", limit: 8
t.integer "sync_time", default: 60, null: false
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......@@ -988,6 +1156,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
add_index "projects", ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree
add_index "projects", ["mirror_last_successful_update_at"], name: "index_projects_on_mirror_last_successful_update_at", using: :btree
add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
......@@ -995,25 +1164,32 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["sync_time"], name: "index_projects_on_sync_time", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
create_table "protected_branch_merge_access_levels", force: :cascade do |t|
t.integer "protected_branch_id", null: false
t.integer "access_level", default: 40, null: false
t.integer "access_level", default: 40
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.integer "group_id"
end
add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree
add_index "protected_branch_merge_access_levels", ["user_id"], name: "index_protected_branch_merge_access_levels_on_user_id", using: :btree
create_table "protected_branch_push_access_levels", force: :cascade do |t|
t.integer "protected_branch_id", null: false
t.integer "access_level", default: 40, null: false
t.integer "access_level", default: 40
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.integer "group_id"
end
add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree
add_index "protected_branch_push_access_levels", ["user_id"], name: "index_protected_branch_push_access_levels_on_user_id", using: :btree
create_table "protected_branches", force: :cascade do |t|
t.integer "project_id", null: false
......@@ -1024,6 +1200,24 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
create_table "push_rules", force: :cascade do |t|
t.string "force_push_regex"
t.string "delete_branch_regex"
t.string "commit_message_regex"
t.boolean "deny_delete_tag"
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "author_email_regex"
t.boolean "member_check", default: false, null: false
t.string "file_name_regex"
t.boolean "is_sample", default: false
t.integer "max_file_size", default: 0, null: false
t.boolean "prevent_secrets", default: false, null: false
end
add_index "push_rules", ["project_id"], name: "index_push_rules_on_project_id", using: :btree
create_table "releases", force: :cascade do |t|
t.string "tag"
t.text "description"
......@@ -1036,6 +1230,26 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree
create_table "remote_mirrors", force: :cascade do |t|
t.integer "project_id"
t.string "url"
t.boolean "enabled", default: false
t.string "update_status"
t.datetime "last_update_at"
t.datetime "last_successful_update_at"
t.string "last_error"
t.text "encrypted_credentials"
t.string "encrypted_credentials_iv"
t.string "encrypted_credentials_salt"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "sync_time", default: 60, null: false
end
add_index "remote_mirrors", ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree
add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree
add_index "remote_mirrors", ["sync_time"], name: "index_remote_mirrors_on_sync_time", using: :btree
create_table "routes", force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
......@@ -1209,6 +1423,20 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
create_table "uploads", force: :cascade do |t|
t.integer "size", limit: 8, null: false
t.string "path", null: false
t.string "checksum", limit: 64
t.integer "model_id"
t.string "model_type"
t.string "uploader", null: false
t.datetime "created_at", null: false
end
add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree
add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree
add_index "uploads", ["path"], name: "index_uploads_on_path", using: :btree
create_table "user_agent_details", force: :cascade do |t|
t.string "user_agent", null: false
t.string "ip_address", null: false
......@@ -1259,6 +1487,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.string "unconfirmed_email"
t.boolean "hide_no_ssh_key", default: false
t.string "website_url", default: "", null: false
t.datetime "admin_email_unsubscribed_at"
t.string "notification_email"
t.boolean "hide_no_password", default: false
t.boolean "password_automatically_set", default: false
......@@ -1274,6 +1503,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.integer "consumed_timestep"
t.integer "layout", default: 0
t.boolean "hide_project_limit", default: false
t.text "note"
t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
......@@ -1281,6 +1511,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.string "incoming_email_token"
t.string "organization"
t.boolean "authorized_projects_populated"
t.boolean "auditor", default: false, null: false
t.boolean "notified_of_own_activity", default: false, null: false
t.boolean "ghost"
end
......@@ -1321,6 +1552,7 @@ ActiveRecord::Schema.define(version: 20170217151947) do
t.boolean "issues_events", default: false, null: false
t.boolean "merge_requests_events", default: false, null: false
t.boolean "tag_push_events", default: false
t.integer "group_id"
t.boolean "note_events", default: false, null: false
t.boolean "enable_ssl_verification", default: true
t.boolean "build_events", default: false, null: false
......@@ -1332,7 +1564,10 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_foreign_key "approver_groups", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "boards", "projects"
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
......@@ -1343,12 +1578,20 @@ ActiveRecord::Schema.define(version: 20170217151947) do
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "namespace_statistics", "namespaces", on_delete: :cascade
add_foreign_key "path_locks", "projects"
add_foreign_key "path_locks", "users"
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "project_statistics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "namespaces", column: "group_id"
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_merge_access_levels", "users"
add_foreign_key "protected_branch_push_access_levels", "namespaces", column: "group_id"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "users"
add_foreign_key "remote_mirrors", "projects"
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
......
......@@ -43,6 +43,7 @@ describe Projects::Boards::IssuesController do
expect(response).to match_response_schema('issues')
expect(parsed_response.length).to eq 2
expect(development.issues.map(&:relative_position)).not_to include(nil)
end
end
......
......@@ -71,16 +71,16 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) }
let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) }
let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) }
let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) }
let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) }
let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning], relative_position: 8) }
let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) }
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) }
let!(:issue8) { create(:closed_issue, project: project) }
let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) }
let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) }
before do
visit namespace_project_board_path(project.namespace, project, board)
......
require 'rails_helper'
describe 'Issue Boards', :feature, :js do
include WaitForVueResource
include DragTo
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:label) { create(:label, project: project) }
let!(:list1) { create(:list, board: board, label: label, position: 0) }
let!(:issue1) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label], relative_position: 3) }
let!(:issue2) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label], relative_position: 2) }
let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
before do
project.team << [user, :master]
login_as(user)
end
context 'un-ordered issues' do
let!(:issue4) { create(:labeled_issue, project: project, labels: [label]) }
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 2)
end
it 'has un-ordered issue as last issue' do
page.within(first('.board')) do
expect(all('.card').last).to have_content(issue4.title)
end
end
it 'moves un-ordered issue to top of list' do
drag(from_index: 3, to_index: 0)
page.within(first('.board')) do
expect(first('.card')).to have_content(issue4.title)
end
end
end
context 'ordering in list' do
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 2)
end
it 'moves from middle to top' do
drag(from_index: 1, to_index: 0)
wait_for_vue_resource
expect(first('.card')).to have_content(issue2.title)
end
it 'moves from middle to bottom' do
drag(from_index: 1, to_index: 2)
wait_for_vue_resource
expect(all('.card').last).to have_content(issue2.title)
end
it 'moves from top to bottom' do
drag(from_index: 0, to_index: 2)
wait_for_vue_resource
expect(all('.card').last).to have_content(issue3.title)
end
it 'moves from bottom to top' do
drag(from_index: 2, to_index: 0)
wait_for_vue_resource
expect(first('.card')).to have_content(issue1.title)
end
it 'moves from top to middle' do
drag(from_index: 0, to_index: 1)
wait_for_vue_resource
expect(first('.card')).to have_content(issue2.title)
end
it 'moves from bottom to middle' do
drag(from_index: 2, to_index: 1)
wait_for_vue_resource
expect(all('.card').last).to have_content(issue2.title)
end
end
context 'ordering when changing list' do
let(:label2) { create(:label, project: project) }
let!(:list2) { create(:list, board: board, label: label2, position: 1) }
let!(:issue4) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label2], relative_position: 3.0) }
let!(:issue5) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label2], relative_position: 2.0) }
let!(:issue6) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label2], relative_position: 1.0) }
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
expect(page).to have_selector('.board', count: 3)
end
it 'moves to top of another list' do
drag(list_from_index: 0, list_to_index: 1)
wait_for_vue_resource
expect(first('.board')).to have_selector('.card', count: 2)
expect(all('.board')[1]).to have_selector('.card', count: 4)
page.within(all('.board')[1]) do
expect(first('.card')).to have_content(issue3.title)
end
end
it 'moves to bottom of another list' do
drag(list_from_index: 0, list_to_index: 1, to_index: 2)
wait_for_vue_resource
expect(first('.board')).to have_selector('.card', count: 2)
expect(all('.board')[1]).to have_selector('.card', count: 4)
page.within(all('.board')[1]) do
expect(all('.card').last).to have_content(issue3.title)
end
end
it 'moves to index of another list' do
drag(list_from_index: 0, list_to_index: 1, to_index: 1)
wait_for_vue_resource
expect(first('.board')).to have_selector('.card', count: 2)
expect(all('.board')[1]).to have_selector('.card', count: 4)
page.within(all('.board')[1]) do
expect(all('.card')[1]).to have_content(issue3.title)
end
end
end
def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0)
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
from_index: from_index,
to_index: to_index,
list_to_index: list_to_index)
end
end
......@@ -11,8 +11,8 @@ describe 'Issue Boards', feature: true, js: true do
let!(:bug) { create(:label, project: project, name: 'Bug') }
let!(:regression) { create(:label, project: project, name: 'Regression') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') }
let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) }
let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development], relative_position: 2) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
let(:card) { first('.board').first('.card') }
......
......@@ -11,6 +11,7 @@
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
"labels": {
"type": "array",
"items": {
......
......@@ -5,6 +5,7 @@
/* global Cookies */
/* global listObj */
/* global listObjDuplicate */
/* global ListIssue */
require('~/lib/utils/url_utility');
require('~/boards/models/issue');
......@@ -14,6 +15,7 @@ require('~/boards/models/user');
require('~/boards/services/board_service');
require('~/boards/stores/boards_store');
require('./mock_data');
require('es6-promise').polyfill();
describe('Store', () => {
beforeEach(() => {
......@@ -21,6 +23,10 @@ describe('Store', () => {
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => {
resolve();
}));
Cookies.set('issue_board_welcome_hidden', 'false', {
expires: 365 * 10,
path: ''
......@@ -154,5 +160,74 @@ describe('Store', () => {
done();
}, 0);
});
it('moves issue to top of another list', (done) => {
const listOne = gl.issueBoards.BoardsStore.addList(listObj);
const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
setTimeout(() => {
listOne.issues[0].id = 2;
expect(listOne.issues.length).toBe(1);
expect(listTwo.issues.length).toBe(1);
gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0);
expect(listOne.issues.length).toBe(0);
expect(listTwo.issues.length).toBe(2);
expect(listTwo.issues[0].id).toBe(2);
expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, null, 1);
done();
}, 0);
});
it('moves issue to bottom of another list', (done) => {
const listOne = gl.issueBoards.BoardsStore.addList(listObj);
const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
setTimeout(() => {
listOne.issues[0].id = 2;
expect(listOne.issues.length).toBe(1);
expect(listTwo.issues.length).toBe(1);
gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1);
expect(listOne.issues.length).toBe(0);
expect(listTwo.issues.length).toBe(2);
expect(listTwo.issues[1].id).toBe(2);
expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, 1, null);
done();
}, 0);
});
it('moves issue in list', (done) => {
const issue = new ListIssue({
title: 'Testing',
iid: 2,
confidential: false,
labels: []
});
const list = gl.issueBoards.BoardsStore.addList(listObj);
setTimeout(() => {
list.addIssue(issue);
expect(list.issues.length).toBe(2);
gl.issueBoards.BoardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]);
expect(list.issues[0].id).toBe(2);
expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null);
done();
});
});
});
});
......@@ -79,4 +79,20 @@ describe('Issue model', () => {
issue.removeLabels([issue.labels[0], issue.labels[1]]);
expect(issue.labels.length).toBe(0);
});
it('sets position to infinity if no position is stored', () => {
expect(issue.position).toBe(Infinity);
});
it('sets position', () => {
const relativePositionIssue = new ListIssue({
title: 'Testing',
iid: 1,
confidential: false,
relative_position: 1,
labels: []
});
expect(relativePositionIssue.position).toBe(1);
});
});
......@@ -103,6 +103,7 @@ describe('List model', () => {
listDup.updateIssueLabel(list, issue);
expect(gl.boardService.moveIssue).toHaveBeenCalledWith(issue.id, list.id, listDup.id);
expect(gl.boardService.moveIssue)
.toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined);
});
});
......@@ -21,6 +21,7 @@ Issue:
- milestone_id
- weight
- time_estimate
- relative_position
Event:
- id
- target_type
......
require 'spec_helper'
describe Issue, 'RelativePositioning' do
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:issue1) { create(:issue, project: project) }
let(:new_issue) { create(:issue, project: project) }
before do
[issue, issue1].each do |issue|
issue.move_to_end && issue.save
end
end
describe '#min_relative_position' do
it 'returns maximum position' do
expect(issue.min_relative_position).to eq issue.relative_position
end
end
describe '#max_relative_position' do
it 'returns maximum position' do
expect(issue.max_relative_position).to eq issue1.relative_position
end
end
describe '#prev_relative_position' do
it 'returns previous position if there is an issue above' do
expect(issue1.prev_relative_position).to eq issue.relative_position
end
it 'returns minimum position if there is no issue above' do
expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION
end
end
describe '#next_relative_position' do
it 'returns next position if there is an issue below' do
expect(issue.next_relative_position).to eq issue1.relative_position
end
it 'returns next position if there is no issue below' do
expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION
end
end
describe '#move_before' do
it 'moves issue before' do
[issue1, issue].each(&:move_to_end)
issue.move_before(issue1)
expect(issue.relative_position).to be < issue1.relative_position
end
end
describe '#move_after' do
it 'moves issue after' do
[issue, issue1].each(&:move_to_end)
issue.move_after(issue1)
expect(issue.relative_position).to be > issue1.relative_position
end
end
describe '#move_to_end' do
it 'moves issue to the end' do
new_issue.move_to_end
expect(new_issue.relative_position).to be > issue1.relative_position
end
end
describe '#move_between' do
it 'positions issue between two other' do
new_issue.move_between(issue, issue1)
expect(new_issue.relative_position).to be > issue.relative_position
expect(new_issue.relative_position).to be < issue1.relative_position
end
it 'positions issue between on top' do
new_issue.move_between(nil, issue)
expect(new_issue.relative_position).to be < issue.relative_position
end
it 'positions issue between to end' do
new_issue.move_between(issue1, nil)
expect(new_issue.relative_position).to be > issue1.relative_position
end
it 'positions issues even when after and before positions are the same' do
issue1.update relative_position: issue.relative_position
new_issue.move_between(issue, issue1)
expect(new_issue.relative_position).to be > issue.relative_position
expect(issue.relative_position).to be < issue1.relative_position
end
end
end
......@@ -43,32 +43,6 @@ describe Boards::Issues::ListService, services: true do
described_class.new(project, user, params).execute
end
context 'sets default order to priority' do
it 'returns opened issues when list id is missing' do
params = { board_id: board.id }
issues = described_class.new(project, user, params).execute
expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
end
it 'returns closed issues when listing issues from Done' do
params = { board_id: board.id, id: done.id }
issues = described_class.new(project, user, params).execute
expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
end
it 'returns opened issues that have label list applied when listing issues from a label list' do
params = { board_id: board.id, id: list1.id }
issues = described_class.new(project, user, params).execute
expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
end
end
context 'with list that does not belong to the board' do
it 'raises an error' do
list = create(:list)
......
......@@ -78,8 +78,10 @@ describe Boards::Issues::MoveService, services: true do
end
context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:issue1) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:issue2) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
it 'returns false' do
expect(described_class.new(project, user, params).execute(issue)).to eq false
......@@ -90,6 +92,18 @@ describe Boards::Issues::MoveService, services: true do
expect(issue.reload.labels).to contain_exactly(bug, development)
end
it 'sorts issues' do
[issue, issue1, issue2].each do |issue|
issue.move_to_end && issue.save!
end
params.merge!(move_after_iid: issue1.iid, move_before_iid: issue2.iid)
described_class.new(project, user, params).execute(issue)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
end
end
end
end
......@@ -58,6 +58,22 @@ describe Issues::UpdateService, services: true do
expect(issue.due_date).to eq Date.tomorrow
end
it 'sorts issues as specified by parameters' do
issue1 = create(:issue, project: project, assignee_id: user3.id)
issue2 = create(:issue, project: project, assignee_id: user3.id)
[issue, issue1, issue2].each do |issue|
issue.move_to_end
issue.save
end
opts[:move_between_iids] = [issue1.iid, issue2.iid]
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
end
context 'when current user cannot admin issues in the project' do
let(:guest) { create(:user) }
before do
......
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