Skip to content
Snippets Groups Projects
Commit 7cc68724 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 46b10c0f
No related branches found
No related tags found
No related merge requests found
Showing
with 935 additions and 736 deletions
/* eslint-disable func-names, no-underscore-dangle, one-var, no-cond-assign, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, no-param-reassign, no-loop-func */ /* eslint-disable one-var, consistent-return */
   
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
Loading
@@ -32,121 +32,124 @@ const FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-fil
Loading
@@ -32,121 +32,124 @@ const FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-fil
   
const NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter'; const NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
   
function GitLabDropdownInput(input, options) { class GitLabDropdownInput {
const _this = this; constructor(input, options) {
this.input = input; this.input = input;
this.options = options; this.options = options;
this.fieldName = this.options.fieldName || 'field-name'; this.fieldName = this.options.fieldName || 'field-name';
const $inputContainer = this.input.parent(); const $inputContainer = this.input.parent();
const $clearButton = $inputContainer.find('.js-dropdown-input-clear'); const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', e => { $clearButton.on('click', e => {
// Clear click // Clear click
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
return this.input return this.input
.val('') .val('')
.trigger('input') .trigger('input')
.focus(); .focus();
});
this.input
.on('keydown', e => {
const keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', e => {
let val = e.currentTarget.value || _this.options.inputFieldName;
val = val
.split(' ')
.join('-') // replaces space with dash
.replace(/[^a-zA-Z0-9 -]/g, '')
.toLowerCase() // replace non alphanumeric
.replace(/(-)\1+/g, '-'); // replace repeated dashes
_this.cb(_this.options.fieldName, val, {}, true);
_this.input
.closest('.dropdown')
.find('.dropdown-toggle-text')
.text(val);
}); });
}
   
GitLabDropdownInput.prototype.onInput = function(cb) { this.input
this.cb = cb; .on('keydown', e => {
}; const keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', e => {
let val = e.currentTarget.value || this.options.inputFieldName;
val = val
.split(' ')
.join('-') // replaces space with dash
.replace(/[^a-zA-Z0-9 -]/g, '')
.toLowerCase() // replace non alphanumeric
.replace(/(-)\1+/g, '-'); // replace repeated dashes
this.cb(this.options.fieldName, val, {}, true);
this.input
.closest('.dropdown')
.find('.dropdown-toggle-text')
.text(val);
});
}
   
function GitLabDropdownFilter(input, options) { onInput(cb) {
let ref, timeout; this.cb = cb;
this.input = input; }
this.options = options;
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
const $inputContainer = this.input.parent();
const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', e => {
// Clear click
e.preventDefault();
e.stopPropagation();
return this.input
.val('')
.trigger('input')
.focus();
});
// Key events
timeout = '';
this.input
.on('keydown', e => {
const keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', () => {
if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.addClass(HAS_VALUE_CLASS);
} else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.removeClass(HAS_VALUE_CLASS);
}
// Only filter asynchronously only if option remote is set
if (this.options.remote) {
clearTimeout(timeout);
return (timeout = setTimeout(() => {
$inputContainer.parent().addClass('is-loading');
return this.options.query(this.input.val(), data => {
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
});
}, 250));
} else {
return this.filter(this.input.val());
}
});
} }
   
GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { class GitLabDropdownFilter {
return BLUR_KEYCODES.indexOf(keyCode) !== -1; constructor(input, options) {
}; let ref, timeout;
this.input = input;
this.options = options;
// eslint-disable-next-line no-cond-assign
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
const $inputContainer = this.input.parent();
const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', e => {
// Clear click
e.preventDefault();
e.stopPropagation();
return this.input
.val('')
.trigger('input')
.focus();
});
// Key events
timeout = '';
this.input
.on('keydown', e => {
const keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', () => {
if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.addClass(HAS_VALUE_CLASS);
} else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.removeClass(HAS_VALUE_CLASS);
}
// Only filter asynchronously only if option remote is set
if (this.options.remote) {
clearTimeout(timeout);
// eslint-disable-next-line no-return-assign
return (timeout = setTimeout(() => {
$inputContainer.parent().addClass('is-loading');
return this.options.query(this.input.val(), data => {
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
});
}, 250));
}
return this.filter(this.input.val());
});
}
   
GitLabDropdownFilter.prototype.filter = function(search_text) { static shouldBlur(keyCode) {
let elements, group, key, results, tmp; return BLUR_KEYCODES.indexOf(keyCode) !== -1;
if (this.options.onFilter) { }
this.options.onFilter(search_text);
} filter(searchText) {
const data = this.options.data(); let group, results, tmp;
if (data != null && !this.options.filterByText) { if (this.options.onFilter) {
results = data; this.options.onFilter(searchText);
if (search_text !== '') { }
// When data is an array of objects therefore [object Array] e.g. const data = this.options.data();
// [ if (data != null && !this.options.filterByText) {
// { prop: 'foo' }, results = data;
// { prop: 'baz' } if (searchText !== '') {
// ] // When data is an array of objects therefore [object Array] e.g.
if (_.isArray(data)) { // [
results = fuzzaldrinPlus.filter(data, search_text, { // { prop: 'foo' },
key: this.options.keys, // { prop: 'baz' }
}); // ]
} else { if (_.isArray(data)) {
results = fuzzaldrinPlus.filter(data, searchText, {
key: this.options.keys,
});
}
// If data is grouped therefore an [object Object]. e.g. // If data is grouped therefore an [object Object]. e.g.
// { // {
// groupName1: [ // groupName1: [
Loading
@@ -158,33 +161,32 @@ GitLabDropdownFilter.prototype.filter = function(search_text) {
Loading
@@ -158,33 +161,32 @@ GitLabDropdownFilter.prototype.filter = function(search_text) {
// { prop: 'def' } // { prop: 'def' }
// ] // ]
// } // }
if (isObject(data)) { else if (isObject(data)) {
results = {}; results = {};
for (key in data) { Object.keys(data).forEach(key => {
group = data[key]; group = data[key];
tmp = fuzzaldrinPlus.filter(group, search_text, { tmp = fuzzaldrinPlus.filter(group, searchText, {
key: this.options.keys, key: this.options.keys,
}); });
if (tmp.length) { if (tmp.length) {
results[key] = tmp.map(item => item); results[key] = tmp.map(item => item);
} }
} });
} }
} }
return this.options.callback(results);
} }
return this.options.callback(results); const elements = this.options.elements();
} else { if (searchText) {
elements = this.options.elements(); // eslint-disable-next-line func-names
if (search_text) {
elements.each(function() { elements.each(function() {
const $el = $(this); const $el = $(this);
const matches = fuzzaldrinPlus.match($el.text().trim(), search_text); const matches = fuzzaldrinPlus.match($el.text().trim(), searchText);
if (!$el.is('.dropdown-header')) { if (!$el.is('.dropdown-header')) {
if (matches.length) { if (matches.length) {
return $el.show().removeClass('option-hidden'); return $el.show().removeClass('option-hidden');
} else {
return $el.hide().addClass('option-hidden');
} }
return $el.hide().addClass('option-hidden');
} }
}); });
} else { } else {
Loading
@@ -196,235 +198,240 @@ GitLabDropdownFilter.prototype.filter = function(search_text) {
Loading
@@ -196,235 +198,240 @@ GitLabDropdownFilter.prototype.filter = function(search_text) {
.find('.dropdown-menu-empty-item') .find('.dropdown-menu-empty-item')
.toggleClass('hidden', elements.is(':visible')); .toggleClass('hidden', elements.is(':visible'));
} }
};
function GitLabDropdownRemote(dataEndpoint, options) {
this.dataEndpoint = dataEndpoint;
this.options = options;
} }
   
GitLabDropdownRemote.prototype.execute = function() { class GitLabDropdownRemote {
if (typeof this.dataEndpoint === 'string') { constructor(dataEndpoint, options) {
return this.fetchData(); this.dataEndpoint = dataEndpoint;
} else if (typeof this.dataEndpoint === 'function') { this.options = options;
}
execute() {
if (typeof this.dataEndpoint === 'string') {
return this.fetchData();
} else if (typeof this.dataEndpoint === 'function') {
if (this.options.beforeSend) {
this.options.beforeSend();
}
return this.dataEndpoint('', data => {
// Fetch the data by calling the data function
if (this.options.success) {
this.options.success(data);
}
if (this.options.beforeSend) {
return this.options.beforeSend();
}
});
}
}
fetchData() {
if (this.options.beforeSend) { if (this.options.beforeSend) {
this.options.beforeSend(); this.options.beforeSend();
} }
return this.dataEndpoint('', data => {
// Fetch the data by calling the data function // Fetch the data through ajax if the data is a string
return axios.get(this.dataEndpoint).then(({ data }) => {
if (this.options.success) { if (this.options.success) {
this.options.success(data); return this.options.success(data);
}
if (this.options.beforeSend) {
return this.options.beforeSend();
} }
}); });
} }
}; }
GitLabDropdownRemote.prototype.fetchData = function() {
if (this.options.beforeSend) {
this.options.beforeSend();
}
   
// Fetch the data through ajax if the data is a string class GitLabDropdown {
return axios.get(this.dataEndpoint).then(({ data }) => { constructor(el1, options) {
if (this.options.success) { let selector, self;
return this.options.success(data); this.el = el1;
this.options = options;
this.updateLabel = this.updateLabel.bind(this);
this.hidden = this.hidden.bind(this);
this.opened = this.opened.bind(this);
this.shouldPropagate = this.shouldPropagate.bind(this);
self = this;
selector = $(this.el).data('target');
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
this.highlight = Boolean(this.options.highlight);
this.icon = Boolean(this.options.icon);
this.filterInputBlur =
this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
// If no input is passed create a default one
self = this;
// If selector was passed
if (_.isString(this.filterInput)) {
this.filterInput = this.getElement(this.filterInput);
} }
}); const searchFields = this.options.search ? this.options.search.fields : [];
}; if (this.options.data) {
// If we provided data
function GitLabDropdown(el1, options) { // data could be an array of objects or a group of arrays
let selector, self; if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
this.el = el1; this.fullData = this.options.data;
this.options = options; currentIndex = -1;
this.updateLabel = this.updateLabel.bind(this); this.parseData(this.options.data);
this.hidden = this.hidden.bind(this); this.focusTextInput();
this.opened = this.opened.bind(this); } else {
this.shouldPropagate = this.shouldPropagate.bind(this); this.remote = new GitLabDropdownRemote(this.options.data, {
self = this; dataType: this.options.dataType,
selector = $(this.el).data('target'); beforeSend: this.toggleLoading.bind(this),
this.dropdown = selector != null ? $(selector) : $(this.el).parent(); success: data => {
// Set Defaults this.fullData = data;
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); this.parseData(this.fullData);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT); this.focusTextInput();
this.highlight = Boolean(this.options.highlight);
this.icon = Boolean(this.options.icon); // Update dropdown position since remote data may have changed dropdown size
this.filterInputBlur = this.options.filterInputBlur != null ? this.options.filterInputBlur : true; this.dropdown.find('.dropdown-menu-toggle').dropdown('update');
// If no input is passed create a default one
self = this; if (
// If selector was passed this.options.filterable &&
if (_.isString(this.filterInput)) { this.filter &&
this.filterInput = this.getElement(this.filterInput); this.filter.input &&
} this.filter.input.val() &&
const searchFields = this.options.search ? this.options.search.fields : []; this.filter.input.val().trim() !== ''
if (this.options.data) { ) {
// If we provided data return this.filter.input.trigger('input');
// data could be an array of objects or a group of arrays }
if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { },
this.fullData = this.options.data; instance: this,
currentIndex = -1; });
this.parseData(this.options.data); }
this.focusTextInput();
} else {
this.remote = new GitLabDropdownRemote(this.options.data, {
dataType: this.options.dataType,
beforeSend: this.toggleLoading.bind(this),
success: data => {
this.fullData = data;
this.parseData(this.fullData);
this.focusTextInput();
// Update dropdown position since remote data may have changed dropdown size
this.dropdown.find('.dropdown-menu-toggle').dropdown('update');
if (
this.options.filterable &&
this.filter &&
this.filter.input &&
this.filter.input.val() &&
this.filter.input.val().trim() !== ''
) {
return this.filter.input.trigger('input');
}
},
instance: this,
});
} }
} if (this.noFilterInput.length) {
if (this.noFilterInput.length) { this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options);
this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options); this.plainInput.onInput(this.addInput.bind(this));
this.plainInput.onInput(this.addInput.bind(this)); }
} // Init filterable
// Init filterable if (this.options.filterable) {
if (this.options.filterable) { this.filter = new GitLabDropdownFilter(this.filterInput, {
this.filter = new GitLabDropdownFilter(this.filterInput, { elIsInput: $(this.el).is('input'),
elIsInput: $(this.el).is('input'), filterInputBlur: this.filterInputBlur,
filterInputBlur: this.filterInputBlur, filterByText: this.options.filterByText,
filterByText: this.options.filterByText, onFilter: this.options.onFilter,
onFilter: this.options.onFilter, remote: this.options.filterRemote,
remote: this.options.filterRemote, query: this.options.data,
query: this.options.data, keys: searchFields,
keys: searchFields, instance: this,
instance: this, elements: () => {
elements: () => { selector = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
selector = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = `.dropdown-page-one ${selector}`;
}
return $(selector, this.dropdown);
},
data: () => this.fullData,
callback: data => {
this.parseData(data);
if (this.filterInput.val() !== '') {
selector = SELECTABLE_CLASSES;
if (this.dropdown.find('.dropdown-toggle-page').length) { if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = `.dropdown-page-one ${selector}`; selector = `.dropdown-page-one ${selector}`;
} }
if ($(this.el).is('input')) { return $(selector, this.dropdown);
currentIndex = -1; },
} else { data: () => this.fullData,
$(selector, this.dropdown) callback: data => {
.first() this.parseData(data);
.find('a') if (this.filterInput.val() !== '') {
.addClass('is-focused'); selector = SELECTABLE_CLASSES;
currentIndex = 0; if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = `.dropdown-page-one ${selector}`;
}
if ($(this.el).is('input')) {
currentIndex = -1;
} else {
$(selector, this.dropdown)
.first()
.find('a')
.addClass('is-focused');
currentIndex = 0;
}
} }
} },
}, });
});
}
// Event listeners
this.dropdown.on('shown.bs.dropdown', this.opened);
this.dropdown.on('hidden.bs.dropdown', this.hidden);
$(this.el).on('update.label', this.updateLabel);
this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
this.dropdown.on('keyup', e => {
// Escape key
if (e.which === 27) {
return $('.dropdown-menu-close', this.dropdown).trigger('click');
} }
}); // Event listeners
this.dropdown.on('blur', 'a', e => { this.dropdown.on('shown.bs.dropdown', this.opened);
let $dropdownMenu, $relatedTarget; this.dropdown.on('hidden.bs.dropdown', this.hidden);
if (e.relatedTarget != null) { $(this.el).on('update.label', this.updateLabel);
$relatedTarget = $(e.relatedTarget); this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu'); this.dropdown.on('keyup', e => {
if ($dropdownMenu.length === 0) { // Escape key
return this.dropdown.removeClass('show'); if (e.which === 27) {
return $('.dropdown-menu-close', this.dropdown).trigger('click');
}
});
this.dropdown.on('blur', 'a', e => {
let $dropdownMenu, $relatedTarget;
if (e.relatedTarget != null) {
$relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
if ($dropdownMenu.length === 0) {
return this.dropdown.removeClass('show');
}
} }
}
});
if (this.dropdown.find('.dropdown-toggle-page').length) {
this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on('click', e => {
e.preventDefault();
e.stopPropagation();
return this.togglePage();
}); });
}
if (this.options.selectable) {
selector = '.dropdown-content a';
if (this.dropdown.find('.dropdown-toggle-page').length) { if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = '.dropdown-page-one .dropdown-content a'; this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on('click', e => {
} e.preventDefault();
this.dropdown.on('click', selector, e => { e.stopPropagation();
const $el = $(e.currentTarget); return this.togglePage();
const selected = self.rowClicked($el); });
const selectedObj = selected ? selected[0] : null; }
const isMarking = selected ? selected[1] : null; if (this.options.selectable) {
if (this.options.clicked) { selector = '.dropdown-content a';
this.options.clicked.call(this, { if (this.dropdown.find('.dropdown-toggle-page').length) {
selectedObj, selector = '.dropdown-page-one .dropdown-content a';
$el,
e,
isMarking,
});
} }
this.dropdown.on('click', selector, e => {
const $el = $(e.currentTarget);
const selected = self.rowClicked($el);
const selectedObj = selected ? selected[0] : null;
const isMarking = selected ? selected[1] : null;
if (this.options.clicked) {
this.options.clicked.call(this, {
selectedObj,
$el,
e,
isMarking,
});
}
   
// Update label right after all modifications in dropdown has been done // Update label right after all modifications in dropdown has been done
if (this.options.toggleLabel) { if (this.options.toggleLabel) {
this.updateLabel(selectedObj, $el, this); this.updateLabel(selectedObj, $el, this);
} }
   
$el.trigger('blur'); $el.trigger('blur');
}); });
}
} }
}
   
// Finds an element inside wrapper element // Finds an element inside wrapper element
GitLabDropdown.prototype.getElement = function(selector) { getElement(selector) {
return this.dropdown.find(selector); return this.dropdown.find(selector);
}; }
   
GitLabDropdown.prototype.toggleLoading = function() { toggleLoading() {
return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
}; }
   
GitLabDropdown.prototype.togglePage = function() { togglePage() {
const menu = $('.dropdown-menu', this.dropdown); const menu = $('.dropdown-menu', this.dropdown);
if (menu.hasClass(PAGE_TWO_CLASS)) { if (menu.hasClass(PAGE_TWO_CLASS)) {
if (this.remote) { if (this.remote) {
this.remote.execute(); this.remote.execute();
}
} }
menu.toggleClass(PAGE_TWO_CLASS);
// Focus first visible input on active page
return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
} }
menu.toggleClass(PAGE_TWO_CLASS);
// Focus first visible input on active page
return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
};
   
GitLabDropdown.prototype.parseData = function(data) { parseData(data) {
let groupData, html, name; let groupData, html;
this.renderedData = data; this.renderedData = data;
if (this.options.filterable && data.length === 0) { if (this.options.filterable && data.length === 0) {
// render no matching results // render no matching results
html = [this.noResults()]; html = [this.noResults()];
} else { }
// Handle array groups // Handle array groups
if (isObject(data)) { else if (isObject(data)) {
html = []; html = [];
for (name in data) {
Object.keys(data).forEach(name => {
groupData = data[name]; groupData = data[name];
html.push( html.push(
this.renderItem( this.renderItem(
Loading
@@ -436,461 +443,455 @@ GitLabDropdown.prototype.parseData = function(data) {
Loading
@@ -436,461 +443,455 @@ GitLabDropdown.prototype.parseData = function(data) {
), ),
); );
this.renderData(groupData, name).map(item => html.push(item)); this.renderData(groupData, name).map(item => html.push(item));
} });
} else { } else {
// Render each row // Render each row
html = this.renderData(data); html = this.renderData(data);
} }
} // Render the full menu
// Render the full menu const fullHtml = this.renderMenu(html);
const full_html = this.renderMenu(html); return this.appendMenu(fullHtml);
return this.appendMenu(full_html); }
};
renderData(data, group) {
GitLabDropdown.prototype.renderData = function(data, group) { return data.map((obj, index) => this.renderItem(obj, group || false, index));
return data.map((obj, index) => this.renderItem(obj, group || false, index)); }
};
shouldPropagate(e) {
GitLabDropdown.prototype.shouldPropagate = function(e) { let $target;
let $target; if (this.options.multiSelect || this.options.shouldPropagate === false) {
if (this.options.multiSelect || this.options.shouldPropagate === false) { $target = $(e.target);
$target = $(e.target); if (
if ( $target &&
$target && !$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') &&
!$target.hasClass('dropdown-menu-close-icon') && !$target.data('isLink')
!$target.data('isLink') ) {
) { e.stopPropagation();
e.stopPropagation();
// This prevents automatic scrolling to the top
// This prevents automatic scrolling to the top if ($target.closest('a').length) {
if ($target.closest('a').length) { return false;
return false; }
} }
}
   
return true; return true;
}
} }
};
GitLabDropdown.prototype.filteredFullData = function() {
return this.fullData.filter(
r =>
typeof r === 'object' &&
!Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
!Object.prototype.hasOwnProperty.call(r, 'header'),
);
};
   
GitLabDropdown.prototype.opened = function(e) { filteredFullData() {
this.resetRows(); return this.fullData.filter(
this.addArrowKeyEvent(); r =>
typeof r === 'object' &&
const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle'); !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update'); !Object.prototype.hasOwnProperty.call(r, 'header'),
const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open');
const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
// Makes indeterminate items effective
if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) {
this.parseData(this.fullData);
}
// Process the data to make sure rendered data
// matches the correct layout
const inputValue = this.filterInput.val();
if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
this.options.processData.call(
this.options,
inputValue,
this.filteredFullData(),
this.parseData.bind(this),
); );
} }
   
const contentHtml = $('.dropdown-content', this.dropdown).html(); opened(e) {
if (this.remote && contentHtml === '') { this.resetRows();
this.remote.execute(); this.addArrowKeyEvent();
} else {
this.focusTextInput();
}
   
if (this.options.showMenuAbove) { const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
this.positionMenuAbove(); const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
} const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open');
const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
   
if (this.options.opened) { // Makes indeterminate items effective
if (this.options.preserveContext) { if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) {
this.options.opened(e); this.parseData(this.fullData);
} else {
this.options.opened.call(this, e);
} }
}
   
