Commit 7c526cf6 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into ce_upstream

parents 3d8e2458 13bb9ed7
This diff is collapsed.
......@@ -3,6 +3,7 @@
LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
function LabelManager(opts) {
// Defaults
var ref, ref1, ref2;
if (opts == null) {
opts = {};
......@@ -28,6 +29,7 @@
$btn = $(e.currentTarget);
$label = $("#" + ($btn.data('domId')));
action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
// Make sure tooltip will hide
$tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
$tooltip.tooltip('destroy');
return _this.toggleLabelPriority($label, action);
......@@ -42,6 +44,7 @@
url = $label.find('.js-toggle-priority').data('url');
$target = this.prioritizedLabels;
$from = this.otherLabels;
// Optimistic update
if (action === 'remove') {
$target = this.otherLabels;
$from = this.prioritizedLabels;
......@@ -53,6 +56,7 @@
$target.find('.empty-message').addClass('hidden');
}
$label.detach().appendTo($target);
// Return if we are not persisting state
if (!persistState) {
return;
}
......@@ -61,6 +65,7 @@
url: url,
type: 'DELETE'
});
// Restore empty message
if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
......
......@@ -25,6 +25,8 @@
return callback(group);
});
},
// Return groups list. Filtered by query
// Only active groups retrieved
groups: function(query, skip_ldap, callback) {
var url = Api.buildUrl(Api.groupsPath);
return $.ajax({
......@@ -39,6 +41,7 @@
return callback(groups);
});
},
// Return namespaces list. Filtered by query
namespaces: function(query, callback) {
var url = Api.buildUrl(Api.namespacesPath);
return $.ajax({
......@@ -53,6 +56,7 @@
return callback(namespaces);
});
},
// Return projects list. Filtered by query
projects: function(query, order, callback) {
var url = Api.buildUrl(Api.projectsPath);
return $.ajax({
......@@ -83,6 +87,7 @@
return callback(message.responseJSON);
});
},
// Return group projects list. Filtered by query
groupProjects: function(group_id, query, callback) {
var url = Api.buildUrl(Api.groupProjectsPath)
.replace(':id', group_id);
......@@ -98,6 +103,7 @@
return callback(projects);
});
},
// Return text for a specific license
licenseText: function(key, data, callback) {
var url = Api.buildUrl(Api.licensePath)
.replace(':key', key);
......
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
/*= require jquery2 */
/*= require jquery-ui/autocomplete */
/*= require jquery-ui/datepicker */
......@@ -77,6 +83,7 @@
}
};
// Disable button if text field is empty
window.disableButtonIfEmptyField = function(field_selector, button_selector) {
var closest_submit, field;
field = $(field_selector);
......@@ -93,6 +100,7 @@
});
};
// Disable button if any input field with given selector is empty
window.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) {
var closest_submit, updateButtons;
closest_submit = form.find(button_selector);
......@@ -131,6 +139,8 @@
$.timeago.settings.allowFuture = true;
window.onload = function() {
// Scroll the window to avoid the topnav bar
// https://github.com/twitter/bootstrap/issues/1768
if (location.hash) {
return setTimeout(shiftWindow, 100);
}
......@@ -152,6 +162,8 @@
return $(this).select().one('mouseup', function(e) {
return e.preventDefault();
});
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
});
$('.remove-row').bind('ajax:success', function() {
$(this).tooltip('destroy')
......@@ -166,6 +178,7 @@
});
$('select.select2').select2({
width: 'resolve',
// Initialize select2 selects
dropdownAutoWidth: true
});
$('.js-select2').bind('select2-close', function() {
......@@ -173,7 +186,9 @@
$('.select2-container-active').removeClass('select2-container-active');
return $(':focus').blur();
}), 1);
// Close select2 on escape
});
// Initialize tooltips
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
placement: function(_, el) {
......@@ -182,14 +197,17 @@
});
$('.trigger-submit').on('change', function() {
return $(this).parents('form').submit();
// Form submitter
});
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
// Flash
if ((flash = $(".flash-container")).length > 0) {
flash.click(function() {
return $(this).fadeOut();
});
flash.show();
}
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) {
var buttons;
buttons = $('[type="submit"]', this);
......@@ -210,6 +228,7 @@
}
});
$('.account-box').hover(function() {
// Show/Hide the profile menu when hovering the account box
return $(this).toggleClass('hover');
});
$document.on('click', '.diff-content .js-show-suppressed-diff', function() {
......@@ -217,6 +236,7 @@
$container = $(this).parent();
$container.next('table').show();
return $container.remove();
// Commit show suppressed diff
});
$('.navbar-toggle').on('click', function() {
$('.header-content .title').toggle();
......@@ -224,6 +244,7 @@
$('.header-content .navbar-collapse').toggle();
return $('.navbar-toggle').toggleClass('active');
});
// Show/hide comments on diff
$body.on("click", ".js-toggle-diff-comments", function(e) {
var $this = $(this);
$this.toggleClass('active');
......
......@@ -16,7 +16,7 @@
}
Autosave.prototype.restore = function() {
var e, error, text;
var e, text;
if (window.localStorage == null) {
return;
}
......@@ -41,7 +41,7 @@
if ((text != null ? text.length : void 0) > 0) {
try {
return window.localStorage.setItem(this.key, text);
} catch (undefined) {}
} catch (error) {}
} else {
return this.reset();
}
......@@ -53,7 +53,7 @@
}
try {
return window.localStorage.removeItem(this.key);
} catch (undefined) {}
} catch (error) {}
};
return Autosave;
......
......@@ -86,6 +86,8 @@
AwardsHandler.prototype.positionMenu = function($menu, $addBtn) {
var css, position;
position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element
// So we position the element absolute in the body
css = {
top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px"
};
......@@ -284,6 +286,7 @@
if (emojiIcon.length > 0) {
unicodeName = emojiIcon.data('unicode-name');
} else {
// Find by alias
unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name');
}
return "emoji-" + unicodeName;
......@@ -350,8 +353,10 @@
return function(ev) {
var found_emojis, h5, term, ul;
term = $(ev.target).val();
// Clean previous search results
$('ul.emoji-menu-search, h5.emoji-search').remove();
if (term) {
// Generate a search result block
h5 = $('<h5>').text('Search results');
found_emojis = _this.searchEmojis(term).show();
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
......
/*= require jquery.ba-resize */
/*= require autosize */
(function() {
......
......@@ -5,6 +5,12 @@
container = $(this).closest(".js-details-container");
return container.toggleClass("open");
});
// Show details content. Hides link after click.
//
// %div
// %a.js-details-expand
// %div.js-details-content
//
return $("body").on("click", ".js-details-expand", function(e) {
$(this).next('.js-details-content').removeClass("hide");
$(this).hide();
......
// Quick Submit behavior
//
// When a child field of a form with a `js-quick-submit` class receives a
// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
// is submitted.
//
/*= require extensions/jquery */
//
// ### Example Markup
//
// <form action="/foo" class="js-quick-submit">
// <input type="text" />
// <textarea></textarea>
// <input type="submit" value="Submit" />
// </form>
//
(function() {
var isMac, keyCodeIs;
......@@ -17,6 +31,7 @@
$(document).on('keydown.quick_submit', '.js-quick-submit', function(e) {
var $form, $submit_button;
// Enter
if (!keyCodeIs(e, 13)) {
return;
}
......@@ -33,8 +48,11 @@
return $form.submit();
});
// If the user tabs to a submit button on a `js-quick-submit` form, display a
// tooltip to let them know they could've used the hotkey
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) {
var $this, title;
// Tab
if (!keyCodeIs(e, 9)) {
return;
}
......
// Requires Input behavior
//
// When called on a form with input fields with the `required` attribute, the
// form's submit button will be disabled until all required fields have values.
//
/*= require extensions/jquery */
//
// ### Example Markup
//
// <form class="js-requires-input">
// <input type="text" required="required">
// <input type="submit" value="Submit">
// </form>
//
(function() {
$.fn.requiresInput = function() {
var $button, $form, fieldSelector, requireInput, required;
......@@ -11,14 +23,17 @@
requireInput = function() {
var values;
values = _.map($(fieldSelector, $form), function(field) {
// Collect the input values of *all* required fields
return field.value;
});
// Disable the button if any required fields are empty
if (values.length && _.any(values, _.isEmpty)) {
return $button.disable();
} else {
return $button.enable();
}
};
// Set initial button state
requireInput();
return $form.on('change input', fieldSelector, requireInput);
};
......@@ -27,6 +42,8 @@
var $form, hideOrShowHelpBlock;
$form = $('form.js-requires-input');
$form.requiresInput();
// Hide or Show the help block when creating a new project
// based on the option selected
hideOrShowHelpBlock = function(form) {
var selected;
selected = $('.js-select-namespace option:selected');
......
(function(w) {
$(function() {
// Toggle button. Show/hide content inside parent container.
// Button does not change visibility. If button has icon - it changes chevron style.
//
// %div.js-toggle-container
// %a.js-toggle-button
// %div.js-toggle-content
//
$('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault();
$(this)
......
......@@ -8,6 +8,8 @@
autoDiscover: false,
autoProcessQueue: false,
url: form.attr('action'),
// Rails uses a hidden input field for PUT
// http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
method: method,
clickable: true,
uploadMultiple: false,
......@@ -36,6 +38,7 @@
formData.append('commit_message', form.find('.js-commit-message').val());
});
},
// Override behavior of adding error underneath preview
error: function(file, errorMessage) {
var stripped;
stripped = $("<div/>").html(errorMessage).text();
......
......@@ -66,6 +66,9 @@
// be added by all subclasses.
};
// To be implemented on the extending class
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
if (!skipFocus) this.editor.focus();
......
......@@ -18,6 +18,8 @@
return function() {
return $("#file-content").val(_this.editor.getValue());
};
// Before a form submission, move the content from the Ace editor into the
// submitted textarea
})(this));
this.initModePanesAndLinks();
new BlobLicenseSelectors({
......
......@@ -23,6 +23,7 @@
if ($(allDeviceSelector.join(",")).length) {
return;
}
// Create all the elements
els = $.map(BREAKPOINTS, function(breakpoint) {
return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
});
......@@ -40,6 +41,7 @@
BreakpointInstance.prototype.getBreakpointSize = function() {
var $visibleDevice;
$visibleDevice = this.visibleDevice;
// the page refreshed via turbolinks
if (!$visibleDevice().length) {
this.setup();
}
......
......@@ -16,6 +16,7 @@
this.toggleSidebar = bind(this.toggleSidebar, this);
this.updateDropdown = bind(this.updateDropdown, this);
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
$('.js-build-sidebar').niceScroll();
......@@ -42,6 +43,9 @@
$(this).data("state", "enabled");
return $(this).text("disable autoscroll");
}
//
// Bind autoscroll button to follow build output
//
});
Build.interval = setInterval((function(_this) {
return function() {
......@@ -49,6 +53,10 @@
return _this.getBuildTrace();
}
};
//
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
//
})(this), 4000);
}
}
......
......@@ -2,6 +2,7 @@
this.ImageFile = (function() {
var prepareFrames;
// Width where images must fits in, for 2-up this gets divided by 2
ImageFile.availWidth = 900;
ImageFile.viewModes = ['two-up', 'swipe'];
......@@ -9,6 +10,7 @@
function ImageFile(file) {
this.file = file;
this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
// Determine if old and new file has same dimensions, if not show 'two-up' view
return function(deletedWidth, deletedHeight) {
return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
if (width === deletedWidth && height === deletedHeight) {
......
......@@ -45,6 +45,7 @@
CommitsList.content.html(data.html);
return history.replaceState({
page: commitsUrl
// Change url so if user reload a page - search results are saved
}, document.title, commitsUrl);
},
dataType: "json"
......
......@@ -6,14 +6,19 @@
genericSuccess = function(e) {
showTooltip(e.trigger, 'Copied!');
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
};
// Safari doesn't support `execCommand`, so instead we inform the user to
// copy manually.
//
// See http://clipboardjs.com/#browser-support
genericError = function(e) {
var key;
if (/Mac/i.test(navigator.userAgent)) {
key = '&#8984;';
key = '&#8984;'; // Command
} else {
key = 'Ctrl';
}
......
......@@ -39,6 +39,9 @@
bottom: unfoldBottom,
offset: offset,
unfold: unfold,
// indent is used to compensate for single space indent to fit
// '+' and '-' prepended to diff lines,
// see https://gitlab.com/gitlab-org/gitlab-ce/issues/707
indent: 1,
view: file.data('view')
};
......
......@@ -164,6 +164,8 @@
}
break;
case 'projects:network:show':
// Ensure we don't create a particular shortcut handler here. This is
// already created, where the network graph is created.
shortcut_handler = true;
break;
case 'projects:forks:new':
......@@ -267,12 +269,14 @@
shortcut_handler = new ShortcutsNavigation();
}
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
return new Shortcuts();
}
};
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
......
......@@ -2,6 +2,7 @@
this.DueDateSelect = (function() {
function DueDateSelect() {
var $datePicker, $dueDate, $loading;
// Milestone edit/new form
$datePicker = $('.datepicker');
if ($datePicker.length) {
$dueDate = $('#milestone_due_date');
......@@ -16,6 +17,7 @@
e.preventDefault();
return $.datepicker._clearDate($datePicker);
});
// Issuable sidebar
$loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
$('.js-due-date-select').each(function(i, dropdown) {
var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL;
......@@ -38,6 +40,7 @@
});
addDueDate = function(isDropdown) {
var data, date, mediumDate, value;
// Create the post date
value = $("input[name='" + fieldName + "']").val();
if (value !== '') {
date = new Date(value.replace(new RegExp('-', 'g'), ','));
......
// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
disable: function() {
......@@ -5,6 +6,7 @@
}
});
// Enable an element and remove the 'disabled' Bootstrap class
$.fn.extend({
enable: function() {
return $(this).removeAttr('disabled').removeClass('disabled');
......
// Creates the variables for setting up GFM auto-completion
(function() {
if (window.GitLab == null) {
window.GitLab = {};
......@@ -8,18 +9,22 @@
dataLoaded: false,
cachedData: {},
dataSource: '',
// Emoji
Emoji: {
template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
},
// Team Members
Members: {
template: '<li>${username} <small>${title}</small></li>'
},
Labels: {
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
},
// Issues and MergeRequests
Issues: {
template: '<li><small>${id}</small> ${title}</li>'
},
// Milestones
Milestones: {
template: '<li>${title}</li>'
},
......@@ -48,8 +53,11 @@
}
},
setup: function(input) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
// destroy previous instances
this.destroyAtWho();
// set up instances
this.setupAtWho();
if (this.dataSource) {
if (!this.dataLoading && !this.cachedData) {
......@@ -63,6 +71,11 @@
return _this.loadData(data);
});
};
// We should wait until initializations are done
// and only trigger the last .setup since
// The previous .dataSource belongs to the previous issuable
// and the last one will have the **proper** .dataSource property
// TODO: Make this a singleton and turn off events when moving to another page
})(this), 1000);
}
if (this.cachedData != null) {
......@@ -71,6 +84,7 @@
}
},
setupAtWho: function() {
// Emoji
this.input.atwho({
at: ':',
displayTpl: (function(_this) {
......@@ -90,6 +104,7 @@
beforeInsert: this.DefaultOptions.beforeInsert
}
});
// Team Members
this.input.atwho({
at: '@',
displayTpl: (function(_this) {
......@@ -321,13 +336,22 @@
loadData: function(data) {
this.cachedData = data;
this.dataLoaded = true;
// load members
this.input.atwho('load', '@', data.members);
// load issues
this.input.atwho('load', 'issues', data.issues);
// load milestones
this.input.atwho('load', 'milestones', data.milestones);
// load merge requests
this.input.atwho('load', 'mergerequests', data.mergerequests);
// load emojis
this.input.atwho('load', ':', data.emojis);
// load labels
this.input.atwho('load', '~', data.labels);
// load commands
this.input.atwho('load', '/', data.commands);
// This trigger at.js again
// otherwise we would be stuck with loading until the user types
return $(':focus').trigger('keyup');
}
};
......
This diff is collapsed.
......@@ -3,12 +3,15 @@
function GLForm(form) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
// Before we start, we should clean up any previous data for this form
this.destroy();
// Setup the form
this.setupForm();
this.form.data('gl-form', this);
}
GLForm.prototype.destroy = function() {
// Clean form listeners
this.clearEventListeners();
return this.form.data('gl-form', null);
};
......@@ -21,12 +24,15 @@
this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form');
disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
// remove notify commit author checkbox for non-commit notes
GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
// form and textarea event listeners
this.addEventListeners();
gl.text.init(this.form);
}
// hide discard button
this.form.find('.js-note-discard').hide();
return this.form.show();
};
......
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
/*= require_tree . */
(function() {
}).call(this);
......@@ -204,6 +204,7 @@
function ContributorsAuthorGraph(data1) {
this.data = data1;
// Don't split graph size in half for mobile devices.
if ($(window).width() < 768) {
this.width = $('.content').width() - 80;
} else {
......
......@@ -53,6 +53,7 @@
return _this.formatSelection.apply(_this, args);
},
dropdownCssClass: "ajax-groups-dropdown",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
......
......@@ -38,9 +38,11 @@
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
var $button;
$button = $(this);
// Remove the label input box
$('input[name="label_name[]"]').filter(function() {
return this.value === $button.data('label');
}).remove();
// Submit the form to get new data
Issuable.filterResults($('.filter-form'));
return $('.js-label-select').trigger('update.label');
});
......
/*= require flash */
/*= require jquery.waitforimages */
/*= require task_list */
(function() {
......@@ -13,6 +9,7 @@
this.Issue = (function() {
function Issue() {
this.submitNoteForm = bind(this.submitNoteForm, this);
// Prevent duplicate event bindings
this.disableTaskList();
if ($('a.btn-close').length) {
this.initTaskList();
......@@ -99,6 +96,8 @@
url: $('form.js-issuable-update').attr('action'),
data: patchData
});
// TODO (rspeicher): Make the issue description inline-editable like a note so
// that we can re-use its form here
};
Issue.prototype.initMergeRequests = function() {
......@@ -128,6 +127,8 @@
Issue.prototype.initCanCreateBranch = function() {
var $container;
$container = $('#new-branch');
// If the user doesn't have the required permissions the container isn't
// rendered at all.
if ($container.length === 0) {
return;
}
......
(function() {
this.IssuableBulkActions = (function() {
function IssuableBulkActions(opts) {
// Set defaults
var ref, ref1, ref2;
if (opts == null) {
opts = {};
}
this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li');
// Save instance
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
this.bindEvents();
// Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
}
......@@ -86,6 +89,7 @@
ref1 = this.getLabelsFromSelection();
for (j = 0, len1 = ref1.length; j < len1; j++) {
id = ref1[j];
// Only the ones that we are not going to keep
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
......@@ -147,6 +151,8 @@
indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
labelsToApply = this.getLabelsToApply();
indeterminatedLabels.map(function(id) {
// We need to exclude label IDs that will be applied
// By not doing this will cause issues from selection to not add labels at all
if (labelsToApply.indexOf(id) === -1) {
return result.push(id);
}
......
......@@ -26,13 +26,16 @@
var previewColor;
previewColor = $('input#label_color').val();
return $('div.label-color-preview').css('background-color', previewColor);
// Updates the the preview color with the hex-color input
};
// Updates the preview color with a click on a suggested color
Labels.prototype.setSuggestedColor = function(e) {
var color;
color = $(e.currentTarget).data('color');
$('input#label_color').val(color);
this.updateColorPreview();
// Notify the form, that color has changed
$('.label-form').trigger('keyup');
return e.preventDefault();
};
......
......@@ -156,11 +156,13 @@
selectedClass.push('is-indeterminate');
}
if (active.indexOf(label.id) !== -1) {
// Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
selectedClass.push('is-active');
// Add input manually
instance.addInput(this.fieldName, label.id);
}
}
......@@ -172,6 +174,7 @@
}
if (label.duplicate) {
spacing = 100 / label.color.length;
// Reduce the colors to 4
label.color = label.color.filter(function(color, i) {
return i < 4;
});
......@@ -192,11 +195,13 @@
} else {
colorEl = '';
}
// We need to identify which items are actually labels
if (label.id) {
selectedClass.push('label-item');
$a.attr('data-label-id', label.id);
}
$a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
// Return generated html
return $li.html($a).prop('outerHTML');
},
persistWhenHide: $dropdown.data('persistWhenHide'),
......@@ -238,6 +243,7 @@
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide();
// display:block overrides the hide-collapse rule
$value.removeAttr('style');
if (page === 'projects:boards:show') {
return;
......@@ -255,6 +261,7 @@
}
}
if ($dropdown.hasClass('js-filter-bulk-update')) {
// If we are persisting state we need the classes
if (!this.options.persistWhenHide) {
return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
}
......@@ -324,7 +331,9 @@
if ($('.selected_issue:checked').length) {
return;
}
// Remove inputs
$('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
// Also restore button text
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
};
......
......@@ -3,5 +3,4 @@
(function() {
}).call(this);
......@@ -3,5 +3,4 @@
(function() {
}).call(this);
......@@ -3,5 +3,4 @@
(function() {
}).call(this);
/*= require raphael */
/*= require g.raphael */
/*= require g.bar */
(function() {
}).call(this);
......@@ -29,6 +29,7 @@
if (setTimeago) {
$timeagoEls.timeago();
$timeagoEls.tooltip('destroy');
// Recreate with custom template
return $timeagoEls.tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
......
gl.emojiAliases = ->
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
(function() {
gl.emojiAliases = function() {
return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>');
};
}).call(this);
......@@ -6,6 +6,7 @@
notification = new Notification(message, opts);
setTimeout(function() {
return notification.close();
// Hide the notification after X amount of seconds
}, 8000);
if (onclick) {
return notification.onclick = onclick;
......@@ -22,12 +23,16 @@
body: body,
icon: icon
};
// Let's check if the browser supports notifications
if (!('Notification' in window)) {
// do nothing
} else if (Notification.permission === 'granted') {
// If it's okay let's create a notification
return notificationGranted(message, opts, onclick);
} else if (Notification.permission !== 'denied') {
return Notification.requestPermission(function(permission) {
// If the user accepts, let's create a notification
if (permission === 'granted') {
return notificationGranted(message, opts, onclick);
}
......
......@@ -29,6 +29,7 @@
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
......@@ -63,11 +64,11 @@
if (!inserted) {
try {
document.execCommand("ms-beginUndoUnit");
} catch (undefined) {}
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (undefined) {}
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap);
};
......
......@@ -7,6 +7,8 @@
if ((base = w.gl).utils == null) {
base.utils = {};
}
// Returns an array containing the value(s) of the
// of the key passed as an argument
w.gl.utils.getParameterValues = function(sParam) {
var i, sPageURL, sParameterName, sURLVariables, values;
sPageURL = decodeURIComponent(window.location.search.substring(1));
......@@ -23,6 +25,8 @@
}
return values;
};
// @param {Object} params - url keys and value to merge
// @param {String} url
w.gl.utils.mergeUrlParams = function(params, url) {
var lastChar, newUrl, paramName, paramValue, pattern;
newUrl = decodeURIComponent(url);
......@@ -37,12 +41,14 @@
newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
}
}
// Remove a trailing ampersand
lastChar = newUrl[newUrl.length - 1];
if (lastChar === '&') {
newUrl = newUrl.slice(0, -1);
}
return newUrl;
};
// removes parameter query string from url. returns the modified url
w.gl.utils.removeParamQueryString = function(url, param) {
var urlVariables, variables;
url = decodeURIComponent(url);
......
// LineHighlighter
//
// Handles single- and multi-line selection and highlight for blob views.
//
/*= require jquery.scrollTo */
//
// ### Example Markup
//
// <div id="blob-content-holder">
// <div class="file-content">
// <div class="line-numbers">
// <a href="#L1" id="L1" data-line-number="1">1</a>
// <a href="#L2" id="L2" data-line-number="2">2</a>
// <a href="#L3" id="L3" data-line-number="3">3</a>
// <a href="#L4" id="L4" data-line-number="4">4</a>
// <a href="#L5" id="L5" data-line-number="5">5</a>
// </div>
// <pre class="code highlight">
// <code>
// <span id="LC1" class="line">...</span>
// <span id="LC2" class="line">...</span>
// <span id="LC3" class="line">...</span>
// <span id="LC4" class="line">...</span>
// <span id="LC5" class="line">...</span>
// </code>
// </pre>
// </div>
// </div>
//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.LineHighlighter = (function() {
// CSS class applied to highlighted lines
LineHighlighter.prototype.highlightClass = 'hll';
// Internal copy of location.hash so we're not dependent on `location` in tests
LineHighlighter.prototype._hash = '';
function LineHighlighter(hash) {
var range;
if (hash == null) {
// Initialize a LineHighlighter object
//
// hash - String URL hash for dependency injection in tests
hash = location.hash;
}
this.setHash = bind(this.setHash, this);
......@@ -24,6 +56,8 @@
if (range[0]) {
this.highlightRange(range);
$.scrollTo("#L" + range[0], {
// Scroll to the first highlighted line on initial load
// Offset -50 for the sticky top bar, and another -100 for some context
offset: -150
});
}
......@@ -32,6 +66,12 @@
LineHighlighter.prototype.bindEvents = function() {
$('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler);
// While it may seem odd to bind to the mousedown event and then throw away
// the click event, there is a method to our madness.
//
// If not done this way, the line number anchor will sometimes keep its
// active state even when the event is cancelled, resulting in an ugly border
// around the link and/or a persisted underline text decoration.
return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
return event.preventDefault();
});
......@@ -44,6 +84,8 @@
lineNumber = $(event.target).closest('a').data('line-number');
current = this.hashToRange(this._hash);
if (!(current[0] && event.shiftKey)) {
// If there's no current selection, or there is but Shift wasn't held,
// treat this like a single-line selection.
this.setHash(lineNumber);
return this.highlightLine(lineNumber);
} else if (event.shiftKey) {
......@@ -59,10 +101,23 @@
LineHighlighter.prototype.clearHighlight = function() {
return $("." + this.highlightClass).removeClass(this.highlightClass);
// Unhighlight previously highlighted lines
};
// Convert a URL hash String into line numbers
//
// hash - Hash String
//
// Examples:
//
// hashToRange('#L5') # => [5, null]
// hashToRange('#L5-15') # => [5, 15]
// hashToRange('#foo') # => [null, null]
//
// Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
//?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
first = parseInt(matches[1]);
......@@ -73,10 +128,16 @@
}
};
// Highlight a single line
//
// lineNumber - Line number to highlight
LineHighlighter.prototype.highlightLine = function(lineNumber) {
return $("#LC" + lineNumber).addClass(this.highlightClass);
};
// Highlight all lines within a range
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
var i, lineNumber, ref, ref1, results;
if (range[1]) {
......@@ -90,6 +151,7 @@
}
};
// Set the URL hash string
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
var hash;
if (lastLineNumber) {
......@@ -101,10 +163,15 @@
return this.__setLocationHash__(hash);
};
// Make the actual hash change in the browser
//
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({
turbolinks: false,
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
}, document.title, value);
};
......
/*= require jquery.waitforimages */
/*= require task_list */
/*= require merge_request_tabs */
(function() {
......@@ -12,6 +8,11 @@
this.MergeRequest = (function() {
function MergeRequest(opts) {
// Initialize MergeRequest behavior
//
// Options:
// action - String, current controller action
//
this.opts = opts != null ? opts : {};
this.submitNoteForm = bind(this.submitNoteForm, this);
this.$el = $('.merge-request');
......@@ -21,6 +22,7 @@
};
})(this));
this.initTabs();
// Prevent duplicate event bindings
this.disableTaskList();
this.initMRBtnListeners();
if ($("a.btn-close").length) {
......@@ -28,14 +30,17 @@
}
}
// Local jQuery finder
MergeRequest.prototype.$ = function(selector) {
return this.$el.find(selector);
};
MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') {
// `MergeRequests#new` has no tab-persisting or lazy-loading behavior
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
// Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
}
};
......@@ -96,6 +101,8 @@
url: $('form.js-issuable-update').attr('action'),
data: patchData
});
// TODO (rspeicher): Make the merge request description inline-editable like a
// note so that we can re-use its form here
};
return MergeRequest;
......
// MergeRequestTabs
//
// Handles persisting and restoring the current tab selection and lazily-loading
// content on the MergeRequests#show page.
//
/*= require jquery.cookie */
//
// ### Example Markup
//
// <ul class="nav-links merge-request-tabs">
// <li class="notes-tab active">
// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
// Discussion
// </a>
// </li>
// <li class="commits-tab">
// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
// Commits
// </a>
// </li>
// <li class="diffs-tab">
// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
// Diffs
// </a>
// </li>
// </ul>
//
// <div class="tab-content">
// <div class="notes tab-pane active" id="notes">
// Notes Content
// </div>
// <div class="commits tab-pane" id="commits">
// Commits Content
// </div>
// <div class="diffs tab-pane" id="diffs">
// Diffs Content
// </div>
// </div>
//
// <div class="mr-loading-status">
// <div class="loading">
// Loading Animation
// </div>
// </div>
//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
......@@ -19,6 +62,7 @@
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
// Store the `location` object, allowing for easier stubbing in tests
this._location = location;
this.bindEvents();
this.activateTab(this.opts.action);
......@@ -77,6 +121,7 @@
}
};
// Activate a tab based on the current action
MergeRequestTabs.prototype.activateTab = function(action) {
if (action === 'show') {
action = 'notes';
......@@ -84,20 +129,48 @@
return $(".merge-request-tabs a[data-action='" + action + "']").tab('show');
};
// Replaces the current Merge Request-specific action in the URL with a new one
//
// If the action is "notes", the URL is reset to the standard
// `MergeRequests#show` route.
//
// Examples:
//
// location.pathname # => "/namespace/project/merge_requests/1"
// setCurrentAction('diffs')
// location.pathname # => "/namespace/project/merge_requests/1/diffs"
//
// location.pathname # => "/namespace/project/merge_requests/1/diffs"
// setCurrentAction('notes')
// location.pathname # => "/namespace/project/merge_requests/1"
//
// location.pathname # => "/namespace/project/merge_requests/1/diffs"
// setCurrentAction('commits')
// location.pathname # => "/namespace/project/merge_requests/1/commits"
//
// Returns the new URL String
MergeRequestTabs.prototype.setCurrentAction = function(action) {
var new_state;
// Normalize action, just to be safe
if (action === 'show') {
action = 'notes';
}
this.currentAction = action;
// Remove a trailing '/commits' or '/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') {
new_state += "/" + action;
}
// Ensure parameters and hash come along for the ride
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
url: new_state
// Replace the current history state with the new one without breaking
// Turbolinks' history.
//
// See https://github.com/rails/turbolinks/issues/363
}, document.title, new_state);
return new_state;
};
......@@ -206,6 +279,9 @@
});
};
// Show or hide the loading spinner
//
// status - Boolean, true to show, false to hide
MergeRequestTabs.prototype.toggleLoading = function(status) {
return $('.mr-loading-status .loading').toggle(status);
};
......@@ -232,6 +308,7 @@
MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
// Returns diff view type
};
MergeRequestTabs.prototype.expandViewContainer = function() {
......@@ -245,6 +322,8 @@
if ($gutterIcon.is('.fa-angle-double-right')) {
return $gutterIcon.closest('a').trigger('click', [true]);
}
// Wait until listeners are set
// Only when sidebar is expanded
}, 0);
};
......@@ -259,6 +338,9 @@
return $gutterIcon.closest('a').trigger('click', [true]);
}
}, 0);
// Expand the issuable sidebar unless the user explicitly collapsed it
// Wait until listeners are set
// Only when sidebar is collapsed
};
return MergeRequestTabs;
......
......@@ -3,6 +3,12 @@
this.MergeRequestWidget = (function() {
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
// check_enable - Boolean, whether to check automerge status
// merge_check_url - String, URL to use to check automerge status
// ci_status_url - String, URL to use to check CI status
//
this.opts = opts;
$('#modal_merge_info').modal({
show: false
......@@ -135,6 +141,8 @@
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
// The first check should only update the UI, a notification
// should only be displayed on status changes
if (showNotification && !_this.firstCICheck) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
......
......@@ -110,6 +110,7 @@
},
update: function(event, ui) {
var data;
// Prevents sorting from container which element has been removed.
if ($(this).find(ui.item).length > 0) {
data = $(this).sortable("serialize");
return Milestone.sortIssues(data);
......
......@@ -92,6 +92,7 @@
},
hidden: function() {
$selectbox.hide();
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
clicked: function(selected, $el, e) {
......
......@@ -90,6 +90,7 @@
results = [];
while (k < this.mspace) {
this.colors.push(Raphael.getColor(.8));
// Skipping a few colors in the spectrum to get more contrast between colors
Raphael.getColor();
Raphael.getColor();
results.push(k++);
......@@ -112,6 +113,7 @@
for (mm = j = 0, len = ref.length; j < len; mm = ++j) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
font: "12px Monaco, monospace",
fill: "#BBB"
......@@ -119,6 +121,7 @@
cuday = day[0];
}
if (cumonth !== day[1]) {
// Months
r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({
font: "12px Monaco, monospace",
fill: "#EEE"
......@@ -207,6 +210,7 @@
}
r = this.r;
shortrefs = commit.refs;
// Truncate if longer than 15 chars
if (shortrefs.length > 17) {
shortrefs = shortrefs.substr(0, 15) + "";
}
......@@ -217,6 +221,7 @@
title: commit.refs
});
textbox = text.getBBox();
// Create rectangle based on the size of the textbox
rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
fill: "#000",
"fill-opacity": .5,
......@@ -229,6 +234,7 @@
});
label = r.set(rect, text);
label.transform(["t", -rect.getBBox().width - 15, 0]);
// Set text to front
return text.toFront();
};
......@@ -283,11 +289,13 @@
parentY = this.offsetY + this.unitTime * parentCommit.time;
parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
// Set line color
if (parentCommit.space <= commit.space) {
color = this.colors[commit.space];
} else {
color = this.colors[parentCommit.space];
}
// Build line shape
if (parent[1] === commit.space) {
offset = [0, 5];
arrow = "l-2,5,4,0,-2,-5,0,5";
......@@ -298,13 +306,17 @@
offset = [-3, 3];
arrow = "l-5,0,2,4,3,-4,-4,2";
}
// Start point
route = ["M", x + offset[0], y + offset[1]];
// Add arrow if not first parent
if (i > 0) {
route.push(arrow);
}
// Circumvent if overlap
if (commit.space !== parentCommit.space || commit.space !== parent[1]) {
route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5);
}
// End point
route.push("L", parentX1, parentY);
results.push(r.path(route).attr({
stroke: color,
......@@ -325,6 +337,7 @@
"fill-opacity": .5,
stroke: "none"
});
// Displayed in the center
return this.element.scrollTop(y - this.graphHeight / 2);
}
};
......
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
/*= require_tree . */
(function() {
......
This diff is collapsed.
// MarkdownPreview
//
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
// and showing a warning when more than `x` users are referenced.
//
(function() {
var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
this.MarkdownPreview = (function() {
function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning
MarkdownPreview.prototype.referenceThreshold = 10;
MarkdownPreview.prototype.ajaxCache = {};
......@@ -101,8 +107,10 @@
return;
}
lastTextareaPreviewed = $form.find('textarea.markdown-area');
// toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active');
$form.find(previewButtonSelector).parent().addClass('active');
// toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
return markdownPreview.showPreview($form);
......@@ -113,8 +121,10 @@
return;
}
lastTextareaPreviewed = null;
// toggle tabs
$form.find(writeButtonSelector).parent().addClass('active');
$form.find(previewButtonSelector).parent().removeClass('active');
// toggle content
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
return $form.find('.md-preview-holder').hide();
......
......@@ -5,6 +5,7 @@
GitLabCrop = (function() {
var FILENAMEREGEX;
// Matches everything but the file name
FILENAMEREGEX = /^.*[\\\/]/;
function GitLabCrop(input, opts) {
......@@ -17,11 +18,18 @@
this.onModalShow = bind(this.onModalShow, this);
this.onPickImageClick = bind(this.onPickImageClick, this);
this.fileInput = $(input);
// We should rename to avoid spec to fail
// Form will submit the proper input filed with a file using FormData
this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
// Set defaults
this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg;
// Required params
// Ensure needed elements are jquery objects
// If selector is provided we will convert them to a jQuery Object
this.filename = this.getElement(this.filename);
this.previewImage = this.getElement(this.previewImage);
this.pickImageEl = this.getElement(this.pickImageEl);
// Modal elements usually are outside the @form element
this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop;
this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn;
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
......@@ -93,8 +101,8 @@
return this.modalCropImg.attr('src', '').cropper('destroy');
};
GitLabCrop.prototype.onUploadImageBtnClick = function(e) {
e.preventDefault();
GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image
e.preventDefault(); // Destroy cropper instance
this.setBlob();
this.setPreview();
this.modalCrop.modal('hide');
......
......@@ -11,9 +11,11 @@
this.form = (ref = opts.form) != null ? ref : $('.edit-user');
$('.js-preferences-form').on('change.preference', 'input[type=radio]', function() {
return $(this).parents('form').submit();
// Automatically submit the Preferences form when any of its radio buttons change
});
$('#user_notification_email').on('change', function() {
return $(this).parents('form').submit();
// Automatically submit email form when it changes
});
$('.update-username').on('ajax:before', function() {
$('.loading-username').show();
......@@ -76,6 +78,7 @@
},
complete: function() {
window.scrollTo(0, 0);
// Enable submit button after requests ends
return self.form.find(':input[disabled]').enable();
}
});
......@@ -93,6 +96,7 @@
if (comment && comment.length > 1 && $title.val() === '') {
return $title.val(comment[1]).change();
}
// Extract the SSH Key title from its comment
});
if (gl.utils.getPagePath() === 'profiles') {
return new Profile();
......
......@@ -3,5 +3,4 @@
(function() {
}).call(this);
......@@ -11,7 +11,13 @@
url = $("#project_clone").val();
$('#project_clone').val(url);
return $('.clone').text(url);
// Git protocol switcher
// Remove the active class for all buttons (ssh, http, kerberos if shown)
// Add the active class for the clicked button
// Update the input field
// Update the command line instructions
});
// Ref switcher
this.initRefSwitcher();
$('.project-refs-select').on('change', function() {
return $(this).parents('form').submit();
......
......@@ -13,8 +13,11 @@
this.selectRowUp = bind(this.selectRowUp, this);
this.filePaths = {};
this.inputElement = this.element.find(".file-finder-input");
// init event
this.initEvent();
// focus text input box
this.inputElement.focus();
// load file list
this.load(this.options.url);
}
......@@ -42,6 +45,7 @@
}
}
});
// init event
};
ProjectFindFile.prototype.findFile = function() {
......@@ -49,8 +53,10 @@
searchText = this.inputElement.val();
result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
return this.renderList(result, searchText);
// find file
};
// files pathes load
ProjectFindFile.prototype.load = function(url) {
return $.ajax({
url: url,
......@@ -67,6 +73,7 @@
});
};
// render result
ProjectFindFile.prototype.renderList = function(filePaths, searchText) {
var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty();
......@@ -86,6 +93,7 @@
return results;
};
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
highlighter = function(element, text, matches) {
var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
lastIndex = 0;
......@@ -110,6 +118,7 @@
return element.append(document.createTextNode(text.substring(lastIndex)));
};
// make tbody row html
ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
var $tr;
$tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>");
......
......@@ -7,3 +7,5 @@
})();
}).call(this);
// I kept class for future
......@@ -33,6 +33,7 @@
$('.projects-list-holder').replaceWith(data.html);
return history.replaceState({
page: project_filter_url
// Change url so if user reload a page - search results are saved
}, document.title, project_filter_url);
},
dataType: "json"
......
......@@ -43,6 +43,12 @@
});
}
onClickCreateWildcard() {
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex(0);
}
bindEvents() {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
}
......
......@@ -24,6 +24,7 @@
this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this);
this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this);
this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || '';
// Dropdown Element
this.dropdown = this.wrap.find('.dropdown');
this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
......@@ -35,6 +36,7 @@
this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input');
this.saveOriginalState();
// Only when user is logged in
if (gon.current_user_id) {
this.createAutocomplete();
}
......@@ -43,6 +45,7 @@
this.bindEvents();
}
// Finds an element inside wrapper element
SearchAutocomplete.prototype.getElement = function(selector) {
return this.wrap.find(selector);
};
......@@ -82,6 +85,7 @@
}
return;
}
// Prevent multiple ajax calls
if (this.loadingSuggestions) {
return;
}
......@@ -92,14 +96,17 @@
term: term
}, function(response) {
var data, firstCategory, i, lastCategory, len, suggestion;
// Hide dropdown menu if no suggestions returns
if (!response.length) {
_this.disableAutocomplete();
return;
}
data = [];
// List results
firstCategory = true;
for (i = 0, len = response.length; i < len; i++) {
suggestion = response[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
if (!firstCategory) {
data.push('separator');
......@@ -119,6 +126,7 @@
url: suggestion.url
});
}
// Add option to proceed with the search
if (data.length) {
data.push('separator');
data.push({
......@@ -169,11 +177,13 @@
SearchAutocomplete.prototype.serializeState = function() {
return {
// Search Criteria
search_project_id: this.projectInputEl.val(),
group_id: this.groupInputEl.val(),
search_code: this.searchCodeInputEl.val(),
repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(),
// Location badge
_location: this.locationBadgeEl.text()
};
};
......@@ -194,6 +204,7 @@
SearchAutocomplete.prototype.enableAutocomplete = function() {
var _this;
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
}
......@@ -206,18 +217,22 @@
};
SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
// Saves last length of the entered text
return this.saveTextLength();
};
SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) {
switch (e.keyCode) {
case KEYCODE.BACKSPACE:
// when trying to remove the location badge
if (this.lastTextLength === 0 && this.badgePresent()) {
this.removeLocationBadge();
}
// When removing the last character and no badge is present
if (this.lastTextLength === 1) {
this.disableAutocomplete();
}
// When removing any character from existin value
if (this.lastTextLength > 1) {
this.enableAutocomplete();
}
......@@ -232,9 +247,12 @@
case KEYCODE.DOWN:
return;
default:
// Handle the case when deleting the input value other than backspace
// e.g. Pressing ctrl + backspace or ctrl + x
if (this.searchInput.val() === '') {
this.disableAutocomplete();
} else {
// We should display the menu only when input is not empty
if (e.keyCode !== KEYCODE.ENTER) {
this.enableAutocomplete();
}
......@@ -243,7 +261,9 @@
this.wrap.toggleClass('has-value', !!e.target.value);
};
// Avoid falsy value to be returned
SearchAutocomplete.prototype.onSearchInputClick = function(e) {
// Prevents closing the dropdown menu
return e.stopImmediatePropagation();
};
......@@ -267,6 +287,7 @@
SearchAutocomplete.prototype.onSearchInputBlur = function(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
if (this.searchInput.val() === '') {
return this.restoreOriginalState();
}
......@@ -311,6 +332,7 @@
results = [];
for (i = 0, len = inputs.length; i < len; i++) {
input = inputs[i];
// _location isnt a input
if (input === '_location') {
break;
}
......
......@@ -86,6 +86,7 @@
var defaultStopCallback;
defaultStopCallback = Mousetrap.stopCallback;
return function(e, element, combo) {
// allowed shortcuts if textarea, input, contenteditable are focused
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
} else {
......
......@@ -14,8 +14,10 @@
ShortcutsFindFile.__super__.constructor.call(this);
_oldStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (function(_this) {
// override to fire shortcuts action when focus in textbox
return function(event, element, combo) {
if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) {
// when press up/down key in textbox, cusor prevent to move to home/end
event.preventDefault();
return false;
}
......
/*= require mousetrap */
/*= require shortcuts_navigation */
(function() {
......@@ -43,16 +41,20 @@
if (selected.trim() === "") {
return;
}
// Put a '>' character before each non-empty line in the selection
quote = _.map(selected.split("\n"), function(val) {
if (val.trim() !== '') {
return "> " + val + "\n";
}
});
// If replyField already has some content, add a newline before our quote
separator = replyField.val().trim() !== "" && "\n" || '';
replyField.val(function(_, current) {
return current + separator + quote.join('') + "\n";
});
// Trigger autosave for the added text
replyField.trigger('input');
// Focus the input field
return replyField.focus();
}
};
......
// Syntax Highlighter
//
// Applies a syntax highlighting color scheme CSS class to any element with the
// `js-syntax-highlight` class
//
// ### Example Markup
//
// <div class="js-syntax-highlight"></div>
//
(function() {
$.fn.syntaxHighlight = function() {
var $children;
if ($(this).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme);
} else {
// Given a parent element, recurse to any of its applicable children
$children = $(this).find('.js-syntax-highlight');
if ($children.length) {
return $children.syntaxHighlight();
......
......@@ -129,16 +129,21 @@
var currPage, currPages, newPages, pageParams, url;
currPages = this.getTotalPages();
currPage = this.getCurrentPage();
// Refresh if no remaining Todos
if (!total) {
location.reload();
return;
}
// Do nothing if no pagination
if (!currPages) {
return;
}
newPages = Math.ceil(total / this.getTodosPerPage());
// Includes query strings
url = location.href;
// If new total of pages is different than we have now
if (newPages !== currPages) {
// Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) {
pageParams = {
page: currPages - 1
......@@ -155,6 +160,7 @@
if (!todoLink) {
return;
}
// Allow Meta-Click or Mouse3-click to open in a new tab
if (e.metaKey || e.which === 2) {
e.preventDefault();
return window.open(todoLink, '_blank');
......
......@@ -2,6 +2,8 @@
this.TreeView = (function() {
function TreeView() {
this.initKeyNav();
// Code browser tree slider
// Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
$(".tree-content-holder .tree-item").on('click', function(e) {
var $clickedEl, path;
$clickedEl = $(e.target);
......@@ -15,6 +17,7 @@
}
}
});
// Show the "Loading commit data" for only the first element
$('span.log_loading:first').removeClass('hide');
}
......
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
......@@ -15,6 +19,17 @@
this.appId = u2fParams.app_id;
this.challenge = u2fParams.challenge;
this.signRequests = u2fParams.sign_requests.map(function(request) {
// The U2F Javascript API v1.1 requires a single challenge, with
// _no challenges per-request_. The U2F Javascript API v1.0 requires a
// challenge per-request, which is done by copying the single challenge
// into every request.
//
// In either case, we don't need the per-request challenges that the server
// has generated, so we can remove them.
//
// Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
// This can be removed once we upgrade.
// https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
return _(request).omit('challenge');
});
}
......@@ -41,6 +56,7 @@
})(this), 10);
};
// Rendering #
U2FAuthenticate.prototype.templates = {
"notSupported": "#js-authenticate-u2f-not-supported",
"setup": '#js-authenticate-u2f-setup',
......@@ -75,6 +91,8 @@
U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
this.renderTemplate('authenticated');
// Prefer to do this instead of interpolating using Underscore templates
// because of JSON escaping issues.
return this.container.find("#js-device-response").val(deviceResponse);
};
......
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
......@@ -39,6 +43,7 @@
})(this), 10);
};
// Rendering #
U2FRegister.prototype.templates = {
"notSupported": "#js-register-u2f-not-supported",
"setup": '#js-register-u2f-setup',
......@@ -73,6 +78,8 @@
U2FRegister.prototype.renderRegistered = function(deviceResponse) {
this.renderTemplate('registered');
// Prefer to do this instead of interpolating using Underscore templates
// because of JSON escaping issues.
return this.container.find("#js-device-response").val(deviceResponse);
};
......
// UserTabs
//
// Handles persisting and restoring the current tab selection and lazily-loading
// content on the Users#show page.
//
// ### Example Markup
//
// <ul class="nav-links">
// <li class="activity-tab active">
// <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
// Activity
// </a>
// </li>
// <li class="groups-tab">
// <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
// Groups
// </a>
// </li>
// <li class="contributed-tab">
// <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
// Contributed projects
// </a>
// </li>
// <li class="projects-tab">
// <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
// Personal projects
// </a>
// </li>
// <li class="snippets-tab">
// <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
// </a>
// </li>
// </ul>
//
// <div class="tab-content">
// <div class="tab-pane" id="activity">
// Activity Content
// </div>
// <div class="tab-pane" id="groups">
// Groups Content
// </div>
// <div class="tab-pane" id="contributed">
// Contributed projects content
// </div>
// <div class="tab-pane" id="projects">
// Projects content
// </div>
// <div class="tab-pane" id="snippets">
// Snippets content
// </div>
// </div>
//
// <div class="loading-status">
// <div class="loading">
// Loading Animation
// </div>
// </div>
//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
......@@ -6,18 +64,23 @@
this.tabShown = bind(this.tabShown, this);
var i, item, len, ref, ref1, ref2, ref3;
this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document);
// Make jQuery object if selector is provided
if (typeof this.parentEl === 'string') {
this.parentEl = $(this.parentEl);
}
// Store the `location` object, allowing for easier stubbing in tests
this._location = location;
// Set tab states
this.loaded = {};
ref3 = this.parentEl.find('.nav-links a');
for (i = 0, len = ref3.length; i < len; i++) {
item = ref3[i];
this.loaded[$(item).attr('data-action')] = false;
}
// Actions
this.actions = Object.keys(this.loaded);
this.bindEvents();
// Set active tab
if (this.action === 'show') {
this.action = this.defaultAction;
}
......@@ -25,6 +88,7 @@
}
UserTabs.prototype.bindEvents = function() {
// Toggle event listeners
return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown);
};
......@@ -74,6 +138,7 @@
tabSelector = 'div#' + action;
_this.parentEl.find(tabSelector).html(data.html);
_this.loaded[action] = true;
// Fix tooltips
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
};
})(this)
......@@ -97,13 +162,17 @@
UserTabs.prototype.setCurrentAction = function(action) {
var new_state, regExp;
// Remove possible actions from URL
regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$');
new_state = this._location.pathname;
// remove trailing slashes
new_state = new_state.replace(/\/+$/, "");
new_state = new_state.replace(regExp, '');
// Append the new action if we're on a tab other than 'activity'
if (action !== this.defaultAction) {
new_state += "/" + action;
}
// Ensure parameters and hash come along for the ride
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
......
......@@ -11,6 +11,8 @@
this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.months = [];
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
this.timestampsTmp = [];
var group = 0;
......@@ -29,12 +31,15 @@
var day = date.getDay();
var count = timestamps[date.getTime() * 0.001];
// Create a new group array if this is the first day of the week
// or if is first object
if ((day === 0 && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group++;
}
var innerArray = this.timestampsTmp[group - 1];
// Push to the inner array the values that will be used to render map
innerArray.push({
count: count || 0,
date: date,
......@@ -42,8 +47,10 @@
});
}
// Init color functions
this.colorKey = this.initColorKey();
this.color = this.initColor();
// Init the svg element
this.renderSvg(group);
this.renderDays();
this.renderMonths();
......
......@@ -3,5 +3,4 @@
(function() {
}).call(this);
......@@ -81,6 +81,7 @@
if (term.length === 0) {
showDivider = 0;
if (firstUser) {
// Move current user to the front of the list
for (index = j = 0, len = users.length; j < len; index = ++j) {
obj = users[index];
if (obj.username === firstUser) {
......@@ -115,6 +116,7 @@
if (showDivider) {
users.splice(showDivider, 0, "divider");
}
// Send the data back
return callback(users);
});
},
......@@ -139,6 +141,7 @@
inputId: 'issue_assignee_id',
hidden: function(e) {
$selectbox.hide();
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
clicked: function(user, $el, e) {
......@@ -177,6 +180,7 @@
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />";
}
}
// split into three parts so we can remove the username section if nessesary
listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>";
listClosingTags = "</a> </li>";
......@@ -215,6 +219,7 @@
};
if (query.term.length === 0) {
if (firstUser) {
// Move current user to the front of the list
ref = data.results;
for (index = j = 0, len = ref.length; j < len; index = ++j) {
obj = ref[index];
......@@ -271,6 +276,7 @@
return _this.formatSelection.apply(_this, args);
},
dropdownCssClass: "ajax-users-dropdown",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
......@@ -318,6 +324,8 @@
});
};
// Return users list. Filtered by query
// Only active users retrieved
UsersSelect.prototype.users = function(query, options, callback) {
var url;
url = this.buildUrl(this.usersPath);
......
// Zen Mode (full screen) textarea
//
/*= provides zen_mode:enter */
/*= provides zen_mode:leave */
//
/*= require jquery.scrollTo */
/*= require dropzone */
/*= require mousetrap */
/*= require mousetrap/pause */
//
// ### Events
//
// `zen_mode:enter`
//
// Fired when the "Edit in fullscreen" link is clicked.
//
// **Synchronicity** Sync
// **Bubbles** Yes
// **Cancelable** No
// **Target** a.js-zen-enter
//
// `zen_mode:leave`
//
// Fired when the "Leave Fullscreen" link is clicked.
//
// **Synchronicity** Sync
// **Bubbles** Yes
// **Cancelable** No
// **Target** a.js-zen-leave
//
(function() {
this.ZenMode = (function() {
function ZenMode() {
......@@ -40,6 +53,7 @@
};
})(this));
$(document).on('keydown', function(e) {
// Esc
if (e.keyCode === 27) {
e.preventDefault();
return $(document).trigger('zen_mode:leave');
......@@ -52,6 +66,7 @@
this.active_backdrop = $(backdrop);
this.active_backdrop.addClass('fullscreen');
this.active_textarea = this.active_backdrop.find('textarea');
// Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style');
return this.active_textarea.focus();
};
......
......@@ -62,6 +62,7 @@ module AuthenticatesWithTwoFactor
session.delete(:otp_user_id)
session.delete(:challenges)
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
flash.now[:alert] = 'Authentication via U2F device failed.'
......
......@@ -154,7 +154,7 @@ class ApplicationSetting < ActiveRecord::Base
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
import_sources: Gitlab::ImportSources.values,
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
......
......@@ -12,6 +12,7 @@ class Service < ActiveRecord::Base
default_value_for :tag_push_events, true
default_value_for :note_events, true
default_value_for :build_events, true
default_value_for :pipeline_events, true
default_value_for :wiki_page_events, true
after_initialize :initialize_properties
......
......@@ -18,6 +18,5 @@
= f.submit "Verify code", class: "btn btn-save"
- if @user.two_factor_u2f_enabled?
%hr
= render "u2f/authenticate"
= render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name }
......@@ -3,6 +3,7 @@
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
%li(class="js-branch-#{branch.name}")
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do
......@@ -25,12 +26,12 @@
diverged from upstream
.controls.hidden-xs
- if create_mr_button?(@repository.root_ref, branch.name)
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
Merge Request
- if branch.name != @repository.root_ref
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do
Compare
= render 'projects/buttons/download', project: @project, ref: branch.name
......
......@@ -20,6 +20,8 @@
%div
%p We heard back from your U2F device. Click this button to authenticate with the GitLab server.
= form_tag(new_user_session_path, method: :post) do |f|
- resource_params = params[resource_name].presence || params
= hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0)
= hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
= submit_tag "Authenticate via U2F Device", class: "btn btn-success"
......
This diff is collapsed.
......@@ -403,7 +403,7 @@ If you are not using Linux you may have to run `gmake` instead of
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse
sudo -u git -H git checkout v0.8.0
sudo -u git -H git checkout v0.8.1
sudo -u git -H make
### Initialize Database and Activate Advanced Features
......
......@@ -82,7 +82,7 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
sudo -u git -H git checkout v0.8.0
sudo -u git -H git checkout v0.8.1
sudo -u git -H make
```
......
......@@ -133,8 +133,7 @@ module Gitlab
if issue.labels.count > 0
label_ids = issue.labels
.map { |raw| LabelFormatter.new(project, raw).attributes }
.map { |attrs| Label.find_by(attrs).try(:id) }
.map { |attrs| project.labels.find_by(title: attrs.name).try(:id) }
.compact
issuable.update_attribute(:label_ids, label_ids)
......
......@@ -13,6 +13,12 @@ module Gitlab
Label
end
def create!
project.labels.find_or_create_by!(title: title) do |label|
label.color = color
end
end
private
def color
......
......@@ -29,31 +29,7 @@ module Gitlab
end
def users(field, value, limit = nil)
if field.to_sym == :dn
options = {
base: value,
scope: Net::LDAP::SearchScope_BaseObject
}
else
options = {
base: config.base,
filter: Net::LDAP::Filter.eq(field, value)
}
end
if config.user_filter.present?
user_filter = Net::LDAP::Filter.construct(config.user_filter)
options[:filter] = if options[:filter]
Net::LDAP::Filter.join(options[:filter], user_filter)
else
user_filter
end
end
if limit.present?
options.merge!(size: limit)
end
options = user_options(field, value, limit)
entries = ldap_search(options).select do |entry|
entry.respond_to? config.uid
......@@ -98,6 +74,38 @@ module Gitlab
Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
[]
end
private
def user_options(field, value, limit)
options = { attributes: %W(#{config.uid} cn mail dn) }
options[:size] = limit if limit
if field.to_sym == :dn
options[:base] = value
options[:scope] = Net::LDAP::SearchScope_BaseObject
options[:filter] = user_filter
else
options[:base] = config.base
options[:filter] = user_filter(Net::LDAP::Filter.eq(field, value))
end
options
end
def user_filter(filter = nil)
if config.user_filter.present?
user_filter = Net::LDAP::Filter.construct(config.user_filter)
end
if user_filter && filter
Net::LDAP::Filter.join(filter, user_filter)
elsif user_filter
user_filter
else
filter
end
end
end
end
end
......@@ -136,6 +136,29 @@ describe SessionsController do
post(:create, { user: user_params }, { otp_user_id: user.id })
end
context 'remember_me field' do
it 'sets a remember_user_token cookie when enabled' do
allow(U2fRegistration).to receive(:authenticate).and_return(true)
allow(controller).to receive(:find_user).and_return(user)
expect(controller).
to receive(:remember_me).with(user).and_call_original
authenticate_2fa_u2f(remember_me: '1', login: user.username, device_response: "{}")
expect(response.cookies['remember_user_token']).to be_present
end
it 'does nothing when disabled' do
allow(U2fRegistration).to receive(:authenticate).and_return(true)
allow(controller).to receive(:find_user).and_return(user)
expect(controller).not_to receive(:remember_me)
authenticate_2fa_u2f(remember_me: '0', login: user.username, device_response: "{}")
expect(response.cookies['remember_user_token']).to be_nil
end
end
it "creates an audit log record" do
allow(U2fRegistration).to receive(:authenticate).and_return(true)
expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1)
......
require 'spec_helper'
describe 'Branches', feature: true do
let(:project) { create(:project) }
let(:project) { create(:project, :public) }
let(:repository) { project.repository }
context 'logged in' do
before do
login_as :user
project.team << [@user, :developer]
......@@ -29,4 +30,17 @@ describe 'Branches', feature: true do
expect(find('.all-branches')).to have_selector('li', count: 1)
end
end
end
context 'logged out' do
before do
visit namespace_project_branches_path(project.namespace, project)
end
it 'does not show merge request button' do
page.within first('.all-branches li') do
expect(page).not_to have_content 'Merge Request'
end
end
end
end
......@@ -156,6 +156,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
describe "when 2FA via OTP is disabled" do
it "allows logging in with the U2F device" do
user.update_attribute(:otp_required_for_login, false)
login_with(user)
@u2f_device.respond_to_u2f_authentication
......@@ -181,6 +182,19 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
end
it 'persists remember_me value via hidden field' do
login_with(user, remember: true)
@u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
expect(page.body).to match('We heard back from your U2F device')
within 'div#js-authenticate-u2f' do
field = first('input#user_remember_me', visible: false)
expect(field.value).to eq '1'
end
end
describe "when a given U2F device has already been registered by another user" do
describe "but not the current user" do
it "does not allow logging in with that particular device" do
......
/*= require awards_handler */
/*= require jquery */
/*= require jquery.cookie */
/*= require ./fixtures/emoji_menu */
(function() {
......@@ -33,6 +27,7 @@
return setTimeout(function() {
assertFn();
return done();
// Maybe jasmine.clock here?
}, 333);
};
......
......@@ -8,6 +8,7 @@
beforeEach(function() {
fixture.load('behaviors/quick_submit.html');
$('form').submit(function(e) {
// Prevent a form submit from moving us off the testing page
return e.preventDefault();
});
return this.spies = {
......@@ -38,6 +39,8 @@
expect($('input[type=submit]')).toBeDisabled();
return expect($('button[type=submit]')).toBeDisabled();
});
// We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll
// only run the tests that apply to the current platform
if (navigator.userAgent.match(/Macintosh/)) {
it('responds to Meta+Enter', function() {
$('input.quick-submit-input').trigger(keydownEvent());
......
= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" }
= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in", params: {}, resource_name: "user" }
/*= require lib/utils/text_utility */
/*= require issue */
(function() {
......
/*= require jquery-ui/autocomplete */
/*= require new_branch_form */
(function() {
......
/*= require bootstrap */
/*= require select2 */
/*= require lib/utils/type_utility */
/*= require gl_dropdown */
/*= require api */
/*= require project_select */
/*= require project */
(function() {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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