gitlab-org--gitlab-foss/app/assets/javascripts/labels_select.js
Brett Walker a8869a571b Dynamically store the valid label endpoint
In order to ensure we have the right endpoint to query for an
issue's possible valid labels, we store that url in the issue
object that gets passed to the frontend.
2018-10-31 19:01:04 -05:00

569 lines
19 KiB
JavaScript

/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, one-var, no-unused-vars, prefer-template, no-new, consistent-return, object-shorthand, no-shadow, no-param-reassign, vars-on-top, no-lonely-if, no-else-return, dot-notation, no-empty */
/* global Issuable */
/* global ListLabel */
import $ from 'jquery';
import _ from 'underscore';
import { sprintf, __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
import flash from './flash';
import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store';
export default class LabelsSelect {
constructor(els, options = {}) {
var _this, $els;
_this = this;
$els = $(els);
if (!els) {
$els = $('.js-label-select');
}
$els.each(function(i, dropdown) {
var $block,
$colorPreview,
$dropdown,
$form,
$loading,
$selectbox,
$sidebarCollapsedValue,
$value,
abilityName,
defaultLabel,
enableLabelCreateButton,
issueURLSplit,
issueUpdateURL,
labelUrl,
namespacePath,
projectPath,
saveLabelData,
selectedLabel,
showAny,
showNo,
$sidebarLabelTooltip,
initialSelected,
$toggleText,
fieldName,
useId,
propertyName,
showMenuAbove,
$container,
$dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespacePath');
projectPath = $dropdown.data('projectPath');
issueUpdateURL = $dropdown.data('issueUpdate');
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');
$loading = $block.find('.block-loading').fadeOut();
fieldName = $dropdown.data('fieldName');
useId = $dropdown.is(
'.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown',
);
propertyName = useId ? 'id' : 'title';
initialSelected = $selectbox
.find('input[name="' + $dropdown.data('fieldName') + '"]')
.map(function() {
return this.value;
})
.get();
const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
new CreateLabelDropdown(
$dropdown.closest('.dropdown').find('.dropdown-new-label'),
namespacePath,
projectPath,
);
}
saveLabelData = function() {
var data, selected;
selected = $dropdown
.closest('.selectbox')
.find("input[name='" + fieldName + "']")
.map(function() {
return this.value;
})
.get();
if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected;
data = {};
data[abilityName] = {};
data[abilityName].label_ids = selected;
if (!selected.length) {
data[abilityName].label_ids = [''];
}
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
axios
.put(issueUpdateURL, data)
.then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
data.issueUpdateURL = issueUpdateURL;
labelCount = 0;
if (data.labels.length && issueUpdateURL) {
template = LabelsSelect.getLabelTemplate({
labels: data.labels,
issueUpdateURL,
});
labelCount = data.labels.length;
} else {
template = '<span class="no-value">None</span>';
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
if (data.labels.length) {
labelTitles = data.labels.map(function(label) {
return label.title;
});
if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5);
labelTitles.push('and ' + (data.labels.length - 5) + ' more');
}
labelTooltipTitle = labelTitles.join(', ');
} else {
labelTooltipTitle = __('Labels');
}
$sidebarLabelTooltip.attr('title', labelTooltipTitle).tooltip('_fixTitle');
$('.has-tooltip', $value).tooltip({
container: 'body',
});
})
.catch(() => flash(__('Error saving label update.')));
};
$dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
labelUrl = $dropdown.attr('data-labels');
axios
.get(labelUrl)
.then(res => {
let data = _.chain(res.data)
.groupBy(function(label) {
return label.title;
})
.map(function(label) {
var color;
color = _.map(label, function(dup) {
return dup.color;
});
return {
id: label[0].id,
title: label[0].title,
color: color,
duplicate: color.length > 1,
};
})
.value();
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
title: 'No Label',
});
}
if (showAny) {
extraData.unshift({
isAny: true,
title: 'Any Label',
});
}
if (extraData.length) {
extraData.push('divider');
data = extraData.concat(data);
}
}
callback(data);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
})
.catch(() => flash(__('Error fetching labels.')));
},
renderRow: function(label, instance) {
var $a,
$li,
color,
colorEl,
indeterminate,
removesAll,
selectedClass,
spacing,
i,
marked,
dropdownName,
dropdownValue;
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
removesAll = label.id <= 0 || label.id == null;
if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = $dropdown.data('indeterminate') || [];
marked = $dropdown.data('marked') || [];
if (indeterminate.indexOf(label.id) !== -1) {
selectedClass.push('is-indeterminate');
}
if (marked.indexOf(label.id) !== -1) {
// Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
selectedClass.push('is-active');
}
} else {
if (this.id(label)) {
dropdownName = $dropdown.data('fieldName');
dropdownValue = this.id(label)
.toString()
.replace(/'/g, "\\'");
if (
$form.find(
"input[type='hidden'][name='" +
dropdownName +
"'][value='" +
dropdownValue +
"']",
).length
) {
selectedClass.push('is-active');
}
}
if ($dropdown.hasClass('js-multiselect') && removesAll) {
selectedClass.push('dropdown-clear-active');
}
}
if (label.duplicate) {
color = DropdownUtils.duplicateLabelColor(label.color);
} else {
if (label.color != null) {
[color] = label.color;
}
}
if (color) {
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
} else {
colorEl = '';
}
// We need to identify which items are actually labels
if (label.id) {
selectedClass.push('label-item');
$a.attr('data-label-id', label.id);
}
$a.addClass(selectedClass.join(' ')).html(`${colorEl} ${_.escape(label.title)}`);
// Return generated html
return $li.html($a).prop('outerHTML');
},
search: {
fields: ['title'],
},
selectable: true,
filterable: true,
selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) {
var $dropdownParent = $dropdown.parent();
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false;
var title = selected ? selected.title : null;
var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
$dropdownParent.find('.dropdown-input-clear').trigger('click');
}
if (selected && selected.id === 0) {
this.selected = [];
return 'No Label';
} else if (isSelected) {
this.selected.push(title);
} else if (!isSelected && title) {
var index = this.selected.indexOf(title);
this.selected.splice(index, 1);
}
if (selectedLabels.length === 1) {
return selectedLabels;
} else if (selectedLabels.length) {
return sprintf(__('%{firstLabel} +%{labelCount} more'), {
firstLabel: selectedLabels[0],
labelCount: selectedLabels.length - 1,
});
} else {
return defaultLabel;
}
},
fieldName: $dropdown.data('fieldName'),
id: function(label) {
if (label.id <= 0) return label.title;
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return label.id;
}
if ($dropdown.hasClass('js-filter-submit') && label.isAny == null) {
return label.title;
} else {
return label.id;
}
},
hidden: function() {
var isIssueIndex, isMRIndex, page, selectedLabels;
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide();
// display:block overrides the hide-collapse rule
$value.removeAttr('style');
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return;
}
if ($('html').hasClass('issue-boards-page')) {
return;
}
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedLabels = $dropdown
.closest('form')
.find("input:hidden[name='" + $dropdown.data('fieldName') + "']");
Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
$dropdown.closest('form').submit();
} else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
}
}
}
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(clickEvent) {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => {
$loading.fadeOut();
};
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown
.parent()
.find('.dropdown-clear-active')
.removeClass('is-active');
}
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return;
}
if ($dropdown.hasClass('js-filter-bulk-update')) {
_this.enableBulkLabelDropdown();
_this.setDropdownData($dropdown, isMarking, label.id);
return;
}
if ($dropdown.closest('.add-issues-modal').length) {
boardsModel = ModalStore.store.filter;
}
if (boardsModel) {
if (label.isAny) {
boardsModel['label_name'] = [];
} else if ($el.hasClass('is-active')) {
boardsModel['label_name'].push(label.title);
}
e.preventDefault();
return;
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form'));
}
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if ($el.hasClass('is-active')) {
boardsStore.detail.issue.labels.push(
new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
textColor: '#fff',
}),
);
} else {
var { labels } = boardsStore.detail.issue;
labels = labels.filter(function(selectedLabel) {
return selectedLabel.id !== label.id;
});
boardsStore.detail.issue.labels = labels;
}
$loading.fadeIn();
boardsStore.detail.issue
.update($dropdown.attr('data-issue-update'))
.then(fadeOutLoader)
.catch(fadeOutLoader);
} else if (handleClick) {
e.preventDefault();
handleClick(label);
} else {
if ($dropdown.hasClass('js-multiselect')) {
} else {
return saveLabelData();
}
}
},
opened: function(e) {
if ($dropdown.hasClass('js-issue-board-sidebar')) {
const previousSelection = $dropdown.attr('data-selected');
this.selected = previousSelection ? previousSelection.split(',') : [];
$dropdown.data('glDropdown').updateLabel();
}
},
preserveContext: true,
});
// Set dropdown data
_this.setOriginalDropdownData($dropdownContainer, $dropdown);
});
this.bindEvents();
}
static getLabelTemplate(tplData) {
// We could use ES6 template string here
// and properly indent markup for readability
// but that also introduces unintended white-space
// so best approach is to use traditional way of
// concatenation
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
const tpl = _.template(
[
'<% _.each(labels, function(label){ %>',
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
'<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
'<%- label.title %>',
'</span>',
'</a>',
'<% }); %>',
].join(''),
);
return tpl(tplData);
}
bindEvents() {
return $('body').on('change', '.selected-issuable', this.onSelectCheckboxIssue);
}
// eslint-disable-next-line class-methods-use-this
onSelectCheckboxIssue() {
if ($('.selected-issuable:checked').length) {
return;
}
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
}
// eslint-disable-next-line class-methods-use-this
enableBulkLabelDropdown() {
IssuableBulkUpdateActions.willUpdateLabels = true;
}
// 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') || [];
if (isMarking) {
markedIds.push(value);
i = indeterminateIds.indexOf(value);
if (i > -1) {
indeterminateIds.splice(i, 1);
}
i = unmarkedIds.indexOf(value);
if (i > -1) {
unmarkedIds.splice(i, 1);
}
} else {
// If marked item (not common) is unmarked
i = markedIds.indexOf(value);
if (i > -1) {
markedIds.splice(i, 1);
}
// If an indeterminate item is being unmarked
if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
// If a marked item is being unmarked
// (a marked item could also be a label that is present in all selection)
if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
}
$dropdown.data('marked', markedIds);
$dropdown.data('unmarked', unmarkedIds);
$dropdown.data('indeterminate', indeterminateIds);
}
// eslint-disable-next-line class-methods-use-this
setOriginalDropdownData($container, $dropdown) {
const labels = [];
$container.find('[name="label_name[]"]').map(function() {
return labels.push(this.value);
});
$dropdown.data('marked', labels);
}
}