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 {
// From issuable's initial bulk selection
getOriginalCommonIds() {
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'));
});
return intersection.apply(this, labelIds);
......@@ -100,7 +100,7 @@ export default {
let issuableLabels = [];
// 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.forEach((labelId) => {
// Store unique IDs
......
......@@ -34,7 +34,7 @@ export default class IssuableBulkUpdateSidebar {
this.$otherFilters = $('.issues-other-filters');
this.$checkAllContainer = $('.check-all-holder');
this.$issueChecks = $('.issue-check');
this.$issuesList = $('.selected-issuable');
this.$issuesList = $('.issuable-list input[type="checkbox"]');
this.$issuableIdsInput = $('#update_issuable_ids');
}
......@@ -46,17 +46,15 @@ export default class IssuableBulkUpdateSidebar {
this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit());
this.$checkAllContainer.on('click', () => this.updateFormState());
if (this.vueIssuablesListFeature) {
issueableEventHub.$on('issuables:updateBulkEdit', () => {
// Danger! Strong coupling ahead!
// The bulk update sidebar and its dropdowns look for .selected-issuable checkboxes, and get data on which issue
// The bulk update sidebar and its dropdowns look for checkboxes, and get data on which issue
// 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.
this.updateFormState();
});
}
}
initDropdowns() {
new LabelsSelect();
......@@ -96,7 +94,7 @@ export default class IssuableBulkUpdateSidebar {
}
updateFormState() {
const noCheckedIssues = !$('.selected-issuable:checked').length;
const noCheckedIssues = !$('.issuable-list input[type="checkbox"]:checked').length;
this.toggleSubmitButtonDisabled(noCheckedIssues);
this.updateSelectedIssuableIds();
......@@ -166,7 +164,7 @@ export default class IssuableBulkUpdateSidebar {
}
static getCheckedIssueIds() {
const $checkedIssues = $('.selected-issuable:checked');
const $checkedIssues = $('.issuable-list input[type="checkbox"]:checked');
if ($checkedIssues.length > 0) {
return $.map($checkedIssues, (value) => $(value).data('id'));
......
import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar';
export default class IssuableIndex {
constructor(pagePrefix) {
constructor(pagePrefix = 'issuable_') {
issuableInitBulkUpdateSidebar.init(pagePrefix);
}
}
......@@ -65,6 +65,9 @@ export default {
labels() {
return this.issuable.labels?.nodes || this.issuable.labels || [];
},
labelIdsString() {
return JSON.stringify(this.labels.map((label) => label.id));
},
assignees() {
return this.issuable.assignees || [];
},
......@@ -149,12 +152,13 @@ export default {
</script>
<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 v-if="showCheckbox" class="issue-check">
<gl-form-checkbox
class="gl-mr-0"
:checked="checked"
:data-id="issuable.id"
@input="$emit('checked-input', $event)"
/>
</div>
......
......@@ -218,11 +218,13 @@ export default {
},
handleIssuableCheckedInput(issuable, value) {
this.checkedIssuables[this.issuableId(issuable)].checked = value;
this.$emit('update-legacy-bulk-edit');
},
handleAllIssuablesCheckedInput(value) {
Object.keys(this.checkedIssuables).forEach((issuableId) => {
this.checkedIssuables[issuableId].checked = value;
});
this.$emit('update-legacy-bulk-edit');
},
handleVueDraggableUpdate({ newIndex, oldIndex }) {
this.$emit('reorder', { newIndex, oldIndex });
......
......@@ -14,6 +14,7 @@ import {
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import eventHub from '../eventhub';
import IssueCardTimeInfo from './issue_card_time_info.vue';
export default {
......@@ -56,6 +57,7 @@ export default {
filters: sortParams[sortKey] || {},
isLoading: false,
issues: [],
showBulkEditSidebar: false,
sortKey: sortKey || CREATED_DESC,
totalIssues: 0,
};
......@@ -73,8 +75,15 @@ export default {
},
},
mounted() {
eventHub.$on('issuables:toggleBulkEdit', (showBulkEditSidebar) => {
this.showBulkEditSidebar = showBulkEditSidebar;
});
this.fetchIssues();
},
beforeDestroy() {
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('issuables:toggleBulkEdit');
},
methods: {
fetchIssues(pageToFetch) {
this.isLoading = true;
......@@ -101,6 +110,13 @@ export default {
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) {
this.fetchIssues(page);
},
......@@ -159,6 +175,7 @@ export default {
current-tab=""
:issuables-loading="isLoading"
:is-manual-ordering="isManualOrdering"
:show-bulk-edit-sidebar="showBulkEditSidebar"
:show-pagination-controls="true"
:total-items="totalIssues"
:current-page="currentPage"
......@@ -168,6 +185,7 @@ export default {
@page-change="handlePageChange"
@reorder="handleReorder"
@sort="handleSort"
@update-legacy-bulk-edit="handleUpdateLegacyBulkEdit"
>
<template #timeframe="{ issuable = {} }">
<issue-card-time-info :issue="issuable" />
......
......@@ -526,11 +526,15 @@ export default class LabelsSelect {
}
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
onSelectCheckboxIssue() {
if ($('.selected-issuable:checked').length) {
if ($('.issuable-list input[type="checkbox"]:checked').length) {
return;
}
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text(__('Label'));
......
......@@ -20,7 +20,12 @@ initFilteredSearch({
useDefaultState: true,
});
new IssuableIndex(ISSUABLE_INDEX.ISSUE);
if (gon.features?.vueIssuesList) {
new IssuableIndex();
} else {
new IssuableIndex(ISSUABLE_INDEX.ISSUE);
}
new ShortcutsNavigation();
new UsersSelect();
......
......@@ -45,6 +45,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:vue_issuables_list, project)
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(:vue_issues_list, project)
end
before_action only: :show do
......
......@@ -26,6 +26,8 @@
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,
issues_path: project_issues_path(@project) } }
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :issues
- else
= render 'shared/issuable/search_bar', type: :issues
......
......@@ -35,6 +35,7 @@ describe('IssuableListRoot', () => {
const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
const findGlPagination = () => wrapper.findComponent(GlPagination);
const findIssuableItem = () => wrapper.findComponent(IssuableItem);
const findIssuableTabs = () => wrapper.findComponent(IssuableTabs);
const findVueDraggable = () => wrapper.findComponent(VueDraggable);
......@@ -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 () => {
wrapper.setProps({
showPaginationControls: true,
......@@ -379,7 +392,7 @@ describe('IssuableListRoot', () => {
});
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', () => {
......
......@@ -13,6 +13,7 @@ import {
sortOptions,
sortParams,
} from '~/issues_list/constants';
import eventHub from '~/issues_list/eventhub';
import axios from '~/lib/utils/axios_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
......@@ -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', () => {
const data = [{ id: 10, title: 'title', state }];
const page = 2;
......@@ -119,7 +137,7 @@ describe('IssuesListApp component', () => {
await waitForPromises();
});
it('fetches issues with expected params', async () => {
it('fetches issues with expected params', () => {
expect(axiosMock.history.get[1].params).toEqual({
page,
per_page: PAGE_SIZE,
......@@ -192,7 +210,7 @@ describe('IssuesListApp component', () => {
describe('when "sort" event is emitted by IssuableList', () => {
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) => {
wrapper = mountComponent();
......@@ -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