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: ...@@ -121,7 +121,8 @@ linters:
# Avoid nesting selectors too deeply. # Avoid nesting selectors too deeply.
NestingDepth: NestingDepth:
enabled: false enabled: true
max_depth: 6
# Always use placeholder selectors in @extend. # Always use placeholder selectors in @extend.
PlaceholderInExtend: PlaceholderInExtend:
......
0.49.0 0.50.0
\ No newline at end of file
...@@ -281,7 +281,7 @@ group :metrics do ...@@ -281,7 +281,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta17' gem 'prometheus-client-mmap', '~>0.7.0.beta18'
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
......
...@@ -623,7 +623,7 @@ GEM ...@@ -623,7 +623,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.7.0.beta17) prometheus-client-mmap (0.7.0.beta18)
mmap2 (~> 2.2, >= 2.2.7) mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
...@@ -1106,7 +1106,7 @@ DEPENDENCIES ...@@ -1106,7 +1106,7 @@ DEPENDENCIES
pg (~> 0.18.2) pg (~> 0.18.2)
poltergeist (~> 1.9.0) poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7) 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-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
......
/* eslint-disable comma-dangle, space-before-function-paren, no-new */ /* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global IssuableContext */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global LabelsSelect */ /* global LabelsSelect */
/* global Sidebar */ /* global Sidebar */
...@@ -11,6 +10,7 @@ import AssigneeTitle from '../../sidebar/components/assignees/assignee_title'; ...@@ -11,6 +10,7 @@ import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees'; import Assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select'; import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue'; import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
......
...@@ -11,8 +11,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -11,8 +11,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async // Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests // instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true; this.isHandledAsync = true;
this.cantEdit = cantEdit.filter(i => typeof i === 'string'); this.cantEdit = cantEdit;
this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
} }
updateObject(path) { updateObject(path) {
...@@ -43,9 +42,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -43,9 +42,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input')); this.filteredSearchInput.dispatchEvent(new Event('input'));
} }
canEdit(tokenName, tokenValue) { canEdit(tokenName) {
if (this.cantEdit.includes(tokenName)) return false; return this.cantEdit.indexOf(tokenName) === -1;
return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
token.value === tokenValue) === -1;
} }
} }
...@@ -14,18 +14,16 @@ gl.issueBoards.BoardsStore = { ...@@ -14,18 +14,16 @@ gl.issueBoards.BoardsStore = {
}, },
state: {}, state: {},
detail: { detail: {
issue: {}, issue: {}
}, },
moving: { moving: {
issue: {}, issue: {},
list: {}, list: {}
}, },
create () { create () {
this.state.lists = []; this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&'); this.filter.path = getUrlParamsArray().join('&');
this.detail = { this.detail = { issue: {} };
issue: {},
};
}, },
addList (listObj, defaultAvatar) { addList (listObj, defaultAvatar) {
const list = new List(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 */ /* 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 ProjectSelect */
/* global IssuableIndex */ import IssuableIndex from './issuable_index';
/* global Milestone */ /* global Milestone */
/* global IssuableForm */ import IssuableForm from './issuable_form';
/* global LabelsSelect */ /* global LabelsSelect */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global NewBranchForm */ /* global NewBranchForm */
...@@ -173,7 +173,7 @@ import Diff from './diff'; ...@@ -173,7 +173,7 @@ import Diff from './diff';
filteredSearchManager.setup(); filteredSearchManager.setup();
} }
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix); new IssuableIndex(pagePrefix);
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new UsersSelect(); new UsersSelect();
...@@ -231,12 +231,16 @@ import Diff from './diff'; ...@@ -231,12 +231,16 @@ import Diff from './diff';
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
case 'projects:milestones:update': case 'projects:milestones:update':
new ZenMode();
new DueDateSelectors();
new GLForm($('.milestone-form'), true);
break;
case 'groups:milestones:new': case 'groups:milestones:new':
case 'groups:milestones:edit': case 'groups:milestones:edit':
case 'groups:milestones:update': case 'groups:milestones:update':
new ZenMode(); new ZenMode();
new DueDateSelectors(); new DueDateSelectors();
new GLForm($('.milestone-form'), true); new GLForm($('.milestone-form'), false);
break; break;
case 'projects:compare:show': case 'projects:compare:show':
new Diff(); new Diff();
......
...@@ -147,16 +147,6 @@ class DropdownUtils { ...@@ -147,16 +147,6 @@ class DropdownUtils {
return dataValue !== null; 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) // Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) { static getSearchQuery(untilInput = false) {
const container = FilteredSearchContainer.container; const container = FilteredSearchContainer.container;
......
...@@ -185,8 +185,8 @@ class FilteredSearchManager { ...@@ -185,8 +185,8 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken); const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue); const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); gl.FilteredSearchVisualTokens.removeLastTokenPartial();
...@@ -336,8 +336,8 @@ class FilteredSearchManager { ...@@ -336,8 +336,8 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token'); let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) { if (canClearToken) {
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t); const tokenKey = t.querySelector('.name').textContent.trim();
canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue); canClearToken = this.canEdit && this.canEdit(tokenKey);
} }
if (canClearToken) { if (canClearToken) {
...@@ -469,7 +469,7 @@ class FilteredSearchManager { ...@@ -469,7 +469,7 @@ class FilteredSearchManager {
} }
hasFilteredSearch = true; hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); const canEdit = this.canEdit && this.canEdit(sanitizedKey);
gl.FilteredSearchVisualTokens.addFilterVisualToken( gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey, sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
......
...@@ -38,14 +38,21 @@ class FilteredSearchVisualTokens { ...@@ -38,14 +38,21 @@ class FilteredSearchVisualTokens {
} }
static createVisualTokenElementHTML(canEdit = true) { static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return ` return `
<div class="${canEdit ? 'selectable' : 'hidden'}" role="button"> <div class="selectable" role="button">
<div class="name"></div> <div class="name"></div>
<div class="value-container"> <div class="value-container">
<div class="value"></div> <div class="value"></div>
<div class="remove-token" role="button"> ${removeTokenMarkup}
<i class="fa fa-close"></i>
</div>
</div> </div>
</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() { initStatusPage() {
window.ImporterStatus = (function() { $('.js-add-to-import')
function ImporterStatus(jobs_url, import_url) { .off('click')
this.jobs_url = jobs_url; .on('click', (event) => {
this.import_url = import_url; const $btn = $(event.currentTarget);
this.initStatusPage(); const $tr = $btn.closest('tr');
this.setAutoUpdate(); 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() { return $.post(this.importUrl, {
$('.js-add-to-import').off('click').on('click', (function(_this) { repo_id: id,
return function(e) { target_namespace: targetNamespace,
var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName; new_name: newName,
$btn = $(e.currentTarget); }, {
$tr = $btn.closest('tr'); dataType: 'script',
$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; $('.js-import-all')
newName = null; .off('click')
if ($namespace_input.length > 0) { .on('click', function onClickImportAll() {
target_namespace = $namespace_input[0].innerHTML; const $btn = $(this);
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);
$btn.disable().addClass('is-loading'); $btn.disable().addClass('is-loading');
return $('.js-add-to-import').each(function() { return $('.js-add-to-import').each(function triggerAddImport() {
return $(this).trigger('click'); 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() { const spinner = '<i class="fa fa-spinner fa-spin"></i>';
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);
};
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() { // eslint-disable-next-line consistent-return
if ($('.js-importer-status').length) { export default function initImporterStatus() {
var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); const importerStatus = document.querySelector('.js-importer-status');
var importPath = $('.js-importer-status').data('import-path');
new window.ImporterStatus(jobsImportPath, importPath); if (importerStatus) {
} const data = importerStatus.dataset;
}); return new ImporterStatus(data.jobsImportPath, data.importPath);
}).call(window); }
}
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global LabelsSelect */ /* global LabelsSelect */
/* global IssuableContext */ import IssuableContext from './issuable_context';
/* global Sidebar */ /* global Sidebar */
import DueDateSelectors from './due_date_select'; 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 */ /* 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 _ from 'underscore';
import Flash from './flash'; import Flash from './flash';
......
...@@ -5,6 +5,10 @@ ...@@ -5,6 +5,10 @@
/* global SubscriptionSelect */ /* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; 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 HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content'; 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 Cookies from 'js-cookie';
import bp from './breakpoints'; import bp from './breakpoints';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7; const PARTICIPANTS_ROW_COUNT = 7;
(function() { export default class IssuableContext {
this.IssuableContext = (function() { constructor(currentUser) {
function IssuableContext(currentUser) { this.initParticipants();
this.initParticipants(); this.userSelect = new UsersSelect(currentUser);
new UsersSelect(currentUser);
$('select.select2').select2({ $('select.select2').select2({
width: 'resolve', width: 'resolve',
dropdownAutoWidth: true dropdownAutoWidth: true,
}); });
$(".issuable-sidebar .inline-update").on("change", "select", function() {
return $(this).submit(); $('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
}); return $(this).submit();
$(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() { });
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', function(e) { });
return e.preventDefault(); $(document)
}); .off('click', '.issuable-sidebar .dropdown-content a')
$(document).off('click', '.edit-link').on('click', '.edit-link', function(e) { .on('click', '.issuable-sidebar .dropdown-content a', e => e.preventDefault());
var $block, $selectbox;
$(document)
.off('click', '.edit-link')
.on('click', '.edit-link', function onClickEdit(e) {
e.preventDefault(); e.preventDefault();
$block = $(this).parents('.block'); const $block = $(this).parents('.block');
$selectbox = $block.find('.selectbox'); const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) { if ($selectbox.is(':visible')) {
$selectbox.hide(); $selectbox.hide();
$block.find('.value').show(); $block.find('.value').show();
...@@ -35,46 +37,43 @@ const PARTICIPANTS_ROW_COUNT = 7; ...@@ -35,46 +37,43 @@ const PARTICIPANTS_ROW_COUNT = 7;
$selectbox.show(); $selectbox.show();
$block.find('.value').hide(); $block.find('.value').hide();
} }
if ($selectbox.is(':visible')) { if ($selectbox.is(':visible')) {
return setTimeout(function() { setTimeout(() => $block.find('.dropdown-menu-toggle').trigger('click'), 0);
return $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() { window.addEventListener('beforeunload', () => {
$(document).on('click', '.js-participants-more', this.toggleHiddenParticipants); // collapsed_gutter cookie hides the sidebar
return $('.js-participants-author').each(function(i) { const bpBreakpoint = bp.getBreakpointSize();
if (i >= PARTICIPANTS_ROW_COUNT) { if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
return $(this).addClass('js-participants-hidden').hide(); Cookies.set('collapsed_gutter', true);
} }
}); });
}; }
IssuableContext.prototype.toggleHiddenParticipants = function() { initParticipants() {
const currentText = $(this).text().trim(); $(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
const lessText = $(this).data('less-text'); return $('.js-participants-author').each(function forEachAuthor(i) {
const originalText = $(this).data('original-text'); if (i >= PARTICIPANTS_ROW_COUNT) {
$(this).addClass('js-participants-hidden').hide();
}
});
}
if (currentText === originalText) { toggleHiddenParticipants() {
$(this).text(lessText); const currentText = $(this).text().trim();
const lessText = $(this).data('less-text');
const originalText = $(this).data('original-text');
if (gl.lazyLoader) gl.lazyLoader.loadCheck(); if (currentText === originalText) {
} else { $(this).text(lessText);
$(this).text(originalText);
}
$('.js-participants-hidden').toggle(); if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}; } else {
$(this).text(originalText);
}
return IssuableContext; $('.js-participants-hidden').toggle();
})(); }
}).call(window); }
/* 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 */ /* global GitLab */
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
...@@ -8,103 +8,100 @@ import GfmAutoComplete from './gfm_auto_complete'; ...@@ -8,103 +8,100 @@ import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode'; import ZenMode from './zen_mode';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
(function() { export default class IssuableForm {
this.IssuableForm = (function() { constructor(form) {
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; this.form = form;
this.toggleWip = this.toggleWip.bind(this);
function IssuableForm(form) { this.renderWipExplanation = this.renderWipExplanation.bind(this);
var $issuableDueDate, calendar; this.resetAutosave = this.resetAutosave.bind(this);
this.form = form; this.handleSubmit = this.handleSubmit.bind(this);
this.toggleWip = this.toggleWip.bind(this); this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this); new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
this.handleSubmit = this.handleSubmit.bind(this); new UsersSelect();
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); new ZenMode();
new UsersSelect();
new ZenMode(); this.titleField = this.form.find('input[name*="[title]"]');
this.titleField = this.form.find("input[name*='[title]']"); this.descriptionField = this.form.find('textarea[name*="[description]"]');
this.descriptionField = this.form.find("textarea[name*='[description]']"); if (!(this.titleField.length && this.descriptionField.length)) {
if (!(this.titleField.length && this.descriptionField.length)) { return;
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()));
}
} }
IssuableForm.prototype.initAutosave = function() { this.initAutosave();
new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]); this.form.on('submit', this.handleSubmit);
return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]); this.form.on('click', '.btn-cancel', this.resetAutosave);
}; this.initWip();
IssuableForm.prototype.handleSubmit = function() { const $issuableDueDate = $('#issuable-due-date');
return this.resetAutosave();
}; if ($issuableDueDate.length) {
const calendar = new Pikaday({
IssuableForm.prototype.resetAutosave = function() { field: $issuableDueDate.get(0),
this.titleField.data("autosave").reset(); theme: 'gitlab-theme animate-picker',
return this.descriptionField.data("autosave").reset(); format: 'yyyy-mm-dd',
}; container: $issuableDueDate.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
IssuableForm.prototype.initWip = function() { toString: date => pikadayToString(date),
this.$wipExplanation = this.form.find(".js-wip-explanation"); onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)),
this.$noWipExplanation = this.form.find(".js-no-wip-explanation"); });
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) { calendar.setDate(parsePikadayDate($issuableDueDate.val()));
return; }
} }
this.form.on("click", ".js-toggle-wip", this.toggleWip);
this.titleField.on("keyup blur", this.renderWipExplanation); initAutosave() {
return this.renderWipExplanation(); 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.workInProgress = function() {
return this.wipRegex.test(this.titleField.val()); handleSubmit() {
}; return this.resetAutosave();
}
IssuableForm.prototype.renderWipExplanation = function() {
if (this.workInProgress()) { resetAutosave() {
this.$wipExplanation.show(); this.titleField.data('autosave').reset();
return this.$noWipExplanation.hide(); return this.descriptionField.data('autosave').reset();
} else { }
this.$wipExplanation.hide();
return this.$noWipExplanation.show(); 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)) {
IssuableForm.prototype.toggleWip = function(event) { return;
event.preventDefault(); }
if (this.workInProgress()) { this.form.on('click', '.js-toggle-wip', this.toggleWip);
this.removeWip(); this.titleField.on('keyup blur', this.renderWipExplanation);
} else { return this.renderWipExplanation();
this.addWip(); }
}
return this.renderWipExplanation(); workInProgress() {
}; return this.wipRegex.test(this.titleField.val());
}
IssuableForm.prototype.removeWip = function() {
return this.titleField.val(this.titleField.val().replace(this.wipRegex, "")); renderWipExplanation() {
}; if (this.workInProgress()) {
this.$wipExplanation.show();
IssuableForm.prototype.addWip = function() { return this.$noWipExplanation.hide();
return this.titleField.val("WIP: " + (this.titleField.val())); } else {
}; this.$wipExplanation.hide();
return this.$noWipExplanation.show();
return IssuableForm; }
})(); }
}).call(window);
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 IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
((global) => { export default class IssuableIndex {
var issuable_created; constructor(pagePrefix) {
this.initBulkUpdate(pagePrefix);
issuable_created = false; IssuableIndex.resetIncomingEmailToken();
}
global.IssuableIndex = { initBulkUpdate(pagePrefix) {
init: function(pagePrefix) { const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
IssuableIndex.initTemplates(); const alreadyInitialized = !!this.bulkUpdateSidebar;
IssuableIndex.initSearch();
IssuableIndex.initBulkUpdate(pagePrefix); if (userCanBulkUpdate && !alreadyInitialized) {
IssuableIndex.initResetFilters(); IssuableBulkUpdateActions.init({
IssuableIndex.resetIncomingEmailToken(); prefixId: pagePrefix,
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);
}); });
},
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({ this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
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');
}
});
});
} }
}; }
})(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'; ...@@ -8,7 +8,7 @@ import CreateLabelDropdown from './create_label';
(function() { (function() {
this.LabelsSelect = (function() { this.LabelsSelect = (function() {
function LabelsSelect(els, options = {}) { function LabelsSelect(els) {
var _this, $els; var _this, $els;
_this = this; _this = this;
...@@ -58,7 +58,6 @@ import CreateLabelDropdown from './create_label'; ...@@ -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> <% }); %>'); 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>'; labelNoneHTMLTemplate = '<span class="no-value">None</span>';
} }
const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip(); $sidebarLabelTooltip.tooltip();
...@@ -317,9 +316,9 @@ import CreateLabelDropdown from './create_label'; ...@@ -317,9 +316,9 @@ import CreateLabelDropdown from './create_label';
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(clickEvent) { clicked: function(options) {
const { $el, e, isMarking } = clickEvent; const { $el, e, isMarking } = options;
const label = clickEvent.selectedObj; const label = options.selectedObj;
var isIssueIndex, isMRIndex, page, boardsModel; var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => { var fadeOutLoader = () => {
...@@ -392,10 +391,6 @@ import CreateLabelDropdown from './create_label'; ...@@ -392,10 +391,6 @@ import CreateLabelDropdown from './create_label';
.then(fadeOutLoader) .then(fadeOutLoader)
.catch(fadeOutLoader); .catch(fadeOutLoader);
} }
else if (handleClick) {
e.preventDefault();
handleClick(label);
}
else { else {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
......
...@@ -55,9 +55,7 @@ import './gl_field_error'; ...@@ -55,9 +55,7 @@ import './gl_field_error';
import './gl_field_errors'; import './gl_field_errors';
import './gl_form'; import './gl_form';
import './header'; import './header';
import './importer_status'; import initImporterStatus from './importer_status';
import './issuable_index';
import './issuable_context';
import './issuable_form'; import './issuable_form';
import './issue'; import './issue';
import './issue_status_select'; import './issue_status_select';
...@@ -138,6 +136,7 @@ $(function () { ...@@ -138,6 +136,7 @@ $(function () {
var fitSidebarForSize; var fitSidebarForSize;
initBreadcrumbs(); initBreadcrumbs();
initImporterStatus();
// Set the default path for all cookies to GitLab's root directory // Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/'; Cookies.defaults.path = gon.relative_url_root || '/';
......
...@@ -5,7 +5,7 @@ import _ from 'underscore'; ...@@ -5,7 +5,7 @@ import _ from 'underscore';
(function() { (function() {
this.MilestoneSelect = (function() { this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els, options = {}) { function MilestoneSelect(currentProject, els) {
var _this, $els; var _this, $els;
if (currentProject != null) { if (currentProject != null) {
_this = this; _this = this;
...@@ -136,26 +136,19 @@ import _ from 'underscore'; ...@@ -136,26 +136,19 @@ import _ from 'underscore';
}, },
opened: function(e) { opened: function(e) {
const $el = $(e.currentTarget); 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; selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
} }
$('a.is-active', $el).removeClass('is-active'); $('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(clickEvent) { clicked: function(options) {
const { $el, e } = clickEvent; const { $el, e } = options;
let selected = clickEvent.selectedObj; let selected = options.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return; if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
page = $('body').attr('data-page'); page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
......
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
import Api from './api'; import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button'; import ProjectSelectComboButton from './project_select_combo_button';
(function() { (function () {
this.ProjectSelect = (function() { this.ProjectSelect = (function () {
function ProjectSelect() { function ProjectSelect() {
$('.ajax-project-select').each(function(i, select) { $('.ajax-project-select').each(function(i, select) {
var placeholder; var placeholder;
const simpleFilter = $(select).data('simple-filter') || false;
this.groupId = $(select).data('group-id'); this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups'); this.includeGroups = $(select).data('include-groups');
this.allProjects = $(select).data('all-projects') || false;
this.orderBy = $(select).data('order-by') || 'id'; this.orderBy = $(select).data('order-by') || 'id';
this.withIssuesEnabled = $(select).data('with-issues-enabled'); this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
...@@ -21,10 +23,10 @@ import ProjectSelectComboButton from './project_select_combo_button'; ...@@ -21,10 +23,10 @@ import ProjectSelectComboButton from './project_select_combo_button';
$(select).select2({ $(select).select2({
placeholder: placeholder, placeholder: placeholder,
minimumInputLength: 0, minimumInputLength: 0,
query: (function(_this) { query: (function (_this) {
return function(query) { return function (query) {
var finalCallback, projectsCallback; var finalCallback, projectsCallback;
finalCallback = function(projects) { finalCallback = function (projects) {
var data; var data;
data = { data = {
results: projects results: projects
...@@ -32,9 +34,9 @@ import ProjectSelectComboButton from './project_select_combo_button'; ...@@ -32,9 +34,9 @@ import ProjectSelectComboButton from './project_select_combo_button';
return query.callback(data); return query.callback(data);
}; };
if (_this.includeGroups) { if (_this.includeGroups) {
projectsCallback = function(projects) { projectsCallback = function (projects) {
var groupsCallback; var groupsCallback;
groupsCallback = function(groups) { groupsCallback = function (groups) {
var data; var data;
data = groups.concat(projects); data = groups.concat(projects);
return finalCallback(data); return finalCallback(data);
...@@ -50,23 +52,25 @@ import ProjectSelectComboButton from './project_select_combo_button'; ...@@ -50,23 +52,25 @@ import ProjectSelectComboButton from './project_select_combo_button';
return Api.projects(query.term, { return Api.projects(query.term, {
order_by: _this.orderBy, order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled, with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled with_merge_requests_enabled: _this.withMergeRequestsEnabled,
membership: !_this.allProjects,
}, projectsCallback); }, projectsCallback);
} }
}; };
})(this), })(this),
id: function(project) { id: function(project) {
if (simpleFilter) return project.id;
return JSON.stringify({ return JSON.stringify({
name: project.name, name: project.name,
url: project.web_url, url: project.web_url,
}); });
}, },
text: function(project) { text: function (project) {
return project.name_with_namespace || project.name; return project.name_with_namespace || project.name;
}, },
dropdownCssClass: "ajax-project-dropdown" dropdownCssClass: "ajax-project-dropdown"
}); });
if (simpleFilter) return select;
return new ProjectSelectComboButton(select); return new ProjectSelectComboButton(select);
}); });
} }
......
...@@ -6,7 +6,7 @@ import _ from 'underscore'; ...@@ -6,7 +6,7 @@ import _ from 'underscore';
// TODO: remove eventHub hack after code splitting refactor // TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop; window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els, options = {}) { function UsersSelect(currentUser, els) {
var $els; var $els;
this.users = this.users.bind(this); this.users = this.users.bind(this);
this.user = this.user.bind(this); this.user = this.user.bind(this);
...@@ -20,8 +20,6 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -20,8 +20,6 @@ function UsersSelect(currentUser, els, options = {}) {
} }
} }
const { handleClick } = options;
$els = $(els); $els = $(els);
if (!els) { if (!els) {
...@@ -444,9 +442,6 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -444,9 +442,6 @@ function UsersSelect(currentUser, els, options = {}) {
} }
if ($el.closest('.add-issues-modal').length) { if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; 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)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
......
...@@ -18,12 +18,6 @@ ...@@ -18,12 +18,6 @@
required: false, required: false,
default: false, default: false,
}, },
class: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
...@@ -31,7 +25,7 @@ ...@@ -31,7 +25,7 @@
return this.inline ? 'span' : 'div'; return this.inline ? 'span' : 'div';
}, },
cssClass() { cssClass() {
return `fa-${this.size}x ${this.class}`.trim(); return `fa-${this.size}x`;
}, },
}, },
}; };
......
...@@ -5,27 +5,17 @@ export default { ...@@ -5,27 +5,17 @@ export default {
props: { props: {
title: { title: {
type: String, type: String,
required: false, required: true,
}, },
text: { text: {
type: String, type: String,
required: false, required: false,
}, },
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: { kind: {
type: String, type: String,
required: false, required: false,
default: 'primary', default: 'primary',
}, },
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: { closeKind: {
type: String, type: String,
required: false, required: false,
...@@ -40,11 +30,6 @@ export default { ...@@ -40,11 +30,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...@@ -72,57 +57,43 @@ export default { ...@@ -72,57 +57,43 @@ export default {
</script> </script>
<template> <template>
<div class="modal-open"> <div
<div class="modal popup-dialog"
class="modal popup-dialog" role="dialog"
role="dialog" tabindex="-1">
tabindex="-1" <div class="modal-dialog" role="document">
> <div class="modal-content">
<div <div class="modal-header">
:class="modalDialogClass" <button type="button"
class="modal-dialog" class="close"
role="document" @click="close"
> aria-label="Close">
<div class="modal-content"> <span aria-hidden="true">&times;</span>
<div class="modal-header"> </button>
<slot name="header"> <h4 class="modal-title">{{this.title}}</h4>
<h4 class="modal-title pull-left"> </div>
{{this.title}} <div class="modal-body">
</h4> <slot name="body" :text="text">
<button <p>{{text}}</p>
type="button" </slot>
class="close pull-right" </div>
@click="close" <div class="modal-footer">
aria-label="Close" <button
> type="button"
<span aria-hidden="true">&times;</span> class="btn"
</button> :class="btnCancelKindClass"
</slot> @click="close">
</div> {{ closeButtonLabel }}
<div class="modal-body"> </button>
<slot name="body" :text="text"> <button
<p>{{this.text}}</p> type="button"
</slot> class="btn"
</div> :class="btnKindClass"
<div class="modal-footer" v-if="!hideFooter"> @click="emitSubmit(true)">
<button {{ primaryButtonLabel }}
type="button" </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> </div>
</div> </div>
</div> </div>
<div class="modal-backdrop fade in" />
</div> </div>
</template> </template>
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
.cred { color: $common-red; } .cred { color: $common-red; }
.cgreen { color: $common-green; } .cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; } .cdark { color: $common-gray-dark; }
.text-secondary {
color: $gl-text-color-secondary;
}
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; } .prepend-top-0 { margin-top: 0; }
......
...@@ -37,7 +37,6 @@ ...@@ -37,7 +37,6 @@
.dropdown-menu-nav { .dropdown-menu-nav {
@include set-visible; @include set-visible;
display: block; display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
width: 100%; width: 100%;
......
...@@ -216,12 +216,9 @@ body { ...@@ -216,12 +216,9 @@ body {
color: $theme-gray-900; color: $theme-gray-900;
} }
&.active > a { &.active > a,
&.active > a:hover {
color: $white-light; color: $white-light;
&:hover {
color: $white-light;
}
} }
} }
} }
......
...@@ -239,10 +239,8 @@ ...@@ -239,10 +239,8 @@
fill: currentColor; fill: currentColor;
} }
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle .header-user-avatar {
.header-user-avatar { border-color: $white-light;
border-color: $white-light;
}
} }
} }
} }
......
...@@ -42,11 +42,3 @@ body.modal-open { ...@@ -42,11 +42,3 @@ body.modal-open {
width: 98%; width: 98%;
} }
} }
.modal.popup-dialog {
display: block;
}
.modal-body {
background-color: $modal-body-bg;
}
...@@ -164,36 +164,3 @@ $pre-border-color: $border-color; ...@@ -164,36 +164,3 @@ $pre-border-color: $border-color;
$table-bg-accent: $gray-light; $table-bg-accent: $gray-light;
$zindex-popover: 900; $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 { ...@@ -269,7 +269,7 @@ ul.notes {
display: none; display: none;
} }
&.system-note-commit-list { &.system-note-commit-list:not(.hide-shade) {
max-height: 70px; max-height: 70px;
overflow: hidden; overflow: hidden;
display: block; display: block;
...@@ -291,16 +291,6 @@ ul.notes { ...@@ -291,16 +291,6 @@ ul.notes {
bottom: 0; bottom: 0;
background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%); 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-form,
.project-refs-target-form { .project-refs-target-form {
display: inline-block; display: inline-block;
......
...@@ -20,6 +20,17 @@ module BoardsHelper ...@@ -20,6 +20,17 @@ module BoardsHelper
project_issues_path(@project) project_issues_path(@project)
end 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 def board_base_url
project_boards_path(@project) project_boards_path(@project)
end end
......
...@@ -420,7 +420,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -420,7 +420,7 @@ class ApplicationSetting < ActiveRecord::Base
# the enabling/disabling is `performance_bar_allowed_group_id` # the enabling/disabling is `performance_bar_allowed_group_id`
# - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil` # - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
def performance_bar_enabled=(enable) def performance_bar_enabled=(enable)
return if enable return if Gitlab::Utils.to_boolean(enable)
self.performance_bar_allowed_group_id = nil self.performance_bar_allowed_group_id = nil
end end
......
...@@ -878,7 +878,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -878,7 +878,7 @@ class MergeRequest < ActiveRecord::Base
# #
def all_commit_shas def all_commit_shas
if persisted? 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) serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
(column_shas + serialised_shas).uniq (column_shas + serialised_shas).uniq
......
...@@ -9,6 +9,7 @@ module Issues ...@@ -9,6 +9,7 @@ module Issues
notification_service.reopen_issue(issue, current_user) notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen') execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees) invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
end end
issue issue
......
...@@ -11,6 +11,7 @@ module MergeRequests ...@@ -11,6 +11,7 @@ module MergeRequests
merge_request.reload_diff(current_user) merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
invalidate_cache_counts(merge_request, users: merge_request.assignees) invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
end end
merge_request merge_request
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do = 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 .clearfix
.error-alert .error-alert
......
...@@ -274,7 +274,7 @@ ...@@ -274,7 +274,7 @@
Members Members
%ul.sidebar-sub-level-items.is-fly-out-only %ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do = 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 %strong.fly-out-top-item-name
#{ _('Members') } #{ _('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 namespace :ci do
resource :lint, only: [:show, :create] resource :lint, only: [:show, :create]
root to: redirect('/') root to: redirect('')
end end
...@@ -393,7 +393,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -393,7 +393,7 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
namespace :settings do 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 :ci_cd, only: [:show], controller: 'ci_cd'
resource :integrations, only: [:show] resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository resource :repository, only: [:show], controller: :repository
......
...@@ -17,5 +17,5 @@ resources :snippets, concerns: :awardable do ...@@ -17,5 +17,5 @@ resources :snippets, concerns: :awardable do
end end
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)/ } constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
...@@ -22,17 +22,17 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d ...@@ -22,17 +22,17 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :contributed, as: :contributed_projects get :contributed, as: :contributed_projects
get :snippets get :snippets
get :exists get :exists
get '/', to: redirect('/%{username}'), as: nil get '/', to: redirect('%{username}'), as: nil
end end
# Compatibility with old routing # Compatibility with old routing
# TODO (dzaporozhets): remove in 10.0 # TODO (dzaporozhets): remove in 10.0
get '/u/:username', to: redirect('/%{username}') get '/u/:username', to: redirect('%{username}')
# TODO (dzaporozhets): remove in 9.0 # TODO (dzaporozhets): remove in 9.0
get '/u/:username/groups', to: redirect('/users/%{username}/groups') get '/u/:username/groups', to: redirect('users/%{username}/groups')
get '/u/:username/projects', to: redirect('/users/%{username}/projects') get '/u/:username/projects', to: redirect('users/%{username}/projects')
get '/u/:username/snippets', to: redirect('/users/%{username}/snippets') get '/u/:username/snippets', to: redirect('users/%{username}/snippets')
get '/u/:username/contributed', to: redirect('/users/%{username}/contributed') get '/u/:username/contributed', to: redirect('users/%{username}/contributed')
end end
constraints(UserUrlConstrainer.new) do constraints(UserUrlConstrainer.new) do
......
...@@ -28,6 +28,12 @@ will be allowed to display the Performance Bar. ...@@ -28,6 +28,12 @@ will be allowed to display the Performance Bar.
Make sure _Enable the Performance Bar_ is checked and hit Make sure _Enable the Performance Bar_ is checked and hit
**Save** to save the changes. **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) ![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]. ...@@ -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 Currently the embedded Prometheus server is not automatically configured to
collect metrics from this endpoint. We recommend setting up another Prometheus collect metrics from this endpoint. We recommend setting up another Prometheus
server, because the embedded server configuration is overwritten once every 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 ## Metrics available
...@@ -45,6 +45,8 @@ In this experimental phase, only a few metrics are 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_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 | | 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 | | 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 ## Metrics shared directory
......
...@@ -106,6 +106,10 @@ Frontend security practices. ...@@ -106,6 +106,10 @@ Frontend security practices.
## [Accessibility](accessibility.md) ## [Accessibility](accessibility.md)
Our accessibility standards and resources. 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/ [rails]: http://rubyonrails.org/
[haml]: http://haml.info/ [haml]: http://haml.info/
......
...@@ -180,15 +180,43 @@ aren't in the message with id `1 pipeline`. ...@@ -180,15 +180,43 @@ aren't in the message with id `1 pipeline`.
## Working with special content ## 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 ### Interpolation
- In Ruby/HAML: - In Ruby/HAML:
```ruby ```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 ### Plurals
...@@ -234,14 +262,6 @@ Sometimes you need to add some context to the text that you want to translate ...@@ -234,14 +262,6 @@ Sometimes you need to add some context to the text that you want to translate
s__('OpenedNDaysAgo|Opened') 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 ## Adding a new language
Let's suppose you want to add translations for a new language, let's say French. 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 ...@@ -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 for AWS, Google, OpenStack Swift, Rackspace and Aliyun as well. A local driver is
[also available](#uploading-to-locally-mounted-shares). [also available](#uploading-to-locally-mounted-shares).
For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`: #### Using Amazon S3
```ruby For Omnibus GitLab packages:
gitlab_rails['backup_upload_connection'] = {
'provider' => 'AWS', 1. Add the following to `/etc/gitlab/gitlab.rb`:
'region' => 'eu-west-1',
'aws_access_key_id' => 'AKIAKIAKI', ```ruby
'aws_secret_access_key' => 'secret123' gitlab_rails['backup_upload_connection'] = {
# If using an IAM Profile, don't configure aws_access_key_id & aws_secret_access_key 'provider' => 'AWS',
# 'use_iam_profile' => true 'region' => 'eu-west-1',
} 'aws_access_key_id' => 'AKIAKIAKI',
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket' '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: For installations from source:
```yaml 1. Edit `home/git/gitlab/config/gitlab.yml`:
backup:
# snip ```yaml
upload: backup:
# Fog storage connection settings, see http://fog.io/storage/ . # snip
connection: upload:
provider: AWS # Fog storage connection settings, see http://fog.io/storage/ .
region: eu-west-1 connection:
aws_access_key_id: AKIAKIAKI provider: AWS
aws_secret_access_key: 'secret123' region: eu-west-1
# If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty aws_access_key_id: AKIAKIAKI
# ie. aws_access_key_id: '' aws_secret_access_key: 'secret123'
# use_iam_profile: 'true' # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
# The remote 'directory' to store your backups. For S3, this would be the bucket name. # ie. aws_access_key_id: ''
remote_directory: 'my.s3.bucket' # use_iam_profile: 'true'
# Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional # The remote 'directory' to store your backups. For S3, this would be the bucket name.
# encryption: 'AES256' remote_directory: 'my.s3.bucket'
# Specifies Amazon S3 storage class to use for backups, this is optional # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
# storage_class: 'STANDARD' # 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 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 IAM user with restricted access rights. To give the upload user access only for
...@@ -226,6 +236,50 @@ with the name of your bucket: ...@@ -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 ### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by 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 ...@@ -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. 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). 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 + ?' ...@@ -9,7 +9,7 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>n</kbd> | Main navigation | | <kbd>n</kbd> | Main navigation |
| <kbd>s</kbd> | Focus search | | <kbd>s</kbd> | Focus search |
| <kbd>f</kbd> | Focus filter | | <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> | Show/hide this dialog |
| <kbd></kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview | | <kbd></kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
| <kbd></kbd> | Edit last comment (when focused on an empty textarea) | | <kbd></kbd> | Edit last comment (when focused on an empty textarea) |
......
...@@ -94,7 +94,9 @@ module Gitlab ...@@ -94,7 +94,9 @@ module Gitlab
end end
def diff_file(repository) def diff_file(repository)
@diff_file ||= begin return @diff_file if defined?(@diff_file)
@diff_file = begin
if RequestStore.active? if RequestStore.active?
key = { key = {
project_id: repository.project.id, project_id: repository.project.id,
...@@ -122,8 +124,8 @@ module Gitlab ...@@ -122,8 +124,8 @@ module Gitlab
def find_diff_file(repository) def find_diff_file(repository)
return unless diff_refs.complete? return unless diff_refs.complete?
return unless comparison = diff_refs.compare_in(repository.project)
diff_refs.compare_in(repository.project).diffs(paths: paths, expanded: true).diff_files.first comparison.diffs(paths: paths, expanded: true).diff_files.first
end end
def get_formatter_class(type) def get_formatter_class(type)
......
...@@ -95,6 +95,29 @@ feature 'Admin updates settings' do ...@@ -95,6 +95,29 @@ feature 'Admin updates settings' do
expect(find_field('ED25519 SSH keys').value).to eq(forbidden) expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
end 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 def check_all_events
page.check('Active') page.check('Active')
page.check('Push') page.check('Push')
......
...@@ -19,9 +19,9 @@ feature 'Group milestones', :js do ...@@ -19,9 +19,9 @@ feature 'Group milestones', :js do
end end
it 'renders description preview' do 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') click_link('Preview')
...@@ -31,7 +31,7 @@ feature 'Group milestones', :js do ...@@ -31,7 +31,7 @@ feature 'Group milestones', :js do
click_link('Write') click_link('Write')
form.fill_in(:milestone_description, with: ':+1: Nice') description.native.send_keys(':+1: Nice')
click_link('Preview') click_link('Preview')
...@@ -51,6 +51,13 @@ feature 'Group milestones', :js do ...@@ -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')) expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y'))
end 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 end
context 'milestones list' do context 'milestones list' do
......
/* global IssuableContext */
import '~/issuable_context';
import $ from 'jquery'; import $ from 'jquery';
import IssuableContext from '~/issuable_context';
describe('IssuableContext', () => { describe('IssuableContext', () => {
describe('toggleHiddenParticipants', () => { describe('toggleHiddenParticipants', () => {
......
/* global IssuableIndex */ import IssuableIndex from '~/issuable_index';
import '~/lib/utils/url_utility'; describe('Issuable', () => {
import '~/issuable_index'; let Issuable;
describe('initBulkUpdate', () => {
(() => { it('should not set bulkUpdateSidebar', () => {
const BASE_URL = '/user/project/issues?scope=all&state=closed'; Issuable = new IssuableIndex('issue_');
const DEFAULT_PARAMS = '&utf8=%E2%9C%93'; expect(Issuable.bulkUpdateSidebar).not.toBeDefined();
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', '');
}); });
}
describe('Issuable', () => { it('should set bulkUpdateSidebar', () => {
preloadFixtures('static/issuable_filter.html.raw'); const element = document.createElement('div');
element.classList.add('issues-bulk-update');
document.body.appendChild(element);
beforeEach(() => { Issuable = new IssuableIndex('issue_');
loadFixtures('static/issuable_filter.html.raw'); expect(Issuable.bulkUpdateSidebar).toBeDefined();
IssuableIndex.init();
});
it('should be defined', () => {
expect(window.IssuableIndex).toBeDefined();
}); });
});
describe('filtering', () => { describe('resetIncomingEmailToken', () => {
let $filtersForm; beforeEach(() => {
const element = document.createElement('a');
beforeEach(() => { element.classList.add('incoming-email-token-reset');
$filtersForm = $('.js-filter-form'); element.setAttribute('href', 'foo');
loadFixtures('static/issuable_filter.html.raw'); document.body.appendChild(element);
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');
// initial filter const input = document.createElement('input');
updateForm({ milestone_title: 'v1.0' }, $filtersForm); input.setAttribute('id', 'issue_email');
document.body.appendChild(input);
IssuableIndex.filterResults($filtersForm); Issuable = new IssuableIndex('issue_');
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; });
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
// update filter it('should send request to reset email token', () => {
updateForm({ label_name: 'Frontend' }, $filtersForm); spyOn(jQuery, 'ajax').and.callThrough();
document.querySelector('.incoming-email-token-reset').click();
IssuableIndex.filterResults($filtersForm); expect(jQuery.ajax).toHaveBeenCalled();
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo');
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
});
}); });
}); });
})(); });
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global IssuableContext */ import IssuableContext from '~/issuable_context';
/* global LabelsSelect */ /* global LabelsSelect */
import '~/gl_dropdown'; import '~/gl_dropdown';
import 'select2'; import 'select2';
import '~/api'; import '~/api';
import '~/create_label'; import '~/create_label';
import '~/issuable_context';
import '~/users_select'; import '~/users_select';
import '~/labels_select'; import '~/labels_select';
......
...@@ -364,6 +364,43 @@ describe Gitlab::Diff::Position do ...@@ -364,6 +364,43 @@ describe Gitlab::Diff::Position do
end end
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 describe "position for a file in the initial commit" do
let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") } 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