return this.dropdown.trigger('shown.gl.dropdown'); // Process the data to make sure rendered data
}; // matches the correct layout
const inputValue = this.filterInput.val();
if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
this.options.processData.call(
this.options,
inputValue,
this.filteredFullData(),
this.parseData.bind(this),
);
}
   
GitLabDropdown.prototype.positionMenuAbove = function() { const contentHtml = $('.dropdown-content', this.dropdown).html();
const $menu = this.dropdown.find('.dropdown-menu'); if (this.remote && contentHtml === '') {
this.remote.execute();
} else {
this.focusTextInput();
}
   
$menu.addClass('dropdown-open-top'); if (this.options.showMenuAbove) {
$menu.css('top', 'initial'); this.positionMenuAbove();
$menu.css('bottom', '100%'); }
};
if (this.options.opened) {
if (this.options.preserveContext) {
this.options.opened(e);
} else {
this.options.opened.call(this, e);
}
}
   
GitLabDropdown.prototype.hidden = function(e) { return this.dropdown.trigger('shown.gl.dropdown');
this.resetRows();
this.removeArrowKeyEvent();
const $input = this.dropdown.find('.dropdown-input-field');
if (this.options.filterable) {
$input.blur();
} }
if (this.dropdown.find('.dropdown-toggle-page').length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); positionMenuAbove() {
const $menu = this.dropdown.find('.dropdown-menu');
$menu.addClass('dropdown-open-top');
$menu.css('top', 'initial');
$menu.css('bottom', '100%');
} }
if (this.options.hidden) {
this.options.hidden.call(this, e); hidden(e) {
this.resetRows();
this.removeArrowKeyEvent();
const $input = this.dropdown.find('.dropdown-input-field');
if (this.options.filterable) {
$input.blur();
}
if (this.dropdown.find('.dropdown-toggle-page').length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
}
if (this.options.hidden) {
this.options.hidden.call(this, e);
}
return this.dropdown.trigger('hidden.gl.dropdown');
} }
return this.dropdown.trigger('hidden.gl.dropdown');
};
   
// Render the full menu // Render the full menu
GitLabDropdown.prototype.renderMenu = function(html) { renderMenu(html) {
if (this.options.renderMenu) { if (this.options.renderMenu) {
return this.options.renderMenu(html); return this.options.renderMenu(html);
} else { }
return $('<ul>').append(html); return $('<ul>').append(html);
} }
};
   
// Append the menu into the dropdown // Append the menu into the dropdown
GitLabDropdown.prototype.appendMenu = function(html) { appendMenu(html) {
return this.clearMenu().append(html); return this.clearMenu().append(html);
}; }
   
GitLabDropdown.prototype.clearMenu = function() { clearMenu() {
let selector; let selector = '.dropdown-content';
selector = '.dropdown-content'; if (this.dropdown.find('.dropdown-toggle-page').length) {
if (this.dropdown.find('.dropdown-toggle-page').length) { if (this.options.containerSelector) {
if (this.options.containerSelector) { selector = this.options.containerSelector;
selector = this.options.containerSelector; } else {
} else { selector = '.dropdown-page-one .dropdown-content';
selector = '.dropdown-page-one .dropdown-content'; }
} }
return $(selector, this.dropdown).empty();
} }
   
return $(selector, this.dropdown).empty(); renderItem(data, group, index) {
}; let parent;
   
GitLabDropdown.prototype.renderItem = function(data, group, index) { if (this.dropdown && this.dropdown[0]) {
let parent; parent = this.dropdown[0].parentNode;
}
if (this.dropdown && this.dropdown[0]) {
parent = this.dropdown[0].parentNode;
}
return renderItem({
instance: this,
options: Object.assign({}, this.options, {
icon: this.icon,
highlight: this.highlight,
highlightText: text => this.highlightTextMatches(text, this.filterInput.val()),
highlightTemplate: this.highlightTemplate.bind(this),
parent,
}),
data,
group,
index,
});
};
   
GitLabDropdown.prototype.highlightTemplate = function(text, template) { return renderItem({
return `"<b>${_.escape(text)}</b>" ${template}`; instance: this,
}; options: Object.assign({}, this.options, {
icon: this.icon,
highlight: this.highlight,
highlightText: text => this.highlightTextMatches(text, this.filterInput.val()),
highlightTemplate: this.highlightTemplate.bind(this),
parent,
}),
data,
group,
index,
});
}
   
GitLabDropdown.prototype.highlightTextMatches = function(text, term) { // eslint-disable-next-line class-methods-use-this
const occurrences = fuzzaldrinPlus.match(text, term); highlightTemplate(text, template) {
const { indexOf } = []; return `"<b>${_.escape(text)}</b>" ${template}`;
}
   
return text // eslint-disable-next-line class-methods-use-this
.split('') highlightTextMatches(text, term) {
.map((character, i) => { const occurrences = fuzzaldrinPlus.match(text, term);
if (indexOf.call(occurrences, i) !== -1) { const { indexOf } = [];
return `<b>${character}</b>`;
} else { return text
.split('')
.map((character, i) => {
if (indexOf.call(occurrences, i) !== -1) {
return `<b>${character}</b>`;
}
return character; return character;
})
.join('');
}
// eslint-disable-next-line class-methods-use-this
noResults() {
return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>';
}
rowClicked(el) {
let field, groupName, selectedIndex, selectedObject, isMarking;
const { fieldName } = this.options;
const isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
if (groupName) {
selectedIndex = el.data('index');
selectedObject = this.renderedData[groupName][selectedIndex];
} else {
selectedIndex = el.closest('li').index();
this.selectedIndex = selectedIndex;
selectedObject = this.renderedData[selectedIndex];
} }
}) }
.join('');
};
   
GitLabDropdown.prototype.noResults = function() { if (this.options.vue) {
return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>'; if (el.hasClass(ACTIVE_CLASS)) {
}; el.removeClass(ACTIVE_CLASS);
} else {
el.addClass(ACTIVE_CLASS);
}
   
GitLabDropdown.prototype.rowClicked = function(el) { return [selectedObject];
let field, groupName, selectedIndex, selectedObject, isMarking;
const { fieldName } = this.options;
const isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
if (groupName) {
selectedIndex = el.data('index');
selectedObject = this.renderedData[groupName][selectedIndex];
} else {
selectedIndex = el.closest('li').index();
this.selectedIndex = selectedIndex;
selectedObject = this.renderedData[selectedIndex];
} }
}
   
if (this.options.vue) { field = [];
if (el.hasClass(ACTIVE_CLASS)) { const value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
el.removeClass(ACTIVE_CLASS); if (isInput) {
} else { field = $(this.el);
el.addClass(ACTIVE_CLASS); } else if (value != null) {
field = this.dropdown
.parent()
.find(`input[name='${fieldName}'][value='${value.toString().replace(/'/g, "\\'")}']`);
} }
   
return [selectedObject]; if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
} return [selectedObject];
}
   
