Commit 106ce49c authored by Coung Ngo's avatar Coung Ngo

Fix bulk editing labels bug

Fix bug where labels were being bulk added that the user did not
explicitly select
parent 7ce7a8a3
/* eslint-disable consistent-return, func-names, array-callback-return */
import $ from 'jquery'; import $ from 'jquery';
import { intersection } from 'lodash'; import { difference, intersection, union } from 'lodash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import Flash from './flash'; import Flash from './flash';
import { __ } from './locale'; import { __ } from './locale';
...@@ -36,43 +34,6 @@ export default { ...@@ -36,43 +34,6 @@ export default {
return new Flash(__('Issue update failed')); return new Flash(__('Issue update failed'));
}, },
getSelectedIssues() {
return this.issues.has('.selected-issuable:checked');
},
getLabelsFromSelection() {
const labels = [];
this.getSelectedIssues().map(function() {
const labelsData = $(this).data('labels');
if (labelsData) {
return labelsData.map(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 * Simple form serialization, it will return just what we need
* Returns key/value pairs from form data * Returns key/value pairs from form data
...@@ -93,35 +54,37 @@ export default { ...@@ -93,35 +54,37 @@ export default {
}, },
}; };
if (this.willUpdateLabels) { if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked'); formData.update.add_label_ids = this.$labelDropdown.data('user-checked');
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); formData.update.remove_label_ids = this.$labelDropdown.data('user-unchecked');
} }
return formData; return formData;
}, },
setOriginalDropdownData() { setOriginalDropdownData() {
const $labelSelect = $('.bulk-update .js-label-select'); const $labelSelect = $('.bulk-update .js-label-select');
const dirtyLabelIds = $labelSelect.data('marked') || []; const userCheckedIds = $labelSelect.data('user-checked') || [];
const chosenLabelIds = [...this.getOriginalMarkedIds(), ...dirtyLabelIds]; const userUncheckedIds = $labelSelect.data('user-unchecked') || [];
$labelSelect.data('common', this.getOriginalCommonIds()); // Common labels plus user checked labels minus user unchecked labels
$labelSelect.data('marked', chosenLabelIds); const checkedIdsToShow = difference(
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds()); union(this.getOriginalCommonIds(), userCheckedIds),
userUncheckedIds,
);
// Indeterminate labels minus user checked labels minus user unchecked labels
const indeterminateIdsToShow = difference(
this.getOriginalIndeterminateIds(),
userCheckedIds,
userUncheckedIds,
);
$labelSelect.data('marked', checkedIdsToShow);
$labelSelect.data('indeterminate', indeterminateIdsToShow);
}, },
// From issuable's initial bulk selection // From issuable's initial bulk selection
getOriginalCommonIds() { getOriginalCommonIds() {
const labelIds = []; const labelIds = [];
this.getElement('.selected-issuable: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-issuable:checked').each((i, el) => { this.getElement('.selected-issuable:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
}); });
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
/* global ListLabel */ /* global ListLabel */
import $ from 'jquery'; import $ from 'jquery';
import { isEqual, escape, sortBy, template } from 'lodash'; import { difference, isEqual, escape, sortBy, template } from 'lodash';
import { sprintf, s__, __ } from './locale'; import { sprintf, s__, __ } from './locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
...@@ -560,45 +560,20 @@ export default class LabelsSelect { ...@@ -560,45 +560,20 @@ export default class LabelsSelect {
IssuableBulkUpdateActions.willUpdateLabels = true; IssuableBulkUpdateActions.willUpdateLabels = true;
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
setDropdownData($dropdown, isMarking, value) { setDropdownData($dropdown, isChecking, labelId) {
const markedIds = $dropdown.data('marked') || []; let userCheckedIds = $dropdown.data('user-checked') || [];
const unmarkedIds = $dropdown.data('unmarked') || []; let userUncheckedIds = $dropdown.data('user-unchecked') || [];
const indeterminateIds = $dropdown.data('indeterminate') || [];
if (isMarking) { if (isChecking) {
markedIds.push(value); userCheckedIds = userCheckedIds.concat(labelId);
userUncheckedIds = difference(userUncheckedIds, [labelId]);
let i = indeterminateIds.indexOf(value);
if (i > -1) {
indeterminateIds.splice(i, 1);
}
i = unmarkedIds.indexOf(value);
if (i > -1) {
unmarkedIds.splice(i, 1);
}
} else { } else {
// If marked item (not common) is unmarked userUncheckedIds = userUncheckedIds.concat(labelId);
const i = markedIds.indexOf(value); userCheckedIds = difference(userCheckedIds, [labelId]);
if (i > -1) {
markedIds.splice(i, 1);
}
// If an indeterminate item is being unmarked
if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
// If a marked item is being unmarked
// (a marked item could also be a label that is present in all selection)
if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
} }
$dropdown.data('marked', markedIds); $dropdown.data('user-checked', userCheckedIds);
$dropdown.data('unmarked', unmarkedIds); $dropdown.data('user-unchecked', userUncheckedIds);
$dropdown.data('indeterminate', indeterminateIds);
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
setOriginalDropdownData($container, $dropdown) { setOriginalDropdownData($container, $dropdown) {
......
---
title: Fix bulk editing labels bug
merge_request: 36981
author:
type: fixed
...@@ -5,11 +5,12 @@ require 'spec_helper' ...@@ -5,11 +5,12 @@ require 'spec_helper'
RSpec.describe 'Issues > Labels bulk assignment' do RSpec.describe 'Issues > Labels bulk assignment' do
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:issue1) { create(:issue, project: project, title: "Issue 1") }
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
let!(:bug) { create(:label, project: project, title: 'bug') } let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:feature) { create(:label, project: project, title: 'feature') } let!(:feature) { create(:label, project: project, title: 'feature') }
let!(:frontend) { create(:label, project: project, title: 'frontend') }
let!(:wontfix) { create(:label, project: project, title: 'wontfix') } let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
let!(:issue1) { create(:issue, project: project, title: "Issue 1", labels: [frontend]) }
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
context 'as an allowed user', :js do context 'as an allowed user', :js do
before do before do
...@@ -52,19 +53,50 @@ RSpec.describe 'Issues > Labels bulk assignment' do ...@@ -52,19 +53,50 @@ RSpec.describe 'Issues > Labels bulk assignment' do
it do it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'bug' expect(find("#issue_#{issue2.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'frontend'
end end
end end
context 'to a issue' do context 'to some issues' do
before do before do
check "selected_issue_#{issue1.id}" check "selected_issue_#{issue1.id}"
check "selected_issue_#{issue2.id}"
open_labels_dropdown ['bug'] open_labels_dropdown ['bug']
update_issues update_issues
end end
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'frontend'
end
end
context 'to an issue' do
before do
check "selected_issue_#{issue1.id}"
open_labels_dropdown ['bug']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'frontend'
end
end
context 'to an issue by selecting the label first' do
before do
open_labels_dropdown ['bug']
check "selected_issue_#{issue1.id}"
update_issues
end
it do it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'frontend'
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment