diff options
author | Bryce Johnson <bryce@gitlab.com> | 2017-05-15 18:12:43 -0400 |
---|---|---|
committer | Bryce Johnson <bryce@gitlab.com> | 2017-06-06 09:16:27 -0400 |
commit | c9a67266d2a79bd55f944516716a59300c1844f6 (patch) | |
tree | 73dd4086376d3674faf9484ff28c6d7edfe9765b /app | |
parent | 34f925fe0bebdc7212c1c960678114a4e44828ef (diff) | |
download | gitlab-ce-c9a67266d2a79bd55f944516716a59300c1844f6.tar.gz |
Move issuable bulk edit form into a new sidebar.28340-mass-edit-issues-and-mrs-from-sidebar
Diffstat (limited to 'app')
19 files changed, 484 insertions, 338 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index baa20d0c34a..aa0871eb771 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -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; diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js new file mode 100644 index 00000000000..e46c0e90255 --- /dev/null +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -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); + }, +}; diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js new file mode 100644 index 00000000000..84bd2e092e6 --- /dev/null +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -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 []; + } +} diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable_index.js index 3bfce32768a..5c96646def8 100644 --- a/app/assets/javascripts/issuable.js +++ b/app/assets/javascripts/issuable_index.js @@ -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'); - - this.issuableBulkActions.willUpdateLabels = false; - this.issuableBulkActions.setOriginalDropdownData(); - - if ($checkedIssues.length > 0) { - const ids = $.map($checkedIssues, function(value) { - return $(value).data('id'); + initBulkUpdate: function(pagePrefix) { + const userCanBulkUpdate = $('.issues-bulk-update').length > 0; + const alreadyInitialized = !!this.bulkUpdateSidebar; + + if (userCanBulkUpdate && !alreadyInitialized) { + IssuableBulkUpdateActions.init({ + prefixId: pagePrefix, }); - $updateIssuesIds.val(ids); - $issuesOtherFilters.hide(); - $issuesBulkUpdate.show(); - } else { - $updateIssuesIds.val([]); - $issuesBulkUpdate.hide(); - $issuesOtherFilters.show(); + + this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar(); } - return true; }, - resetIncomingEmailToken: function() { $('.incoming-email-token-reset').on('click', function(e) { e.preventDefault(); diff --git a/app/assets/javascripts/issues_bulk_assignment.js b/app/assets/javascripts/issues_bulk_assignment.js deleted file mode 100644 index fee3429e2b8..00000000000 --- a/app/assets/javascripts/issues_bulk_assignment.js +++ /dev/null @@ -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 = {})); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index ac5ce84e31b..8d7d3d73571 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -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); } } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 1ac82b7e291..fe367d0c42a 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -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'; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 57387b913dc..00c981f64c5 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -445,3 +445,9 @@ table { word-wrap: break-word; } } + +.disabled-content { + pointer-events: none; + opacity: .5; +} + diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 585f4871f5f..acf5de0e3b5 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -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; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 0140dcf19c3..600a1f53b58 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -29,10 +29,6 @@ display: none; } - .issues-holder .issue-check { - display: none; - } - .rss-btn { display: none; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 5b62d7fa3a7..d4421e3af74 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -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; + } +} diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index c184e0e0022..9e4e6934ca9 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -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 diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 60900e9d660..7183794ce72 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -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 diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 94b9577e9eb..c13110deb16 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -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 diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 2cb3045f83e..6d75a9f34a3 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -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 diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml new file mode 100644 index 00000000000..a8a6d84128d --- /dev/null +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -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] + diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 6cd03f028a9..2cabbc8c560 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -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 diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 1cf662e29c4..34911fd2712 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -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 } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index be9f9ee29c4..d3d290692a2 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -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_', - }); }); |