field = []; if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
const value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; isMarking = false;
if (isInput) { el.removeClass(ACTIVE_CLASS);
field = $(this.el); if (field && field.length) {
} else if (value != null) { this.clearField(field, isInput);
field = this.dropdown }
.parent() } else if (el.hasClass(INDETERMINATE_CLASS)) {
.find(`input[name='${fieldName}'][value='${value.toString().replace(/'/g, "\\'")}']`); isMarking = true;
} el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { if (field && field.length && value == null) {
return [selectedObject]; this.clearField(field, isInput);
}
if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
this.clearField(field, isInput);
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
isMarking = true;
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (field && field.length && value == null) {
this.clearField(field, isInput);
}
if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
}
} else {
isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find(`.${ACTIVE_CLASS}`).removeClass(ACTIVE_CLASS);
if (!isInput) {
this.dropdown
.parent()
.find(`input[name='${fieldName}']`)
.remove();
} }
}
if (field && field.length && value == null) {
this.clearField(field, isInput);
}
// Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
if (value != null) {
if ((!field || !field.length) && fieldName) { if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject); this.addInput(fieldName, value, selectedObject);
} else if (field && field.length) { }
field.val(value).trigger('change'); } else {
isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find(`.${ACTIVE_CLASS}`).removeClass(ACTIVE_CLASS);
if (!isInput) {
this.dropdown
.parent()
.find(`input[name='${fieldName}']`)
.remove();
}
}
if (field && field.length && value == null) {
this.clearField(field, isInput);
}
// Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
if (value != null) {
if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
} else if (field && field.length) {
field.val(value).trigger('change');
}
} }
} }
return [selectedObject, isMarking];
} }
   
return [selectedObject, isMarking]; focusTextInput() {
}; if (this.options.filterable) {
const initialScrollTop = $(window).scrollTop();
   
GitLabDropdown.prototype.focusTextInput = function() { if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) {
if (this.options.filterable) { this.filterInput.focus();
const initialScrollTop = $(window).scrollTop(); }
   
if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) { if ($(window).scrollTop() < initialScrollTop) {
this.filterInput.focus(); $(window).scrollTop(initialScrollTop);
}
} }
}
   
if ($(window).scrollTop() < initialScrollTop) { addInput(fieldName, value, selectedObject, single) {
$(window).scrollTop(initialScrollTop); // Create hidden input for form
if (single) {
$(`input[name="${fieldName}"]`).remove();
} }
}
};
   
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject, single) { const $input = $('<input>')
// Create hidden input for form .attr('type', 'hidden')
if (single) { .attr('name', fieldName)
$(`input[name="${fieldName}"]`).remove(); .val(value);
} if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
}
   
const $input = $('<input>') if (this.options.multiSelect) {
.attr('type', 'hidden') Object.keys(selectedObject).forEach(attribute => {
.attr('name', fieldName) $input.attr(`data-${attribute}`, selectedObject[attribute]);
.val(value); });
if (this.options.inputId != null) { }
$input.attr('id', this.options.inputId);
}
   
if (this.options.multiSelect) { if (this.options.inputMeta) {
Object.keys(selectedObject).forEach(attribute => { $input.attr('data-meta', selectedObject[this.options.inputMeta]);
$input.attr(`data-${attribute}`, selectedObject[attribute]); }
});
}
   
if (this.options.inputMeta) { this.dropdown.before($input).trigger('change');
$input.attr('data-meta', selectedObject[this.options.inputMeta]);
} }
   
this.dropdown.before($input).trigger('change'); selectRowAtIndex(index) {
}; // If we pass an option index
let selector;
GitLabDropdown.prototype.selectRowAtIndex = function(index) { if (typeof index !== 'undefined') {
let selector; selector = `${SELECTABLE_CLASSES}:eq(${index}) a`;
// If we pass an option index
if (typeof index !== 'undefined') {
selector = `${SELECTABLE_CLASSES}:eq(${index}) a`;
} else {
selector = '.dropdown-content .is-focused';
}
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = `.dropdown-page-one ${selector}`;
}
// simulate a click on the first link
const $el = $(selector, this.dropdown);
if ($el.length) {
const href = $el.attr('href');
if (href && href !== '#') {
visitUrl(href);
} else { } else {
$el.trigger('click'); selector = '.dropdown-content .is-focused';
}
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = `.dropdown-page-one ${selector}`;
}
// simulate a click on the first link
const $el = $(selector, this.dropdown);
if ($el.length) {
const href = $el.attr('href');
if (href && href !== '#') {
visitUrl(href);
} else {
$el.trigger('click');
}
} }
} }
};
   
GitLabDropdown.prototype.addArrowKeyEvent = function() { addArrowKeyEvent() {
let selector; const ARROW_KEY_CODES = [38, 40];
const ARROW_KEY_CODES = [38, 40]; let selector = SELECTABLE_CLASSES;
selector = SELECTABLE_CLASSES; if (this.dropdown.find('.dropdown-toggle-page').length) {
if (this.dropdown.find('.dropdown-toggle-page').length) { selector = `.dropdown-page-one ${selector}`;
selector = `.dropdown-page-one ${selector}`; }
} return $('body').on('keydown', e => {
return $('body').on('keydown', e => { let $listItems, PREV_INDEX;
let $listItems, PREV_INDEX; const currentKeyCode = e.which;
const currentKeyCode = e.which; if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { e.preventDefault();
e.preventDefault(); e.stopImmediatePropagation();
e.stopImmediatePropagation(); PREV_INDEX = currentIndex;
PREV_INDEX = currentIndex; $listItems = $(selector, this.dropdown);
$listItems = $(selector, this.dropdown); // if @options.filterable
// if @options.filterable // $input.blur()
// $input.blur() if (currentKeyCode === 40) {
if (currentKeyCode === 40) { // Move down
// Move down if (currentIndex < $listItems.length - 1) {
if (currentIndex < $listItems.length - 1) { currentIndex += 1;
currentIndex += 1; }
} else if (currentKeyCode === 38) {
// Move up
if (currentIndex > 0) {
currentIndex -= 1;
}
} }
} else if (currentKeyCode === 38) { if (currentIndex !== PREV_INDEX) {
// Move up this.highlightRowAtIndex($listItems, currentIndex);
if (currentIndex > 0) {
currentIndex -= 1;
} }
return false;
} }
if (currentIndex !== PREV_INDEX) { if (currentKeyCode === 13 && currentIndex !== -1) {
this.highlightRowAtIndex($listItems, currentIndex); e.preventDefault();
this.selectRowAtIndex();
} }
return false; });
}
if (currentKeyCode === 13 && currentIndex !== -1) {
e.preventDefault();
this.selectRowAtIndex();
}
});
};
GitLabDropdown.prototype.removeArrowKeyEvent = function() {
return $('body').off('keydown');
};
GitLabDropdown.prototype.resetRows = function resetRows() {
currentIndex = -1;
$('.is-focused', this.dropdown).removeClass('is-focused');
};
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
if (!$listItems) {
$listItems = $(SELECTABLE_CLASSES, this.dropdown);
}
// Remove the class for the previously focused row
$('.is-focused', this.dropdown).removeClass('is-focused');
// Update the class for the row at the specific index
const $listItem = $listItems.eq(index);
$listItem.find('a:first-child').addClass('is-focused');
// Dropdown content scroll area
const $dropdownContent = $listItem.closest('.dropdown-content');
const dropdownScrollTop = $dropdownContent.scrollTop();
const dropdownContentHeight = $dropdownContent.outerHeight();
const dropdownContentTop = $dropdownContent.prop('offsetTop');
const dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
// Get the offset bottom of the list item
const listItemHeight = $listItem.outerHeight();
const listItemTop = $listItem.prop('offsetTop');
const listItemBottom = listItemTop + listItemHeight;
if (!index) {
// Scroll the dropdown content to the top
$dropdownContent.scrollTop(0);
} else if (index === $listItems.length - 1) {
// Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
} else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
// Scroll the dropdown content down
$dropdownContent.scrollTop(
listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
);
} else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
// Scroll the dropdown content up
return $dropdownContent.scrollTop(
listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING,
);
} }
};
   
