Commit ac8e94d4 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into multi-file-editor-vuex

parents 662f87ca 6e6cea23
# Documentation
- source: /doc/(.+?)\.md/ # doc/administration/build_artifacts.md
public: '\1.html' # doc/administration/build_artifacts.html
......@@ -121,7 +121,8 @@ linters:
# Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
enabled: true
max_depth: 6
# Always use placeholder selectors in @extend.
PlaceholderInExtend:
......
0.49.0
\ No newline at end of file
0.50.0
......@@ -281,7 +281,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta17'
gem 'prometheus-client-mmap', '~>0.7.0.beta18'
gem 'raindrops', '~> 0.18'
end
......
......@@ -623,7 +623,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta17)
prometheus-client-mmap (0.7.0.beta18)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
......@@ -1106,7 +1106,7 @@ DEPENDENCIES
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta17)
prometheus-client-mmap (~> 0.7.0.beta18)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global IssuableContext */
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
......@@ -11,6 +10,7 @@ import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
const Store = gl.issueBoards.BoardsStore;
......
......@@ -11,8 +11,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true;
this.cantEdit = cantEdit.filter(i => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
this.cantEdit = cantEdit;
}
updateObject(path) {
......@@ -43,9 +42,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input'));
}
canEdit(tokenName, tokenValue) {
if (this.cantEdit.includes(tokenName)) return false;
return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
token.value === tokenValue) === -1;
canEdit(tokenName) {
return this.cantEdit.indexOf(tokenName) === -1;
}
}
......@@ -14,18 +14,16 @@ gl.issueBoards.BoardsStore = {
},
state: {},
detail: {
issue: {},
issue: {}
},
moving: {
issue: {},
list: {},
list: {}
},
create () {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
this.detail = {
issue: {},
};
this.detail = { issue: {} };
},
addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global ProjectSelect */
/* global IssuableIndex */
import IssuableIndex from './issuable_index';
/* global Milestone */
/* global IssuableForm */
import IssuableForm from './issuable_form';
/* global LabelsSelect */
/* global MilestoneSelect */
/* global NewBranchForm */
......@@ -173,7 +173,7 @@ import Diff from './diff';
filteredSearchManager.setup();
}
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix);
new IssuableIndex(pagePrefix);
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
......@@ -231,12 +231,16 @@ import Diff from './diff';
case 'projects:milestones:new':
case 'projects:milestones:edit':
case 'projects:milestones:update':
new ZenMode();
new DueDateSelectors();
new GLForm($('.milestone-form'), true);
break;
case 'groups:milestones:new':
case 'groups:milestones:edit':
case 'groups:milestones:update':
new ZenMode();
new DueDateSelectors();
new GLForm($('.milestone-form'), true);
new GLForm($('.milestone-form'), false);
break;
case 'projects:compare:show':
new Diff();
......
......@@ -147,16 +147,6 @@ class DropdownUtils {
return dataValue !== null;
}
static getVisualTokenValues(visualToken) {
const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim();
let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim();
if (tokenName === 'label' && tokenValue) {
// remove leading symbol and wrapping quotes
tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
}
return { tokenName, tokenValue };
}
// Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) {
const container = FilteredSearchContainer.container;
......
......@@ -185,8 +185,8 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
......@@ -336,8 +336,8 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t);
canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
const tokenKey = t.querySelector('.name').textContent.trim();
canClearToken = this.canEdit && this.canEdit(tokenKey);
}
if (canClearToken) {
......@@ -469,7 +469,7 @@ class FilteredSearchManager {
}
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
const canEdit = this.canEdit && this.canEdit(sanitizedKey);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
......
......@@ -38,14 +38,21 @@ class FilteredSearchVisualTokens {
}
static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return `
<div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
<div class="selectable" role="button">
<div class="name"></div>
<div class="value-container">
<div class="value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
${removeTokenMarkup}
</div>
</div>
`;
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */
class ImporterStatus {
constructor(jobsUrl, importUrl) {
this.jobsUrl = jobsUrl;
this.importUrl = importUrl;
this.initStatusPage();
this.setAutoUpdate();
}
(function() {
window.ImporterStatus = (function() {
function ImporterStatus(jobs_url, import_url) {
this.jobs_url = jobs_url;
this.import_url = import_url;
this.initStatusPage();
this.setAutoUpdate();
}
initStatusPage() {
$('.js-add-to-import')
.off('click')
.on('click', (event) => {
const $btn = $(event.currentTarget);
const $tr = $btn.closest('tr');
const $targetField = $tr.find('.import-target');
const $namespaceInput = $targetField.find('.js-select-namespace option:selected');
const id = $tr.attr('id').replace('repo_', '');
let targetNamespace;
let newName;
if ($namespaceInput.length > 0) {
targetNamespace = $namespaceInput[0].innerHTML;
newName = $targetField.find('#path').prop('value');
$targetField.empty().append(`${targetNamespace}/${newName}`);
}
$btn.disable().addClass('is-loading');
ImporterStatus.prototype.initStatusPage = function() {
$('.js-add-to-import').off('click').on('click', (function(_this) {
return function(e) {
var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName;
$btn = $(e.currentTarget);
$tr = $btn.closest('tr');
$target_field = $tr.find('.import-target');
$namespace_input = $target_field.find('.js-select-namespace option:selected');
id = $tr.attr('id').replace('repo_', '');
target_namespace = null;
newName = null;
if ($namespace_input.length > 0) {
target_namespace = $namespace_input[0].innerHTML;
newName = $target_field.find('#path').prop('value');
$target_field.empty().append(target_namespace + "/" + newName);
}
$btn.disable().addClass('is-loading');
return $.post(_this.import_url, {
repo_id: id,
target_namespace: target_namespace,
new_name: newName
}, {
dataType: 'script'
});
};
})(this));
return $('.js-import-all').off('click').on('click', function(e) {
var $btn;
$btn = $(this);
return $.post(this.importUrl, {
repo_id: id,
target_namespace: targetNamespace,
new_name: newName,
}, {
dataType: 'script',
});
});
$('.js-import-all')
.off('click')
.on('click', function onClickImportAll() {
const $btn = $(this);
$btn.disable().addClass('is-loading');
return $('.js-add-to-import').each(function() {
return $('.js-add-to-import').each(function triggerAddImport() {
return $(this).trigger('click');
});
});
};
}
setAutoUpdate() {
return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => {
const jobItem = $(`#project_${job.id}`);
const statusField = jobItem.find('.job-status');
ImporterStatus.prototype.setAutoUpdate = function() {
return setInterval(((function(_this) {
return function() {
return $.get(_this.jobs_url, function(data) {
return $.each(data, function(i, job) {
var job_item, status_field;
job_item = $("#project_" + job.id);
status_field = job_item.find(".job-status");
if (job.import_status === 'finished') {
job_item.removeClass("active").addClass("success");
return status_field.html('<span><i class="fa fa-check"></i> done</span>');
} else if (job.import_status === 'scheduled') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled");
} else if (job.import_status === 'started') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
} else {
return status_field.html(job.import_status);
}
});
});
};
})(this)), 4000);
};
const spinner = '<i class="fa fa-spinner fa-spin"></i>';
return ImporterStatus;
})();
switch (job.import_status) {
case 'finished':
jobItem.removeClass('active').addClass('success');
statusField.html('<span><i class="fa fa-check"></i> done</span>');
break;
case 'scheduled':
statusField.html(`${spinner} scheduled`);
break;
case 'started':
statusField.html(`${spinner} started`);
break;
default:
statusField.html(job.import_status);
break;
}
})), 4000);
}
}
$(function() {
if ($('.js-importer-status').length) {
var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
var importPath = $('.js-importer-status').data('import-path');
// eslint-disable-next-line consistent-return
export default function initImporterStatus() {
const importerStatus = document.querySelector('.js-importer-status');
new window.ImporterStatus(jobsImportPath, importPath);
}
});
}).call(window);
if (importerStatus) {
const data = importerStatus.dataset;
return new ImporterStatus(data.jobsImportPath, data.importPath);
}
}
/* eslint-disable no-new */
/* global MilestoneSelect */
/* global LabelsSelect */
/* global IssuableContext */
import IssuableContext from './issuable_context';
/* global Sidebar */
import DueDateSelectors from './due_date_select';
......
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global IssuableIndex */
import _ from 'underscore';
import Flash from './flash';
......
......@@ -5,6 +5,10 @@
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
import './issue_status_select';
import './subscription_select';
import './labels_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
import Cookies from 'js-cookie';
import bp from './breakpoints';
import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7;
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
this.initParticipants();
new UsersSelect(currentUser);
$('select.select2').select2({
width: 'resolve',
dropdownAutoWidth: true
});
$(".issuable-sidebar .inline-update").on("change", "select", function() {
return $(this).submit();
});
$(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() {
return $(this).submit();
});
$(document).off('click', '.issuable-sidebar .dropdown-content a').on('click', '.issuable-sidebar .dropdown-content a', function(e) {
return e.preventDefault();
});
$(document).off('click', '.edit-link').on('click', '.edit-link', function(e) {
var $block, $selectbox;
export default class IssuableContext {
constructor(currentUser) {
this.initParticipants();
this.userSelect = new UsersSelect(currentUser);
$('select.select2').select2({
width: 'resolve',
dropdownAutoWidth: true,
});
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
});
$('.issuable-sidebar .inline-update').on('change', '.js-assignee', function onClickAssignee() {
return $(this).submit();
});
$(document)
.off('click', '.issuable-sidebar .dropdown-content a')
.on('click', '.issuable-sidebar .dropdown-content a', e => e.preventDefault());
$(document)
.off('click', '.edit-link')
.on('click', '.edit-link', function onClickEdit(e) {
e.preventDefault();
$block = $(this).parents('.block');
$selectbox = $block.find('.selectbox');
const $block = $(this).parents('.block');
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.value').show();
......@@ -35,46 +37,43 @@ const PARTICIPANTS_ROW_COUNT = 7;
$selectbox.show();
$block.find('.value').hide();
}
if ($selectbox.is(':visible')) {
return setTimeout(function() {
return $block.find('.dropdown-menu-toggle').trigger('click');
}, 0);
setTimeout(() => $block.find('.dropdown-menu-toggle').trigger('click'), 0);
}
});
window.addEventListener('beforeunload', function() {
// collapsed_gutter cookie hides the sidebar
var bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
});
}
IssuableContext.prototype.initParticipants = function() {
$(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
return $('.js-participants-author').each(function(i) {
if (i >= PARTICIPANTS_ROW_COUNT) {
return $(this).addClass('js-participants-hidden').hide();
}
});
};
window.addEventListener('beforeunload', () => {
// collapsed_gutter cookie hides the sidebar
const bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
});
}
IssuableContext.prototype.toggleHiddenParticipants = function() {
const currentText = $(this).text().trim();
const lessText = $(this).data('less-text');
const originalText = $(this).data('original-text');
initParticipants() {
$(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
return $('.js-participants-author').each(function forEachAuthor(i) {
if (i >= PARTICIPANTS_ROW_COUNT) {
$(this).addClass('js-participants-hidden').hide();
}
});
}
if (currentText === originalText) {
$(this).text(lessText);
toggleHiddenParticipants() {
const currentText = $(this).text().trim();
const lessText = $(this).data('less-text');
const originalText = $(this).data('original-text');
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} else {
$(this).text(originalText);
}
if (currentText === originalText) {
$(this).text(lessText);
$('.js-participants-hidden').toggle();
};
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} else {
$(this).text(originalText);
}
return IssuableContext;
})();
}).call(window);
$('.js-participants-hidden').toggle();
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* eslint-disable func-names, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
import Pikaday from 'pikaday';
......@@ -8,103 +8,100 @@ import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
(function() {
this.IssuableForm = (function() {
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) {
var $issuableDueDate, calendar;
this.form = form;
this.toggleWip = this.toggleWip.bind(this);
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
this.descriptionField = this.form.find("textarea[name*='[description]']");
if (!(this.titleField.length && this.descriptionField.length)) {
return;
}
this.initAutosave();
this.form.on("submit", this.handleSubmit);
this.form.on("click", ".btn-cancel", this.resetAutosave);
this.initWip();
$issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: function(dateText) {
$issuableDueDate.val(calendar.toString(dateText));
}
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
export default class IssuableForm {
constructor(form) {
this.form = form;
this.toggleWip = this.toggleWip.bind(this);
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]');
if (!(this.titleField.length && this.descriptionField.length)) {
return;
}
IssuableForm.prototype.initAutosave = function() {
new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]);
return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]);
};
IssuableForm.prototype.handleSubmit = function() {
return this.resetAutosave();
};
IssuableForm.prototype.resetAutosave = function() {
this.titleField.data("autosave").reset();
return this.descriptionField.data("autosave").reset();
};
IssuableForm.prototype.initWip = function() {
this.$wipExplanation = this.form.find(".js-wip-explanation");
this.$noWipExplanation = this.form.find(".js-no-wip-explanation");
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
return;
}
this.form.on("click", ".js-toggle-wip", this.toggleWip);
this.titleField.on("keyup blur", this.renderWipExplanation);
return this.renderWipExplanation();
};
IssuableForm.prototype.workInProgress = function() {
return this.wipRegex.test(this.titleField.val());
};
IssuableForm.prototype.renderWipExplanation = function() {
if (this.workInProgress()) {
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
} else {
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
}
};
IssuableForm.prototype.toggleWip = function(event) {
event.preventDefault();
if (this.workInProgress()) {
this.removeWip();
} else {
this.addWip();
}
return this.renderWipExplanation();
};
IssuableForm.prototype.removeWip = function() {
return this.titleField.val(this.titleField.val().replace(this.wipRegex, ""));
};
IssuableForm.prototype.addWip = function() {
return this.titleField.val("WIP: " + (this.titleField.val()));
};
return IssuableForm;
})();
}).call(window);
this.initAutosave();
this.form.on('submit', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip();
const $issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
const calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)),
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
}
initAutosave() {
new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']);
return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']);
}
handleSubmit() {
return this.resetAutosave();
}
resetAutosave() {
this.titleField.data('autosave').reset();
return this.descriptionField.data('autosave').reset();
}
initWip() {
this.$wipExplanation = this.form.find('.js-wip-explanation');
this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
return;
}
this.form.on('click', '.js-toggle-wip', this.toggleWip);
this.titleField.on('keyup blur', this.renderWipExplanation);
return this.renderWipExplanation();
}
workInProgress() {
return this.wipRegex.test(this.titleField.val());
}
renderWipExplanation() {
if (this.workInProgress()) {
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
} else {
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
}
}
toggleWip(event) {
event.preventDefault();
if (this.workInProgress()) {
this.removeWip();
} else {
this.addWip();
}
return this.renderWipExplanation();
}
removeWip() {
return this.titleField.val(this.titleField.val().replace(this.wipRegex, ''));
}
addWip() {
this.titleField.val(`WIP: ${(this.titleField.val())}`);
}
}
/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
/* global IssuableIndex */
import _ from 'underscore';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
((global) => {
var issuable_created;
issuable_created = false;
global.IssuableIndex = {
init: function(pagePrefix) {
IssuableIndex.initTemplates();
IssuableIndex.initSearch();
IssuableIndex.initBulkUpdate(pagePrefix);
IssuableIndex.initResetFilters();
IssuableIndex.resetIncomingEmailToken();
IssuableIndex.initLabelFilterRemove();
},
initTemplates: function() {
return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
const $searchInput = $('#issuable_search');
IssuableIndex.initSearchState($searchInput);
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false);
$searchInput.off('keyup').on('keyup', debouncedExecSearch);
// ensures existing filters are preserved when manually submitted
$('#issuable_search_form').on('submit', (e) => {
e.preventDefault();
debouncedExecSearch(e);
});
},
initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val();
IssuableIndex.searchState = {
elem: $searchInput,
current: currentSearchVal
};
IssuableIndex.maybeFocusOnSearch();
},
accessSearchPristine: function(set) {
// store reference to previous value to prevent search on non-mutating keyup
const state = IssuableIndex.searchState;
const currentSearchVal = state.elem.val();
if (set) {
state.current = currentSearchVal;
} else {
return state.current === currentSearchVal;
}
},
maybeFocusOnSearch: function() {
const currentSearchVal = IssuableIndex.searchState.current;
if (currentSearchVal && currentSearchVal !== '') {
const queryLength = currentSearchVal.length;
const $searchInput = IssuableIndex.searchState.elem;
/* The following ensures that the cursor is initially placed at
* the end of search input when focus is applied. It accounts
* for differences in browser implementations of `setSelectionRange`
* and cursor placement for elements in focus.
*/
$searchInput.focus();
if ($searchInput.setSelectionRange) {
$searchInput.setSelectionRange(queryLength, queryLength);
} else {
$searchInput.val(currentSearchVal);
}
}
},
executeSearch: function(e) {
const $search = $('#issuable_search');
const $searchName = $search.attr('name');
const $searchValue = $search.val();
const $filtersForm = $('.js-filter-form');
const $input = $(`input[name='${$searchName}']`, $filtersForm);
const isPristine = IssuableIndex.accessSearchPristine();
if (isPristine) {
return;
}
if (!$input.length) {
$filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
} else {
$input.val($searchValue);
}
IssuableIndex.filterResults($filtersForm);
},
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
var $button;
$button = $(this);
// Remove the label input box
$('input[name="label_name[]"]').filter(function() {
return this.value === $button.data('label');
}).remove();
// Submit the form to get new data
IssuableIndex.filterResults($('.filter-form'));
});
},
filterResults: (function(_this) {
return function(form) {
var formAction, formData, issuesUrl;
formData = form.serializeArray();
formData = formData.filter(function(data) {
return data.value !== '';
});
formData = $.param(formData);
formAction = form.attr('action');
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
issuesUrl += formData;
return gl.utils.visitUrl(issuesUrl);
};
})(this),
initResetFilters: function() {
$('.reset-filters').on('click', function(e) {
e.preventDefault();
const target = e.target;
const $form = $(target).parents('.js-filter-form');
const baseIssuesUrl = target.href;
$form.attr('action', baseIssuesUrl);
gl.utils.visitUrl(baseIssuesUrl);
export default class IssuableIndex {
constructor(pagePrefix) {
this.initBulkUpdate(pagePrefix);
IssuableIndex.resetIncomingEmailToken();
}
initBulkUpdate(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
const alreadyInitialized = !!this.bulkUpdateSidebar;
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
prefixId: pagePrefix,
});
},
initBulkUpdate: function(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
const alreadyInitialized = !!this.bulkUpdateSidebar;
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
prefixId: pagePrefix,
});
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
},
resetIncomingEmailToken: function() {
$('.incoming-email-token-reset').on('click', function(e) {
e.preventDefault();
$.ajax({
type: 'PUT',
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success: function(response) {
$('#issue_email').val(response.new_issue_address).focus();
},
beforeSend: function() {
$('.incoming-email-token-reset').text('resetting...');
},
complete: function() {
$('.incoming-email-token-reset').text('reset it');
}
});
});
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
};
})(window);
}
static resetIncomingEmailToken() {
$('.incoming-email-token-reset').on('click', (e) => {
e.preventDefault();
$.ajax({
type: 'PUT',
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success(response) {
$('#issue_email').val(response.new_issue_address).focus();
},
beforeSend() {
$('.incoming-email-token-reset').text('resetting...');
},
complete() {
$('.incoming-email-token-reset').text('reset it');
},
});
});
}
}
......@@ -8,7 +8,7 @@ import CreateLabelDropdown from './create_label';
(function() {
this.LabelsSelect = (function() {
function LabelsSelect(els, options = {}) {
function LabelsSelect(els) {
var _this, $els;
_this = this;
......@@ -58,7 +58,6 @@ import CreateLabelDropdown from './create_label';
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip();
......@@ -317,9 +316,9 @@ import CreateLabelDropdown from './create_label';
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(clickEvent) {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
clicked: function(options) {
const { $el, e, isMarking } = options;
const label = options.selectedObj;
var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => {
......@@ -392,10 +391,6 @@ import CreateLabelDropdown from './create_label';
.then(fadeOutLoader)
.catch(fadeOutLoader);
}
else if (handleClick) {
e.preventDefault();
handleClick(label);
}
else {
if ($dropdown.hasClass('js-multiselect')) {
......
......@@ -55,9 +55,7 @@ import './gl_field_error';
import './gl_field_errors';
import './gl_form';
import './header';
import './importer_status';
import './issuable_index';
import './issuable_context';
import initImporterStatus from './importer_status';
import './issuable_form';
import './issue';
import './issue_status_select';
......@@ -138,6 +136,7 @@ $(function () {
var fitSidebarForSize;
initBreadcrumbs();
initImporterStatus();
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
......
......@@ -5,7 +5,7 @@ import _ from 'underscore';
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els, options = {}) {
function MilestoneSelect(currentProject, els) {
var _this, $els;
if (currentProject != null) {
_this = this;
......@@ -136,26 +136,19 @@ import _ from 'underscore';
},
opened: function(e) {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
if ($dropdown.hasClass('js-issue-board-sidebar')) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(clickEvent) {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
clicked: function(options) {
const { $el, e } = options;
let selected = options.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
......
......@@ -2,13 +2,15 @@
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
(function() {
this.ProjectSelect = (function() {
(function () {
this.ProjectSelect = (function () {
function ProjectSelect() {
$('.ajax-project-select').each(function(i, select) {
var placeholder;
const simpleFilter = $(select).data('simple-filter') || false;
this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups');
this.allProjects = $(select).data('all-projects') || false;
this.orderBy = $(select).data('order-by') || 'id';
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
......@@ -21,10 +23,10 @@ import ProjectSelectComboButton from './project_select_combo_button';
$(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
query: (function(_this) {
return function(query) {
query: (function (_this) {
return function (query) {
var finalCallback, projectsCallback;
finalCallback = function(projects) {
finalCallback = function (projects) {
var data;
data = {
results: projects
......@@ -32,9 +34,9 @@ import ProjectSelectComboButton from './project_select_combo_button';
return query.callback(data);
};
if (_this.includeGroups) {
projectsCallback = function(projects) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function(groups) {
groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
......@@ -50,23 +52,25 @@ import ProjectSelectComboButton from './project_select_combo_button';
return Api.projects(query.term, {
order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled
with_merge_requests_enabled: _this.withMergeRequestsEnabled,
membership: !_this.allProjects,
}, projectsCallback);
}
};
})(this),
id: function(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
});
},
text: function(project) {
text: function (project) {
return project.name_with_namespace || project.name;
},
dropdownCssClass: "ajax-project-dropdown"
});
if (simpleFilter) return select;
return new ProjectSelectComboButton(select);
});
}
......
......@@ -6,7 +6,7 @@ import _ from 'underscore';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els, options = {}) {
function UsersSelect(currentUser, els) {
var $els;
this.users = this.users.bind(this);
this.user = this.user.bind(this);
......@@ -20,8 +20,6 @@ function UsersSelect(currentUser, els, options = {}) {
}
}
const { handleClick } = options;
$els = $(els);
if (!els) {
......@@ -444,9 +442,6 @@ function UsersSelect(currentUser, els, options = {}) {
}
if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if (handleClick) {
e.preventDefault();
handleClick(user, isMarking);
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
......
......@@ -18,12 +18,6 @@
required: false,
default: false,
},
class: {
type: String,
required: false,
default: '',
},
},
computed: {
......@@ -31,7 +25,7 @@
return this.inline ? 'span' : 'div';
},
cssClass() {
return `fa-${this.size}x ${this.class}`.trim();
return `fa-${this.size}x`;
},
},
};
......
......@@ -5,27 +5,17 @@ export default {
props: {
title: {
type: String,
required: false,
required: true,
},
text: {
type: String,
required: false,
},
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: {
type: String,
required: false,
default: 'primary',
},
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: {
type: String,
required: false,
......@@ -40,11 +30,6 @@ export default {
type: String,
required: true,
},
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
......@@ -72,57 +57,43 @@ export default {
</script>
<template>
<div class="modal-open">
<div
class="modal popup-dialog"
role="dialog"
tabindex="-1"
>
<div
:class="modalDialogClass"
class="modal-dialog"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<h4 class="modal-title pull-left">
{{this.title}}
</h4>
<button
type="button"
class="close pull-right"
@click="close"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</slot>
</div>
<div class="modal-body">
<slot name="body" :text="text">
<p>{{this.text}}</p>
</slot>
</div>
<div class="modal-footer" v-if="!hideFooter">
<button
type="button"
class="btn pull-left"
:class="btnCancelKindClass"
@click="close">
{{ closeButtonLabel }}
</button>
<button
type="button"
class="btn pull-right"
:class="btnKindClass"
@click="emitSubmit(true)">
{{ primaryButtonLabel }}
</button>
</div>
<div
class="modal popup-dialog"
role="dialog"
tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button"
class="close"
@click="close"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
<slot name="body" :text="text">
<p>{{text}}</p>
</slot>
</div>
<div class="modal-footer">
<button
type="button"
class="btn"
:class="btnCancelKindClass"
@click="close">
{{ closeButtonLabel }}
</button>
<button
type="button"
class="btn"
:class="btnKindClass"
@click="emitSubmit(true)">
{{ primaryButtonLabel }}
</button>
</div>
</div>
</div>
<div class="modal-backdrop fade in" />
</div>
</template>
......@@ -4,9 +4,6 @@
.cred { color: $common-red; }
.cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; }
.text-secondary {
color: $gl-text-color-secondary;
}
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
......
......@@ -37,7 +37,6 @@
.dropdown-menu-nav {
@include set-visible;
display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) {
width: 100%;
......
......@@ -216,12 +216,9 @@ body {
color: $theme-gray-900;
}
&.active > a {
&.active > a,
&.active > a:hover {
color: $white-light;
&:hover {
color: $white-light;
}
}
}
}
......
......@@ -239,10 +239,8 @@
fill: currentColor;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $white-light;
}
&.header-user-dropdown-toggle .header-user-avatar {
border-color: $white-light;
}
}
}
......
......@@ -42,11 +42,3 @@ body.modal-open {
width: 98%;
}
}
.modal.popup-dialog {
display: block;
}
.modal-body {
background-color: $modal-body-bg;
}
......@@ -164,36 +164,3 @@ $pre-border-color: $border-color;
$table-bg-accent: $gray-light;
$zindex-popover: 900;
//== Modals
//
//##
//** Padding applied to the modal body
$modal-inner-padding: $gl-padding;
//** Padding applied to the modal title
$modal-title-padding: $gl-padding;
//** Modal title line-height
// $modal-title-line-height: $line-height-base
//** Background color of modal content area
$modal-content-bg: $gray-light;
$modal-body-bg: $white-light;
//** Modal content border color
// $modal-content-border-color: rgba(0,0,0,.2)
//** Modal content border color **for IE8**
// $modal-content-fallback-border-color: #999
//** Modal backdrop background color
// $modal-backdrop-bg: #000
//** Modal backdrop opacity
// $modal-backdrop-opacity: .5
//** Modal header border color
// $modal-header-border-color: #e5e5e5
//** Modal footer border color
// $modal-footer-border-color: $modal-header-border-color
// $modal-lg: 900px
// $modal-md: 600px
// $modal-sm: 300px
......@@ -269,7 +269,7 @@ ul.notes {
display: none;
}
&.system-note-commit-list {
&.system-note-commit-list:not(.hide-shade) {
max-height: 70px;
overflow: hidden;
display: block;
......@@ -291,16 +291,6 @@ ul.notes {
bottom: 0;
background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
}
&.hide-shade {
max-height: 100%;
overflow: auto;
&::after {
display: none;
background: transparent;
}
}
}
}
}
......
.modal.popup-dialog {
display: block;
background-color: $black-transparent;
z-index: 2100;
@media (min-width: $screen-md-min) {
.modal-dialog {
width: 600px;
margin: 30px auto;
}
}
}
.project-refs-form,
.project-refs-target-form {
display: inline-block;
......
......@@ -20,6 +20,17 @@ module BoardsHelper
project_issues_path(@project)
end
def current_board_json
board = @board || @boards.first
board.to_json(
only: [:id, :name, :milestone_id],
include: {
milestone: { only: [:title] }
}
)
end
def board_base_url
project_boards_path(@project)
end
......
......@@ -420,7 +420,7 @@ class ApplicationSetting < ActiveRecord::Base
# the enabling/disabling is `performance_bar_allowed_group_id`
# - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
def performance_bar_enabled=(enable)
return if enable
return if Gitlab::Utils.to_boolean(enable)
self.performance_bar_allowed_group_id = nil
end
......
......@@ -878,7 +878,7 @@ class MergeRequest < ActiveRecord::Base
#
def all_commit_shas
if persisted?
column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).pluck('DISTINCT(sha)')
column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).limit(10_000).pluck('sha')
serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
(column_shas + serialised_shas).uniq
......
......@@ -9,6 +9,7 @@ module Issues
notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
end
issue
......
......@@ -11,6 +11,7 @@ module MergeRequests
merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
end
merge_request
......
......@@ -11,7 +11,7 @@
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false
.clearfix
.error-alert
......
......@@ -274,7 +274,7 @@
Members
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
= link_to project_settings_members_path(@project) do
= link_to project_project_members_path(@project) do
%strong.fly-out-top-item-name
#{ _('Members') }
......
---
title: Stop merge requests with thousands of commits from timing out
merge_request: 15063
author:
type: performance
---
title: Allow to disable the Performance Bar
merge_request: 15084
author:
type: fixed
---
title: Enable NestingDepth (level 6) on scss-lint
merge_request: 15073
author: Takuya Noguchi
type: other
---
title: Refresh open Issue and Merge Request project counter caches when re-opening.
merge_request: 15085
author: Rob Ede @robjtede
type: fixed
---
title: Fix 500 errors caused by empty diffs in some discussions
merge_request: 14945
author: Alexander Popov
type: fixed
---
title: Use project select dropdown not only as a combobutton
merge_request: 15043
author:
type: fixed
---
title: Fix broken Members link when relative URL root paths are used
merge_request:
author:
type: fixed
---
title: Update i18n section in FE docs for marking and interpolation
merge_request:
author:
type: changed
namespace :ci do
resource :lint, only: [:show, :create]
root to: redirect('/')
root to: redirect('')
end
......@@ -393,7 +393,7 @@ constraints(ProjectUrlConstrainer.new) do
end
end
namespace :settings do
get :members, to: redirect('/%{namespace_id}/%{project_id}/project_members')
get :members, to: redirect("%{namespace_id}/%{project_id}/project_members")
resource :ci_cd, only: [:show], controller: 'ci_cd'
resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository
......
......@@ -17,5 +17,5 @@ resources :snippets, concerns: :awardable do
end
end
get '/s/:username', to: redirect('/u/%{username}/snippets'),
get '/s/:username', to: redirect('u/%{username}/snippets'),
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
......@@ -22,17 +22,17 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :contributed, as: :contributed_projects
get :snippets
get :exists
get '/', to: redirect('/%{username}'), as: nil
get '/', to: redirect('%{username}'), as: nil
end
# Compatibility with old routing
# TODO (dzaporozhets): remove in 10.0
get '/u/:username', to: redirect('/%{username}')
get '/u/:username', to: redirect('%{username}')
# TODO (dzaporozhets): remove in 9.0
get '/u/:username/groups', to: redirect('/users/%{username}/groups')
get '/u/:username/projects', to: redirect('/users/%{username}/projects')
get '/u/:username/snippets', to: redirect('/users/%{username}/snippets')
get '/u/:username/contributed', to: redirect('/users/%{username}/contributed')
get '/u/:username/groups', to: redirect('users/%{username}/groups')
get '/u/:username/projects', to: redirect('users/%{username}/projects')
get '/u/:username/snippets', to: redirect('users/%{username}/snippets')
get '/u/:username/contributed', to: redirect('users/%{username}/contributed')
end
constraints(UserUrlConstrainer.new) do
......
......@@ -28,6 +28,12 @@ will be allowed to display the Performance Bar.
Make sure _Enable the Performance Bar_ is checked and hit
**Save** to save the changes.
Once the Performance Bar is enabled, you will need to press the [<kbd>p</kbd> +
<kbd>b</kbd> keyboard shortcut](../../../workflow/shortcuts.md) to actually
display it.
You can toggle the Bar using the same shortcut.
---
![GitLab Performance Bar Admin Settings](img/performance_bar_configuration_settings.png)
......
......@@ -20,7 +20,7 @@ it, the client IP needs to be [included in a whitelist][whitelist].
Currently the embedded Prometheus server is not automatically configured to
collect metrics from this endpoint. We recommend setting up another Prometheus
server, because the embedded server configuration is overwritten once every
[reconfigure of GitLab][reconfigure]. In the future this will not be required.
[reconfigure of GitLab][reconfigure]. In the future this will not be required.
## Metrics available
......@@ -45,6 +45,8 @@ In this experimental phase, only a few metrics are available:
| redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded |
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
| filesystem_circuitbreaker_latency_seconds | Histogram | 9.5 | Latency of the stat check the circuitbreaker uses to probe a shard |
| filesystem_circuitbreaker | Gauge | 9.5 | Wether or not the circuit for a certain shard is broken or not |
## Metrics shared directory
......
......@@ -106,6 +106,10 @@ Frontend security practices.
## [Accessibility](accessibility.md)
Our accessibility standards and resources.
## [Internationalization (i18n) and Translations](../i18n/externalization.md)
Frontend internationalization support is described in [this document](../i18n/).
The [externalization part of the guide](../i18n/externalization.md) explains the helpers/methods available.
[rails]: http://rubyonrails.org/
[haml]: http://haml.info/
......
......@@ -180,15 +180,43 @@ aren't in the message with id `1 pipeline`.
## Working with special content
### Just marking content for parsing
- In Ruby/HAML:
```ruby
_('Subscribe')
```
- In JavaScript:
```js
import { __ } from '../../../locale';
const label = __('Subscribe');
```
Sometimes there are some dynamic translations that can't be found by the
parser when running `bundle exec rake gettext:find`. For these scenarios you can
use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
### Interpolation
- In Ruby/HAML:
```ruby
_("Hello %{name}") % { name: 'Joe' }
_("Hello %{name}") % { name: 'Joe' } => 'Hello Joe'
```
- In JavaScript: Not supported at this moment.
- In JavaScript:
```js
import { __, sprintf } from '../../../locale';
sprintf(__('Hello %{username}'), { username: 'Joe' }) => 'Hello Joe'
```
### Plurals
......@@ -234,14 +262,6 @@ Sometimes you need to add some context to the text that you want to translate
s__('OpenedNDaysAgo|Opened')
```
### Just marking content for parsing
Sometimes there are some dynamic translations that can't be found by the
parser when running `bundle exec rake gettext:find`. For these scenarios you can
use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
## Adding a new language
Let's suppose you want to add translations for a new language, let's say French.
......
......@@ -136,44 +136,54 @@ In the example below we use Amazon S3 for storage, but Fog also lets you use
for AWS, Google, OpenStack Swift, Rackspace and Aliyun as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`:
#### Using Amazon S3
```ruby
gitlab_rails['backup_upload_connection'] = {
'provider' => 'AWS',
'region' => 'eu-west-1',
'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123'
# If using an IAM Profile, don't configure aws_access_key_id & aws_secret_access_key
# 'use_iam_profile' => true
}
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
For Omnibus GitLab packages:
1. Add the following to `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['backup_upload_connection'] = {
'provider' => 'AWS',
'region' => 'eu-west-1',
'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123'
# If using an IAM Profile, don't configure aws_access_key_id & aws_secret_access_key
# 'use_iam_profile' => true
}
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
1. [Reconfigure GitLab] for the changes to take effect
Make sure to run `sudo gitlab-ctl reconfigure` after editing `/etc/gitlab/gitlab.rb` to reflect the changes.
---
For installations from source:
```yaml
backup:
# snip
upload:
# Fog storage connection settings, see http://fog.io/storage/ .
connection:
provider: AWS
region: eu-west-1
aws_access_key_id: AKIAKIAKI
aws_secret_access_key: 'secret123'
# If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
# ie. aws_access_key_id: ''
# use_iam_profile: 'true'
# The remote 'directory' to store your backups. For S3, this would be the bucket name.
remote_directory: 'my.s3.bucket'
# Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
# encryption: 'AES256'
# Specifies Amazon S3 storage class to use for backups, this is optional
# storage_class: 'STANDARD'
```
1. Edit `home/git/gitlab/config/gitlab.yml`:
```yaml
backup:
# snip
upload:
# Fog storage connection settings, see http://fog.io/storage/ .
connection:
provider: AWS
region: eu-west-1
aws_access_key_id: AKIAKIAKI
aws_secret_access_key: 'secret123'
# If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
# ie. aws_access_key_id: ''
# use_iam_profile: 'true'
# The remote 'directory' to store your backups. For S3, this would be the bucket name.
remote_directory: 'my.s3.bucket'
# Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
# encryption: 'AES256'
# Specifies Amazon S3 storage class to use for backups, this is optional
# storage_class: 'STANDARD'
```
1. [Restart GitLab] for the changes to take effect
If you are uploading your backups to S3 you will probably want to create a new
IAM user with restricted access rights. To give the upload user access only for
......@@ -226,6 +236,50 @@ with the name of your bucket:
}
```
#### Using Google Cloud Storage
If you want to use Google Cloud Storage to save backups, you'll have to create
an access key from the Google console first:
1. Go to the storage settings page https://console.cloud.google.com/storage/settings
1. Select "Interoperability" and create an access key
1. Make note of the "Access Key" and "Secret" and replace them in the
configurations below
1. Make sure you already have a bucket created
For Omnibus GitLab packages:
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['backup_upload_connection'] = {
'provider' => 'Google',
'google_storage_access_key_id' => 'Access Key',
'google_storage_secret_access_key' => 'Secret'
}
gitlab_rails['backup_upload_remote_directory'] = 'my.google.bucket'
```
1. [Reconfigure GitLab] for the changes to take effect
---
For installations from source:
1. Edit `home/git/gitlab/config/gitlab.yml`:
```yaml
backup:
upload:
connection:
provider: 'Google'
google_storage_access_key_id: 'Access Key'
google_storage_secret_access_key: 'Secret'
remote_directory: 'my.google.bucket'
```
1. [Restart GitLab] for the changes to take effect
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
......@@ -554,3 +608,6 @@ The rake task runs this as the `gitlab` user which does not have the superuser a
Those objects have no influence on the database backup/restore but they give this annoying warning.
For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql).
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
......@@ -9,7 +9,7 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>n</kbd> | Main navigation |
| <kbd>s</kbd> | Focus search |
| <kbd>f</kbd> | Focus filter |
| <kbd>p b</kbd> | Show/hide the Performance Bar |
| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar |
| <kbd>?</kbd> | Show/hide this dialog |
| <kbd></kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
| <kbd></kbd> | Edit last comment (when focused on an empty textarea) |
......
......@@ -94,7 +94,9 @@ module Gitlab
end
def diff_file(repository)
@diff_file ||= begin
return @diff_file if defined?(@diff_file)
@diff_file = begin
if RequestStore.active?
key = {
project_id: repository.project.id,
......@@ -122,8 +124,8 @@ module Gitlab
def find_diff_file(repository)
return unless diff_refs.complete?
diff_refs.compare_in(repository.project).diffs(paths: paths, expanded: true).diff_files.first
return unless comparison = diff_refs.compare_in(repository.project)
comparison.diffs(paths: paths, expanded: true).diff_files.first
end
def get_formatter_class(type)
......
......@@ -95,6 +95,29 @@ feature 'Admin updates settings' do
expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
end
scenario 'Change Performance Bar settings' do
group = create(:group)
check 'Enable the Performance Bar'
fill_in 'Allowed group', with: group.path
click_on 'Save'
expect(page).to have_content 'Application settings saved successfully'
expect(find_field('Enable the Performance Bar')).to be_checked
expect(find_field('Allowed group').value).to eq group.path
uncheck 'Enable the Performance Bar'
click_on 'Save'
expect(page).to have_content 'Application settings saved successfully'
expect(find_field('Enable the Performance Bar')).not_to be_checked
expect(find_field('Allowed group').value).to be_nil
end
def check_all_events
page.check('Active')
page.check('Push')
......
......@@ -19,9 +19,9 @@ feature 'Group milestones', :js do
end
it 'renders description preview' do
form = find('.gfm-form')
description = find('.note-textarea')
form.fill_in(:milestone_description, with: '')
description.native.send_keys('')
click_link('Preview')
......@@ -31,7 +31,7 @@ feature 'Group milestones', :js do
click_link('Write')
form.fill_in(:milestone_description, with: ':+1: Nice')
description.native.send_keys(':+1: Nice')
click_link('Preview')
......@@ -51,6 +51,13 @@ feature 'Group milestones', :js do
expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y'))
end
it 'description input does not support autocomplete' do
description = find('.note-textarea')
description.native.send_keys('!')
expect(page).not_to have_selector('.atwho-view')
end
end
context 'milestones list' do
......
/* global IssuableContext */
import '~/issuable_context';
import $ from 'jquery';
import IssuableContext from '~/issuable_context';
describe('IssuableContext', () => {
describe('toggleHiddenParticipants', () => {
......
/* global IssuableIndex */
import '~/lib/utils/url_utility';
import '~/issuable_index';
(() => {
const BASE_URL = '/user/project/issues?scope=all&state=closed';
const DEFAULT_PARAMS = '&utf8=%E2%9C%93';
function updateForm(formValues, form) {
$.each(formValues, (id, value) => {
$(`#${id}`, form).val(value);
});
}
function resetForm(form) {
$('input[name!="utf8"]', form).each((index, input) => {
input.setAttribute('value', '');
import IssuableIndex from '~/issuable_index';
describe('Issuable', () => {
let Issuable;
describe('initBulkUpdate', () => {
it('should not set bulkUpdateSidebar', () => {
Issuable = new IssuableIndex('issue_');
expect(Issuable.bulkUpdateSidebar).not.toBeDefined();
});
}
describe('Issuable', () => {
preloadFixtures('static/issuable_filter.html.raw');
it('should set bulkUpdateSidebar', () => {
const element = document.createElement('div');
element.classList.add('issues-bulk-update');
document.body.appendChild(element);
beforeEach(() => {
loadFixtures('static/issuable_filter.html.raw');
IssuableIndex.init();
});
it('should be defined', () => {
expect(window.IssuableIndex).toBeDefined();
Issuable = new IssuableIndex('issue_');
expect(Issuable.bulkUpdateSidebar).toBeDefined();
});
});
describe('filtering', () => {
let $filtersForm;
beforeEach(() => {
$filtersForm = $('.js-filter-form');
loadFixtures('static/issuable_filter.html.raw');
resetForm($filtersForm);
});
it('should contain only the default parameters', () => {
spyOn(gl.utils, 'visitUrl');
IssuableIndex.filterResults($filtersForm);
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
});
it('should filter for the phrase "broken"', () => {
spyOn(gl.utils, 'visitUrl');
updateForm({ search: 'broken' }, $filtersForm);
IssuableIndex.filterResults($filtersForm);
const params = `${DEFAULT_PARAMS}&search=broken`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
});
it('should keep query parameters after modifying filter', () => {
spyOn(gl.utils, 'visitUrl');
describe('resetIncomingEmailToken', () => {
beforeEach(() => {
const element = document.createElement('a');
element.classList.add('incoming-email-token-reset');
element.setAttribute('href', 'foo');
document.body.appendChild(element);
// initial filter
updateForm({ milestone_title: 'v1.0' }, $filtersForm);
const input = document.createElement('input');
input.setAttribute('id', 'issue_email');
document.body.appendChild(input);
IssuableIndex.filterResults($filtersForm);
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
Issuable = new IssuableIndex('issue_');
});
// update filter
updateForm({ label_name: 'Frontend' }, $filtersForm);
it('should send request to reset email token', () => {
spyOn(jQuery, 'ajax').and.callThrough();
document.querySelector('.incoming-email-token-reset').click();
IssuableIndex.filterResults($filtersForm);
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
});
expect(jQuery.ajax).toHaveBeenCalled();
expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo');
});
});
})();
});
/* eslint-disable no-new */
/* global IssuableContext */
import IssuableContext from '~/issuable_context';
/* global LabelsSelect */
import '~/gl_dropdown';
import 'select2';
import '~/api';
import '~/create_label';
import '~/issuable_context';
import '~/users_select';
import '~/labels_select';
......
......@@ -364,6 +364,43 @@ describe Gitlab::Diff::Position do
end
end
describe "position for a missing ref" do
let(:diff_refs) do
Gitlab::Diff::DiffRefs.new(
base_sha: "not_existing_sha",
head_sha: "existing_sha"
)
end
subject do
described_class.new(
old_path: "files/ruby/feature.rb",
new_path: "files/ruby/feature.rb",
old_line: 3,
new_line: nil,
diff_refs: diff_refs
)
end
describe "#diff_file" do
it "does not raise exception" do
expect { subject.diff_file(project.repository) }.not_to raise_error
end
end
describe "#diff_line" do
it "does not raise exception" do
expect { subject.diff_line(project.repository) }.not_to raise_error
end
end
describe "#line_code" do
it "does not raise exception" do
expect { subject.line_code(project.repository) }.not_to raise_error
end
end
end
describe "position for a file in the initial commit" do
let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
......
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