diff --git a/.gitignore b/.gitignore
index 65befc20963..6bec6d2596f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,3 +82,4 @@ jsdoc/
**/tmp/rubocop_cache/**
.overcommit.yml
.projections.json
+/qa/.rakeTasks
diff --git a/Gemfile b/Gemfile
index ffe456cae08..d3689e4a211 100644
--- a/Gemfile
+++ b/Gemfile
@@ -159,6 +159,7 @@ gem 'icalendar'
# Diffs
gem 'diffy', '~> 3.1.0'
+gem 'diff_match_patch', '~> 0.1.0'
# Application server
gem 'rack', '~> 2.0.7'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7582eb28e49..5951eb4bf71 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -224,6 +224,7 @@ GEM
railties
rotp (~> 2.0)
diff-lcs (1.3)
+ diff_match_patch (0.1.0)
diffy (3.1.0)
discordrb-webhooks-blackst0ne (3.3.0)
rest-client (~> 2.0)
@@ -1133,6 +1134,7 @@ DEPENDENCIES
device_detector
devise (~> 4.6)
devise-two-factor (~> 3.0.0)
+ diff_match_patch (~> 0.1.0)
diffy (~> 3.1.0)
discordrb-webhooks-blackst0ne (~> 3.3)
doorkeeper (~> 4.3)
diff --git a/app/assets/javascripts/helpers/monitor_helper.js b/app/assets/javascripts/helpers/monitor_helper.js
index 900f0cf5bb8..d172aa8a444 100644
--- a/app/assets/javascripts/helpers/monitor_helper.js
+++ b/app/assets/javascripts/helpers/monitor_helper.js
@@ -1,11 +1,9 @@
-/* eslint-disable import/prefer-default-export */
-import _ from 'underscore';
-
/**
* @param {Array} queryResults - Array of Result objects
* @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
* @returns {Array} The formatted values
*/
+// eslint-disable-next-line import/prefer-default-export
export const makeDataSeries = (queryResults, defaultConfig) =>
queryResults
.map(result => {
@@ -19,10 +17,13 @@ export const makeDataSeries = (queryResults, defaultConfig) =>
if (name) {
series.name = `${defaultConfig.name}: ${name}`;
} else {
- const template = _.template(defaultConfig.name, {
- interpolate: /\{\{(.+?)\}\}/g,
+ series.name = defaultConfig.name;
+ Object.keys(result.metric).forEach(templateVar => {
+ const value = result.metric[templateVar];
+ const regex = new RegExp(`{{\\s*${templateVar}\\s*}}`, 'g');
+
+ series.name = series.name.replace(regex, value);
});
- series.name = template(result.metric);
}
return { ...defaultConfig, ...series };
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index a9e086fade8..9136a47d542 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-var, one-var, consistent-return */
+/* eslint-disable consistent-return */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
@@ -91,18 +91,17 @@ export default class Issue {
'click',
'.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen',
e => {
- var $button, shouldSubmit, url;
e.preventDefault();
e.stopImmediatePropagation();
- $button = $(e.currentTarget);
- shouldSubmit = $button.hasClass('btn-comment');
+ const $button = $(e.currentTarget);
+ const shouldSubmit = $button.hasClass('btn-comment');
if (shouldSubmit) {
Issue.submitNoteForm($button.closest('form'));
}
this.disableCloseReopenButton($button);
- url = $button.attr('href');
+ const url = $button.attr('href');
return axios
.put(url)
.then(({ data }) => {
@@ -139,16 +138,14 @@ export default class Issue {
}
static submitNoteForm(form) {
- var noteText;
- noteText = form.find('textarea.js-note-text').val();
+ const noteText = form.find('textarea.js-note-text').val();
if (noteText && noteText.trim().length > 0) {
return form.submit();
}
}
static initRelatedBranches() {
- var $container;
- $container = $('#related-branches');
+ const $container = $('#related-branches');
return axios
.get($container.data('url'))
.then(({ data }) => {
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 72de3b5d726..6abf723be9a 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, one-var, no-new, consistent-return, no-shadow, no-param-reassign, vars-on-top, no-lonely-if, no-else-return, dot-notation, no-empty */
+/* eslint-disable no-useless-return, func-names, no-underscore-dangle, no-new, consistent-return, no-shadow, no-param-reassign, no-lonely-if, no-else-return, dot-notation, no-empty */
/* global Issuable */
/* global ListLabel */
@@ -15,63 +15,39 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
export default class LabelsSelect {
constructor(els, options = {}) {
- var _this, $els;
- _this = this;
+ const _this = this;
- $els = $(els);
+ let $els = $(els);
if (!els) {
$els = $('.js-label-select');
}
$els.each((i, dropdown) => {
- var $block,
- $dropdown,
- $form,
- $loading,
- $selectbox,
- $sidebarCollapsedValue,
- $value,
- $dropdownMenu,
- abilityName,
- defaultLabel,
- issueUpdateURL,
- labelUrl,
- namespacePath,
- projectPath,
- saveLabelData,
- selectedLabel,
- showAny,
- showNo,
- $sidebarLabelTooltip,
- initialSelected,
- fieldName,
- showMenuAbove,
- $dropdownContainer;
- $dropdown = $(dropdown);
- $dropdownContainer = $dropdown.closest('.labels-filter');
- namespacePath = $dropdown.data('namespacePath');
- projectPath = $dropdown.data('projectPath');
- issueUpdateURL = $dropdown.data('issueUpdate');
- selectedLabel = $dropdown.data('selected');
+ const $dropdown = $(dropdown);
+ const $dropdownContainer = $dropdown.closest('.labels-filter');
+ const namespacePath = $dropdown.data('namespacePath');
+ const projectPath = $dropdown.data('projectPath');
+ const issueUpdateURL = $dropdown.data('issueUpdate');
+ let selectedLabel = $dropdown.data('selected');
if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
selectedLabel = selectedLabel.split(',');
}
- showNo = $dropdown.data('showNo');
- showAny = $dropdown.data('showAny');
- showMenuAbove = $dropdown.data('showMenuAbove');
- defaultLabel = $dropdown.data('defaultLabel') || __('Label');
- abilityName = $dropdown.data('abilityName');
- $selectbox = $dropdown.closest('.selectbox');
- $block = $selectbox.closest('.block');
- $form = $dropdown.closest('form, .js-issuable-update');
- $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
- $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
- $value = $block.find('.value');
- $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
- $loading = $block.find('.block-loading').fadeOut();
- fieldName = $dropdown.data('fieldName');
- initialSelected = $selectbox
+ const showNo = $dropdown.data('showNo');
+ const showAny = $dropdown.data('showAny');
+ const showMenuAbove = $dropdown.data('showMenuAbove');
+ const defaultLabel = $dropdown.data('defaultLabel') || __('Label');
+ const abilityName = $dropdown.data('abilityName');
+ const $selectbox = $dropdown.closest('.selectbox');
+ const $block = $selectbox.closest('.block');
+ const $form = $dropdown.closest('form, .js-issuable-update');
+ const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
+ const $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
+ const $value = $block.find('.value');
+ const $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
+ const $loading = $block.find('.block-loading').fadeOut();
+ const fieldName = $dropdown.data('fieldName');
+ let initialSelected = $selectbox
.find(`input[name="${$dropdown.data('fieldName')}"]`)
.map(function() {
return this.value;
@@ -90,9 +66,8 @@ export default class LabelsSelect {
);
}
- saveLabelData = function() {
- var data, selected;
- selected = $dropdown
+ const saveLabelData = function() {
+ const selected = $dropdown
.closest('.selectbox')
.find(`input[name='${fieldName}']`)
.map(function() {
@@ -103,7 +78,7 @@ export default class LabelsSelect {
if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected;
- data = {};
+ const data = {};
data[abilityName] = {};
data[abilityName].label_ids = selected;
if (!selected.length) {
@@ -114,12 +89,13 @@ export default class LabelsSelect {
axios
.put(issueUpdateURL, data)
.then(({ data }) => {
- var labelCount, template, labelTooltipTitle, labelTitles;
+ let labelTooltipTitle;
+ let template;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
data.issueUpdateURL = issueUpdateURL;
- labelCount = 0;
+ let labelCount = 0;
if (data.labels.length && issueUpdateURL) {
template = LabelsSelect.getLabelTemplate({
labels: _.sortBy(data.labels, 'title'),
@@ -174,7 +150,7 @@ export default class LabelsSelect {
$sidebarCollapsedValue.text(labelCount);
if (data.labels.length) {
- labelTitles = data.labels.map(label => label.title);
+ let labelTitles = data.labels.map(label => label.title);
if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5);
@@ -199,13 +175,13 @@ export default class LabelsSelect {
$dropdown.glDropdown({
showMenuAbove,
data(term, callback) {
- labelUrl = $dropdown.attr('data-labels');
+ const labelUrl = $dropdown.attr('data-labels');
axios
.get(labelUrl)
.then(res => {
let { data } = res;
if ($dropdown.hasClass('js-extra-options')) {
- var extraData = [];
+ const extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
@@ -232,22 +208,14 @@ export default class LabelsSelect {
.catch(() => flash(__('Error fetching labels.')));
},
renderRow(label) {
- var linkEl,
- listItemEl,
- colorEl,
- indeterminate,
- removesAll,
- selectedClass,
- i,
- marked,
- dropdownValue;
+ let colorEl;
- selectedClass = [];
- removesAll = label.id <= 0 || label.id == null;
+ const selectedClass = [];
+ const removesAll = label.id <= 0 || label.id == null;
if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = $dropdown.data('indeterminate') || [];
- marked = $dropdown.data('marked') || [];
+ const indeterminate = $dropdown.data('indeterminate') || [];
+ const marked = $dropdown.data('marked') || [];
if (indeterminate.indexOf(label.id) !== -1) {
selectedClass.push('is-indeterminate');
@@ -255,7 +223,7 @@ export default class LabelsSelect {
if (marked.indexOf(label.id) !== -1) {
// Remove is-indeterminate class if the item will be marked as active
- i = selectedClass.indexOf('is-indeterminate');
+ const i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
@@ -263,7 +231,7 @@ export default class LabelsSelect {
}
} else {
if (this.id(label)) {
- dropdownValue = this.id(label)
+ const dropdownValue = this.id(label)
.toString()
.replace(/'/g, "\\'");
@@ -287,7 +255,7 @@ export default class LabelsSelect {
colorEl = '';
}
- linkEl = document.createElement('a');
+ const linkEl = document.createElement('a');
linkEl.href = '#';
// We need to identify which items are actually labels
@@ -300,7 +268,7 @@ export default class LabelsSelect {
linkEl.className = selectedClass.join(' ');
linkEl.innerHTML = `${colorEl} ${_.escape(label.title)}`;
- listItemEl = document.createElement('li');
+ const listItemEl = document.createElement('li');
listItemEl.appendChild(linkEl);
return listItemEl;
@@ -312,12 +280,12 @@ export default class LabelsSelect {
filterable: true,
selected: $dropdown.data('selected') || [],
toggleLabel(selected, el) {
- var $dropdownParent = $dropdown.parent();
- var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
- var isSelected = el !== null ? el.hasClass('is-active') : false;
+ const $dropdownParent = $dropdown.parent();
+ const $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
+ const isSelected = el !== null ? el.hasClass('is-active') : false;
- var title = selected ? selected.title : null;
- var selectedLabels = this.selected;
+ const title = selected ? selected.title : null;
+ const selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
$dropdownParent.find('.dropdown-input-clear').trigger('click');
@@ -329,7 +297,7 @@ export default class LabelsSelect {
} else if (isSelected) {
this.selected.push(title);
} else if (!isSelected && title) {
- var index = this.selected.indexOf(title);
+ const index = this.selected.indexOf(title);
this.selected.splice(index, 1);
}
@@ -359,10 +327,9 @@ export default class LabelsSelect {
}
},
hidden() {
- var isIssueIndex, isMRIndex, page;
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide();
// display:block overrides the hide-collapse rule
$value.removeAttr('style');
@@ -393,14 +360,13 @@ export default class LabelsSelect {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
- var isIssueIndex, isMRIndex, page, boardsModel;
- var fadeOutLoader = () => {
+ const fadeOutLoader = () => {
$loading.fadeOut();
};
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown
@@ -419,6 +385,7 @@ export default class LabelsSelect {
return;
}
+ let boardsModel;
if ($dropdown.closest('.add-issues-modal').length) {
boardsModel = ModalStore.store.filter;
}
@@ -450,7 +417,7 @@ export default class LabelsSelect {
}),
);
} else {
- var { labels } = boardsStore.detail.issue;
+ let { labels } = boardsStore.detail.issue;
labels = labels.filter(selectedLabel => selectedLabel.id !== label.id);
boardsStore.detail.issue.labels = labels;
}
@@ -578,16 +545,14 @@ export default class LabelsSelect {
}
// eslint-disable-next-line class-methods-use-this
setDropdownData($dropdown, isMarking, value) {
- var i, markedIds, unmarkedIds, indeterminateIds;
-
- markedIds = $dropdown.data('marked') || [];
- unmarkedIds = $dropdown.data('unmarked') || [];
- indeterminateIds = $dropdown.data('indeterminate') || [];
+ const markedIds = $dropdown.data('marked') || [];
+ const unmarkedIds = $dropdown.data('unmarked') || [];
+ const indeterminateIds = $dropdown.data('indeterminate') || [];
if (isMarking) {
markedIds.push(value);
- i = indeterminateIds.indexOf(value);
+ let i = indeterminateIds.indexOf(value);
if (i > -1) {
indeterminateIds.splice(i, 1);
}
@@ -598,7 +563,7 @@ export default class LabelsSelect {
}
} else {
// If marked item (not common) is unmarked
- i = markedIds.indexOf(value);
+ const i = markedIds.indexOf(value);
if (i > -1) {
markedIds.splice(i, 1);
}
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index a670becf91a..6a8e3cc82f5 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -84,7 +84,10 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
.then(resp => resp.data)
.then(response => {
- dispatch('receiveMetricsDashboardSuccess', { response, params });
+ dispatch('receiveMetricsDashboardSuccess', {
+ response,
+ params,
+ });
})
.catch(error => {
dispatch('receiveMetricsDashboardFailure', error);
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 26d4c56ca78..696af5aed75 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -94,7 +94,7 @@ export default {
state.emptyState = 'noData';
},
[types.SET_ALL_DASHBOARDS](state, dashboards) {
- state.allDashboards = dashboards;
+ state.allDashboards = dashboards || [];
},
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
state.showErrorBanner = enabled;
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 3158e086f6c..e4f09492d9c 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -101,6 +101,7 @@ export default {
', - paragraphEnd: '
', - descriptionChangedTimes, - timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes), - }, - false, - ); - - descriptionNote.times_updated = descriptionChangedTimes; - - return descriptionNote; -}; - /** * Checks the time difference between two notes from their 'created_at' dates * returns an integer */ - export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => { const descriptionNoteBegin = new Date(noteBeggining.created_at); const descriptionNoteEnd = new Date(noteEnd.created_at); @@ -57,7 +32,6 @@ export const isDescriptionSystemNote = note => note.system && note.note === DESC export const collapseSystemNotes = notes => { let lastDescriptionSystemNote = null; let lastDescriptionSystemNoteIndex = -1; - let descriptionChangedTimes = 1; return notes.slice(0).reduce((acc, currentNote) => { const note = currentNote.notes[0]; @@ -70,32 +44,24 @@ export const collapseSystemNotes = notes => { } else if (lastDescriptionSystemNote) { const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note); - // are they less than 10 minutes apart? - if (timeDifferenceMinutes > 10) { - // reset counter - descriptionChangedTimes = 1; + // are they less than 10 minutes apart from the same user? + if (timeDifferenceMinutes > 10 || note.author.id !== lastDescriptionSystemNote.author.id) { // update the previous system note lastDescriptionSystemNote = note; lastDescriptionSystemNoteIndex = acc.length; } else { - // increase counter - descriptionChangedTimes += 1; + // set the first version to fetch grouped system note versions + note.start_description_version_id = lastDescriptionSystemNote.description_version_id; // delete the previous one acc.splice(lastDescriptionSystemNoteIndex, 1); - // replace the text of the current system note with the collapsed note. - currentNote.notes.splice( - 0, - 1, - changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes), - ); - // update the previous system note index lastDescriptionSystemNoteIndex = acc.length; } } } + acc.push(currentNote); return acc; }, []); diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 95f8270b5d0..5a6f9370564 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -8,12 +8,13 @@ import { GlModalDirective, GlEmptyState, } from '@gitlab/ui'; -import createFlash from '../../flash'; -import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; -import Icon from '../../vue_shared/components/icon.vue'; +import createFlash from '~/flash'; +import Tracking from '~/tracking'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import Icon from '~/vue_shared/components/icon.vue'; import TableRegistry from './table_registry.vue'; -import { errorMessages, errorMessagesTypes } from '../constants'; -import { __ } from '../../locale'; +import { DELETE_REPO_ERROR_MESSAGE } from '../constants'; +import { __ } from '~/locale'; export default { name: 'CollapsibeContainerRegisty', @@ -30,6 +31,7 @@ export default { GlTooltip: GlTooltipDirective, GlModal: GlModalDirective, }, + mixins: [Tracking.mixin({})], props: { repo: { type: Object, @@ -40,6 +42,10 @@ export default { return { isOpen: false, modalId: `confirm-repo-deletion-modal-${this.repo.id}`, + tracking: { + category: document.body.dataset.page, + label: 'registry_repository_delete', + }, }; }, computed: { @@ -61,15 +67,13 @@ export default { } }, handleDeleteRepository() { + this.track('confirm_delete', {}); return this.deleteItem(this.repo) .then(() => { createFlash(__('This container registry has been scheduled for deletion.'), 'notice'); this.fetchRepos(); }) - .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); - }, - showError(message) { - createFlash(errorMessages[message]); + .catch(() => createFlash(DELETE_REPO_ERROR_MESSAGE)); }, }, }; @@ -97,10 +101,9 @@ export default { v-gl-modal="modalId" :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" - data-track-event="click_button" - data-track-label="registry_repository_delete" class="js-remove-repo btn-inverted" variant="danger" + @click="track('click_button', {})" >
changed the description 2 times within 1 minute changed the description changed the description 3 times within 5 minutes
+
+
+