Commit 9ea6affb authored by Coung Ngo's avatar Coung Ngo Committed by Kushal Pandya

Add bulk edit to issues list page refactor

This commit adds bulk edit to the Vue issues list page,
which is under the feature flag `vue_issues_list` defaulted to off.
parent 82da9ec1
......@@ -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