Move issuable bulk edit form into a new sidebar.
This commit is contained in:
parent
34f925fe0b
commit
c9a67266d2
24 changed files with 553 additions and 394 deletions
|
@ -3,7 +3,7 @@
|
|||
/* global ActiveTabMemoizer */
|
||||
/* global ShortcutsNavigation */
|
||||
/* global Build */
|
||||
/* global Issuable */
|
||||
/* global IssuableIndex */
|
||||
/* global ShortcutsIssuable */
|
||||
/* global ZenMode */
|
||||
/* global Milestone */
|
||||
|
@ -127,10 +127,9 @@ import ShortcutsBlob from './shortcuts_blob';
|
|||
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
|
||||
filteredSearchManager.setup();
|
||||
}
|
||||
Issuable.init();
|
||||
new gl.IssuableBulkActions({
|
||||
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
|
||||
});
|
||||
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
|
||||
IssuableIndex.init(pagePrefix);
|
||||
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
new UsersSelect();
|
||||
break;
|
||||
|
|
159
app/assets/javascripts/issuable_bulk_update_actions.js
Normal file
159
app/assets/javascripts/issuable_bulk_update_actions.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
|
||||
/* global IssuableIndex */
|
||||
/* global Flash */
|
||||
|
||||
export default {
|
||||
init({ container, form, issues, prefixId } = {}) {
|
||||
this.prefixId = prefixId || 'issue_';
|
||||
this.form = form || this.getElement('.bulk-update');
|
||||
this.$labelDropdown = this.form.find('.js-label-select');
|
||||
this.issues = issues || this.getElement('.issues-list .issue');
|
||||
this.willUpdateLabels = false;
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents() {
|
||||
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
|
||||
},
|
||||
|
||||
onFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
return this.submit();
|
||||
},
|
||||
|
||||
submit() {
|
||||
const _this = this;
|
||||
const xhr = $.ajax({
|
||||
url: this.form.attr('action'),
|
||||
method: this.form.attr('method'),
|
||||
dataType: 'JSON',
|
||||
data: this.getFormDataAsObject()
|
||||
});
|
||||
xhr.done(() => window.location.reload());
|
||||
xhr.fail(() => this.onFormSubmitFailure());
|
||||
},
|
||||
|
||||
onFormSubmitFailure() {
|
||||
this.form.find('[type="submit"]').enable();
|
||||
return new Flash("Issue update failed");
|
||||
},
|
||||
|
||||
getSelectedIssues() {
|
||||
return this.issues.has('.selected_issue:checked');
|
||||
},
|
||||
|
||||
getLabelsFromSelection() {
|
||||
const labels = [];
|
||||
this.getSelectedIssues().map(function() {
|
||||
const labelsData = $(this).data('labels');
|
||||
if (labelsData) {
|
||||
return labelsData.map(function(labelId) {
|
||||
if (labels.indexOf(labelId) === -1) {
|
||||
return labels.push(labelId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return labels;
|
||||
},
|
||||
|
||||
/**
|
||||
* Will return only labels that were marked previously and the user has unmarked
|
||||
* @return {Array} Label IDs
|
||||
*/
|
||||
|
||||
getUnmarkedIndeterminedLabels() {
|
||||
const result = [];
|
||||
const labelsToKeep = this.$labelDropdown.data('indeterminate');
|
||||
|
||||
this.getLabelsFromSelection().forEach((id) => {
|
||||
if (labelsToKeep.indexOf(id) === -1) {
|
||||
result.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Simple form serialization, it will return just what we need
|
||||
* Returns key/value pairs from form data
|
||||
*/
|
||||
|
||||
getFormDataAsObject() {
|
||||
const formData = {
|
||||
update: {
|
||||
state_event: this.form.find('input[name="update[state_event]"]').val(),
|
||||
// For Merge Requests
|
||||
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
|
||||
// For Issues
|
||||
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
|
||||
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
|
||||
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
|
||||
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
|
||||
add_label_ids: [],
|
||||
remove_label_ids: []
|
||||
}
|
||||
};
|
||||
if (this.willUpdateLabels) {
|
||||
formData.update.add_label_ids = this.$labelDropdown.data('marked');
|
||||
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
|
||||
}
|
||||
return formData;
|
||||
},
|
||||
|
||||
setOriginalDropdownData() {
|
||||
const $labelSelect = $('.bulk-update .js-label-select');
|
||||
$labelSelect.data('common', this.getOriginalCommonIds());
|
||||
$labelSelect.data('marked', this.getOriginalMarkedIds());
|
||||
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
|
||||
},
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalCommonIds() {
|
||||
const labelIds = [];
|
||||
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
||||
});
|
||||
return _.intersection.apply(this, labelIds);
|
||||
},
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalMarkedIds() {
|
||||
const labelIds = [];
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
||||
});
|
||||
return _.intersection.apply(this, labelIds);
|
||||
},
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalIndeterminateIds() {
|
||||
const uniqueIds = [];
|
||||
const labelIds = [];
|
||||
let issuableLabels = [];
|
||||
|
||||
// Collect unique label IDs for all checked issues
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
|
||||
issuableLabels.forEach((labelId) => {
|
||||
// Store unique IDs
|
||||
if (uniqueIds.indexOf(labelId) === -1) {
|
||||
uniqueIds.push(labelId);
|
||||
}
|
||||
});
|
||||
// Store array of IDs per issuable
|
||||
labelIds.push(issuableLabels);
|
||||
});
|
||||
// Add uniqueIds to add it as argument for _.intersection
|
||||
labelIds.unshift(uniqueIds);
|
||||
// Return IDs that are present but not in all selected issueables
|
||||
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
|
||||
},
|
||||
|
||||
getElement(selector) {
|
||||
this.scopeEl = this.scopeEl || $('.content');
|
||||
return this.scopeEl.find(selector);
|
||||
},
|
||||
};
|
165
app/assets/javascripts/issuable_bulk_update_sidebar.js
Normal file
165
app/assets/javascripts/issuable_bulk_update_sidebar.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
/* eslint-disable class-methods-use-this, no-new */
|
||||
/* global LabelsSelect */
|
||||
/* global MilestoneSelect */
|
||||
/* global IssueStatusSelect */
|
||||
/* global SubscriptionSelect */
|
||||
|
||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
|
||||
const HIDDEN_CLASS = 'hidden';
|
||||
const DISABLED_CONTENT_CLASS = 'disabled-content';
|
||||
const SIDEBAR_EXPANDED_CLASS = 'right-sidebar-expanded issuable-bulk-update-sidebar';
|
||||
const SIDEBAR_COLLAPSED_CLASS = 'right-sidebar-collapsed issuable-bulk-update-sidebar';
|
||||
|
||||
export default class IssuableBulkUpdateSidebar {
|
||||
constructor() {
|
||||
this.initDomElements();
|
||||
this.bindEvents();
|
||||
this.initDropdowns();
|
||||
this.setupBulkUpdateActions();
|
||||
}
|
||||
|
||||
initDomElements() {
|
||||
this.$page = $('.page-with-sidebar');
|
||||
this.$sidebar = $('.right-sidebar');
|
||||
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
|
||||
this.$bulkEditSubmitBtn = $('.update-selected-issues');
|
||||
this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
|
||||
this.$otherFilters = $('.issues-other-filters');
|
||||
this.$checkAllContainer = $('.check-all-holder');
|
||||
this.$issueChecks = $('.issue-check');
|
||||
this.$issuesList = $('.selected_issue');
|
||||
this.$issuableIdsInput = $('#update_issuable_ids');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.$bulkUpdateEnableBtn.on('click', e => this.toggleBulkEdit(e, true));
|
||||
this.$bulkEditCancelBtn.on('click', e => this.toggleBulkEdit(e, false));
|
||||
this.$checkAllContainer.on('click', e => this.selectAll(e));
|
||||
this.$issuesList.on('change', () => this.updateFormState());
|
||||
this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit());
|
||||
this.$checkAllContainer.on('click', () => this.updateFormState());
|
||||
}
|
||||
|
||||
initDropdowns() {
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
new IssueStatusSelect();
|
||||
new SubscriptionSelect();
|
||||
}
|
||||
|
||||
getNavHeight() {
|
||||
const navbarHeight = $('.navbar-gitlab').outerHeight();
|
||||
const layoutNavHeight = $('.layout-nav').outerHeight();
|
||||
const subNavScroll = $('.sub-nav-scroll').outerHeight();
|
||||
return navbarHeight + layoutNavHeight + subNavScroll;
|
||||
}
|
||||
|
||||
initSidebar() {
|
||||
if (!this.navHeight) {
|
||||
this.navHeight = this.getNavHeight();
|
||||
}
|
||||
|
||||
if (!this.sidebarInitialized) {
|
||||
$(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
|
||||
$(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
|
||||
this.sidebarInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
setupBulkUpdateActions() {
|
||||
IssuableBulkUpdateActions.setOriginalDropdownData();
|
||||
}
|
||||
|
||||
updateFormState() {
|
||||
const noCheckedIssues = !$('.selected_issue:checked').length;
|
||||
|
||||
this.toggleSubmitButtonDisabled(noCheckedIssues);
|
||||
this.updateSelectedIssuableIds();
|
||||
|
||||
IssuableBulkUpdateActions.setOriginalDropdownData();
|
||||
}
|
||||
|
||||
prepForSubmit() {
|
||||
// if submit button is disabled, submission is blocked. This ensures we disable after
|
||||
// form submission is carried out
|
||||
setTimeout(() => this.$bulkEditSubmitBtn.disable());
|
||||
this.updateSelectedIssuableIds();
|
||||
}
|
||||
|
||||
toggleBulkEdit(e, enable) {
|
||||
e.preventDefault();
|
||||
|
||||
this.toggleSidebarDisplay(enable);
|
||||
this.toggleBulkEditButtonDisabled(enable);
|
||||
this.toggleOtherFiltersDisabled(enable);
|
||||
this.toggleCheckboxDisplay(enable);
|
||||
|
||||
if (enable) {
|
||||
this.initSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedIssuableIds() {
|
||||
this.$issuableIdsInput.val(IssuableBulkUpdateSidebar.getCheckedIssueIds());
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
const checkAllButtonState = this.$checkAllContainer.find('input').prop('checked');
|
||||
|
||||
this.$issuesList.prop('checked', checkAllButtonState);
|
||||
}
|
||||
|
||||
toggleSidebarDisplay(show) {
|
||||
this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
|
||||
this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
|
||||
this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
|
||||
this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
|
||||
}
|
||||
|
||||
toggleBulkEditButtonDisabled(disable) {
|
||||
if (disable) {
|
||||
this.$bulkUpdateEnableBtn.disable();
|
||||
} else {
|
||||
this.$bulkUpdateEnableBtn.enable();
|
||||
}
|
||||
}
|
||||
|
||||
toggleCheckboxDisplay(show) {
|
||||
this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show);
|
||||
this.$issueChecks.toggleClass(HIDDEN_CLASS, !show);
|
||||
}
|
||||
|
||||
toggleOtherFiltersDisabled(disable) {
|
||||
this.$otherFilters.toggleClass(DISABLED_CONTENT_CLASS, disable);
|
||||
}
|
||||
|
||||
toggleSubmitButtonDisabled(disable) {
|
||||
if (disable) {
|
||||
this.$bulkEditSubmitBtn.disable();
|
||||
} else {
|
||||
this.$bulkEditSubmitBtn.enable();
|
||||
}
|
||||
}
|
||||
// loosely based on method of the same name in right_sidebar.js
|
||||
setSidebarHeight() {
|
||||
const currentScrollDepth = window.pageYOffset || 0;
|
||||
const diff = this.navHeight - currentScrollDepth;
|
||||
|
||||
if (diff > 0) {
|
||||
this.$sidebar.outerHeight(window.innerHeight - diff);
|
||||
} else {
|
||||
this.$sidebar.outerHeight('100%');
|
||||
}
|
||||
}
|
||||
|
||||
static getCheckedIssueIds() {
|
||||
const $checkedIssues = $('.selected_issue:checked');
|
||||
|
||||
if ($checkedIssues.length > 0) {
|
||||
return $.map($checkedIssues, value => $(value).data('id'));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -1,30 +1,33 @@
|
|||
/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
|
||||
/* global Issuable */
|
||||
/* global IssuableIndex */
|
||||
|
||||
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
|
||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
|
||||
((global) => {
|
||||
var issuable_created;
|
||||
|
||||
issuable_created = false;
|
||||
|
||||
global.Issuable = {
|
||||
init: function() {
|
||||
Issuable.initTemplates();
|
||||
Issuable.initSearch();
|
||||
Issuable.initChecks();
|
||||
Issuable.initResetFilters();
|
||||
Issuable.resetIncomingEmailToken();
|
||||
return Issuable.initLabelFilterRemove();
|
||||
global.IssuableIndex = {
|
||||
init: function(pagePrefix) {
|
||||
IssuableIndex.initTemplates();
|
||||
IssuableIndex.initSearch();
|
||||
IssuableIndex.initBulkUpdate(pagePrefix);
|
||||
IssuableIndex.initResetFilters();
|
||||
IssuableIndex.resetIncomingEmailToken();
|
||||
IssuableIndex.initLabelFilterRemove();
|
||||
},
|
||||
initTemplates: function() {
|
||||
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
|
||||
return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
|
||||
},
|
||||
initSearch: function() {
|
||||
const $searchInput = $('#issuable_search');
|
||||
|
||||
Issuable.initSearchState($searchInput);
|
||||
IssuableIndex.initSearchState($searchInput);
|
||||
|
||||
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
|
||||
const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
|
||||
const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false);
|
||||
|
||||
$searchInput.off('keyup').on('keyup', debouncedExecSearch);
|
||||
|
||||
|
@ -37,16 +40,16 @@
|
|||
initSearchState: function($searchInput) {
|
||||
const currentSearchVal = $searchInput.val();
|
||||
|
||||
Issuable.searchState = {
|
||||
IssuableIndex.searchState = {
|
||||
elem: $searchInput,
|
||||
current: currentSearchVal
|
||||
};
|
||||
|
||||
Issuable.maybeFocusOnSearch();
|
||||
IssuableIndex.maybeFocusOnSearch();
|
||||
},
|
||||
accessSearchPristine: function(set) {
|
||||
// store reference to previous value to prevent search on non-mutating keyup
|
||||
const state = Issuable.searchState;
|
||||
const state = IssuableIndex.searchState;
|
||||
const currentSearchVal = state.elem.val();
|
||||
|
||||
if (set) {
|
||||
|
@ -56,10 +59,10 @@
|
|||
}
|
||||
},
|
||||
maybeFocusOnSearch: function() {
|
||||
const currentSearchVal = Issuable.searchState.current;
|
||||
const currentSearchVal = IssuableIndex.searchState.current;
|
||||
if (currentSearchVal && currentSearchVal !== '') {
|
||||
const queryLength = currentSearchVal.length;
|
||||
const $searchInput = Issuable.searchState.elem;
|
||||
const $searchInput = IssuableIndex.searchState.elem;
|
||||
|
||||
/* The following ensures that the cursor is initially placed at
|
||||
* the end of search input when focus is applied. It accounts
|
||||
|
@ -80,7 +83,7 @@
|
|||
const $searchValue = $search.val();
|
||||
const $filtersForm = $('.js-filter-form');
|
||||
const $input = $(`input[name='${$searchName}']`, $filtersForm);
|
||||
const isPristine = Issuable.accessSearchPristine();
|
||||
const isPristine = IssuableIndex.accessSearchPristine();
|
||||
|
||||
if (isPristine) {
|
||||
return;
|
||||
|
@ -92,7 +95,7 @@
|
|||
$input.val($searchValue);
|
||||
}
|
||||
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
},
|
||||
initLabelFilterRemove: function() {
|
||||
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
|
||||
|
@ -103,7 +106,7 @@
|
|||
return this.value === $button.data('label');
|
||||
}).remove();
|
||||
// Submit the form to get new data
|
||||
Issuable.filterResults($('.filter-form'));
|
||||
IssuableIndex.filterResults($('.filter-form'));
|
||||
});
|
||||
},
|
||||
filterResults: (function(_this) {
|
||||
|
@ -132,38 +135,18 @@
|
|||
gl.utils.visitUrl(baseIssuesUrl);
|
||||
});
|
||||
},
|
||||
initChecks: function() {
|
||||
this.issuableBulkActions = $('.bulk-update').data('bulkActions');
|
||||
$('.check_all_issues').off('click').on('click', function() {
|
||||
$('.selected_issue').prop('checked', this.checked);
|
||||
return Issuable.checkChanged();
|
||||
});
|
||||
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
|
||||
},
|
||||
checkChanged: function() {
|
||||
const $checkedIssues = $('.selected_issue:checked');
|
||||
const $updateIssuesIds = $('#update_issuable_ids');
|
||||
const $issuesOtherFilters = $('.issues-other-filters');
|
||||
const $issuesBulkUpdate = $('.issues_bulk_update');
|
||||
initBulkUpdate: function(pagePrefix) {
|
||||
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
|
||||
const alreadyInitialized = !!this.bulkUpdateSidebar;
|
||||
|
||||
this.issuableBulkActions.willUpdateLabels = false;
|
||||
this.issuableBulkActions.setOriginalDropdownData();
|
||||
|
||||
if ($checkedIssues.length > 0) {
|
||||
const ids = $.map($checkedIssues, function(value) {
|
||||
return $(value).data('id');
|
||||
if (userCanBulkUpdate && !alreadyInitialized) {
|
||||
IssuableBulkUpdateActions.init({
|
||||
prefixId: pagePrefix,
|
||||
});
|
||||
$updateIssuesIds.val(ids);
|
||||
$issuesOtherFilters.hide();
|
||||
$issuesBulkUpdate.show();
|
||||
} else {
|
||||
$updateIssuesIds.val([]);
|
||||
$issuesBulkUpdate.hide();
|
||||
$issuesOtherFilters.show();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
|
||||
}
|
||||
},
|
||||
resetIncomingEmailToken: function() {
|
||||
$('.incoming-email-token-reset').on('click', function(e) {
|
||||
e.preventDefault();
|
|
@ -1,166 +0,0 @@
|
|||
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
|
||||
/* global Issuable */
|
||||
/* global Flash */
|
||||
|
||||
((global) => {
|
||||
class IssuableBulkActions {
|
||||
constructor({ container, form, issues, prefixId } = {}) {
|
||||
this.prefixId = prefixId || 'issue_';
|
||||
this.form = form || this.getElement('.bulk-update');
|
||||
this.$labelDropdown = this.form.find('.js-label-select');
|
||||
this.issues = issues || this.getElement('.issues-list .issue');
|
||||
this.form.data('bulkActions', this);
|
||||
this.willUpdateLabels = false;
|
||||
this.bindEvents();
|
||||
// Fixes bulk-assign not working when navigating through pages
|
||||
Issuable.initChecks();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
|
||||
}
|
||||
|
||||
onFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
return this.submit();
|
||||
}
|
||||
|
||||
submit() {
|
||||
const _this = this;
|
||||
const xhr = $.ajax({
|
||||
url: this.form.attr('action'),
|
||||
method: this.form.attr('method'),
|
||||
dataType: 'JSON',
|
||||
data: this.getFormDataAsObject()
|
||||
});
|
||||
xhr.done(() => window.location.reload());
|
||||
xhr.fail(() => new Flash("Issue update failed"));
|
||||
return xhr.always(this.onFormSubmitAlways.bind(this));
|
||||
}
|
||||
|
||||
onFormSubmitAlways() {
|
||||
return this.form.find('[type="submit"]').enable();
|
||||
}
|
||||
|
||||
getSelectedIssues() {
|
||||
return this.issues.has('.selected_issue:checked');
|
||||
}
|
||||
|
||||
getLabelsFromSelection() {
|
||||
const labels = [];
|
||||
this.getSelectedIssues().map(function() {
|
||||
const labelsData = $(this).data('labels');
|
||||
if (labelsData) {
|
||||
return labelsData.map(function(labelId) {
|
||||
if (labels.indexOf(labelId) === -1) {
|
||||
return labels.push(labelId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return only labels that were marked previously and the user has unmarked
|
||||
* @return {Array} Label IDs
|
||||
*/
|
||||
|
||||
getUnmarkedIndeterminedLabels() {
|
||||
const result = [];
|
||||
const labelsToKeep = this.$labelDropdown.data('indeterminate');
|
||||
|
||||
this.getLabelsFromSelection().forEach((id) => {
|
||||
if (labelsToKeep.indexOf(id) === -1) {
|
||||
result.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple form serialization, it will return just what we need
|
||||
* Returns key/value pairs from form data
|
||||
*/
|
||||
|
||||
getFormDataAsObject() {
|
||||
const formData = {
|
||||
update: {
|
||||
state_event: this.form.find('input[name="update[state_event]"]').val(),
|
||||
// For Merge Requests
|
||||
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
|
||||
// For Issues
|
||||
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
|
||||
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
|
||||
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
|
||||
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
|
||||
add_label_ids: [],
|
||||
remove_label_ids: []
|
||||
}
|
||||
};
|
||||
if (this.willUpdateLabels) {
|
||||
formData.update.add_label_ids = this.$labelDropdown.data('marked');
|
||||
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
setOriginalDropdownData() {
|
||||
const $labelSelect = $('.bulk-update .js-label-select');
|
||||
$labelSelect.data('common', this.getOriginalCommonIds());
|
||||
$labelSelect.data('marked', this.getOriginalMarkedIds());
|
||||
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
|
||||
}
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalCommonIds() {
|
||||
const labelIds = [];
|
||||
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
||||
});
|
||||
return _.intersection.apply(this, labelIds);
|
||||
}
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalMarkedIds() {
|
||||
const labelIds = [];
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
||||
});
|
||||
return _.intersection.apply(this, labelIds);
|
||||
}
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalIndeterminateIds() {
|
||||
const uniqueIds = [];
|
||||
const labelIds = [];
|
||||
let issuableLabels = [];
|
||||
|
||||
// Collect unique label IDs for all checked issues
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
|
||||
issuableLabels.forEach((labelId) => {
|
||||
// Store unique IDs
|
||||
if (uniqueIds.indexOf(labelId) === -1) {
|
||||
uniqueIds.push(labelId);
|
||||
}
|
||||
});
|
||||
// Store array of IDs per issuable
|
||||
labelIds.push(issuableLabels);
|
||||
});
|
||||
// Add uniqueIds to add it as argument for _.intersection
|
||||
labelIds.unshift(uniqueIds);
|
||||
// Return IDs that are present but not in all selected issueables
|
||||
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
|
||||
}
|
||||
|
||||
getElement(selector) {
|
||||
this.scopeEl = this.scopeEl || $('.content');
|
||||
return this.scopeEl.find(selector);
|
||||
}
|
||||
}
|
||||
|
||||
global.IssuableBulkActions = IssuableBulkActions;
|
||||
})(window.gl || (window.gl = {}));
|
|
@ -2,6 +2,8 @@
|
|||
/* global Issuable */
|
||||
/* global ListLabel */
|
||||
|
||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
|
||||
(function() {
|
||||
this.LabelsSelect = (function() {
|
||||
function LabelsSelect(els) {
|
||||
|
@ -430,20 +432,15 @@
|
|||
if ($('.selected_issue:checked').length) {
|
||||
return;
|
||||
}
|
||||
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
|
||||
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
|
||||
};
|
||||
|
||||
LabelsSelect.prototype.enableBulkLabelDropdown = function() {
|
||||
var issuableBulkActions;
|
||||
if ($('.selected_issue:checked').length) {
|
||||
issuableBulkActions = $('.bulk-update').data('bulkActions');
|
||||
return issuableBulkActions.willUpdateLabels = true;
|
||||
}
|
||||
IssuableBulkUpdateActions.willUpdateLabels = true;
|
||||
};
|
||||
|
||||
LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
|
||||
var i, markedIds, unmarkedIds, indeterminateIds;
|
||||
var issuableBulkActions = $('.bulk-update').data('bulkActions');
|
||||
|
||||
markedIds = $dropdown.data('marked') || [];
|
||||
unmarkedIds = $dropdown.data('unmarked') || [];
|
||||
|
@ -469,13 +466,13 @@
|
|||
}
|
||||
|
||||
// If an indeterminate item is being unmarked
|
||||
if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
|
||||
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 (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
|
||||
if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
|
||||
unmarkedIds.push(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,12 +104,11 @@ import './group_label_subscription';
|
|||
import './groups_select';
|
||||
import './header';
|
||||
import './importer_status';
|
||||
import './issuable';
|
||||
import './issuable_index';
|
||||
import './issuable_context';
|
||||
import './issuable_form';
|
||||
import './issue';
|
||||
import './issue_status_select';
|
||||
import './issues_bulk_assignment';
|
||||
import './label_manager';
|
||||
import './labels';
|
||||
import './labels_select';
|
||||
|
|
|
@ -445,3 +445,9 @@ table {
|
|||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled-content {
|
||||
pointer-events: none;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,12 +22,6 @@
|
|||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.issues_bulk_update {
|
||||
.dropdown-menu-toggle {
|
||||
width: 132px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-item:not(:last-child) {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
@ -376,12 +370,6 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
.issue-bulk-update-dropdown-toggle {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
.issues-details-filters {
|
||||
padding: 0 0 10px;
|
||||
|
|
|
@ -29,10 +29,6 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.issues-holder .issue-check {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rss-btn {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
padding-right: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
|
||||
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
|
||||
padding-right: $gutter_collapsed_width;
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@
|
|||
z-index: 300;
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
|
||||
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
|
||||
padding-right: $gutter_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
@ -88,3 +88,35 @@
|
|||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin maintain-sidebar-dimensions {
|
||||
display: block;
|
||||
width: $gutter-width;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.issues-bulk-update.right-sidebar {
|
||||
@include maintain-sidebar-dimensions;
|
||||
transition: right $sidebar-transition-duration;
|
||||
right: -$gutter-width;
|
||||
|
||||
&.right-sidebar-expanded {
|
||||
@include maintain-sidebar-dimensions;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.right-sidebar-collapsed {
|
||||
@include maintain-sidebar-dimensions;
|
||||
right: -$gutter-width;
|
||||
|
||||
.block {
|
||||
padding: 16px 0;
|
||||
width: 250px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-sidebar {
|
||||
padding: 0 3px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
|
||||
.issue-box
|
||||
- if @bulk_edit
|
||||
.issue-check
|
||||
- if @can_bulk_update
|
||||
.issue-check.hidden
|
||||
= check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
|
||||
.issue-info-container
|
||||
.issue-title.title
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- @no_container = true
|
||||
- @bulk_edit = can?(current_user, :admin_issue, @project)
|
||||
- @can_bulk_update = can?(current_user, :admin_issue, @project)
|
||||
|
||||
- page_title "Issues"
|
||||
- new_issue_email = @project.new_issue_address(current_user)
|
||||
|
@ -20,6 +20,8 @@
|
|||
.nav-controls
|
||||
= link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
|
||||
= icon('rss')
|
||||
- if @can_bulk_update
|
||||
= button_tag "Edit Issues", class: "btn btn-default js-bulk-update-toggle"
|
||||
= link_to new_namespace_project_issue_path(@project.namespace,
|
||||
@project,
|
||||
issue: { assignee_id: issues_finder.assignee.try(:id),
|
||||
|
@ -30,6 +32,9 @@
|
|||
New issue
|
||||
= render 'shared/issuable/search_bar', type: :issues
|
||||
|
||||
- if @can_bulk_update
|
||||
= render 'shared/issuable/bulk_update_sidebar', type: :issues
|
||||
|
||||
.issues-holder
|
||||
= render 'issues'
|
||||
- if new_issue_email
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
|
||||
- if @bulk_edit
|
||||
.issue-check
|
||||
- if @can_bulk_update
|
||||
.issue-check.hidden
|
||||
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
|
||||
|
||||
.issue-info-container
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- @no_container = true
|
||||
- @bulk_edit = can?(current_user, :admin_merge_request, @project)
|
||||
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
|
||||
|
||||
- page_title "Merge Requests"
|
||||
- unless @project.default_issues_tracker?
|
||||
|
@ -18,6 +18,8 @@
|
|||
.top-area
|
||||
= render 'shared/issuable/nav', type: :merge_requests
|
||||
.nav-controls
|
||||
- if @can_bulk_update
|
||||
= button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
|
||||
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
|
||||
- if merge_project
|
||||
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do
|
||||
|
@ -25,6 +27,9 @@
|
|||
|
||||
= render 'shared/issuable/search_bar', type: :merge_requests
|
||||
|
||||
- if @can_bulk_update
|
||||
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
|
||||
|
||||
.merge-requests-holder
|
||||
= render 'merge_requests'
|
||||
- else
|
||||
|
|
53
app/views/shared/issuable/_bulk_update_sidebar.html.haml
Normal file
53
app/views/shared/issuable/_bulk_update_sidebar.html.haml
Normal file
|
@ -0,0 +1,53 @@
|
|||
- type = local_assigns.fetch(:type)
|
||||
|
||||
%aside.issues-bulk-update.js-right-sidebar.right-sidebar.affix-top{ data: { "offset-top" => "50", "spy" => "affix" }, "aria-live" => "polite" }
|
||||
.issuable-sidebar
|
||||
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
|
||||
.block
|
||||
.filter-item.inline.update-issues-btn.pull-left
|
||||
= button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true
|
||||
= button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide pull-right"
|
||||
.block
|
||||
.title
|
||||
Status
|
||||
.filter-item
|
||||
= dropdown_tag("Select status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "reopen" } } Open
|
||||
%li
|
||||
%a{ href: "#", data: { id: "close" } } Closed
|
||||
.block
|
||||
.title
|
||||
Assignee
|
||||
.filter-item
|
||||
- if type == :issues
|
||||
- field_name = "update[assignee_ids][]"
|
||||
- else
|
||||
- field_name = "update[assignee_id]"
|
||||
= dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
|
||||
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
|
||||
.block
|
||||
.title
|
||||
Milestone
|
||||
.filter-item
|
||||
= dropdown_tag("Select milestone", options: { title: "Assign milestone", toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
|
||||
.block
|
||||
.title
|
||||
Labels
|
||||
.filter-item.labels-filter
|
||||
= render "shared/issuable/label_dropdown", classes: ["js-filter-bulk-update", "js-multiselect"], dropdown_title: "Apply a label", show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }, label_name: "Select labels", no_default_styles: true
|
||||
.block
|
||||
.title
|
||||
Subscriptions
|
||||
.filter-item
|
||||
= dropdown_tag("Select subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "subscribe" } } Subscribe
|
||||
%li
|
||||
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
|
||||
|
||||
= hidden_field_tag "update[issuable_ids]", []
|
||||
= hidden_field_tag :state_event, params[:state_event]
|
||||
|
|
@ -6,10 +6,6 @@
|
|||
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
|
||||
- if params[:search].present?
|
||||
= hidden_field_tag :search, params[:search]
|
||||
- if @bulk_edit
|
||||
.check-all-holder
|
||||
= check_box_tag "check_all_issues", nil, false,
|
||||
class: "check_all_issues left"
|
||||
.issues-other-filters
|
||||
.filter-item.inline
|
||||
- if params[:author_id].present?
|
||||
|
@ -36,35 +32,6 @@
|
|||
.pull-right
|
||||
= render 'shared/sort_dropdown'
|
||||
|
||||
- if @bulk_edit
|
||||
.issues_bulk_update.hide
|
||||
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Status", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "reopen" } } Open
|
||||
%li
|
||||
%a{ href: "#", data: {id: "close" } } Closed
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Assignee", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
|
||||
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'issue-bulk-update-dropdown-toggle js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
|
||||
.filter-item.inline.labels-filter
|
||||
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Subscription", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "subscribe" } } Subscribe
|
||||
%li
|
||||
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
|
||||
|
||||
= hidden_field_tag 'update[issuable_ids]', []
|
||||
= hidden_field_tag :state_event, params[:state_event]
|
||||
.filter-item.inline
|
||||
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
|
||||
- has_labels = @labels && @labels.any?
|
||||
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
|
||||
- if has_labels
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
|
||||
- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
|
||||
- dropdown_data.merge!(data_options)
|
||||
- label_name = local_assigns.fetch(:label_name, "Labels")
|
||||
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
|
||||
- classes << 'js-extra-options' if extra_options
|
||||
- classes << 'js-filter-submit' if filter_submit
|
||||
|
||||
|
@ -20,8 +22,9 @@
|
|||
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data }
|
||||
%span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
|
||||
= multi_label_name(selected, "Labels")
|
||||
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
|
||||
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
|
||||
= multi_label_name(selected, label_name)
|
||||
= icon('chevron-down')
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
|
||||
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
|
||||
- if params[:search].present?
|
||||
= hidden_field_tag :search, params[:search]
|
||||
- if @bulk_edit
|
||||
.check-all-holder
|
||||
= check_box_tag "check_all_issues", nil, false,
|
||||
class: "check_all_issues left"
|
||||
- if @can_bulk_update
|
||||
.check-all-holder.hidden
|
||||
= check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
|
||||
.issues-other-filters.filtered-search-wrapper
|
||||
.filtered-search-box
|
||||
- if type != :boards_modal && type != :boards
|
||||
|
@ -110,55 +109,11 @@
|
|||
- elsif type != :boards_modal
|
||||
= render 'shared/sort_dropdown'
|
||||
|
||||
- if @bulk_edit
|
||||
.issues_bulk_update.hide
|
||||
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "reopen" } } Open
|
||||
%li
|
||||
%a{ href: "#", data: { id: "close" } } Closed
|
||||
.filter-item.inline
|
||||
- if type == :issues
|
||||
- field_name = "update[assignee_ids][]"
|
||||
- else
|
||||
- field_name = "update[assignee_id]"
|
||||
|
||||
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
|
||||
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
|
||||
.filter-item.inline.labels-filter
|
||||
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "subscribe" } } Subscribe
|
||||
%li
|
||||
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
|
||||
|
||||
= hidden_field_tag 'update[issuable_ids]', []
|
||||
= hidden_field_tag :state_event, params[:state_event]
|
||||
.filter-item.inline.update-issues-btn
|
||||
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
|
||||
|
||||
- unless type === :boards_modal
|
||||
:javascript
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
new IssueStatusSelect();
|
||||
new SubscriptionSelect();
|
||||
|
||||
$(document).off('page:restore').on('page:restore', function (event) {
|
||||
if (gl.FilteredSearchManager) {
|
||||
const filteredSearchManager = new gl.FilteredSearchManager();
|
||||
filteredSearchManager.setup();
|
||||
}
|
||||
Issuable.init();
|
||||
new gl.IssuableBulkActions({
|
||||
prefixId: 'issue_',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,13 +18,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
|
||||
context 'can bulk assign' do
|
||||
before do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
context 'a label' do
|
||||
context 'to all issues' do
|
||||
before do
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
open_labels_dropdown ['bug']
|
||||
update_issues
|
||||
end
|
||||
|
@ -52,7 +52,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
context 'multiple labels' do
|
||||
context 'to all issues' do
|
||||
before do
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
open_labels_dropdown %w(bug feature)
|
||||
update_issues
|
||||
end
|
||||
|
@ -86,9 +86,10 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
before do
|
||||
issue2.labels << bug
|
||||
issue2.labels << feature
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
check 'check_all_issues'
|
||||
enable_bulk_update
|
||||
check 'check-all-issues'
|
||||
|
||||
open_labels_dropdown ['bug']
|
||||
update_issues
|
||||
end
|
||||
|
@ -107,9 +108,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue2.labels << bug
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
check 'check_all_issues'
|
||||
enable_bulk_update
|
||||
check 'check-all-issues'
|
||||
unmark_labels_in_dropdown %w(bug feature)
|
||||
update_issues
|
||||
end
|
||||
|
@ -127,8 +127,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue1.labels << bug
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
enable_bulk_update
|
||||
check_issue issue1
|
||||
unmark_labels_in_dropdown ['bug']
|
||||
update_issues
|
||||
|
@ -147,8 +146,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue2.labels << bug
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
enable_bulk_update
|
||||
check_issue issue1
|
||||
check_issue issue2
|
||||
unmark_labels_in_dropdown ['bug']
|
||||
|
@ -171,14 +169,15 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
before do
|
||||
issue1.labels << bug
|
||||
issue2.labels << feature
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'keeps labels' do
|
||||
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
|
||||
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
|
||||
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
|
||||
open_milestone_dropdown(['First Release'])
|
||||
update_issues
|
||||
|
||||
|
@ -192,14 +191,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
context 'setting a milestone and adding another label' do
|
||||
before do
|
||||
issue1.labels << bug
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'keeps existing label and new label is present' do
|
||||
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
|
||||
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
open_milestone_dropdown ['First Release']
|
||||
open_labels_dropdown ['feature']
|
||||
update_issues
|
||||
|
@ -218,7 +216,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue1.labels << feature
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'keeps existing label and new label is present' do
|
||||
|
@ -226,7 +224,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
|
||||
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
|
||||
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
|
||||
open_milestone_dropdown ['First Release']
|
||||
unmark_labels_in_dropdown ['feature']
|
||||
update_issues
|
||||
|
@ -248,7 +247,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue1.labels << bug
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'keeps labels' do
|
||||
|
@ -257,7 +256,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
|
||||
expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
|
||||
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
open_milestone_dropdown(['No Milestone'])
|
||||
update_issues
|
||||
|
||||
|
@ -272,8 +271,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
context 'toggling checked issues' do
|
||||
before do
|
||||
issue1.labels << bug
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it do
|
||||
|
@ -298,14 +296,14 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue1.labels << feature
|
||||
issue2.labels << bug
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'applies label from filtered results' do
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
|
||||
page.within('.issues_bulk_update') do
|
||||
click_button 'Labels'
|
||||
page.within('.issues-bulk-update') do
|
||||
click_button 'Select labels'
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active')
|
||||
|
@ -340,15 +338,16 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
|
||||
context 'cannot bulk assign labels' do
|
||||
it do
|
||||
expect(page).not_to have_css '.check_all_issues'
|
||||
expect(page).not_to have_button 'Edit Issues'
|
||||
expect(page).not_to have_css '.check-all-issues'
|
||||
expect(page).not_to have_css '.issue-check'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def open_milestone_dropdown(items = [])
|
||||
page.within('.issues_bulk_update') do
|
||||
click_button 'Milestone'
|
||||
page.within('.issues-bulk-update') do
|
||||
click_button 'Select milestone'
|
||||
wait_for_requests
|
||||
items.map do |item|
|
||||
click_link item
|
||||
|
@ -357,8 +356,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
end
|
||||
|
||||
def open_labels_dropdown(items = [], unmark = false)
|
||||
page.within('.issues_bulk_update') do
|
||||
click_button 'Labels'
|
||||
page.within('.issues-bulk-update') do
|
||||
click_button 'Select labels'
|
||||
wait_for_requests
|
||||
items.map do |item|
|
||||
click_link item
|
||||
|
@ -391,7 +390,12 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
end
|
||||
|
||||
def update_issues
|
||||
click_button 'Update issues'
|
||||
click_button 'Update all'
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
def enable_bulk_update
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
click_button 'Edit Issues'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
it 'sets to closed' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
find('.js-issue-status').click
|
||||
|
||||
find('.dropdown-menu-status a', text: 'Closed').click
|
||||
|
@ -26,7 +27,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
create_closed
|
||||
visit namespace_project_issues_path(project.namespace, project, state: 'closed')
|
||||
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
find('.js-issue-status').click
|
||||
|
||||
find('.dropdown-menu-status a', text: 'Open').click
|
||||
|
@ -39,7 +41,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
it 'updates to current user' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
click_update_assignee_button
|
||||
|
||||
find('.dropdown-menu-user-link', text: user.username).click
|
||||
|
@ -54,7 +57,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
create_assigned
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
click_update_assignee_button
|
||||
|
||||
click_link 'Unassigned'
|
||||
|
@ -69,8 +73,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
it 'updates milestone' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
find('#check_all_issues').click
|
||||
find('.issues_bulk_update .js-milestone-select').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
find('.issues-bulk-update .js-milestone-select').click
|
||||
|
||||
find('.dropdown-menu-milestone a', text: milestone.title).click
|
||||
click_update_issues_button
|
||||
|
@ -84,8 +89,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
|
||||
expect(first('.issue')).to have_content milestone.title
|
||||
|
||||
find('#check_all_issues').click
|
||||
find('.issues_bulk_update .js-milestone-select').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
find('.issues-bulk-update .js-milestone-select').click
|
||||
|
||||
find('.dropdown-menu-milestone a', text: "No Milestone").click
|
||||
click_update_issues_button
|
||||
|
@ -112,7 +118,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
end
|
||||
|
||||
def click_update_issues_button
|
||||
find('.update_selected_issues').click
|
||||
find('.update-selected-issues').click
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,14 +98,16 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
|
|||
end
|
||||
|
||||
def change_status(text)
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Merge Requests'
|
||||
find('#check-all-issues').click
|
||||
find('.js-issue-status').click
|
||||
find('.dropdown-menu-status a', text: text).click
|
||||
click_update_merge_requests_button
|
||||
end
|
||||
|
||||
def change_assignee(text)
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Merge Requests'
|
||||
find('#check-all-issues').click
|
||||
find('.js-update-assignee').click
|
||||
wait_for_requests
|
||||
|
||||
|
@ -117,14 +119,15 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
|
|||
end
|
||||
|
||||
def change_milestone(text)
|
||||
find('#check_all_issues').click
|
||||
find('.issues_bulk_update .js-milestone-select').click
|
||||
click_button 'Edit Merge Requests'
|
||||
find('#check-all-issues').click
|
||||
find('.issues-bulk-update .js-milestone-select').click
|
||||
find('.dropdown-menu-milestone a', text: text).click
|
||||
click_update_merge_requests_button
|
||||
end
|
||||
|
||||
def click_update_merge_requests_button
|
||||
find('.update_selected_issues').click
|
||||
find('.update-selected-issues').click
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
|
||||
%input{id: 'utf8', name: 'utf8', value: '✓'}
|
||||
%input{id: 'check_all_issues', name: 'check_all_issues'}
|
||||
%input{id: 'check-all-issues', name: 'check-all-issues'}
|
||||
%input{id: 'search', name: 'search'}
|
||||
%input{id: 'author_id', name: 'author_id'}
|
||||
%input{id: 'assignee_id', name: 'assignee_id'}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global Issuable */
|
||||
/* global IssuableIndex */
|
||||
|
||||
import '~/lib/utils/url_utility';
|
||||
import '~/issuable';
|
||||
import '~/issuable_index';
|
||||
|
||||
(() => {
|
||||
const BASE_URL = '/user/project/issues?scope=all&state=closed';
|
||||
|
@ -24,11 +24,11 @@ import '~/issuable';
|
|||
|
||||
beforeEach(() => {
|
||||
loadFixtures('static/issuable_filter.html.raw');
|
||||
Issuable.init();
|
||||
IssuableIndex.init();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(window.Issuable).toBeDefined();
|
||||
expect(window.IssuableIndex).toBeDefined();
|
||||
});
|
||||
|
||||
describe('filtering', () => {
|
||||
|
@ -43,7 +43,7 @@ import '~/issuable';
|
|||
it('should contain only the default parameters', () => {
|
||||
spyOn(gl.utils, 'visitUrl');
|
||||
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
|
||||
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
|
||||
});
|
||||
|
@ -52,7 +52,7 @@ import '~/issuable';
|
|||
spyOn(gl.utils, 'visitUrl');
|
||||
|
||||
updateForm({ search: 'broken' }, $filtersForm);
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
const params = `${DEFAULT_PARAMS}&search=broken`;
|
||||
|
||||
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
|
||||
|
@ -64,14 +64,14 @@ import '~/issuable';
|
|||
// initial filter
|
||||
updateForm({ milestone_title: 'v1.0' }, $filtersForm);
|
||||
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
|
||||
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
|
||||
|
||||
// update filter
|
||||
updateForm({ label_name: 'Frontend' }, $filtersForm);
|
||||
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
|
||||
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue