Commit dd6a24d4 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into revert-c676283b-existing

# Conflicts:
#	app/assets/javascripts/dispatcher.js
parents 5c8c33c9 c36544de
...@@ -34,6 +34,7 @@ v 8.13.0 (unreleased) ...@@ -34,6 +34,7 @@ v 8.13.0 (unreleased)
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Append issue template to existing description !6149 (Joseph Frazier) - Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree - Fix Long commit messages overflow viewport in file tree
...@@ -49,10 +50,13 @@ v 8.13.0 (unreleased) ...@@ -49,10 +50,13 @@ v 8.13.0 (unreleased)
- API: expose pipeline data in builds API (!6502, Guilherme Salazar) - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- Notify the Merger about merge after successful build (Dimitris Karakasilis) - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Reduce queries needed to find users using their SSH keys when pushing commits - Reduce queries needed to find users using their SSH keys when pushing commits
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- Fix broken repository 500 errors in project list - Fix broken repository 500 errors in project list
- Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone) - Close todos when accepting merge requests via the API !6486 (tonygambone)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer) - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined) - Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Grouped pipeline dropdown is a scrollable container
v 8.12.4 (unreleased) v 8.12.4 (unreleased)
- Fix type mismatch bug when closing Jira issue - Fix type mismatch bug when closing Jira issue
...@@ -79,6 +83,7 @@ v 8.12.2 ...@@ -79,6 +83,7 @@ v 8.12.2
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv) - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
- Fix resolve discussion buttons endpoint path - Fix resolve discussion buttons endpoint path
- Refactor remnants of CoffeeScript destructured opts and super !6261
v 8.12.1 v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
......
(function() { ((global) => {
this.LabelManager = (function() {
LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
function LabelManager(opts) { class LabelManager {
// Defaults constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
var ref, ref1, ref2; this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
if (opts == null) { this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
opts = {}; this.otherLabels = otherLabels || $('.js-other-labels');
} this.errorMessage = 'Unable to update label prioritization at this time';
this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels');
this.prioritizedLabels.sortable({ this.prioritizedLabels.sortable({
items: 'li', items: 'li',
placeholder: 'list-placeholder', placeholder: 'list-placeholder',
...@@ -18,33 +15,30 @@ ...@@ -18,33 +15,30 @@
this.bindEvents(); this.bindEvents();
} }
LabelManager.prototype.bindEvents = function() { bindEvents() {
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
}; }
LabelManager.prototype.onTogglePriorityClick = function(e) { onTogglePriorityClick(e) {
var $btn, $label, $tooltip, _this, action;
e.preventDefault(); e.preventDefault();
_this = e.data; const _this = e.data;
$btn = $(e.currentTarget); const $btn = $(e.currentTarget);
$label = $("#" + ($btn.data('domId'))); const $label = $(`#${$btn.data('domId')}`);
action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
// Make sure tooltip will hide const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
$tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
$tooltip.tooltip('destroy'); $tooltip.tooltip('destroy');
return _this.toggleLabelPriority($label, action); return _this.toggleLabelPriority($label, action);
}; }
LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) { toggleLabelPriority($label, action, persistState) {
var $from, $target, _this, url, xhr;
if (persistState == null) { if (persistState == null) {
persistState = true; persistState = true;
} }
_this = this; let xhr;
url = $label.find('.js-toggle-priority').data('url'); const _this = this;
$target = this.prioritizedLabels; const url = $label.find('.js-toggle-priority').data('url');
$from = this.otherLabels; let $target = this.prioritizedLabels;
// Optimistic update let $from = this.otherLabels;
if (action === 'remove') { if (action === 'remove') {
$target = this.otherLabels; $target = this.otherLabels;
$from = this.prioritizedLabels; $from = this.prioritizedLabels;
...@@ -62,7 +56,7 @@ ...@@ -62,7 +56,7 @@
} }
if (action === 'remove') { if (action === 'remove') {
xhr = $.ajax({ xhr = $.ajax({
url: url, url,
type: 'DELETE' type: 'DELETE'
}); });
// Restore empty message // Restore empty message
...@@ -73,43 +67,40 @@ ...@@ -73,43 +67,40 @@
xhr = this.savePrioritySort($label, action); xhr = this.savePrioritySort($label, action);
} }
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
}; }
LabelManager.prototype.onPrioritySortUpdate = function() { onPrioritySortUpdate() {
var xhr; const xhr = this.savePrioritySort();
xhr = this.savePrioritySort();
return xhr.fail(function() { return xhr.fail(function() {
return new Flash(this.errorMessage, 'alert'); return new Flash(this.errorMessage, 'alert');
}); });
}; }
LabelManager.prototype.savePrioritySort = function() { savePrioritySort() {
return $.post({ return $.post({
url: this.prioritizedLabels.data('url'), url: this.prioritizedLabels.data('url'),
data: { data: {
label_ids: this.getSortedLabelsIds() label_ids: this.getSortedLabelsIds()
} }
}); });
}; }
LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) { rollbackLabelPosition($label, originalAction) {
var action; const action = originalAction === 'remove' ? 'add' : 'remove';
action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false); this.toggleLabelPriority($label, action, false);
return new Flash(this.errorMessage, 'alert'); return new Flash(this.errorMessage, 'alert');
}; }
LabelManager.prototype.getSortedLabelsIds = function() { getSortedLabelsIds() {
var sortedIds; const sortedIds = [];
sortedIds = [];
this.prioritizedLabels.find('li').each(function() { this.prioritizedLabels.find('li').each(function() {
return sortedIds.push($(this).data('id')); sortedIds.push($(this).data('id'));
}); });
return sortedIds; return sortedIds;
}; }
}
return LabelManager; gl.LabelManager = LabelManager;
})(); })(window.gl || (window.gl = {}));
}).call(this);
/*= require blob/template_selector */
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
this.BlobCiYamlSelector = (function(superClass) {
extend(BlobCiYamlSelector, superClass);
function BlobCiYamlSelector() {
return BlobCiYamlSelector.__super__.constructor.apply(this, arguments);
}
BlobCiYamlSelector.prototype.requestFile = function(query) {
return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
};
return BlobCiYamlSelector;
})(TemplateSelector);
this.BlobCiYamlSelectors = (function() {
function BlobCiYamlSelectors(opts) {
var ref;
this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor;
this.$dropdowns.each((function(_this) {
return function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return new BlobCiYamlSelector({
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown,
editor: _this.editor
});
};
})(this));
}
return BlobCiYamlSelectors;
})();
}).call(this);
/*= require blob/template_selector */
((global) => {
class BlobCiYamlSelector extends gl.TemplateSelector {
requestFile(query) {
return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
}
requestFileSuccess(file) {
return super.requestFileSuccess(file);
}
}
global.BlobCiYamlSelector = BlobCiYamlSelector;
class BlobCiYamlSelectors {
constructor({ editor, $dropdowns } = {}) {
this.editor = editor;
this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
this.initSelectors();
}
initSelectors() {
const editor = this.editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobCiYamlSelector({
editor,
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown
});
});
}
}
global.BlobCiYamlSelectors = BlobCiYamlSelectors;
})(window.gl || (window.gl = {}));
...@@ -18,6 +18,6 @@ ...@@ -18,6 +18,6 @@
return BlobGitignoreSelector; return BlobGitignoreSelector;
})(TemplateSelector); })(gl.TemplateSelector);
}).call(this); }).call(this);
...@@ -23,6 +23,6 @@ ...@@ -23,6 +23,6 @@
return BlobLicenseSelector; return BlobLicenseSelector;
})(TemplateSelector); })(gl.TemplateSelector);
}).call(this); }).call(this);
(function() {
this.BlobLicenseSelectors = (function() {
function BlobLicenseSelectors(opts) {
var ref;
this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor;
this.$dropdowns.each((function(_this) {
return function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return new BlobLicenseSelector({
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
editor: _this.editor
});
};
})(this));
}
return BlobLicenseSelectors;
})();
}).call(this);
((global) => {
class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
this.$dropdowns = $('.js-license-selector');
this.editor = editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobLicenseSelector({
editor,
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
global.BlobLicenseSelectors = BlobLicenseSelectors;
})(window.gl || (window.gl = {}));
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.TemplateSelector = (function() {
function TemplateSelector(opts) {
var ref;
if (opts == null) {
opts = {};
}
this.onClick = bind(this.onClick, this);
this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
this.autosizeUpdateEvent = document.createEvent('Event');
this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
}
TemplateSelector.prototype.buildDropdown = function() {
return this.dropdown.glDropdown({
data: this.data,
filterable: true,
selectable: true,
toggleLabel: this.toggleLabel,
search: {
fields: ['name']
},
clicked: this.onClick,
text: function(item) {
return item.name;
}
});
};
TemplateSelector.prototype.bindEvents = function() {
return this.$input.on('keyup blur', (function(_this) {
return function(e) {
return _this.onFilenameUpdate();
};
})(this));
};
TemplateSelector.prototype.toggleLabel = function(item) {
return item.name;
};
TemplateSelector.prototype.onFilenameUpdate = function() {
var filenameMatches;
if (!this.$input.length) {
return;
}
filenameMatches = this.pattern.test(this.$input.val().trim());
if (!filenameMatches) {
this.wrapper.addClass('hidden');
return;
}
return this.wrapper.removeClass('hidden');
};
TemplateSelector.prototype.onClick = function(item, el, e) {
e.preventDefault();
return this.requestFile(item);
};
TemplateSelector.prototype.requestFile = function(item) {
// This `requestFile` method is an abstract method that should
// 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, opts) {
var oldValue = this.editor.getValue();
var newValue = file.content;
if (opts == null) {
opts = {};
}
if (opts.append && oldValue.length && oldValue !== newValue) {
newValue = oldValue + '\n\n' + newValue;
}
this.editor.setValue(newValue, 1);
if (!opts.skipFocus) this.editor.focus();
if (this.editor instanceof jQuery) {
this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
}
};
TemplateSelector.prototype.startLoadingSpinner = function() {
this.dropdownIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
};
TemplateSelector.prototype.stopLoadingSpinner = function() {
this.dropdownIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
};
return TemplateSelector;
})();
}).call(this);
((global) => {
class TemplateSelector {
constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
this.onClick = this.onClick.bind(this);
this.dropdown = dropdown;
this.data = data;
this.pattern = pattern;
this.wrapper = wrapper;
this.editor = editor;
this.fileEndpoint = fileEndpoint;
this.$input = $input || $('#file_name');
this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
this.autosizeUpdateEvent = document.createEvent('Event');
this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
}
buildDropdown() {
return this.dropdown.glDropdown({
data: this.data,
filterable: true,
selectable: true,
toggleLabel: this.toggleLabel,
search: {
fields: ['name']
},
clicked: this.onClick,
text: function(item) {
return item.name;
}
});
}
bindEvents() {
return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
}
toggleLabel(item) {
return item.name;
}
onFilenameUpdate() {
var filenameMatches;
if (!this.$input.length) {
return;
}
filenameMatches = this.pattern.test(this.$input.val().trim());
if (!filenameMatches) {
this.wrapper.addClass('hidden');
return;
}
return this.wrapper.removeClass('hidden');
}
onClick(item, el, e) {
e.preventDefault();
return this.requestFile(item);
}
requestFile(item) {
// This `requestFile` method is an abstract method that should
// be added by all subclasses.
}
// To be implemented on the extending class
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess(file, { skipFocus, append } = {}) {
const oldValue = this.editor.getValue();
let newValue = file.content;
if (append && oldValue.length && oldValue !== newValue) {
newValue = oldValue + '\n\n' + newValue;
}
this.editor.setValue(newValue, 1);
if (!skipFocus) this.editor.focus();
if (this.editor instanceof jQuery) {
this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
}
}
startLoadingSpinner() {
this.dropdownIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
}
stopLoadingSpinner() {
this.dropdownIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
}
}
global.TemplateSelector = TemplateSelector;
})(window.gl || ( window.gl = {}));
...@@ -23,13 +23,13 @@ ...@@ -23,13 +23,13 @@
})(this)); })(this));
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap(); this.initSoftWrap();
new BlobLicenseSelectors({ new gl.BlobLicenseSelectors({
editor: this.editor editor: this.editor
}); });
new BlobGitignoreSelectors({ new BlobGitignoreSelectors({
editor: this.editor editor: this.editor
}); });
new BlobCiYamlSelectors({ new gl.BlobCiYamlSelectors({
editor: this.editor editor: this.editor
}); });
} }
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
Issuable.init(); Issuable.init();
new IssuableBulkActions(); new gl.IssuableBulkActions();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
break; break;
case 'projects:issues:show': case 'projects:issues:show':
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
new Milestone(); new Milestone();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new Todos(); new gl.Todos();
break; break;
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
...@@ -62,6 +62,7 @@ ...@@ -62,6 +62,7 @@
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
new IssuableTemplateSelectors(); new IssuableTemplateSelectors();
new gl.IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:new': case 'projects:merge_requests:new':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
...@@ -72,6 +73,7 @@ ...@@ -72,6 +73,7 @@
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
new IssuableTemplateSelectors(); new IssuableTemplateSelectors();
new gl.IssuableTemplateSelectors();
break; break;
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
...@@ -169,7 +171,7 @@ ...@@ -169,7 +171,7 @@
break; break;
case 'projects:labels:index': case 'projects:labels:index':
if ($('.prioritized-labels').length) { if ($('.prioritized-labels').length) {
new LabelManager(); new gl.LabelManager();
} }
break; break;
case 'projects:network:show': case 'projects:network:show':
...@@ -283,7 +285,7 @@ ...@@ -283,7 +285,7 @@
Dispatcher.prototype.initSearch = function() { Dispatcher.prototype.initSearch = function() {
// Only when search form is present // Only when search form is present
if ($('.search').length) { if ($('.search').length) {
return new SearchAutocomplete(); return new gl.SearchAutocomplete();
} }
}; };
......
(function() { ((global) => {
this.IssuableBulkActions = (function() {
function IssuableBulkActions(opts) { class IssuableBulkActions {
// Set defaults constructor({ container, form, issues } = {}) {
var ref, ref1, ref2; this.container = container || $('.content'),
if (opts == null) { this.form = form || this.getElement('.bulk-update');
opts = {}; this.issues = issues || this.getElement('.issues-list .issue');
}
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.form.data('bulkActions', this);
this.willUpdateLabels = false; this.willUpdateLabels = false;
this.bindEvents(); this.bindEvents();
...@@ -15,53 +12,46 @@ ...@@ -15,53 +12,46 @@
Issuable.initChecks(); Issuable.initChecks();
} }
IssuableBulkActions.prototype.getElement = function(selector) { getElement(selector) {
return this.container.find(selector); return this.container.find(selector);
}; }
IssuableBulkActions.prototype.bindEvents = function() { bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}; }
IssuableBulkActions.prototype.onFormSubmit = function(e) { onFormSubmit(e) {
e.preventDefault(); e.preventDefault();
return this.submit(); return this.submit();
}; }
IssuableBulkActions.prototype.submit = function() { submit() {
var _this, xhr; const _this = this;
_this = this; const xhr = $.ajax({
xhr = $.ajax({
url: this.form.attr('action'), url: this.form.attr('action'),
method: this.form.attr('method'), method: this.form.attr('method'),
dataType: 'JSON', dataType: 'JSON',
data: this.getFormDataAsObject() data: this.getFormDataAsObject()
}); });
xhr.done(function(response, status, xhr) { xhr.done(() => window.location.reload());
return location.reload(); xhr.fail(() => new Flash("Issue update failed"));
});
xhr.fail(function() {
return new Flash("Issue update failed");
});
return xhr.always(this.onFormSubmitAlways.bind(this)); return xhr.always(this.onFormSubmitAlways.bind(this));
}; }
IssuableBulkActions.prototype.onFormSubmitAlways = function() { onFormSubmitAlways() {
return this.form.find('[type="submit"]').enable(); return this.form.find('[type="submit"]').enable();
}; }
IssuableBulkActions.prototype.getSelectedIssues = function() { getSelectedIssues() {
return this.issues.has('.selected_issue:checked'); return this.issues.has('.selected_issue:checked');
}; }
IssuableBulkActions.prototype.getLabelsFromSelection = function() { getLabelsFromSelection() {
var labels; const labels = [];
labels = [];
this.getSelectedIssues().map(function() { this.getSelectedIssues().map(function() {
var _labels; const labelsData = $(this).data('labels');
_labels = $(this).data('labels'); if (labelsData) {
if (_labels) { return labelsData.map(function(labelId) {
return _labels.map(function(labelId) {
if (labels.indexOf(labelId) === -1) { if (labels.indexOf(labelId) === -1) {
return labels.push(labelId); return labels.push(labelId);
} }
...@@ -69,7 +59,7 @@ ...@@ -69,7 +59,7 @@
} }
}); });
return labels; return labels;
}; }
/** /**
...@@ -77,25 +67,21 @@ ...@@ -77,25 +67,21 @@
* @return {Array} Label IDs * @return {Array} Label IDs
*/ */
IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() { getUnmarkedIndeterminedLabels() {
var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result; const result = [];
result = []; const labelsToKeep = [];
labelsToKeep = [];
ref = this.getElement('.labels-filter .is-indeterminate'); this.getElement('.labels-filter .is-indeterminate')
for (i = 0, len = ref.length; i < len; i++) { .each((i, el) => labelsToKeep.push($(el).data('labelId')));
el = ref[i];
labelsToKeep.push($(el).data('labelId')); this.getLabelsFromSelection().forEach((id) => {
}
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) { if (labelsToKeep.indexOf(id) === -1) {
result.push(id); result.push(id);
} }
} });
return result; return result;
}; }
/** /**
...@@ -103,9 +89,8 @@ ...@@ -103,9 +89,8 @@
* Returns key/value pairs from form data * Returns key/value pairs from form data
*/ */
IssuableBulkActions.prototype.getFormDataAsObject = function() { getFormDataAsObject() {
var formData; const formData = {
formData = {
update: { update: {
state_event: this.form.find('input[name="update[state_event]"]').val(), state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
...@@ -125,19 +110,18 @@ ...@@ -125,19 +110,18 @@
}); });
} }
return formData; return formData;
}; }
IssuableBulkActions.prototype.getLabelsToApply = function() { getLabelsToApply() {
var $labels, labelIds; const labelIds = [];
labelIds = []; const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels.each(function(k, label) { $labels.each(function(k, label) {
if (label) { if (label) {
return labelIds.push(parseInt($(label).val())); return labelIds.push(parseInt($(label).val()));
} }
}); });
return labelIds; return labelIds;
}; }
/** /**
...@@ -145,11 +129,10 @@ ...@@ -145,11 +129,10 @@
* @return {Array} Array of labels IDs * @return {Array} Array of labels IDs
*/ */
IssuableBulkActions.prototype.getLabelsToRemove = function() { getLabelsToRemove() {
var indeterminatedLabels, labelsToApply, result; const result = [];
result = []; const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); const labelsToApply = this.getLabelsToApply();
labelsToApply = this.getLabelsToApply();
indeterminatedLabels.map(function(id) { indeterminatedLabels.map(function(id) {
// We need to exclude label IDs that will be applied // 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 // By not doing this will cause issues from selection to not add labels at all
...@@ -158,10 +141,9 @@ ...@@ -158,10 +141,9 @@
} }
}); });
return result; return result;
}; }
}
return IssuableBulkActions;
})(); global.IssuableBulkActions = IssuableBulkActions;
}).call(this); })(window.gl || (window.gl = {}));
(function() { ((global) => {
var GitLabCrop,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
GitLabCrop = (function() {
var FILENAMEREGEX;
// Matches everything but the file name // Matches everything but the file name
FILENAMEREGEX = /^.*[\\\/]/; const FILENAMEREGEX = /^.*[\\\/]/;
function GitLabCrop(input, opts) { class GitLabCrop {
var ref, ref1, ref2, ref3, ref4; constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
if (opts == null) { exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
opts = {};
} this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this); this.onModalHide = this.onModalHide.bind(this);
this.onModalHide = bind(this.onModalHide, this); this.onModalShow = this.onModalShow.bind(this);
this.onModalShow = bind(this.onModalShow, this); this.onPickImageClick = this.onPickImageClick.bind(this);
this.onPickImageClick = bind(this.onPickImageClick, this);
this.fileInput = $(input); 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; this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`);
this.exportWidth = exportWidth;
this.exportHeight = exportHeight;
this.cropBoxWidth = cropBoxWidth;
this.cropBoxHeight = cropBoxHeight;
this.form = this.fileInput.parents('form');
this.filename = filename;
this.previewImage = previewImage;
this.modalCrop = modalCrop;
this.pickImageEl = pickImageEl;
this.uploadImageBtn = uploadImageBtn;
this.modalCropImg = modalCropImg;
this.filename = this.getElement(filename);
this.previewImage = this.getElement(previewImage);
this.pickImageEl = this.getElement(pickImageEl);
this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]'); this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.bindEvents(); this.bindEvents();
} }
GitLabCrop.prototype.getElement = function(selector) { getElement(selector) {
return $(selector, this.form); return $(selector, this.form);
}; }
GitLabCrop.prototype.bindEvents = function() { bindEvents() {
var _this; var _this;
_this = this; _this = this;
this.fileInput.on('change', function(e) { this.fileInput.on('change', function(e) {
...@@ -57,13 +55,13 @@ ...@@ -57,13 +55,13 @@
return _this.onActionBtnClick(btn); return _this.onActionBtnClick(btn);
}); });
return this.croppedImageBlob = null; return this.croppedImageBlob = null;
}; }
GitLabCrop.prototype.onPickImageClick = function() { onPickImageClick() {
return this.fileInput.trigger('click'); return this.fileInput.trigger('click');
}; }
GitLabCrop.prototype.onModalShow = function() { onModalShow() {
var _this; var _this;
_this = this; _this = this;
return this.modalCropImg.cropper({ return this.modalCropImg.cropper({
...@@ -95,44 +93,44 @@ ...@@ -95,44 +93,44 @@
}); });
} }
}); });
}; }
GitLabCrop.prototype.onModalHide = function() { onModalHide() {
return this.modalCropImg.attr('src', '').cropper('destroy'); return this.modalCropImg.attr('src', '').cropper('destroy');
}; }
GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image onUploadImageBtnClick(e) {
e.preventDefault(); // Destroy cropper instance e.preventDefault();
this.setBlob(); this.setBlob();
this.setPreview(); this.setPreview();
this.modalCrop.modal('hide'); this.modalCrop.modal('hide');
return this.fileInput.val(''); return this.fileInput.val('');
}; }
GitLabCrop.prototype.onActionBtnClick = function(btn) { onActionBtnClick(btn) {
var data, result; var data, result;
data = $(btn).data(); data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) { if (this.modalCropImg.data('cropper') && data.method) {
return result = this.modalCropImg.cropper(data.method, data.option); return result = this.modalCropImg.cropper(data.method, data.option);
} }
}; }
GitLabCrop.prototype.onFileInputChange = function(e, input) { onFileInputChange(e, input) {
return this.readFile(input); return this.readFile(input);
}; }
GitLabCrop.prototype.readFile = function(input) { readFile(input) {
var _this, reader; var _this, reader;
_this = this; _this = this;
reader = new FileReader; reader = new FileReader;
reader.onload = function() { reader.onload = () => {
_this.modalCropImg.attr('src', reader.result); _this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show'); return _this.modalCrop.modal('show');
}; };
return reader.readAsDataURL(input.files[0]); return reader.readAsDataURL(input.files[0]);
}; }
GitLabCrop.prototype.dataURLtoBlob = function(dataURL) { dataURLtoBlob(dataURL) {
var array, binary, i, k, len, v; var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]); binary = atob(dataURL.split(',')[1]);
array = []; array = [];
...@@ -143,35 +141,32 @@ ...@@ -143,35 +141,32 @@
return new Blob([new Uint8Array(array)], { return new Blob([new Uint8Array(array)], {
type: 'image/png' type: 'image/png'
}); });
}; }
GitLabCrop.prototype.setPreview = function() { setPreview() {
var filename; var filename;
this.previewImage.attr('src', this.dataURL); this.previewImage.attr('src', this.dataURL);
filename = this.fileInput.val().replace(FILENAMEREGEX, ''); filename = this.fileInput.val().replace(FILENAMEREGEX, '');
return this.filename.text(filename); return this.filename.text(filename);
}; }
GitLabCrop.prototype.setBlob = function() { setBlob() {
this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
width: 200, width: 200,
height: 200 height: 200
}).toDataURL('image/png'); }).toDataURL('image/png');
return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
}; }
GitLabCrop.prototype.getBlob = function() { getBlob() {
return this.croppedImageBlob; return this.croppedImageBlob;
}; }
}
return GitLabCrop;
})();
$.fn.glCrop = function(opts) { $.fn.glCrop = function(opts) {
return this.each(function() { return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts)); return $(this).data('glcrop', new GitLabCrop(this, opts));
}); });
}; }
}).call(this); })(window.gl || (window.gl = {}));
(function() { ((global) => {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.Profile = (function() { class Profile {
function Profile(opts) { constructor({ form } = {}) {
var cropOpts, ref; this.onSubmitForm = this.onSubmitForm.bind(this);
if (opts == null) { this.form = form || $('.edit-user');
opts = {};
}
this.onSubmitForm = bind(this.onSubmitForm, this);
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();
$(this).find('.update-success').hide();
return $(this).find('.update-failed').hide();
});
$('.update-username').on('ajax:complete', function() {
$('.loading-username').hide();
$(this).find('.btn-save').enable();
return $(this).find('.loading-gif').hide();
});
$('.update-notifications').on('ajax:success', function(e, data) {
if (data.saved) {
return new Flash("Notification settings saved", "notice");
} else {
return new Flash("Failed to save new settings", "alert");
}
});
this.bindEvents(); this.bindEvents();
cropOpts = { this.initAvatarGlCrop();
}
initAvatarGlCrop() {
const cropOpts = {
filename: '.js-avatar-filename', filename: '.js-avatar-filename',
previewImage: '.avatar-image .avatar', previewImage: '.avatar-image .avatar',
modalCrop: '.modal-profile-crop', modalCrop: '.modal-profile-crop',
...@@ -46,23 +20,51 @@ ...@@ -46,23 +20,51 @@
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
} }
Profile.prototype.bindEvents = function() { bindEvents() {
return this.form.on('submit', this.onSubmitForm); $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
}; $('#user_notification_email').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
this.form.on('submit', this.onSubmitForm);
}
submitForm() {
return $(this).parents('form').submit();
}
Profile.prototype.onSubmitForm = function(e) { onSubmitForm(e) {
e.preventDefault(); e.preventDefault();
return this.saveForm(); return this.saveForm();
}; }
beforeUpdateUsername() {
$('.loading-username').show();
$(this).find('.update-success').hide();
return $(this).find('.update-failed').hide();
}
afterUpdateUsername() {
$('.loading-username').hide();
$(this).find('.btn-save').enable();
return $(this).find('.loading-gif').hide();
}
onUpdateNotifs(e, data) {
return data.saved ?
new Flash("Notification settings saved", "notice") :
new Flash("Failed to save new settings", "alert");
}
saveForm() {
const self = this;
const formData = new FormData(this.form[0]);
const avatarBlob = this.avatarGlCrop.getBlob();
Profile.prototype.saveForm = function() {
var avatarBlob, formData, self;
self = this;
formData = new FormData(this.form[0]);
avatarBlob = this.avatarGlCrop.getBlob();
if (avatarBlob != null) { if (avatarBlob != null) {
formData.append('user[avatar]', avatarBlob, 'avatar.png'); formData.append('user[avatar]', avatarBlob, 'avatar.png');
} }
return $.ajax({ return $.ajax({
url: this.form.attr('action'), url: this.form.attr('action'),
type: this.form.attr('method'), type: this.form.attr('method'),
...@@ -70,37 +72,29 @@ ...@@ -70,37 +72,29 @@
dataType: "json", dataType: "json",
processData: false, processData: false,
contentType: false, contentType: false,
success: function(response) { success: response => new Flash(response.message, 'notice'),
return new Flash(response.message, 'notice'); error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
}, complete: () => {
error: function(jqXHR) {
return new Flash(jqXHR.responseJSON.message, 'alert');
},
complete: function() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
// Enable submit button after requests ends // Enable submit button after requests ends
return self.form.find(':input[disabled]').enable(); return self.form.find(':input[disabled]').enable();
} }
}); });
}; }
}
return Profile;
})();
$(function() { $(function() {
$(document).on('focusout.ssh_key', '#key_key', function() { $(document).on('focusout.ssh_key', '#key_key', function() {
var $title, comment; const $title = $('#key_title');
$title = $('#key_title'); const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
if (comment && comment.length > 1 && $title.val() === '') { if (comment && comment.length > 1 && $title.val() === '') {
return $title.val(comment[1]).change(); return $title.val(comment[1]).change();
} }
// Extract the SSH Key title from its comment // Extract the SSH Key title from its comment
}); });
if (gl.utils.getPagePath() === 'profiles') { if (global.utils.getPagePath() === 'profiles') {
return new Profile(); return new Profile();
} }
}); });
}).call(this); })(window.gl || (window.gl = {}));
(function() { ((global) => {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.SearchAutocomplete = (function() { const KEYCODE = {
var KEYCODE;
KEYCODE = {
ESCAPE: 27, ESCAPE: 27,
BACKSPACE: 8, BACKSPACE: 8,
ENTER: 13, ENTER: 13,
...@@ -12,19 +8,14 @@ ...@@ -12,19 +8,14 @@
DOWN: 40 DOWN: 40
}; };
function SearchAutocomplete(opts) { class SearchAutocomplete {
var ref, ref1, ref2, ref3, ref4; constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
if (opts == null) { this.bindEventContext();
opts = {}; this.wrap = wrap || $('.search');
} this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
this.onSearchInputBlur = bind(this.onSearchInputBlur, this); this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
this.onClearInputClick = bind(this.onClearInputClick, this); this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
this.onSearchInputFocus = bind(this.onSearchInputFocus, this); this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
this.onSearchInputClick = bind(this.onSearchInputClick, this);
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.dropdown = this.wrap.find('.dropdown');
this.dropdownContent = this.dropdown.find('.dropdown-content'); this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge'); this.locationBadgeEl = this.getElement('.location-badge');
...@@ -46,19 +37,27 @@ ...@@ -46,19 +37,27 @@
} }
// Finds an element inside wrapper element // Finds an element inside wrapper element
SearchAutocomplete.prototype.getElement = function(selector) { bindEventContext() {
this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
this.onClearInputClick = this.onClearInputClick.bind(this);
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputClick = this.onSearchInputClick.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
}
getElement(selector) {
return this.wrap.find(selector); return this.wrap.find(selector);
}; }
SearchAutocomplete.prototype.saveOriginalState = function() { saveOriginalState() {
return this.originalState = this.serializeState(); return this.originalState = this.serializeState();
}; }
SearchAutocomplete.prototype.saveTextLength = function() { saveTextLength() {
return this.lastTextLength = this.searchInput.val().length; return this.lastTextLength = this.searchInput.val().length;
}; }
SearchAutocomplete.prototype.createAutocomplete = function() { createAutocomplete() {
return this.searchInput.glDropdown({ return this.searchInput.glDropdown({
filterInputBlur: false, filterInputBlur: false,
filterable: true, filterable: true,
...@@ -73,9 +72,9 @@ ...@@ -73,9 +72,9 @@
selectable: true, selectable: true,
clicked: this.onClick.bind(this) clicked: this.onClick.bind(this)
}); });
}; }
SearchAutocomplete.prototype.getData = function(term, callback) { getData(term, callback) {
var _this, contents, jqXHR; var _this, contents, jqXHR;
_this = this; _this = this;
if (!term) { if (!term) {
...@@ -138,9 +137,9 @@ ...@@ -138,9 +137,9 @@
}).always(function() { }).always(function() {
return _this.loadingSuggestions = false; return _this.loadingSuggestions = false;
}); });
}; }
SearchAutocomplete.prototype.getCategoryContents = function() { getCategoryContents() {
var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
userId = gon.current_user_id; userId = gon.current_user_id;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
...@@ -173,9 +172,9 @@ ...@@ -173,9 +172,9 @@
items.splice(0, 1); items.splice(0, 1);
} }
return items; return items;
}; }
SearchAutocomplete.prototype.serializeState = function() { serializeState() {
return { return {
// Search Criteria // Search Criteria
search_project_id: this.projectInputEl.val(), search_project_id: this.projectInputEl.val(),
...@@ -186,9 +185,9 @@ ...@@ -186,9 +185,9 @@
// Location badge // Location badge
_location: this.locationBadgeEl.text() _location: this.locationBadgeEl.text()
}; };
}; }
SearchAutocomplete.prototype.bindEvents = function() { bindEvents() {
this.searchInput.on('keydown', this.onSearchInputKeyDown); this.searchInput.on('keydown', this.onSearchInputKeyDown);
this.searchInput.on('keyup', this.onSearchInputKeyUp); this.searchInput.on('keyup', this.onSearchInputKeyUp);
this.searchInput.on('click', this.onSearchInputClick); this.searchInput.on('click', this.onSearchInputClick);
...@@ -200,9 +199,9 @@ ...@@ -200,9 +199,9 @@
return _this.searchInput.focus(); return _this.searchInput.focus();
}; };
})(this)); })(this));
}; }
SearchAutocomplete.prototype.enableAutocomplete = function() { enableAutocomplete() {
var _this; var _this;
// No need to enable anything if user is not logged in // No need to enable anything if user is not logged in
if (!gon.current_user_id) { if (!gon.current_user_id) {
...@@ -216,12 +215,12 @@ ...@@ -216,12 +215,12 @@
} }
}; };
SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
// Saves last length of the entered text // Saves last length of the entered text
onSearchInputKeyDown() {
return this.saveTextLength(); return this.saveTextLength();
}; }
SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) { onSearchInputKeyUp(e) {
switch (e.keyCode) { switch (e.keyCode) {
case KEYCODE.BACKSPACE: case KEYCODE.BACKSPACE:
// when trying to remove the location badge // when trying to remove the location badge
...@@ -259,54 +258,53 @@ ...@@ -259,54 +258,53 @@
} }
} }
this.wrap.toggleClass('has-value', !!e.target.value); this.wrap.toggleClass('has-value', !!e.target.value);
}; }
// Avoid falsy value to be returned // Avoid falsy value to be returned
SearchAutocomplete.prototype.onSearchInputClick = function(e) { onSearchInputClick(e) {
// Prevents closing the dropdown menu
return e.stopImmediatePropagation(); return e.stopImmediatePropagation();
}; }
SearchAutocomplete.prototype.onSearchInputFocus = function() { onSearchInputFocus() {
this.isFocused = true; this.isFocused = true;
this.wrap.addClass('search-active'); this.wrap.addClass('search-active');
if (this.getValue() === '') { if (this.getValue() === '') {
return this.getData(); return this.getData();
} }
}; }
SearchAutocomplete.prototype.getValue = function() { getValue() {
return this.searchInput.val(); return this.searchInput.val();
}; }
SearchAutocomplete.prototype.onClearInputClick = function(e) { onClearInputClick(e) {
e.preventDefault(); e.preventDefault();
return this.searchInput.val('').focus(); return this.searchInput.val('').focus();
}; }
SearchAutocomplete.prototype.onSearchInputBlur = function(e) { onSearchInputBlur(e) {
this.isFocused = false; this.isFocused = false;
this.wrap.removeClass('search-active'); this.wrap.removeClass('search-active');
// If input is blank then restore state // If input is blank then restore state
if (this.searchInput.val() === '') { if (this.searchInput.val() === '') {
return this.restoreOriginalState(); return this.restoreOriginalState();
} }
}; }
SearchAutocomplete.prototype.addLocationBadge = function(item) { addLocationBadge(item) {
var badgeText, category, value; var badgeText, category, value;
category = item.category != null ? item.category + ": " : ''; category = item.category != null ? item.category + ": " : '';
value = item.value != null ? item.value : ''; value = item.value != null ? item.value : '';
badgeText = "" + category + value; badgeText = "" + category + value;
this.locationBadgeEl.text(badgeText).show(); this.locationBadgeEl.text(badgeText).show();
return this.wrap.addClass('has-location-badge'); return this.wrap.addClass('has-location-badge');
}; }
SearchAutocomplete.prototype.hasLocationBadge = function() { hasLocationBadge() {
return this.wrap.is('.has-location-badge'); return this.wrap.is('.has-location-badge');
}; };
SearchAutocomplete.prototype.restoreOriginalState = function() { restoreOriginalState() {
var i, input, inputs, len; var i, input, inputs, len;
inputs = Object.keys(this.originalState); inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i++) { for (i = 0, len = inputs.length; i < len; i++) {
...@@ -320,13 +318,13 @@ ...@@ -320,13 +318,13 @@
value: this.originalState._location value: this.originalState._location
}); });
} }
}; }
SearchAutocomplete.prototype.badgePresent = function() { badgePresent() {
return this.locationBadgeEl.length; return this.locationBadgeEl.length;
}; }
SearchAutocomplete.prototype.resetSearchState = function() { resetSearchState() {
var i, input, inputs, len, results; var i, input, inputs, len, results;
inputs = Object.keys(this.originalState); inputs = Object.keys(this.originalState);
results = []; results = [];
...@@ -339,30 +337,30 @@ ...@@ -339,30 +337,30 @@
results.push(this.getElement("#" + input).val('')); results.push(this.getElement("#" + input).val(''));
} }
return results; return results;
}; }
SearchAutocomplete.prototype.removeLocationBadge = function() { removeLocationBadge() {
this.locationBadgeEl.hide(); this.locationBadgeEl.hide();
this.resetSearchState(); this.resetSearchState();
this.wrap.removeClass('has-location-badge'); this.wrap.removeClass('has-location-badge');
return this.disableAutocomplete(); return this.disableAutocomplete();
}; }
SearchAutocomplete.prototype.disableAutocomplete = function() { disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
this.searchInput.addClass('disabled'); this.searchInput.addClass('disabled');
this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
this.restoreMenu(); this.restoreMenu();
} }
}; }
SearchAutocomplete.prototype.restoreMenu = function() { restoreMenu() {
var html; var html;
html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>"; html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
return this.dropdownContent.html(html); return this.dropdownContent.html(html);
}; };
SearchAutocomplete.prototype.onClick = function(item, $el, e) { onClick(item, $el, e) {
if (location.pathname.indexOf(item.url) !== -1) { if (location.pathname.indexOf(item.url) !== -1) {
e.preventDefault(); e.preventDefault();
if (!this.badgePresent) { if (!this.badgePresent) {
...@@ -385,9 +383,9 @@ ...@@ -385,9 +383,9 @@
} }
}; };
return SearchAutocomplete; }
})(); global.SearchAutocomplete = SearchAutocomplete;
$(function() { $(function() {
var $projectOptionsDataEl = $('.js-search-project-options'); var $projectOptionsDataEl = $('.js-search-project-options');
...@@ -426,4 +424,4 @@ ...@@ -426,4 +424,4 @@
} }
}); });
}).call(this); })(window.gl || (window.gl = {}));
/*= require ../blob/template_selector */ /*= require ../blob/template_selector */
((global) => { ((global) => {
class IssuableTemplateSelector extends TemplateSelector { class IssuableTemplateSelector extends gl.TemplateSelector {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.projectPath = this.dropdown.data('project-path'); this.projectPath = this.dropdown.data('project-path');
...@@ -50,4 +50,4 @@ ...@@ -50,4 +50,4 @@
} }
global.IssuableTemplateSelector = IssuableTemplateSelector; global.IssuableTemplateSelector = IssuableTemplateSelector;
})(window); })(window.gl || (window.gl = {}));
((global) => { ((global) => {
class IssuableTemplateSelectors { class IssuableTemplateSelectors {
constructor(opts = {}) { constructor({ $dropdowns, editor } = {}) {
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector'); this.$dropdowns = $dropdowns || $('.js-issuable-selector');
this.editor = opts.editor || this.initEditor(); this.editor = editor || this.initEditor();
this.$dropdowns.each((i, dropdown) => { this.$dropdowns.each((i, dropdown) => {
let $dropdown = $(dropdown); const $dropdown = $(dropdown);
new IssuableTemplateSelector({ new gl.IssuableTemplateSelector({
pattern: /(\.md)/, pattern: /(\.md)/,
data: $dropdown.data('data'), data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-issuable-selector-wrap'), wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
...@@ -26,4 +26,4 @@ ...@@ -26,4 +26,4 @@
} }
global.IssuableTemplateSelectors = IssuableTemplateSelectors; global.IssuableTemplateSelectors = IssuableTemplateSelectors;
})(window); })(window.gl || (window.gl = {}));
(function() { ((global) => {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
class Todos {
this.Todos = (function() { constructor({ el } = {}) {
function Todos(opts) { this.allDoneClicked = this.allDoneClicked.bind(this);
var ref; this.doneClicked = this.doneClicked.bind(this);
if (opts == null) { this.el = el || $('.js-todos-options');
opts = {};
}
this.allDoneClicked = bind(this.allDoneClicked, this);
this.doneClicked = bind(this.doneClicked, this);
this.el = (ref = opts.el) != null ? ref : $('.js-todos-options');
this.perPage = this.el.data('perPage'); this.perPage = this.el.data('perPage');
this.clearListeners(); this.clearListeners();
this.initBtnListeners(); this.initBtnListeners();
this.initFilters(); this.initFilters();
} }
Todos.prototype.clearListeners = function() { clearListeners() {
$('.done-todo').off('click'); $('.done-todo').off('click');
$('.js-todos-mark-all').off('click'); $('.js-todos-mark-all').off('click');
return $('.todo').off('click'); return $('.todo').off('click');
}; }
Todos.prototype.initBtnListeners = function() { initBtnListeners() {
$('.done-todo').on('click', this.doneClicked); $('.done-todo').on('click', this.doneClicked);
$('.js-todos-mark-all').on('click', this.allDoneClicked); $('.js-todos-mark-all').on('click', this.allDoneClicked);
return $('.todo').on('click', this.goToTodoUrl); return $('.todo').on('click', this.goToTodoUrl);
}; }
Todos.prototype.initFilters = function() { initFilters() {
new UsersSelect(); new UsersSelect();
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type'); this.initFilterDropdown($('.js-type-search'), 'type');
...@@ -38,125 +33,117 @@ ...@@ -38,125 +33,117 @@
event.preventDefault(); event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize()); Turbolinks.visit(this.action + '&' + $(this).serialize());
}); });
}; }
Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) { initFilterDropdown($dropdown, fieldName, searchFields) {
$dropdown.glDropdown({ $dropdown.glDropdown({
fieldName,
selectable: true, selectable: true,
filterable: searchFields ? true : false, filterable: searchFields ? true : false,
fieldName: fieldName,
search: { fields: searchFields }, search: { fields: searchFields },
data: $dropdown.data('data'), data: $dropdown.data('data'),
clicked: function() { clicked: function() {
return $dropdown.closest('form.filter-form').submit(); return $dropdown.closest('form.filter-form').submit();
} }
}) })
}; }
Todos.prototype.doneClicked = function(e) { doneClicked(e) {
var $this;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
$this = $(e.currentTarget); const $target = $(e.currentTarget);
$this.disable(); $target.disable();
return $.ajax({ return $.ajax({
type: 'POST', type: 'POST',
url: $this.attr('href'), url: $target.attr('href'),
dataType: 'json', dataType: 'json',
data: { data: {
'_method': 'delete' '_method': 'delete'
}, },
success: (function(_this) { success: (data) => {
return function(data) { this.redirectIfNeeded(data.count);
_this.redirectIfNeeded(data.count); this.clearDone($target.closest('li'));
_this.clearDone($this.closest('li')); return this.updateBadges(data);
return _this.updateBadges(data); }
};
})(this)
}); });
}; }
Todos.prototype.allDoneClicked = function(e) { allDoneClicked(e) {
var $this;
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
$this = $(e.currentTarget); $target = $(e.currentTarget);
$this.disable(); $target.disable();
return $.ajax({ return $.ajax({
type: 'POST', type: 'POST',
url: $this.attr('href'), url: $target.attr('href'),
dataType: 'json', dataType: 'json',
data: { data: {
'_method': 'delete' '_method': 'delete'
}, },
success: (function(_this) { success: (data) => {
return function(data) { $target.remove();
$this.remove();
$('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
return _this.updateBadges(data); return this.updateBadges(data);
}; }
})(this)
}); });
}; }
Todos.prototype.clearDone = function($row) { clearDone($row) {
var $ul; const $ul = $row.closest('ul');
$ul = $row.closest('ul');
$row.remove(); $row.remove();
if (!$ul.find('li').length) { if (!$ul.find('li').length) {
return $ul.parents('.panel').remove(); return $ul.parents('.panel').remove();
} }
}; }
Todos.prototype.updateBadges = function(data) { updateBadges(data) {
$('.todos-pending .badge, .todos-pending-count').text(data.count); $('.todos-pending .badge, .todos-pending-count').text(data.count);
return $('.todos-done .badge').text(data.done_count); return $('.todos-done .badge').text(data.done_count);
}; }
Todos.prototype.getTotalPages = function() { getTotalPages() {
return this.el.data('totalPages'); return this.el.data('totalPages');
}; }
Todos.prototype.getCurrentPage = function() { getCurrentPage() {
return this.el.data('currentPage'); return this.el.data('currentPage');
}; }
Todos.prototype.getTodosPerPage = function() { getTodosPerPage() {
return this.el.data('perPage'); return this.el.data('perPage');
}; }
redirectIfNeeded(total) {
const currPages = this.getTotalPages();
const currPage = this.getCurrentPage();
Todos.prototype.redirectIfNeeded = function(total) {
var currPage, currPages, newPages, pageParams, url;
currPages = this.getTotalPages();
currPage = this.getCurrentPage();
// Refresh if no remaining Todos // Refresh if no remaining Todos
if (!total) { if (!total) {
location.reload(); window.location.reload();
return; return;
} }
// Do nothing if no pagination // Do nothing if no pagination
if (!currPages) { if (!currPages) {
return; return;
} }
newPages = Math.ceil(total / this.getTodosPerPage());
// Includes query strings const newPages = Math.ceil(total / this.getTodosPerPage());
url = location.href; let url = location.href;
// If new total of pages is different than we have now
if (newPages !== currPages) { if (newPages !== currPages) {
// Redirect to previous page if there's one available // Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) { if (currPages > 1 && currPage === currPages) {
pageParams = { const pageParams = {
page: currPages - 1 page: currPages - 1
}; };
url = gl.utils.mergeUrlParams(pageParams, url); url = gl.utils.mergeUrlParams(pageParams, url);
} }
return Turbolinks.visit(url); return Turbolinks.visit(url);
} }
}; }
Todos.prototype.goToTodoUrl = function(e) { goToTodoUrl(e) {
var todoLink; const todoLink = $(this).data('url');
todoLink = $(this).data('url');
if (!todoLink) { if (!todoLink) {
return; return;
} }
...@@ -167,10 +154,8 @@ ...@@ -167,10 +154,8 @@
} else { } else {
return Turbolinks.visit(todoLink); return Turbolinks.visit(todoLink);
} }
}; }
}
return Todos;
})();
}).call(this); global.Todos = Todos;
})(window.gl || (window.gl = {}));
(global => { ((global) => {
global.User = class { global.User = class {
constructor(opts) { constructor({ action }) {
this.opts = opts; this.action = action;
this.placeProfileAvatarsToTop(); this.placeProfileAvatarsToTop();
this.initTabs(); this.initTabs();
this.hideProjectLimitMessage(); this.hideProjectLimitMessage();
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
} }
initTabs() { initTabs() {
return new UserTabs({ return new global.UserTabs({
parentEl: '.user-profile', parentEl: '.user-profile',
action: this.opts.action action: this.action
}); });
} }
......
// 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); }; };
this.UserTabs = (function() {
function UserTabs(opts) {
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;
}
this.activateTab(this.action);
}
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);
};
UserTabs.prototype.tabShown = function(event) {
var $target, action, source;
$target = $(event.target);
action = $target.data('action');
source = $target.attr('href');
this.setTab(source, action);
return this.setCurrentAction(action);
};
UserTabs.prototype.activateTab = function(action) {
return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show');
};
UserTabs.prototype.setTab = function(source, action) {
if (this.loaded[action] === true) {
return;
}
if (action === 'activity') {
this.loadActivities(source);
}
if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') {
return this.loadTab(source, action);
}
};
UserTabs.prototype.loadTab = function(source, action) {
return $.ajax({
beforeSend: (function(_this) {
return function() {
return _this.toggleLoading(true);
};
})(this),
complete: (function(_this) {
return function() {
return _this.toggleLoading(false);
};
})(this),
dataType: 'json',
type: 'GET',
url: source + ".json",
success: (function(_this) {
return function(data) {
var tabSelector;
tabSelector = 'div#' + action;
_this.parentEl.find(tabSelector).html(data.html);
_this.loaded[action] = true;
// Fix tooltips
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
};
})(this)
});
};
UserTabs.prototype.loadActivities = function(source) {
var $calendarWrap;
if (this.loaded['activity'] === true) {
return;
}
$calendarWrap = this.parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
new Activities();
return this.loaded['activity'] = true;
};
UserTabs.prototype.toggleLoading = function(status) {
return this.parentEl.find('.loading-status .loading').toggle(status);
};
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,
url: new_state
}, document.title, new_state);
return new_state;
};
return UserTabs;
})();
}).call(this);
/*
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>
*/
((global) => {
class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this._location = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
});
this.actions = Object.keys(this.loaded);
this.bindEvents();
if (this.action === 'show') {
this.action = this.defaultAction;
}
this.activateTab(this.action);
}
bindEvents() {
return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
}
tabShown(event) {
const $target = $(event.target);
const action = $target.data('action');
const source = $target.attr('href');
this.setTab(source, action);
return this.setCurrentAction(action);
}
activateTab(action) {
return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
.tab('show');
}
setTab(source, action) {
if (this.loaded[action]) {
return;
}
if (action === 'activity') {
this.loadActivities(source);
}
const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ];
if (loadableActions.indexOf(action) > -1) {
return this.loadTab(source, action);
}
}
loadTab(source, action) {
return $.ajax({
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: `${source}.json`,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
}
});
}
loadActivities(source) {
if (this.loaded['activity']) {
return;
}
const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
new Activities();
return this.loaded['activity'] = true;
}
toggleLoading(status) {
return this.$parentEl.find('.loading-status .loading')
.toggle(status);
}
setCurrentAction(action) {
const regExp = new RegExp(`\/(${this.actions.join('|')})(\.html)?\/?$`);
let new_state = this._location.pathname;
new_state = new_state.replace(/\/+$/, '');
new_state = new_state.replace(regExp, '');
if (action !== this.defaultAction) {
new_state += `/${action}`;
}
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
url: new_state
}, document.title, new_state);
return new_state;
}
}
global.UserTabs = UserTabs;
})(window.gl || (window.gl = {}));
...@@ -22,6 +22,11 @@ ...@@ -22,6 +22,11 @@
.table.builds { .table.builds {
min-width: 1200px; min-width: 1200px;
.branch-commit {
width: 33%;
}
} }
} }
...@@ -385,6 +390,8 @@ ...@@ -385,6 +390,8 @@
left: auto; left: auto;
right: -214px; right: -214px;
top: -9px; top: -9px;
max-height: 245px;
overflow-y: scroll;
a:hover { a:hover {
.ci-status-text { .ci-status-text {
......
...@@ -146,7 +146,8 @@ ...@@ -146,7 +146,8 @@
} }
.project-repo-btn-group, .project-repo-btn-group,
.notification-dropdown { .notification-dropdown,
.project-dropdown {
margin-left: 10px; margin-left: 10px;
} }
......
...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def trending def trending
@projects = TrendingProjectsFinder.new.execute(current_user) @projects = TrendingProjectsFinder.new.execute
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])
......
# Finder for retrieving public trending projects in a given time range.
class TrendingProjectsFinder class TrendingProjectsFinder
def execute(current_user, start_date = 1.month.ago) # current_user - The currently logged in User, if any.
projects_for(current_user).trending(start_date) # last_months - The number of months to limit the trending data to.
def execute(months_limit = 1)
Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do
Project.public_only.trending(months_limit.months.ago)
end
end end
private private
def projects_for(current_user) def cache_key_for(months)
ProjectsFinder.new.execute(current_user) "trending_projects/#{months}"
end end
end end
...@@ -380,6 +380,7 @@ class Project < ActiveRecord::Base ...@@ -380,6 +380,7 @@ class Project < ActiveRecord::Base
SELECT project_id, COUNT(*) AS amount SELECT project_id, COUNT(*) AS amount
FROM notes FROM notes
WHERE created_at >= #{sanitize(since)} WHERE created_at >= #{sanitize(since)}
AND system IS FALSE
GROUP BY project_id GROUP BY project_id
) join_note_counts ON projects.id = join_note_counts.project_id" ) join_note_counts ON projects.id = join_note_counts.project_id"
......
- admin = local_assigns.fetch(:admin, false) - admin = local_assigns.fetch(:admin, false)
- if builds.blank? - if builds.blank?
%li %div
.nothing-here-block No builds to show .nothing-here-block No builds to show
- else - else
.table-holder .table-holder
......
...@@ -19,5 +19,5 @@ ...@@ -19,5 +19,5 @@
= link_to ci_lint_path, class: 'btn btn-default' do = link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint %span CI Lint
%ul.content-list.builds-content-list %div.content-list.builds-content-list
= render "table", builds: @builds, project: @project = render "table", builds: @builds, project: @project
- if !project.empty_repo? && can?(current_user, :download_code, project) - if !project.empty_repo? && can?(current_user, :download_code, project)
%span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'} %span{class: 'hidden-xs hidden-sm'}
.dropdown.inline .dropdown.inline
%button.btn{ 'data-toggle' => 'dropdown' } %button.btn{ 'data-toggle' => 'dropdown' }
= icon('download') = icon('download')
%span.caret = icon("caret-down")
%span.sr-only %span.sr-only
Select Archive Format Select Archive Format
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
......
- if current_user - if current_user
.btn-group .dropdown.inline.project-dropdown
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus') = icon('plus')
= icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
- can_create_issue = can?(current_user, :create_issue, @project) - can_create_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
......
...@@ -13,8 +13,7 @@ ...@@ -13,8 +13,7 @@
- else - else
= ci_status_with_icon(build.status) = ci_status_with_icon(build.status)
%td %td.branch-commit
.branch-commit
- if can?(current_user, :read_build, build) - if can?(current_user, :read_build, build)
= link_to namespace_project_build_url(build.project.namespace, build.project, build) do = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%span.build-link ##{build.id} %span.build-link ##{build.id}
......
...@@ -9,8 +9,7 @@ ...@@ -9,8 +9,7 @@
= ci_icon_for_status(status) = ci_icon_for_status(status)
- else - else
= ci_status_with_icon(status) = ci_status_with_icon(status)
%td %td.branch-commit
.branch-commit
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
%span ##{pipeline.id} %span ##{pipeline.id}
- if pipeline.ref && show_branch - if pipeline.ref && show_branch
...@@ -58,8 +57,8 @@ ...@@ -58,8 +57,8 @@
= icon("calendar") = icon("calendar")
#{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)} #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
%td.pipeline-actions %td.pipeline-actions.hidden-xs
.controls.hidden-xs.pull-right .controls.pull-right
- artifacts = pipeline.builds.latest.with_artifacts_not_expired - artifacts = pipeline.builds.latest.with_artifacts_not_expired
- actions = pipeline.manual_actions - actions = pipeline.manual_actions
- if artifacts.present? || actions.any? - if artifacts.present? || actions.any?
......
...@@ -36,20 +36,20 @@ ...@@ -36,20 +36,20 @@
= link_to ci_lint_path, class: 'btn btn-default' do = link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint %span CI Lint
%ul.content-list.pipelines %div.content-list.pipelines
- stages = @pipelines.stages - stages = @pipelines.stages
- if @pipelines.blank? - if @pipelines.blank?
%li %div
.nothing-here-block No pipelines to show .nothing-here-block No pipelines to show
- else - else
.table-holder .table-holder
%table.table.builds %table.table.builds
%tbody %thead
%th Status %th.col-xs-1.col-sm-1 Status
%th Pipeline %th.col-xs-2.col-sm-4 Pipeline
%th Stages %th.col-xs-2.col-sm-2 Stages
%th %th.col-xs-2.col-sm-2
%th %th.hidden-xs.col-sm-3
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
= paginate @pipelines, theme: 'gitlab' = paginate @pipelines, theme: 'gitlab'
...@@ -71,7 +71,6 @@ ...@@ -71,7 +71,6 @@
= render 'shared/members/access_request_buttons', source: @project = render 'shared/members/access_request_buttons', source: @project
= render "projects/buttons/koding" = render "projects/buttons/koding"
.btn-group.project-repo-btn-group
= render 'projects/buttons/download', project: @project, ref: @ref = render 'projects/buttons/download', project: @project, ref: @ref
= render 'projects/buttons/dropdown' = render 'projects/buttons/dropdown'
......
...@@ -2,6 +2,12 @@ require 'sidekiq/web' ...@@ -2,6 +2,12 @@ require 'sidekiq/web'
require 'sidekiq/cron/web' require 'sidekiq/cron/web'
require 'api/api' require 'api/api'
class ActionDispatch::Routing::Mapper
def draw(routes_name)
instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
end
end
Rails.application.routes.draw do Rails.application.routes.draw do
if Gitlab::Sherlock.enabled? if Gitlab::Sherlock.enabled?
namespace :sherlock do namespace :sherlock do
...@@ -94,14 +100,10 @@ Rails.application.routes.draw do ...@@ -94,14 +100,10 @@ Rails.application.routes.draw do
get 'help/ui' => 'help#ui' get 'help/ui' => 'help#ui'
get 'help/*path' => 'help#show', as: :help_page get 'help/*path' => 'help#show', as: :help_page
#
# Koding route # Koding route
#
get 'koding' => 'koding#index' get 'koding' => 'koding#index'
#
# Global snippets # Global snippets
#
resources :snippets, concerns: :awardable do resources :snippets, concerns: :awardable do
member do member do
get 'raw' get 'raw'
...@@ -111,9 +113,7 @@ Rails.application.routes.draw do ...@@ -111,9 +113,7 @@ Rails.application.routes.draw do
get '/s/:username', to: redirect('/u/%{username}/snippets'), get '/s/:username', to: redirect('/u/%{username}/snippets'),
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
#
# Invites # Invites
#
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do member do
post :accept post :accept
...@@ -127,810 +127,26 @@ Rails.application.routes.draw do ...@@ -127,810 +127,26 @@ Rails.application.routes.draw do
end end
end end
#
# Spam reports # Spam reports
#
resources :abuse_reports, only: [:new, :create] resources :abuse_reports, only: [:new, :create]
#
# Notification settings # Notification settings
#
resources :notification_settings, only: [:create, :update] resources :notification_settings, only: [:create, :update]
# draw :import
# Import draw :uploads
# draw :explore
namespace :import do draw :admin
resource :github, only: [:create, :new], controller: :github do draw :profile
post :personal_access_token draw :dashboard
get :status draw :group
get :callback draw :user
get :jobs draw :project
end
resource :gitlab, only: [:create], controller: :gitlab do
get :status
get :callback
get :jobs
end
resource :bitbucket, only: [:create], controller: :bitbucket do
get :status
get :callback
get :jobs
end
resource :google_code, only: [:create, :new], controller: :google_code do
get :status
post :callback
get :jobs
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
end
resource :fogbugz, only: [:create, :new], controller: :fogbugz do
get :status
post :callback
get :jobs
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
end
resource :gitlab_project, only: [:create, :new] do
post :create
end
end
#
# Uploads
#
scope path: :uploads do
# Note attachments and User/Group/Project avatars
get ":model/:mounted_as/:id/:filename",
to: "uploads#show",
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
# Appearance
get ":model/:mounted_as/:id/:filename",
to: "uploads#show",
constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
# Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename",
to: "projects/uploads#show",
constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
end
# Redirect old note attachments path to new uploads path.
get "files/note/:id/:filename",
to: redirect("uploads/note/attachment/%{id}/%{filename}"),
constraints: { filename: /[^\/]+/ }
#
# Explore area
#
namespace :explore do
resources :projects, only: [:index] do
collection do
get :trending
get :starred
end
end
resources :groups, only: [:index]
resources :snippets, only: [:index]
root to: 'projects#trending'
end
# Compatibility with old routing
get 'public' => 'explore/projects#index'
get 'public/projects' => 'explore/projects#index'
#
# Admin Area
#
namespace :admin do
resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
resources :keys, only: [:show, :destroy]
resources :identities, except: [:show]
member do
get :projects
get :keys
get :groups
put :block
put :unblock
put :unlock
put :confirm
post :impersonate
patch :disable_two_factor
delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
end
end
resource :impersonation, only: :destroy
resources :abuse_reports, only: [:index, :destroy]
resources :spam_logs, only: [:index, :destroy] do
member do
post :mark_as_ham
end
end
resources :applications
resources :groups, constraints: { id: /[^\/]+/ } do
member do
put :members_update
end
end
resources :deploy_keys, only: [:index, :new, :create, :destroy]
resources :hooks, only: [:index, :create, :destroy] do
get :test
end
resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
post :preview, on: :collection
end
resource :logs, only: [:show]
resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
root to: 'projects#index', as: :projects
resources(:projects,
path: '/',
constraints: { id: /[a-zA-Z.0-9_\-]+/ },
only: [:index, :show]) do
root to: 'projects#show'
member do
put :transfer
post :repository_check
end
resources :runner_projects, only: [:create, :destroy]
end
end
resource :appearances, only: [:show, :create, :update], path: 'appearance' do
member do
get :preview
delete :logo
delete :header_logos
end
end
resource :application_settings, only: [:show, :update] do
resources :services, only: [:index, :edit, :update]
put :reset_runners_token
put :reset_health_check_token
put :clear_repository_check_states
end
resources :labels
resources :runners, only: [:index, :show, :update, :destroy] do
member do
get :resume
get :pause
end
end
resources :builds, only: :index do
collection do
post :cancel_all
end
end
root to: 'dashboard#index'
end
#
# Profile Area
#
resource :profile, only: [:show, :update] do
member do
get :audit_log
get :applications, to: 'oauth/applications#index'
put :reset_private_token
put :update_username
end
scope module: :profiles do
resource :account, only: [:show] do
member do
delete :unlink
end
end
resource :notifications, only: [:show, :update]
resource :password, only: [:new, :create, :edit, :update] do
member do
put :reset
end
end
resource :preferences, only: [:show, :update]
resources :keys, only: [:index, :show, :new, :create, :destroy]
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
resources :personal_access_tokens, only: [:index, :create] do
member do
put :revoke
end
end
resource :two_factor_auth, only: [:show, :create, :destroy] do
member do
post :create_u2f
post :codes
patch :skip
end
end
resources :u2f_registrations, only: [:destroy]
end
end
scope(path: 'u/:username',
as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
controller: :users) do
get :calendar
get :calendar_activities
get :groups
get :projects
get :contributed, as: :contributed_projects
get :snippets
get '/', action: :show
end
#
# Dashboard Area
#
resource :dashboard, controller: 'dashboard', only: [] do
get :issues
get :merge_requests
get :activity
scope module: :dashboard do
resources :milestones, only: [:index, :show]
resources :labels, only: [:index]
resources :groups, only: [:index]
resources :snippets, only: [:index]
resources :todos, only: [:index, :destroy] do
collection do
delete :destroy_all
end
end
resources :projects, only: [:index] do
collection do
get :starred
end
end
end
root to: "dashboard/projects#index"
end
#
# Groups Area
#
resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
member do
get :issues
get :merge_requests
get :projects
get :activity
end
scope module: :groups do
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
end
resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
end
end
resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations,
passwords: :passwords,
sessions: :sessions,
confirmations: :confirmations }
devise_scope :user do
get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
get '/users/almost_there' => 'confirmations#almost_there'
end
root to: "root#index"
#
# Project Area
#
resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
[:new, :create, :index], path: "/") do
member do
put :transfer
delete :remove_fork
post :archive
post :unarchive
post :housekeeping
post :toggle_star
post :preview_markdown
post :export
post :remove_export
post :generate_new_export
get :download_export
get :autocomplete_sources
get :activity
get :refs
end
scope module: :projects do
scope constraints: { id: /.+\.git/, format: nil } do
# Git HTTP clients ('git clone' etc.)
get '/info/refs', to: 'git_http#info_refs'
post '/git-upload-pack', to: 'git_http#git_upload_pack'
post '/git-receive-pack', to: 'git_http#git_receive_pack'
# Git LFS API (metadata)
post '/info/lfs/objects/batch', to: 'lfs_api#batch'
post '/info/lfs/objects', to: 'lfs_api#deprecated'
get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
# GitLab LFS object storage
scope constraints: { oid: /[a-f0-9]{64}/ } do
get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
scope constraints: { size: /[0-9]+/ } do
put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
end
end
end
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
git_http_handshake = lambda do |request|
request.query_string.blank? ||
request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
end
ref_redirect = redirect do |params, request|
path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
path << "?#{request.query_string}" unless request.query_string.blank?
path
end
get '/info/refs', constraints: git_http_handshake, to: ref_redirect
# Blob routes:
get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
#
# Templates
#
get '/templates/:template_type/:key' => 'templates#show', as: :template
scope do
get(
'/blob/*id/diff',
to: 'blob#diff',
constraints: { id: /.+/, format: false },
as: :blob_diff
)
get(
'/blob/*id',
to: 'blob#show',
constraints: { id: /.+/, format: false },
as: :blob
)
delete(
'/blob/*id',
to: 'blob#destroy',
constraints: { id: /.+/, format: false }
)
put(
'/blob/*id',
to: 'blob#update',
constraints: { id: /.+/, format: false }
)
post(
'/blob/*id',
to: 'blob#create',
constraints: { id: /.+/, format: false }
)
end
scope do
get(
'/raw/*id',
to: 'raw#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :raw
)
end
scope do
get(
'/tree/*id',
to: 'tree#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :tree
)
end
scope do
get(
'/find_file/*id',
to: 'find_file#show',
constraints: { id: /.+/, format: /html/ },
as: :find_file
)
end
scope do
get(
'/files/*id',
to: 'find_file#list',
constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
as: :files
)
end
scope do
post(
'/create_dir/*id',
to: 'tree#create_dir',
constraints: { id: /.+/ },
as: 'create_dir'
)
end
scope do
get(
'/blame/*id',
to: 'blame#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :blame
)
end
scope do
get(
'/commits/*id',
to: 'commits#show',
constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
as: :commits
)
end
resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
get :branches
get :builds
get :pipelines
post :cancel_builds
post :retry_builds
post :revert
post :cherry_pick
get :diff_for_path
end
end
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
end
end
get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
# Don't use format parameter as file extension (old 3.0.x behavior)
# See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
scope format: false do
resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
member do
get :commits
get :ci
get :languages
end
end
end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
end
end
WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
scope do
# Order matters to give priority to these matches
get '/wikis/git_access', to: 'wikis#git_access'
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
post '/wikis', to: 'wikis#create'
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
end
resource :repository, only: [:create] do
member do
get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
end
end
resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
member do
get :test
end
end
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
member do
put :enable
put :disable
end
end
resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
resources :refs, only: [] do
collection do
get 'switch'
end
member do
# tree viewer logs
get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
# Directories with leading dots erroneously get rejected if git
# ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
id: /.*/,
path: /.*/
}
end
end
resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :commits
get :diffs
get :conflicts
get :builds
get :pipelines
get :merge_check
post :merge
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
post :remove_wip
get :diff_for_path
post :resolve_conflicts
end
collection do
get :branch_from
get :branch_to
get :update_branches
get :diff_for_path
post :bulk_update
end
resources :discussions, only: [], constraints: { id: /\h{40}/ } do
member do
post :resolve
delete :resolve, action: :unresolve
end
end
end
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
resource :release, only: [:edit, :update]
end
resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :destroy]
resources :pipelines, only: [:index, :new, :create, :show] do
collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update]
end
member do
post :cancel
post :retry
end
end
resources :environments
resource :cycle_analytics, only: [:show]
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
resources :artifacts, only: [] do
collection do
get :latest_succeeded,
path: '*ref_name_and_path',
format: false
end
end
end
member do
get :status
post :cancel
post :retry
post :play
post :erase
get :trace
get :raw
end
resource :artifacts, only: [] do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
post :keep
end
end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
member do
get :test
end
end
resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
resources :milestones, constraints: { id: /\d+/ } do
member do
put :sort_issues
put :sort_merge_requests
end
end
resources :labels, except: [:show], constraints: { id: /\d+/ } do
collection do
post :generate
post :set_priorities
end
member do
post :toggle_subscription
delete :remove_priority
end
end
resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
post :mark_as_spam
get :referenced_merge_requests
get :related_branches
get :can_create_branch
end
collection do
post :bulk_update
end
end
resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
collection do
delete :leave
# Used for import team
# from another project
get :import
post :apply_import
end
member do
post :resend_invite
end
end
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
delete :delete_attachment
post :resolve
delete :resolve, action: :unresolve
end
end
resource :board, only: [:show] do
scope module: :boards do
resources :issues, only: [:update]
resources :lists, only: [:index, :create, :update, :destroy] do
collection do
post :generate
end
resources :issues, only: [:index]
end
end
end
resources :todos, only: [:create]
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
end
end
resources :runners, only: [:index, :edit, :update, :destroy, :show] do
member do
get :resume
get :pause
end
collection do
post :toggle_shared_runners
end
end
resources :runner_projects, only: [:create, :destroy]
resources :badges, only: [:index] do
collection do
scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
constraints format: /svg/ do
get :build
get :coverage
end
end
end
end
end
end
end
# Get all keys of user # Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ } get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
root to: "root#index"
end end
namespace :admin do
resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
resources :keys, only: [:show, :destroy]
resources :identities, except: [:show]
member do
get :projects
get :keys
get :groups
put :block
put :unblock
put :unlock
put :confirm
post :impersonate
patch :disable_two_factor
delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
end
end
resource :impersonation, only: :destroy
resources :abuse_reports, only: [:index, :destroy]
resources :spam_logs, only: [:index, :destroy] do
member do
post :mark_as_ham
end
end
resources :applications
resources :groups, constraints: { id: /[^\/]+/ } do
member do
put :members_update
end
end
resources :deploy_keys, only: [:index, :new, :create, :destroy]
resources :hooks, only: [:index, :create, :destroy] do
get :test
end
resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
post :preview, on: :collection
end
resource :logs, only: [:show]
resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
root to: 'projects#index', as: :projects
resources(:projects,
path: '/',
constraints: { id: /[a-zA-Z.0-9_\-]+/ },
only: [:index, :show]) do
root to: 'projects#show'
member do
put :transfer
post :repository_check
end
resources :runner_projects, only: [:create, :destroy]
end
end
resource :appearances, only: [:show, :create, :update], path: 'appearance' do
member do
get :preview
delete :logo
delete :header_logos
end
end
resource :application_settings, only: [:show, :update] do
resources :services, only: [:index, :edit, :update]
put :reset_runners_token
put :reset_health_check_token
put :clear_repository_check_states
end
resources :labels
resources :runners, only: [:index, :show, :update, :destroy] do
member do
get :resume
get :pause
end
end
resources :builds, only: :index do
collection do
post :cancel_all
end
end
root to: 'dashboard#index'
end
resource :dashboard, controller: 'dashboard', only: [] do
get :issues
get :merge_requests
get :activity
scope module: :dashboard do
resources :milestones, only: [:index, :show]
resources :labels, only: [:index]
resources :groups, only: [:index]
resources :snippets, only: [:index]
resources :todos, only: [:index, :destroy] do
collection do
delete :destroy_all
end
end
resources :projects, only: [:index] do
collection do
get :starred
end
end
end
root to: "dashboard/projects#index"
end
namespace :explore do
resources :projects, only: [:index] do
collection do
get :trending
get :starred
end
end
resources :groups, only: [:index]
resources :snippets, only: [:index]
root to: 'projects#trending'
end
# Compatibility with old routing
get 'public' => 'explore/projects#index'
get 'public/projects' => 'explore/projects#index'
resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
member do
get :issues
get :merge_requests
get :projects
get :activity
end
scope module: :groups do
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
end
resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
end
end
namespace :import do
resource :github, only: [:create, :new], controller: :github do
post :personal_access_token
get :status
get :callback
get :jobs
end
resource :gitlab, only: [:create], controller: :gitlab do
get :status
get :callback
get :jobs
end
resource :bitbucket, only: [:create], controller: :bitbucket do
get :status
get :callback
get :jobs
end
resource :google_code, only: [:create, :new], controller: :google_code do
get :status
post :callback
get :jobs
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
end
resource :fogbugz, only: [:create, :new], controller: :fogbugz do
get :status
post :callback
get :jobs
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
end
resource :gitlab_project, only: [:create, :new] do
post :create
end
end
resource :profile, only: [:show, :update] do
member do
get :audit_log
get :applications, to: 'oauth/applications#index'
put :reset_private_token
put :update_username
end
scope module: :profiles do
resource :account, only: [:show] do
member do
delete :unlink
end
end
resource :notifications, only: [:show, :update]
resource :password, only: [:new, :create, :edit, :update] do
member do
put :reset
end
end
resource :preferences, only: [:show, :update]
resources :keys, only: [:index, :show, :new, :create, :destroy]
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
resources :personal_access_tokens, only: [:index, :create] do
member do
put :revoke
end
end
resource :two_factor_auth, only: [:show, :create, :destroy] do
member do
post :create_u2f
post :codes
patch :skip
end
end
resources :u2f_registrations, only: [:destroy]
end
end
resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
[:new, :create, :index], path: "/") do
member do
put :transfer
delete :remove_fork
post :archive
post :unarchive
post :housekeeping
post :toggle_star
post :preview_markdown
post :export
post :remove_export
post :generate_new_export
get :download_export
get :autocomplete_sources
get :activity
get :refs
end
scope module: :projects do
scope constraints: { id: /.+\.git/, format: nil } do
# Git HTTP clients ('git clone' etc.)
get '/info/refs', to: 'git_http#info_refs'
post '/git-upload-pack', to: 'git_http#git_upload_pack'
post '/git-receive-pack', to: 'git_http#git_receive_pack'
# Git LFS API (metadata)
post '/info/lfs/objects/batch', to: 'lfs_api#batch'
post '/info/lfs/objects', to: 'lfs_api#deprecated'
get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
# GitLab LFS object storage
scope constraints: { oid: /[a-f0-9]{64}/ } do
get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
scope constraints: { size: /[0-9]+/ } do
put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
end
end
end
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
git_http_handshake = lambda do |request|
request.query_string.blank? ||
request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
end
ref_redirect = redirect do |params, request|
path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
path << "?#{request.query_string}" unless request.query_string.blank?
path
end
get '/info/refs', constraints: git_http_handshake, to: ref_redirect
# Blob routes:
get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
#
# Templates
#
get '/templates/:template_type/:key' => 'templates#show', as: :template
scope do
get(
'/blob/*id/diff',
to: 'blob#diff',
constraints: { id: /.+/, format: false },
as: :blob_diff
)
get(
'/blob/*id',
to: 'blob#show',
constraints: { id: /.+/, format: false },
as: :blob
)
delete(
'/blob/*id',
to: 'blob#destroy',
constraints: { id: /.+/, format: false }
)
put(
'/blob/*id',
to: 'blob#update',
constraints: { id: /.+/, format: false }
)
post(
'/blob/*id',
to: 'blob#create',
constraints: { id: /.+/, format: false }
)
end
scope do
get(
'/raw/*id',
to: 'raw#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :raw
)
end
scope do
get(
'/tree/*id',
to: 'tree#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :tree
)
end
scope do
get(
'/find_file/*id',
to: 'find_file#show',
constraints: { id: /.+/, format: /html/ },
as: :find_file
)
end
scope do
get(
'/files/*id',
to: 'find_file#list',
constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
as: :files
)
end
scope do
post(
'/create_dir/*id',
to: 'tree#create_dir',
constraints: { id: /.+/ },
as: 'create_dir'
)
end
scope do
get(
'/blame/*id',
to: 'blame#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :blame
)
end
scope do
get(
'/commits/*id',
to: 'commits#show',
constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
as: :commits
)
end
resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
get :branches
get :builds
get :pipelines
post :cancel_builds
post :retry_builds
post :revert
post :cherry_pick
get :diff_for_path
end
end
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
end
end
get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
# Don't use format parameter as file extension (old 3.0.x behavior)
# See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
scope format: false do
resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
member do
get :commits
get :ci
get :languages
end
end
end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
end
end
WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
scope do
# Order matters to give priority to these matches
get '/wikis/git_access', to: 'wikis#git_access'
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
post '/wikis', to: 'wikis#create'
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
end
resource :repository, only: [:create] do
member do
get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
end
end
resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
member do
get :test
end
end
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
member do
put :enable
put :disable
end
end
resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
resources :refs, only: [] do
collection do
get 'switch'
end
member do
# tree viewer logs
get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
# Directories with leading dots erroneously get rejected if git
# ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
id: /.*/,
path: /.*/
}
end
end
resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :commits
get :diffs
get :conflicts
get :builds
get :pipelines
get :merge_check
post :merge
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
post :remove_wip
get :diff_for_path
post :resolve_conflicts
end
collection do
get :branch_from
get :branch_to
get :update_branches
get :diff_for_path
post :bulk_update
end
resources :discussions, only: [], constraints: { id: /\h{40}/ } do
member do
post :resolve
delete :resolve, action: :unresolve
end
end
end
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
resource :release, only: [:edit, :update]
end
resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :destroy]
resources :pipelines, only: [:index, :new, :create, :show] do
collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update]
end
member do
post :cancel
post :retry
end
end
resources :environments
resource :cycle_analytics, only: [:show]
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
resources :artifacts, only: [] do
collection do
get :latest_succeeded,
path: '*ref_name_and_path',
format: false
end
end
end
member do
get :status
post :cancel
post :retry
post :play
post :erase
get :trace
get :raw
end
resource :artifacts, only: [] do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
post :keep
end
end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
member do
get :test
end
end
resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
resources :milestones, constraints: { id: /\d+/ } do
member do
put :sort_issues
put :sort_merge_requests
end
end
resources :labels, except: [:show], constraints: { id: /\d+/ } do
collection do
post :generate
post :set_priorities
end
member do
post :toggle_subscription
delete :remove_priority
end
end
resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
post :mark_as_spam
get :referenced_merge_requests
get :related_branches
get :can_create_branch
end
collection do
post :bulk_update
end
end
resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
collection do
delete :leave
# Used for import team
# from another project
get :import
post :apply_import
end
member do
post :resend_invite
end
end
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
delete :delete_attachment
post :resolve
delete :resolve, action: :unresolve
end
end
resource :board, only: [:show] do
scope module: :boards do
resources :issues, only: [:update]
resources :lists, only: [:index, :create, :update, :destroy] do
collection do
post :generate
end
resources :issues, only: [:index]
end
end
end
resources :todos, only: [:create]
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
end
end
resources :runners, only: [:index, :edit, :update, :destroy, :show] do
member do
get :resume
get :pause
end
collection do
post :toggle_shared_runners
end
end
resources :runner_projects, only: [:create, :destroy]
resources :badges, only: [:index] do
collection do
scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
constraints format: /svg/ do
get :build
get :coverage
end
end
end
end
end
end
end
scope path: :uploads do
# Note attachments and User/Group/Project avatars
get ":model/:mounted_as/:id/:filename",
to: "uploads#show",
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
# Appearance
get ":model/:mounted_as/:id/:filename",
to: "uploads#show",
constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
# Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename",
to: "projects/uploads#show",
constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
end
# Redirect old note attachments path to new uploads path.
get "files/note/:id/:filename",
to: redirect("uploads/note/attachment/%{id}/%{filename}"),
constraints: { filename: /[^\/]+/ }
scope(path: 'u/:username',
as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
controller: :users) do
get :calendar
get :calendar_activities
get :groups
get :projects
get :contributed, as: :contributed_projects
get :snippets
get '/', action: :show
end
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations,
passwords: :passwords,
sessions: :sessions,
confirmations: :confirmations }
devise_scope :user do
get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
get '/users/almost_there' => 'confirmations#almost_there'
end
...@@ -106,6 +106,9 @@ module Banzai ...@@ -106,6 +106,9 @@ module Banzai
project = context[:project] project = context[:project]
author = context[:author] author = context[:author]
if author && !project.team.member?(author)
link_text
else
url = urls.namespace_project_url(project.namespace, project, url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path]) only_path: context[:only_path])
...@@ -114,6 +117,7 @@ module Banzai ...@@ -114,6 +117,7 @@ module Banzai
link_tag(url, data, text, 'All Project and Group Members') link_tag(url, data, text, 'All Project and Group Members')
end end
end
def link_to_namespace(namespace, link_text: nil) def link_to_namespace(namespace, link_text: nil)
if namespace.is_a?(Group) if namespace.is_a?(Group)
......
require 'spec_helper' require 'spec_helper'
describe TrendingProjectsFinder do describe TrendingProjectsFinder do
let(:user) { build(:user) } let(:user) { create(:user) }
let(:public_project1) { create(:empty_project, :public) }
let(:public_project2) { create(:empty_project, :public) }
let(:private_project) { create(:empty_project, :private) }
let(:internal_project) { create(:empty_project, :internal) }
before do
3.times do
create(:note_on_commit, project: public_project1)
end
describe '#execute' do 2.times do
describe 'without an explicit start date' do create(:note_on_commit, project: public_project2, created_at: 5.weeks.ago)
subject { described_class.new } end
it 'returns the trending projects' do create(:note_on_commit, project: private_project)
relation = double(:ar_relation) create(:note_on_commit, project: internal_project)
end
allow(subject).to receive(:projects_for) describe '#execute', caching: true do
.with(user) context 'without an explicit time range' do
.and_return(relation) it 'returns public trending projects' do
projects = described_class.new.execute
allow(relation).to receive(:trending) expect(projects).to eq([public_project1])
.with(an_instance_of(ActiveSupport::TimeWithZone))
end end
end end
describe 'with an explicit start date' do context 'with an explicit time range' do
let(:date) { 2.months.ago } it 'returns public trending projects' do
projects = described_class.new.execute(2)
subject { described_class.new } expect(projects).to eq([public_project1, public_project2])
end
end
it 'returns the trending projects' do it 'caches the list of projects' do
relation = double(:ar_relation) projects = described_class.new
allow(subject).to receive(:projects_for) expect(Project).to receive(:trending).once
.with(user)
.and_return(relation)
allow(relation).to receive(:trending) 2.times { projects.execute }
.with(date)
end
end end
end end
end end
...@@ -112,7 +112,7 @@ ...@@ -112,7 +112,7 @@
fixture.preload('search_autocomplete.html'); fixture.preload('search_autocomplete.html');
beforeEach(function() { beforeEach(function() {
fixture.load('search_autocomplete.html'); fixture.load('search_autocomplete.html');
return widget = new SearchAutocomplete; return widget = new gl.SearchAutocomplete;
}); });
it('should show Dashboard specific dropdown menu', function() { it('should show Dashboard specific dropdown menu', function() {
var list; var list;
......
...@@ -31,13 +31,16 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do ...@@ -31,13 +31,16 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end end
it 'supports a special @all mention' do it 'supports a special @all mention' do
project.team << [user, :developer]
doc = reference_filter("Hey #{reference}", author: user) doc = reference_filter("Hey #{reference}", author: user)
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href')) expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project) .to eq urls.namespace_project_url(project.namespace, project)
end end
it 'includes a data-author attribute when there is an author' do it 'includes a data-author attribute when there is an author' do
project.team << [user, :developer]
doc = reference_filter(reference, author: user) doc = reference_filter(reference, author: user)
expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s) expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
...@@ -48,6 +51,12 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do ...@@ -48,6 +51,12 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(doc.css('a').first.has_attribute?('data-author')).to eq(false) expect(doc.css('a').first.has_attribute?('data-author')).to eq(false)
end end
it 'ignores reference to all when the user is not a project member' do
doc = reference_filter("Hey #{reference}", author: user)
expect(doc.css('a').length).to eq 0
end
end end
context 'mentioning a user' do context 'mentioning a user' do
......
...@@ -824,6 +824,14 @@ describe Project, models: true do ...@@ -824,6 +824,14 @@ describe Project, models: true do
expect(subject).to eq([project2, project1]) expect(subject).to eq([project2, project1])
end end
end end
it 'does not take system notes into account' do
10.times do
create(:note_on_commit, project: project2, system: true)
end
expect(described_class.trending.to_a).to eq([project1, project2])
end
end end
describe '.visible_to_user' do describe '.visible_to_user' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment