Commit c1d45f9c authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '322755-add-bulk-edit-to-issues-list-page-refactor' into 'master'

Add bulk edit to issues list page refactor

See merge request gitlab-org/gitlab!57230
parents 1ed3d56c 9ea6affb
...@@ -87,7 +87,7 @@ export default { ...@@ -87,7 +87,7 @@ export default {
// 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) => { this.getElement('.issuable-list input[type="checkbox"]: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'));
}); });
return intersection.apply(this, labelIds); return intersection.apply(this, labelIds);
...@@ -100,7 +100,7 @@ export default { ...@@ -100,7 +100,7 @@ export default {
let issuableLabels = []; let issuableLabels = [];
// Collect unique label IDs for all checked issues // Collect unique label IDs for all checked issues
this.getElement('.selected-issuable:checked').each((i, el) => { this.getElement('.issuable-list input[type="checkbox"]:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => { issuableLabels.forEach((labelId) => {
// Store unique IDs // Store unique IDs
......
...@@ -34,7 +34,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -34,7 +34,7 @@ export default class IssuableBulkUpdateSidebar {
this.$otherFilters = $('.issues-other-filters'); this.$otherFilters = $('.issues-other-filters');
this.$checkAllContainer = $('.check-all-holder'); this.$checkAllContainer = $('.check-all-holder');
this.$issueChecks = $('.issue-check'); this.$issueChecks = $('.issue-check');
this.$issuesList = $('.selected-issuable'); this.$issuesList = $('.issuable-list input[type="checkbox"]');
this.$issuableIdsInput = $('#update_issuable_ids'); this.$issuableIdsInput = $('#update_issuable_ids');
} }
...@@ -46,16 +46,14 @@ export default class IssuableBulkUpdateSidebar { ...@@ -46,16 +46,14 @@ export default class IssuableBulkUpdateSidebar {
this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit()); this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit());
this.$checkAllContainer.on('click', () => this.updateFormState()); this.$checkAllContainer.on('click', () => this.updateFormState());
if (this.vueIssuablesListFeature) { issueableEventHub.$on('issuables:updateBulkEdit', () => {
issueableEventHub.$on('issuables:updateBulkEdit', () => { // Danger! Strong coupling ahead!
// Danger! Strong coupling ahead! // The bulk update sidebar and its dropdowns look for checkboxes, and get data on which issue
// The bulk update sidebar and its dropdowns look for .selected-issuable checkboxes, and get data on which issue // is selected by inspecting the DOM. Ideally, we would pass the selected issuable IDs and their properties
// is selected by inspecting the DOM. Ideally, we would pass the selected issuable IDs and their properties // explicitly, but this component is used in too many places right now to refactor straight away.
// explicitly, but this component is used in too many places right now to refactor straight away.
this.updateFormState(); this.updateFormState();
}); });
}
} }
initDropdowns() { initDropdowns() {
...@@ -96,7 +94,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -96,7 +94,7 @@ export default class IssuableBulkUpdateSidebar {
} }
updateFormState() { updateFormState() {
const noCheckedIssues = !$('.selected-issuable:checked').length; const noCheckedIssues = !$('.issuable-list input[type="checkbox"]:checked').length;
this.toggleSubmitButtonDisabled(noCheckedIssues); this.toggleSubmitButtonDisabled(noCheckedIssues);
this.updateSelectedIssuableIds(); this.updateSelectedIssuableIds();
...@@ -166,7 +164,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -166,7 +164,7 @@ export default class IssuableBulkUpdateSidebar {
} }
static getCheckedIssueIds() { static getCheckedIssueIds() {
const $checkedIssues = $('.selected-issuable:checked'); const $checkedIssues = $('.issuable-list input[type="checkbox"]:checked');
if ($checkedIssues.length > 0) { if ($checkedIssues.length > 0) {
return $.map($checkedIssues, (value) => $(value).data('id')); return $.map($checkedIssues, (value) => $(value).data('id'));
......
import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar'; import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar';
export default class IssuableIndex { export default class IssuableIndex {
constructor(pagePrefix) { constructor(pagePrefix = 'issuable_') {
issuableInitBulkUpdateSidebar.init(pagePrefix); issuableInitBulkUpdateSidebar.init(pagePrefix);
} }
} }
...@@ -65,6 +65,9 @@ export default { ...@@ -65,6 +65,9 @@ export default {
labels() { labels() {
return this.issuable.labels?.nodes || this.issuable.labels || []; return this.issuable.labels?.nodes || this.issuable.labels || [];
}, },
labelIdsString() {
return JSON.stringify(this.labels.map((label) => label.id));
},
assignees() { assignees() {
return this.issuable.assignees || []; return this.issuable.assignees || [];
}, },
...@@ -149,12 +152,13 @@ export default { ...@@ -149,12 +152,13 @@ export default {
</script> </script>
<template> <template>
<li class="issue gl-px-5!"> <li :id="`issuable_${issuable.id}`" class="issue gl-px-5!" :data-labels="labelIdsString">
<div class="issuable-info-container"> <div class="issuable-info-container">
<div v-if="showCheckbox" class="issue-check"> <div v-if="showCheckbox" class="issue-check">
<gl-form-checkbox <gl-form-checkbox
class="gl-mr-0" class="gl-mr-0"
:checked="checked" :checked="checked"
:data-id="issuable.id"
@input="$emit('checked-input', $event)" @input="$emit('checked-input', $event)"
/> />
</div> </div>
......
...@@ -218,11 +218,13 @@ export default { ...@@ -218,11 +218,13 @@ export default {
}, },
handleIssuableCheckedInput(issuable, value) { handleIssuableCheckedInput(issuable, value) {
this.checkedIssuables[this.issuableId(issuable)].checked = value; this.checkedIssuables[this.issuableId(issuable)].checked = value;
this.$emit('update-legacy-bulk-edit');
}, },
handleAllIssuablesCheckedInput(value) { handleAllIssuablesCheckedInput(value) {
Object.keys(this.checkedIssuables).forEach((issuableId) => { Object.keys(this.checkedIssuables).forEach((issuableId) => {
this.checkedIssuables[issuableId].checked = value; this.checkedIssuables[issuableId].checked = value;
}); });
this.$emit('update-legacy-bulk-edit');
}, },
handleVueDraggableUpdate({ newIndex, oldIndex }) { handleVueDraggableUpdate({ newIndex, oldIndex }) {
this.$emit('reorder', { newIndex, oldIndex }); this.$emit('reorder', { newIndex, oldIndex });
......
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import eventHub from '../eventhub';
import IssueCardTimeInfo from './issue_card_time_info.vue'; import IssueCardTimeInfo from './issue_card_time_info.vue';
export default { export default {
...@@ -56,6 +57,7 @@ export default { ...@@ -56,6 +57,7 @@ export default {
filters: sortParams[sortKey] || {}, filters: sortParams[sortKey] || {},
isLoading: false, isLoading: false,
issues: [], issues: [],
showBulkEditSidebar: false,
sortKey: sortKey || CREATED_DESC, sortKey: sortKey || CREATED_DESC,
totalIssues: 0, totalIssues: 0,
}; };
...@@ -73,8 +75,15 @@ export default { ...@@ -73,8 +75,15 @@ export default {
}, },
}, },
mounted() { mounted() {
eventHub.$on('issuables:toggleBulkEdit', (showBulkEditSidebar) => {
this.showBulkEditSidebar = showBulkEditSidebar;
});
this.fetchIssues(); this.fetchIssues();
}, },
beforeDestroy() {
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('issuables:toggleBulkEdit');
},
methods: { methods: {
fetchIssues(pageToFetch) { fetchIssues(pageToFetch) {
this.isLoading = true; this.isLoading = true;
...@@ -101,6 +110,13 @@ export default { ...@@ -101,6 +110,13 @@ export default {
this.isLoading = false; this.isLoading = false;
}); });
}, },
handleUpdateLegacyBulkEdit() {
// If "select all" checkbox was checked, wait for all checkboxes
// to be checked before updating IssuableBulkUpdateSidebar class
this.$nextTick(() => {
eventHub.$emit('issuables:updateBulkEdit');
});
},
handlePageChange(page) { handlePageChange(page) {
this.fetchIssues(page); this.fetchIssues(page);
}, },
...@@ -159,6 +175,7 @@ export default { ...@@ -159,6 +175,7 @@ export default {
current-tab="" current-tab=""
:issuables-loading="isLoading" :issuables-loading="isLoading"
:is-manual-ordering="isManualOrdering" :is-manual-ordering="isManualOrdering"
:show-bulk-edit-sidebar="showBulkEditSidebar"
:show-pagination-controls="true" :show-pagination-controls="true"
:total-items="totalIssues" :total-items="totalIssues"
:current-page="currentPage" :current-page="currentPage"
...@@ -168,6 +185,7 @@ export default { ...@@ -168,6 +185,7 @@ export default {
@page-change="handlePageChange" @page-change="handlePageChange"
@reorder="handleReorder" @reorder="handleReorder"
@sort="handleSort" @sort="handleSort"
@update-legacy-bulk-edit="handleUpdateLegacyBulkEdit"
> >
<template #timeframe="{ issuable = {} }"> <template #timeframe="{ issuable = {} }">
<issue-card-time-info :issue="issuable" /> <issue-card-time-info :issue="issuable" />
......
...@@ -526,11 +526,15 @@ export default class LabelsSelect { ...@@ -526,11 +526,15 @@ export default class LabelsSelect {
} }
bindEvents() { bindEvents() {
return $('body').on('change', '.selected-issuable', this.onSelectCheckboxIssue); return $('body').on(
'change',
'.issuable-list input[type="checkbox"]',
this.onSelectCheckboxIssue,
);
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
onSelectCheckboxIssue() { onSelectCheckboxIssue() {
if ($('.selected-issuable:checked').length) { if ($('.issuable-list input[type="checkbox"]:checked').length) {
return; return;
} }
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text(__('Label')); return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text(__('Label'));
......
...@@ -20,7 +20,12 @@ initFilteredSearch({ ...@@ -20,7 +20,12 @@ initFilteredSearch({
useDefaultState: true, useDefaultState: true,
}); });
new IssuableIndex(ISSUABLE_INDEX.ISSUE); if (gon.features?.vueIssuesList) {
new IssuableIndex();
} else {
new IssuableIndex(ISSUABLE_INDEX.ISSUE);
}
new ShortcutsNavigation(); new ShortcutsNavigation();
new UsersSelect(); new UsersSelect();
......
...@@ -45,6 +45,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -45,6 +45,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:vue_issuables_list, project) push_frontend_feature_flag(:vue_issuables_list, project)
push_frontend_feature_flag(:usage_data_design_action, project, default_enabled: true) push_frontend_feature_flag(:usage_data_design_action, project, default_enabled: true)
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml) push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
push_frontend_feature_flag(:vue_issues_list, project)
end end
before_action only: :show do before_action only: :show do
......
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
has_issuable_health_status_feature: Gitlab.ee? && @project.feature_available?(:issuable_health_status).to_s, has_issuable_health_status_feature: Gitlab.ee? && @project.feature_available?(:issuable_health_status).to_s,
has_issue_weights_feature: Gitlab.ee? && @project.feature_available?(:issue_weights).to_s, has_issue_weights_feature: Gitlab.ee? && @project.feature_available?(:issue_weights).to_s,
issues_path: project_issues_path(@project) } } issues_path: project_issues_path(@project) } }
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :issues
- else - else
= render 'shared/issuable/search_bar', type: :issues = render 'shared/issuable/search_bar', type: :issues
......
...@@ -35,6 +35,7 @@ describe('IssuableListRoot', () => { ...@@ -35,6 +35,7 @@ describe('IssuableListRoot', () => {
const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar); const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
const findGlPagination = () => wrapper.findComponent(GlPagination); const findGlPagination = () => wrapper.findComponent(GlPagination);
const findIssuableItem = () => wrapper.findComponent(IssuableItem);
const findIssuableTabs = () => wrapper.findComponent(IssuableTabs); const findIssuableTabs = () => wrapper.findComponent(IssuableTabs);
const findVueDraggable = () => wrapper.findComponent(VueDraggable); const findVueDraggable = () => wrapper.findComponent(VueDraggable);
...@@ -351,6 +352,18 @@ describe('IssuableListRoot', () => { ...@@ -351,6 +352,18 @@ describe('IssuableListRoot', () => {
}); });
}); });
it('emits `update-legacy-bulk-edit` when filtered-search-bar checkbox is checked', () => {
findFilteredSearchBar().vm.$emit('checked-input');
expect(wrapper.emitted('update-legacy-bulk-edit')).toEqual([[]]);
});
it('emits `update-legacy-bulk-edit` when issuable-item checkbox is checked', () => {
findIssuableItem().vm.$emit('checked-input');
expect(wrapper.emitted('update-legacy-bulk-edit')).toEqual([[]]);
});
it('gl-pagination component emits `page-change` event on `input` event', async () => { it('gl-pagination component emits `page-change` event on `input` event', async () => {
wrapper.setProps({ wrapper.setProps({
showPaginationControls: true, showPaginationControls: true,
...@@ -379,7 +392,7 @@ describe('IssuableListRoot', () => { ...@@ -379,7 +392,7 @@ describe('IssuableListRoot', () => {
}); });
it('IssuableItem has grab cursor', () => { it('IssuableItem has grab cursor', () => {
expect(wrapper.findComponent(IssuableItem).classes()).toContain('gl-cursor-grab'); expect(findIssuableItem().classes()).toContain('gl-cursor-grab');
}); });
it('emits a "reorder" event when user updates the issue order', () => { it('emits a "reorder" event when user updates the issue order', () => {
......
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
sortOptions, sortOptions,
sortParams, sortParams,
} from '~/issues_list/constants'; } from '~/issues_list/constants';
import eventHub from '~/issues_list/eventhub';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { setUrlParams } from '~/lib/utils/url_utility'; import { setUrlParams } from '~/lib/utils/url_utility';
...@@ -101,6 +102,23 @@ describe('IssuesListApp component', () => { ...@@ -101,6 +102,23 @@ describe('IssuesListApp component', () => {
}); });
}); });
describe('bulk edit', () => {
describe.each([true, false])(
'when "issuables:toggleBulkEdit" event is received with payload `%s`',
(isBulkEdit) => {
beforeEach(() => {
wrapper = mountComponent();
eventHub.$emit('issuables:toggleBulkEdit', isBulkEdit);
});
it(`${isBulkEdit ? 'enables' : 'disables'} bulk edit`, () => {
expect(findIssuableList().props('showBulkEditSidebar')).toBe(isBulkEdit);
});
},
);
});
describe('when "page-change" event is emitted by IssuableList', () => { describe('when "page-change" event is emitted by IssuableList', () => {
const data = [{ id: 10, title: 'title', state }]; const data = [{ id: 10, title: 'title', state }];
const page = 2; const page = 2;
...@@ -119,7 +137,7 @@ describe('IssuesListApp component', () => { ...@@ -119,7 +137,7 @@ describe('IssuesListApp component', () => {
await waitForPromises(); await waitForPromises();
}); });
it('fetches issues with expected params', async () => { it('fetches issues with expected params', () => {
expect(axiosMock.history.get[1].params).toEqual({ expect(axiosMock.history.get[1].params).toEqual({
page, page,
per_page: PAGE_SIZE, per_page: PAGE_SIZE,
...@@ -192,7 +210,7 @@ describe('IssuesListApp component', () => { ...@@ -192,7 +210,7 @@ describe('IssuesListApp component', () => {
describe('when "sort" event is emitted by IssuableList', () => { describe('when "sort" event is emitted by IssuableList', () => {
it.each(Object.keys(sortParams))( it.each(Object.keys(sortParams))(
'fetches issues with correct params for "sort" payload %s', 'fetches issues with correct params for "sort" payload `%s`',
async (sortKey) => { async (sortKey) => {
wrapper = mountComponent(); wrapper = mountComponent();
...@@ -210,4 +228,19 @@ describe('IssuesListApp component', () => { ...@@ -210,4 +228,19 @@ describe('IssuesListApp component', () => {
}, },
); );
}); });
describe('when "update-legacy-bulk-edit" event is emitted by IssuableList', () => {
beforeEach(() => {
wrapper = mountComponent();
jest.spyOn(eventHub, '$emit');
});
it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', async () => {
findIssuableList().vm.$emit('update-legacy-bulk-edit');
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
});
});
}); });
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