GitLabDropdown.prototype.updateLabel = function(selected, el, instance) { // eslint-disable-next-line class-methods-use-this
if (selected == null) { removeArrowKeyEvent() {
selected = null; return $('body').off('keydown');
} }
if (el == null) {
el = null; resetRows() {
} currentIndex = -1;
if (instance == null) { $('.is-focused', this.dropdown).removeClass('is-focused');
instance = null;
} }
   
let toggleText = this.options.toggleLabel(selected, el, instance); highlightRowAtIndex($listItems, index) {
if (this.options.updateLabel) { if (!$listItems) {
// Option to override the dropdown label text // eslint-disable-next-line no-param-reassign
toggleText = this.options.updateLabel; $listItems = $(SELECTABLE_CLASSES, this.dropdown);
}
// Remove the class for the previously focused row
$('.is-focused', this.dropdown).removeClass('is-focused');
// Update the class for the row at the specific index
const $listItem = $listItems.eq(index);
$listItem.find('a:first-child').addClass('is-focused');
// Dropdown content scroll area
const $dropdownContent = $listItem.closest('.dropdown-content');
const dropdownScrollTop = $dropdownContent.scrollTop();
const dropdownContentHeight = $dropdownContent.outerHeight();
const dropdownContentTop = $dropdownContent.prop('offsetTop');
const dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
// Get the offset bottom of the list item
const listItemHeight = $listItem.outerHeight();
const listItemTop = $listItem.prop('offsetTop');
const listItemBottom = listItemTop + listItemHeight;
if (!index) {
// Scroll the dropdown content to the top
$dropdownContent.scrollTop(0);
} else if (index === $listItems.length - 1) {
// Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
} else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
// Scroll the dropdown content down
$dropdownContent.scrollTop(
listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
);
} else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
// Scroll the dropdown content up
return $dropdownContent.scrollTop(
listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING,
);
}
} }
   
return $(this.el) updateLabel(selected = null, el = null, instance = null) {
.find('.dropdown-toggle-text') let toggleText = this.options.toggleLabel(selected, el, instance);
.text(toggleText); if (this.options.updateLabel) {
}; // Option to override the dropdown label text
toggleText = this.options.updateLabel;
}
   
GitLabDropdown.prototype.clearField = function(field, isInput) { return $(this.el)
return isInput ? field.val('') : field.remove(); .find('.dropdown-toggle-text')
}; .text(toggleText);
}
// eslint-disable-next-line class-methods-use-this
clearField(field, isInput) {
return isInput ? field.val('') : field.remove();
}
}
   
