Commit 32e91bd6 authored by Ruben Davila's avatar Ruben Davila

Merge remote-tracking branch 'ce/8-11-stable' into 8-11-stable-ee

parents f6bb1db8 9510073d
......@@ -31,9 +31,8 @@
this.input
.on('keydown', function (e) {
var keyCode = e.which;
if (keyCode === 13) {
e.preventDefault()
e.preventDefault();
}
})
.on('keyup', function(e) {
......@@ -111,9 +110,9 @@
matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
if (!$el.is('.dropdown-header')) {
if (matches.length) {
return $el.show();
return $el.show().removeClass('option-hidden');
} else {
return $el.hide();
return $el.hide().addClass('option-hidden');
}
}
});
......@@ -179,7 +178,7 @@
})();
GitLabDropdown = (function() {
var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex;
var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
LOADING_CLASS = "is-loading";
......@@ -191,6 +190,12 @@
currentIndex = -1;
NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden';
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")";
CURSOR_SELECT_SCROLL_PADDING = 5
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
function GitLabDropdown(el1, options) {
......@@ -213,6 +218,7 @@
if (this.options.data) {
if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
this.fullData = this.options.data;
currentIndex = -1;
this.parseData(this.options.data);
} else {
this.remote = new GitLabDropdownRemote(this.options.data, {
......@@ -240,7 +246,7 @@
keys: searchFields,
elements: (function(_this) {
return function() {
selector = '.dropdown-content li:not(.divider)';
selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
......@@ -256,7 +262,7 @@
return function(data) {
_this.parseData(data);
if (_this.filterInput.val() !== '') {
selector = '.dropdown-content li:not(.divider):visible';
selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
......@@ -376,7 +382,7 @@
var $target;
if (this.options.multiSelect) {
$target = $(e.target);
if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
e.stopPropagation();
return false;
} else {
......@@ -387,7 +393,7 @@
GitLabDropdown.prototype.opened = function() {
var contentHtml;
currentIndex = -1;
this.resetRows();
this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
......@@ -410,6 +416,7 @@
GitLabDropdown.prototype.hidden = function(e) {
var $input;
this.resetRows();
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
......@@ -463,7 +470,7 @@
return "<li class='separator'></li>";
}
if (data.header != null) {
return "<li class='dropdown-header'>" + data.header + "</li>";
return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
}
if (this.options.renderRow) {
html = this.options.renderRow.call(this.options, data, this);
......@@ -495,11 +502,16 @@
text = this.highlightTextMatches(text, this.filterInput.val());
}
if (group) {
groupAttrs = "data-group='" + group + "' data-index='" + index + "'";
groupAttrs = 'data-group=' + group + ' data-index=' + index;
} else {
groupAttrs = '';
}
html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>";
html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({
url: url,
groupAttrs: groupAttrs,
cssClass: cssClass,
text: text
});
}
return html;
};
......@@ -521,17 +533,6 @@
return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
};
GitLabDropdown.prototype.highlightRow = function(index) {
var selector;
if (this.filterInput.val() !== "") {
selector = '.dropdown-content li:first-child a';
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one .dropdown-content li:first-child a";
}
return this.getElement(selector).addClass('is-focused');
}
};
GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
isInput = $(this.el).is('input');
......@@ -612,13 +613,15 @@
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector;
selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a";
selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
$el = $(selector, this.dropdown);
if ($el.length) {
return $el.first().trigger('click');
$el.first().trigger('click');
var href = $el.attr('href');
if (href && href !== '#') Turbolinks.visit(href);
}
};
......@@ -626,7 +629,7 @@
var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find(".dropdown-input-field");
selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible';
selector = SELECTABLE_CLASSES;
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
......@@ -654,7 +657,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1);
return _this.selectRowAtIndex(currentIndex);
}
};
})(this));
......@@ -664,6 +667,11 @@
return $('body').off('keydown');
};
GitLabDropdown.prototype.resetRows = function resetRows() {
currentIndex = -1;
$('.is-focused', this.dropdown).removeClass('is-focused');
};
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
$('.is-focused', this.dropdown).removeClass('is-focused');
......@@ -677,10 +685,14 @@
listItemHeight = $listItem.outerHeight();
listItemTop = $listItem.prop('offsetTop');
listItemBottom = listItemTop + listItemHeight;
if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom);
} else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
return $dropdownContent.scrollTop(listItemTop - dropdownContentTop);
if (!index) {
$dropdownContent.scrollTop(0)
} else if (index === ($listItems.length - 1)) {
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
} else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
$dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
} else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
}
};
......
......@@ -7,7 +7,9 @@
KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
ENTER: 13
ENTER: 13,
UP: 38,
DOWN: 40
};
function SearchAutocomplete(opts) {
......@@ -223,6 +225,12 @@
case KEYCODE.ESCAPE:
this.restoreOriginalState();
break;
case KEYCODE.ENTER:
this.disableAutocomplete();
break;
case KEYCODE.UP:
case KEYCODE.DOWN:
return;
default:
if (this.searchInput.val() === '') {
this.disableAutocomplete();
......@@ -319,9 +327,11 @@
};
SearchAutocomplete.prototype.disableAutocomplete = function() {
this.searchInput.addClass('disabled');
this.dropdown.removeClass('open');
return this.restoreMenu();
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
this.searchInput.addClass('disabled');
this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
this.restoreMenu();
}
};
SearchAutocomplete.prototype.restoreMenu = function() {
......
......@@ -179,40 +179,46 @@ class Ability
end
def project_abilities(user, project)
rules = []
key = "/user/#{user.id}/project/#{project.id}"
RequestStore.store[key] ||= begin
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
if RequestStore.active?
RequestStore.store[key] ||= uncached_project_abilities(user, project)
else
uncached_project_abilities(user, project)
end
end
rules << :change_repository_storage if user.admin?
def uncached_project_abilities(user, project)
rules = []
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
owner = user.admin? ||
project.owner == user ||
(project.group && project.group.has_owner?(user))
rules << :change_repository_storage if user.admin?
if owner
rules.push(*project_owner_rules)
end
owner = user.admin? ||
project.owner == user ||
(project.group && project.group.has_owner?(user))
if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
if owner
rules.push(*project_owner_rules)
end
# Allow to read builds for internal projects
rules << :read_build if project.public_builds?
if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
unless owner || project.team.member?(user) || project_group_member?(project, user)
rules << :request_access if project.request_access_enabled
end
end
# Allow to read builds for internal projects
rules << :read_build if project.public_builds?
if project.archived?
rules -= project_archived_rules
unless owner || project.team.member?(user) || project_group_member?(project, user)
rules << :request_access if project.request_access_enabled
end
end
rules - project_disabled_features_rules(project)
if project.archived?
rules -= project_archived_rules
end
(rules - project_disabled_features_rules(project)).uniq
end
def project_team_rules(team, user)
......
......@@ -265,6 +265,8 @@ class Note < ActiveRecord::Base
def ensure_discussion_id
return unless self.persisted?
# Needed in case the SELECT statement doesn't ask for `discussion_id`
return unless self.has_attribute?(:discussion_id)
return if self.discussion_id
set_discussion_id
......
......@@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-11-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v3.3.3
sudo -u git -H git checkout v3.4.0
```
### 5. Update gitlab-workhorse
......
%div
.dropdown.inline
%button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Projects
%i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Go to project
%button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
%i.fa.fa-times.dropdown-menu-close-icon
.dropdown-input
%input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
%i.fa.fa-search.dropdown-input-search
.dropdown-content
.dropdown-loading
%i.fa.fa-spinner.fa-spin
/*= require jquery */
/*= require gl_dropdown */
/*= require turbolinks */
/*= require lib/utils/common_utils */
/*= require lib/utils/type_utility */
(() => {
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
const ARROW_KEYS = {
DOWN: 40,
UP: 38,
ENTER: 13,
ESC: 27
};
let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
i = i || 0;
if (!i) direction = direction.toUpperCase();
$('body').trigger({
type: 'keydown',
which: ARROW_KEYS[direction],
keyCode: ARROW_KEYS[direction]
});
i++;
if (i <= steps) {
navigateWithKeys(direction, steps, cb, i);
} else {
cb();
}
};
describe('Dropdown', function describeDropdown() {
fixture.preload('gl_dropdown.html');
fixture.preload('projects.json');
beforeEach(() => {
fixture.load('gl_dropdown.html');
this.dropdownContainerElement = $('.dropdown.inline');
this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
this.projectsData = fixture.load('projects.json')[0];
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
selectable: true,
data: this.projectsData,
text: (project) => {
(project.name_with_namespace || project.name);
},
id: (project) => {
project.id;
}
});
});
afterEach(() => {
$('body').unbind('keydown');
this.dropdownContainerElement.unbind('keyup');
});
it('should open on click', () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
this.dropdownButtonElement.click();
expect(this.dropdownContainerElement).toHaveClass('open');
});
describe('that is open', () => {
beforeEach(() => {
this.dropdownButtonElement.click();
});
it('should select a following item on DOWN keypress', () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
navigateWithKeys('down', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
});
});
it('should select a previous item on UP keypress', () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
navigateWithKeys('down', (this.projectsData.length - 1), () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
navigateWithKeys('up', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
});
});
});
it('should click the selected item on ENTER keypress', () => {
expect(this.dropdownContainerElement).toHaveClass('open')
let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
navigateWithKeys('down', randomIndex, () => {
spyOn(Turbolinks, 'visit').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
expect(link).toHaveClass('is-active');
let linkedLocation = link.attr('href');
if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
});
});
});
it('should close on ESC keypress', () => {
expect(this.dropdownContainerElement).toHaveClass('open');
this.dropdownContainerElement.trigger({
type: 'keyup',
which: ARROW_KEYS.ESC,
keyCode: ARROW_KEYS.ESC
});
expect(this.dropdownContainerElement).not.toHaveClass('open');
});
});
});
})();
......@@ -105,13 +105,13 @@
a3 = "a[href='" + mrsAssignedToMeLink + "']";
a4 = "a[href='" + mrsIHaveCreatedLink + "']";
expect(list.find(a1).length).toBe(1);
expect(list.find(a1).text()).toBe(' Issues assigned to me ');
expect(list.find(a1).text()).toBe('Issues assigned to me');
expect(list.find(a2).length).toBe(1);
expect(list.find(a2).text()).toBe(" Issues I've created ");
expect(list.find(a2).text()).toBe("Issues I've created");
expect(list.find(a3).length).toBe(1);
expect(list.find(a3).text()).toBe(' Merge requests assigned to me ');
expect(list.find(a3).text()).toBe('Merge requests assigned to me');
expect(list.find(a4).length).toBe(1);
return expect(list.find(a4).text()).toBe(" Merge requests I've created ");
return expect(list.find(a4).text()).toBe("Merge requests I've created");
};
describe('Search autocomplete dropdown', function() {
......
......@@ -171,6 +171,70 @@ describe Ability, lib: true do
end
end
shared_examples_for ".project_abilities" do |enable_request_store|
before do
RequestStore.begin! if enable_request_store
end
after do
if enable_request_store
RequestStore.end!
RequestStore.clear!
end
end
describe '.project_abilities' do
let!(:project) { create(:empty_project, :public) }
let!(:user) { create(:user) }
it 'returns permissions for admin user' do
admin = create(:admin)
results = described_class.project_abilities(admin, project)
expect(results.count).to eq(68)
end
it 'returns permissions for an owner' do
results = described_class.project_abilities(project.owner, project)
expect(results.count).to eq(68)
end
it 'returns permissions for a master' do
project.team << [user, :master]
results = described_class.project_abilities(user, project)
expect(results.count).to eq(60)
end
it 'returns permissions for a developer' do
project.team << [user, :developer]
results = described_class.project_abilities(user, project)
expect(results.count).to eq(44)
end
it 'returns permissions for a guest' do
project.team << [user, :guest]
results = described_class.project_abilities(user, project)
expect(results.count).to eq(21)
end
end
end
describe '.project_abilities with RequestStore' do
it_behaves_like ".project_abilities", true
end
describe '.project_abilities without RequestStore' do
it_behaves_like ".project_abilities", false
end
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
......
require 'spec_helper'
describe Network::Graph, models: true do
let(:project) { create(:project) }
let!(:note_on_commit) { create(:note_on_commit, project: project) }
it '#initialize' do
graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil)
expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
end
end
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