// eslint-disable-next-line func-names
$.fn.glDropdown = function(opts) { $.fn.glDropdown = function(opts) {
// eslint-disable-next-line func-names
return this.each(function() { return this.each(function() {
if (!$.data(this, 'glDropdown')) { if (!$.data(this, 'glDropdown')) {
return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
Loading
Loading
Loading
@@ -10,6 +10,7 @@ import {
Loading
@@ -10,6 +10,7 @@ import {
GlLoadingIcon, GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import { import {
NAME_REGEX_LENGTH, NAME_REGEX_LENGTH,
UPDATE_SETTINGS_ERROR_MESSAGE, UPDATE_SETTINGS_ERROR_MESSAGE,
Loading
@@ -27,10 +28,18 @@ export default {
Loading
@@ -27,10 +28,18 @@ export default {
GlCard, GlCard,
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [Tracking.mixin()],
labelsConfig: { labelsConfig: {
cols: 3, cols: 3,
align: 'right', align: 'right',
}, },
data() {
return {
tracking: {
label: 'docker_container_retention_and_expiration_policies',
},
};
},
computed: { computed: {
...mapState(['formOptions', 'isLoading']), ...mapState(['formOptions', 'isLoading']),
...mapComputed( ...mapComputed(
Loading
@@ -86,7 +95,12 @@ export default {
Loading
@@ -86,7 +95,12 @@ export default {
}, },
methods: { methods: {
...mapActions(['resetSettings', 'saveSettings']), ...mapActions(['resetSettings', 'saveSettings']),
reset() {
this.track('reset_form');
this.resetSettings();
},
submit() { submit() {
this.track('submit_form');
this.saveSettings() this.saveSettings()
.then(() => this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' })) .then(() => this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' }))
.catch(() => this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' })); .catch(() => this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' }));
Loading
@@ -96,7 +110,7 @@ export default {
Loading
@@ -96,7 +110,7 @@ export default {
</script> </script>
   
<template> <template>
<form ref="form-element" @submit.prevent="submit" @reset.prevent="resetSettings"> <form ref="form-element" @submit.prevent="submit" @reset.prevent="reset">
<gl-card> <gl-card>
<template #header> <template #header>
{{ s__('ContainerRegistry|Tag expiration policy') }} {{ s__('ContainerRegistry|Tag expiration policy') }}
Loading
Loading
Loading
@@ -3,7 +3,13 @@
Loading
@@ -3,7 +3,13 @@
class Admin::ApplicationSettingsController < Admin::ApplicationController class Admin::ApplicationSettingsController < Admin::ApplicationController
include InternalRedirect include InternalRedirect
   
# NOTE: Use @application_setting in this controller when you need to access
# application_settings after it has been modified. This is because the
# ApplicationSetting model uses Gitlab::ThreadMemoryCache for caching and the
# cache might be stale immediately after an update.
# https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30233
before_action :set_application_setting before_action :set_application_setting
before_action :whitelist_query_limiting, only: [:usage_data] before_action :whitelist_query_limiting, only: [:usage_data]
before_action :validate_self_monitoring_feature_flag_enabled, only: [ before_action :validate_self_monitoring_feature_flag_enabled, only: [
:create_self_monitoring_project, :create_self_monitoring_project,
Loading
@@ -79,6 +85,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
Loading
@@ -79,6 +85,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url
end end
   
# Specs are in spec/requests/self_monitoring_project_spec.rb
def create_self_monitoring_project def create_self_monitoring_project
job_id = SelfMonitoringProjectCreateWorker.perform_async job_id = SelfMonitoringProjectCreateWorker.perform_async
   
Loading
@@ -88,6 +95,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
Loading
@@ -88,6 +95,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
   
# Specs are in spec/requests/self_monitoring_project_spec.rb
def status_create_self_monitoring_project def status_create_self_monitoring_project
job_id = params[:job_id].to_s job_id = params[:job_id].to_s
   
Loading
@@ -98,10 +106,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
Loading
@@ -98,10 +106,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
   
if Gitlab::CurrentSettings.self_monitoring_project_id.present? if SelfMonitoringProjectCreateWorker.in_progress?(job_id)
return render status: :ok, json: self_monitoring_data
elsif SelfMonitoringProjectCreateWorker.in_progress?(job_id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000) ::Gitlab::PollingInterval.set_header(response, interval: 3_000)
   
return render status: :accepted, json: { return render status: :accepted, json: {
Loading
@@ -109,12 +114,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
Loading
@@ -109,12 +114,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
   
if @application_setting.self_monitoring_project_id.present?
return render status: :ok, json: self_monitoring_data
end
render status: :bad_request, json: { render status: :bad_request, json: {
message: _('Self-monitoring project does not exist. Please check logs ' \ message: _('Self-monitoring project does not exist. Please check logs ' \
'for any error messages') 'for any error messages')
} }
end end
   
# Specs are in spec/requests/self_monitoring_project_spec.rb
def delete_self_monitoring_project def delete_self_monitoring_project
job_id = SelfMonitoringProjectDeleteWorker.perform_async job_id = SelfMonitoringProjectDeleteWorker.perform_async
   
Loading
@@ -124,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
Loading
@@ -124,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
   
# Specs are in spec/requests/self_monitoring_project_spec.rb
def status_delete_self_monitoring_project def status_delete_self_monitoring_project
job_id = params[:job_id].to_s job_id = params[:job_id].to_s
   
Loading
@@ -134,12 +145,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
Loading
@@ -134,12 +145,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
   
if Gitlab::CurrentSettings.self_monitoring_project_id.nil? if SelfMonitoringProjectDeleteWorker.in_progress?(job_id)
return render status: :ok, json: {
message: _('Self-monitoring project has been successfully deleted')
}
elsif SelfMonitoringProjectDeleteWorker.in_progress?(job_id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000) ::Gitlab::PollingInterval.set_header(response, interval: 3_000)
   
return render status: :accepted, json: { return render status: :accepted, json: {
Loading
@@ -147,6 +153,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
Loading
@@ -147,6 +153,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
   
if @application_setting.self_monitoring_project_id.nil?
return render status: :ok, json: {
message: _('Self-monitoring project has been successfully deleted')
}
end
render status: :bad_request, json: { render status: :bad_request, json: {
message: _('Self-monitoring project was not deleted. Please check logs ' \ message: _('Self-monitoring project was not deleted. Please check logs ' \
'for any error messages') 'for any error messages')
Loading
@@ -161,8 +173,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
Loading
@@ -161,8 +173,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
   
def self_monitoring_data def self_monitoring_data
{ {
project_id: Gitlab::CurrentSettings.self_monitoring_project_id, project_id: @application_setting.self_monitoring_project_id,
project_full_path: Gitlab::CurrentSettings.self_monitoring_project&.full_path project_full_path: @application_setting.self_monitoring_project&.full_path
} }
end end
   
Loading
Loading
Loading
@@ -24,7 +24,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
Loading
@@ -24,7 +24,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
def mark_as_ham def mark_as_ham
spam_log = SpamLog.find(params[:id]) spam_log = SpamLog.find(params[:id])
   
if HamService.new(spam_log).mark_as_ham! if Spam::HamService.new(spam_log).mark_as_ham!
redirect_to admin_spam_logs_path, notice: _('Spam log successfully submitted as ham.') redirect_to admin_spam_logs_path, notice: _('Spam log successfully submitted as ham.')
else else
redirect_to admin_spam_logs_path, alert: _('Error with Akismet. Please check the logs for more info.') redirect_to admin_spam_logs_path, alert: _('Error with Akismet. Please check the logs for more info.')
Loading
Loading
Loading
@@ -8,7 +8,6 @@ module Resolvers
Loading
@@ -8,7 +8,6 @@ module Resolvers
description: 'ID of the Sentry issue' description: 'ID of the Sentry issue'
   
def resolve(**args) def resolve(**args)
project = object
current_user = context[:current_user] current_user = context[:current_user]
issue_id = GlobalID.parse(args[:id]).model_id issue_id = GlobalID.parse(args[:id]).model_id
   
Loading
@@ -23,6 +22,14 @@ module Resolvers
Loading
@@ -23,6 +22,14 @@ module Resolvers
   
issue issue
end end
private
def project
return object.gitlab_project if object.respond_to?(:gitlab_project)
object
end
end end
end end
end end
# frozen_string_literal: true
module Resolvers
module ErrorTracking
class SentryErrorCollectionResolver < BaseResolver
def resolve(**args)
project = object
service = ::ErrorTracking::ListIssuesService.new(
project,
context[:current_user]
)
Gitlab::ErrorTracking::ErrorCollection.new(
external_url: service.external_url,
project: project
)
end
end
end
end
# frozen_string_literal: true
module Resolvers
module ErrorTracking
class SentryErrorsResolver < BaseResolver
def resolve(**args)
args[:cursor] = args.delete(:after)
project = object.project
result = ::ErrorTracking::ListIssuesService.new(
project,
context[:current_user],
args
).execute
next_cursor = result[:pagination]&.dig('next', 'cursor')
previous_cursor = result[:pagination]&.dig('previous', 'cursor')
issues = result[:issues]
# ReactiveCache is still fetching data
return if issues.nil?
Gitlab::Graphql::ExternallyPaginatedArray.new(previous_cursor, next_cursor, *issues)
end
end
end
end
Loading
@@ -4,8 +4,9 @@ module Types
Loading
@@ -4,8 +4,9 @@ module Types
module ErrorTracking module ErrorTracking
class SentryDetailedErrorType < ::Types::BaseObject class SentryDetailedErrorType < ::Types::BaseObject
graphql_name 'SentryDetailedError' graphql_name 'SentryDetailedError'
description 'A Sentry error.'
   
present_using SentryDetailedErrorPresenter present_using SentryErrorPresenter
   
authorize :read_sentry_issue authorize :read_sentry_issue
   
Loading
@@ -92,18 +93,6 @@ module Types
Loading
@@ -92,18 +93,6 @@ module Types
field :tags, Types::ErrorTracking::SentryErrorTagsType, field :tags, Types::ErrorTracking::SentryErrorTagsType,
null: false, null: false,
description: 'Tags associated with the Sentry Error' description: 'Tags associated with the Sentry Error'
def first_seen
DateTime.parse(object.first_seen)
end
def last_seen
DateTime.parse(object.last_seen)
end
def project_id
Gitlab::GlobalId.build(model_name: 'Project', id: object.project_id).to_s
end
end end
end end
end end
# frozen_string_literal: true
module Types
module ErrorTracking
class SentryErrorCollectionType < ::Types::BaseObject
graphql_name 'SentryErrorCollection'
description 'An object containing a collection of Sentry errors, and a detailed error.'
authorize :read_sentry_issue
field :errors,
Types::ErrorTracking::SentryErrorType.connection_type,
connection: false,
null: true,
description: "Collection of Sentry Errors",
extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension],
resolver: Resolvers::ErrorTracking::SentryErrorsResolver do
argument :search_term,
String,
description: 'Search term for the Sentry error.',
required: false
argument :sort,
String,
description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.',
required: false
end
field :detailed_error, Types::ErrorTracking::SentryDetailedErrorType,
null: true,
description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
field :external_url,
GraphQL::STRING_TYPE,
null: true,
description: "External URL for Sentry"
end
end
end
# frozen_string_literal: true
module Types
module ErrorTracking
# rubocop: disable Graphql/AuthorizeTypes
class SentryErrorType < ::Types::BaseObject
graphql_name 'SentryError'
description 'A Sentry error. A simplified version of SentryDetailedError.'
present_using SentryErrorPresenter
field :id, GraphQL::ID_TYPE,
null: false,
description: 'ID (global ID) of the error'
field :sentry_id, GraphQL::STRING_TYPE,
method: :id,
null: false,
description: 'ID (Sentry ID) of the error'
field :first_seen, Types::TimeType,
null: false,
description: 'Timestamp when the error was first seen'
field :last_seen, Types::TimeType,
null: false,
description: 'Timestamp when the error was last seen'
field :title, GraphQL::STRING_TYPE,
null: false,
description: 'Title of the error'
field :type, GraphQL::STRING_TYPE,
null: false,
description: 'Type of the error'
field :user_count, GraphQL::INT_TYPE,
null: false,
description: 'Count of users affected by the error'
field :count, GraphQL::INT_TYPE,
null: false,
description: 'Count of occurrences'
field :message, GraphQL::STRING_TYPE,
null: true,
description: 'Sentry metadata message of the error'
field :culprit, GraphQL::STRING_TYPE,
null: false,
description: 'Culprit of the error'
field :external_url, GraphQL::STRING_TYPE,
null: false,
description: 'External URL of the error'
field :short_id, GraphQL::STRING_TYPE,
null: false,
description: 'Short ID (Sentry ID) of the error'
field :status, Types::ErrorTracking::SentryErrorStatusEnum,
null: false,
description: 'Status of the error'
field :frequency, [Types::ErrorTracking::SentryErrorFrequencyType],
null: false,
description: 'Last 24hr stats of the error'
field :sentry_project_id, GraphQL::ID_TYPE,
method: :project_id,
null: false,
description: 'ID of the project (Sentry project)'
field :sentry_project_name, GraphQL::STRING_TYPE,
method: :project_name,
null: false,
description: 'Name of the project affected by the error'
field :sentry_project_slug, GraphQL::STRING_TYPE,
method: :project_slug,
null: false,
description: 'Slug of the project affected by the error'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
Loading
@@ -173,6 +173,12 @@ module Types
Loading
@@ -173,6 +173,12 @@ module Types
null: true, null: true,
description: 'Snippets of the project', description: 'Snippets of the project',
resolver: Resolvers::Projects::SnippetsResolver resolver: Resolvers::Projects::SnippetsResolver
field :sentry_errors,
Types::ErrorTracking::SentryErrorCollectionType,
null: true,
description: 'Paginated collection of Sentry errors on the project',
resolver: Resolvers::ErrorTracking::SentryErrorCollectionResolver
end end
end end
   
Loading
Loading
Loading
@@ -484,10 +484,10 @@ class Commit
Loading
@@ -484,10 +484,10 @@ class Commit
end end
   
def commit_reference(from, referable_commit_id, full: false) def commit_reference(from, referable_commit_id, full: false)
reference = project.to_reference(from, full: full) base = project.to_reference_base(from, full: full)
   
if reference.present? if base.present?
"#{reference}#{self.class.reference_prefix}#{referable_commit_id}" "#{base}#{self.class.reference_prefix}#{referable_commit_id}"
else else
referable_commit_id referable_commit_id
end end
Loading
Loading
Loading
@@ -92,7 +92,7 @@ class CommitRange
Loading
@@ -92,7 +92,7 @@ class CommitRange
alias_method :id, :to_s alias_method :id, :to_s
   
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
project_reference = project.to_reference(from, full: full) project_reference = project.to_reference_base(from, full: full)
   
if project_reference.present? if project_reference.present?
project_reference + self.class.reference_prefix + self.id project_reference + self.class.reference_prefix + self.id
Loading
@@ -102,7 +102,7 @@ class CommitRange
Loading
@@ -102,7 +102,7 @@ class CommitRange
end end
   
def reference_link_text(from = nil) def reference_link_text(from = nil)
project_reference = project.to_reference(from) project_reference = project.to_reference_base(from)
reference = ref_from + notation + ref_to reference = ref_from + notation + ref_to
   
if project_reference.present? if project_reference.present?
Loading
Loading
Loading
@@ -23,6 +23,14 @@ module Referable
Loading
@@ -23,6 +23,14 @@ module Referable
'' ''
end end
   
# If this referable object can serve as the base for the
# reference of child objects (e.g. projects are the base of
# issues), but it is formatted differently, then you may wish
# to override this method.
def to_reference_base(from = nil, full:)
to_reference(from, full: full)
end
def reference_link_text(from = nil) def reference_link_text(from = nil)
to_reference(from) to_reference(from)
end end
Loading
Loading
Loading
@@ -173,7 +173,7 @@ class Issue < ApplicationRecord
Loading
@@ -173,7 +173,7 @@ class Issue < ApplicationRecord
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
   
"#{project.to_reference(from, full: full)}#{reference}" "#{project.to_reference_base(from, full: full)}#{reference}"
end end
   
def suggested_branch_name def suggested_branch_name
Loading
Loading
Loading
@@ -225,7 +225,7 @@ class Label < ApplicationRecord
Loading
@@ -225,7 +225,7 @@ class Label < ApplicationRecord
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
   
if from if from
"#{from.to_reference(target_project, full: full)}#{reference}" "#{from.to_reference_base(target_project, full: full)}#{reference}"
else else
reference reference
end end
Loading
Loading
Loading
@@ -396,7 +396,7 @@ class MergeRequest < ApplicationRecord
Loading
@@ -396,7 +396,7 @@ class MergeRequest < ApplicationRecord
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
   
"#{project.to_reference(from, full: full)}#{reference}" "#{project.to_reference_base(from, full: full)}#{reference}"
end end
   
def commits(limit: nil) def commits(limit: nil)
Loading
Loading
Loading
@@ -228,7 +228,7 @@ class Milestone < ApplicationRecord
Loading
@@ -228,7 +228,7 @@ class Milestone < ApplicationRecord
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
   
if project if project
"#{project.to_reference(from, full: full)}#{reference}" "#{project.to_reference_base(from, full: full)}#{reference}"
else else
reference reference
end end
Loading
Loading
Loading
@@ -1068,12 +1068,19 @@ class Project < ApplicationRecord
Loading
@@ -1068,12 +1068,19 @@ class Project < ApplicationRecord
end end
end end
   
def to_reference_with_postfix # Produce a valid reference (see Referable#to_reference)
"#{to_reference(full: true)}#{self.class.reference_postfix}" #
# NB: For projects, all references are 'full' - i.e. they all include the
# full_path, rather than just the project name. For this reason, we ignore
# the value of `full:` passed to this method, which is part of the Referable
# interface.
def to_reference(from = nil, full: false)
base = to_reference_base(from, full: true)
"#{base}#{self.class.reference_postfix}"
end end
   
# `from` argument can be a Namespace or Project. # `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false) def to_reference_base(from = nil, full: false)
if full || cross_namespace_reference?(from) if full || cross_namespace_reference?(from)
full_path full_path
elsif cross_project_reference?(from) elsif cross_project_reference?(from)
Loading
Loading
Loading
@@ -180,7 +180,7 @@ class Snippet < ApplicationRecord
Loading
@@ -180,7 +180,7 @@ class Snippet < ApplicationRecord
reference = "#{self.class.reference_prefix}#{id}" reference = "#{self.class.reference_prefix}#{id}"
   
if project.present? if project.present?
"#{project.to_reference(from, full: full)}#{reference}" "#{project.to_reference_base(from, full: full)}#{reference}"
else else
reference reference
end end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment