Commit ebc57b2d authored by Rajat Jain's avatar Rajat Jain Committed by Eugenia Grieff

Bulk edit health status

Add a dropdown in group and project issue bulk edit sidebar
to give an ability to change health status of multiple issues
parent 982ed637
...@@ -79,6 +79,8 @@ export default { ...@@ -79,6 +79,8 @@ export default {
*/ */
getFormDataAsObject() { getFormDataAsObject() {
const healthStatusValue = this.form.find('input[name="update[health_status]"]').val();
const formData = { const formData = {
update: { update: {
state_event: this.form.find('input[name="update[state_event]"]').val(), state_event: this.form.find('input[name="update[state_event]"]').val(),
...@@ -86,6 +88,7 @@ export default { ...@@ -86,6 +88,7 @@ export default {
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
health_status: healthStatusValue === 'null' ? null : healthStatusValue,
add_label_ids: [], add_label_ids: [],
remove_label_ids: [], remove_label_ids: [],
}, },
......
...@@ -7,6 +7,8 @@ import MilestoneSelect from './milestone_select'; ...@@ -7,6 +7,8 @@ import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select'; import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_select'; import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select'; import LabelsSelect from './labels_select';
import HealthStatusSelect from 'ee_else_ce/vue_shared/components/sidebar/health_status_select/health_status_bundle';
import issueableEventHub from './issuables_list/eventhub'; import issueableEventHub from './issuables_list/eventhub';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
...@@ -63,6 +65,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -63,6 +65,7 @@ export default class IssuableBulkUpdateSidebar {
new MilestoneSelect(); new MilestoneSelect();
issueStatusSelect(); issueStatusSelect();
subscriptionSelect(); subscriptionSelect();
HealthStatusSelect();
} }
setupBulkUpdateActions() { setupBulkUpdateActions() {
......
- type = local_assigns.fetch(:type) - type = local_assigns.fetch(:type)
- bulk_issue_health_status_flag = @project&.group&.feature_available?(:issuable_health_status) && Feature.enabled?(:bulk_update_health_status) && type == :issues
%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? } } %aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? } }
.issuable-sidebar.hidden .issuable-sidebar.hidden
...@@ -36,6 +37,13 @@ ...@@ -36,6 +37,13 @@
= _('Labels') = _('Labels')
.filter-item.labels-filter .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 = 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
- if bulk_issue_health_status_flag
.block
.title
= _('Health status')
.filter-item.health-status.health-status-filter
#js-bulk-update-health-status-root
%input{ id: 'issue_health_status_value', type: 'hidden', name: 'update[health_status]' }
.block .block
.title .title
= _('Subscriptions') = _('Subscriptions')
......
<script>
import Tracking from '~/tracking';
import { GlButton, GlDropdownItem, GlDropdown, GlDropdownDivider } from '@gitlab/ui';
import { s__ } from '~/locale';
import { healthStatusTextMap } from '../../constants';
export default {
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
},
mixins: [Tracking.mixin()],
props: {
isEditable: {
type: Boolean,
required: false,
default: false,
},
isFetching: {
type: Boolean,
required: false,
default: false,
},
status: {
type: String,
required: false,
default: '',
},
},
data() {
return {
isDropdownShowing: false,
selectedStatus: this.status,
statusOptions: Object.keys(healthStatusTextMap).map(key => ({
key,
value: healthStatusTextMap[key],
})),
};
},
computed: {
canRemoveStatus() {
return this.isEditable && this.status;
},
statusText() {
return this.status ? healthStatusTextMap[this.status] : s__('Sidebar|None');
},
dropdownText() {
if (this.status === null) {
return s__('No status');
}
return this.status ? healthStatusTextMap[this.status] : s__('Select health status');
},
tooltipText() {
let tooltipText = s__('Sidebar|Health status');
if (this.status) {
tooltipText += `: ${this.statusText}`;
}
return tooltipText;
},
},
watch: {
status(status) {
this.selectedStatus = status;
},
},
methods: {
handleDropdownClick(status) {
this.selectedStatus = status;
this.$emit('onDropdownClick', status);
this.track('change_health_status', { property: status });
this.hideDropdown();
},
hideDropdown() {
this.isDropdownShowing = false;
},
toggleFormDropdown() {
this.isDropdownShowing = !this.isDropdownShowing;
/**
* We need to programmatically open the dropdown to make the
* outside click on document close the dropdown.
*/
const { dropdown } = this.$refs.dropdown.$refs;
if (dropdown && this.isDropdownShowing) {
dropdown.show();
}
},
removeStatus() {
this.handleDropdownClick(null);
},
isSelected(status) {
return this.status === status;
},
},
};
</script>
<template>
<div class="dropdown dropdown-menu-selectable">
<gl-dropdown
ref="dropdown"
class="w-100"
:text="dropdownText"
@keydown.esc.native="hideDropdown"
@hide="hideDropdown"
>
<div class="dropdown-title">
<span class="health-title">{{ s__('Sidebar|Assign health status') }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
class="dropdown-title-button dropdown-menu-close"
icon="close"
@click="hideDropdown"
/>
</div>
<div class="dropdown-content dropdown-body">
<gl-dropdown-item @click="handleDropdownClick(null)">
<gl-button
variant="link"
class="dropdown-item health-dropdown-item"
:class="{ 'is-active': isSelected(null) }"
>
{{ s__('Sidebar|No status') }}
</gl-button>
</gl-dropdown-item>
<gl-dropdown-divider class="divider health-divider" />
<gl-dropdown-item
v-for="option in statusOptions"
:key="option.key"
@click="handleDropdownClick(option.key)"
>
<gl-button
variant="link"
class="dropdown-item health-dropdown-item"
:class="{ 'is-active': isSelected(option.key) }"
>
{{ option.value }}
</gl-button>
</gl-dropdown-item>
</div>
</gl-dropdown>
</div>
</template>
...@@ -18,3 +18,10 @@ export const iterationSelectTextMap = { ...@@ -18,3 +18,10 @@ export const iterationSelectTextMap = {
noIterationItem: [{ title: __('No iteration'), id: null }], noIterationItem: [{ title: __('No iteration'), id: null }],
iterationSelectFail: __('Failed to set iteration on this issue. Please try again.'), iterationSelectFail: __('Failed to set iteration on this issue. Please try again.'),
}; };
export const healthStatusForRestApi = {
NO_STATUS: null,
[healthStatus.ON_TRACK]: 'on_track',
[healthStatus.NEEDS_ATTENTION]: 'needs_attention',
[healthStatus.AT_RISK]: 'at_risk',
};
import Vue from 'vue';
import HealthStatusSelect from 'ee/sidebar/components/status/health_status_dropdown.vue';
import { healthStatusForRestApi } from 'ee/sidebar/constants';
export default () => {
const el = document.getElementById('js-bulk-update-health-status-root');
const healthStatusFormFieldEl = document.getElementById('issue_health_status_value');
if (!el && !healthStatusFormFieldEl) {
return false;
}
return new Vue({
el,
components: {
HealthStatusSelect,
},
data() {
return {
selectedStatus: undefined,
};
},
methods: {
handleHealthStatusSelect(selectedStatus) {
this.selectedStatus = selectedStatus;
healthStatusFormFieldEl.setAttribute(
'value',
selectedStatus ? healthStatusForRestApi[selectedStatus] : selectedStatus,
);
},
},
render(createElement) {
return createElement('health-status-select', {
props: {
isFetching: false,
isEditable: true,
showDropdown: true,
status: this.selectedStatus,
},
on: {
onDropdownClick: this.handleHealthStatusSelect.bind(this),
},
});
},
});
};
...@@ -7,6 +7,7 @@ module EE ...@@ -7,6 +7,7 @@ module EE
EE_PERMITTED_KEYS = %w[ EE_PERMITTED_KEYS = %w[
weight weight
health_status
].freeze ].freeze
private private
......
- group = local_assigns.fetch(:group) - group = local_assigns.fetch(:group)
- type = local_assigns.fetch(:type) - type = local_assigns.fetch(:type)
- bulk_issue_health_status_flag = @group&.feature_available?(:issuable_health_status) && Feature.enabled?(:bulk_update_health_status) && type == :issues
%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ 'aria-live' => 'polite', data: { 'signed-in': current_user.present? } } %aside.issues-bulk-update.js-right-sidebar.right-sidebar{ 'aria-live' => 'polite', data: { 'signed-in': current_user.present? } }
.issuable-sidebar.hidden .issuable-sidebar.hidden
...@@ -19,6 +20,13 @@ ...@@ -19,6 +20,13 @@
= _('Labels') = _('Labels')
.filter-item.labels-filter .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, edit_context: group = 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, edit_context: group
- if bulk_issue_health_status_flag
.block
.title
= _('Health status')
.filter-item.health-status.health-status-filter
#js-bulk-update-health-status-root
%input{ id: 'issue_health_status_value', type: 'hidden', name: 'update[health_status]' }
= hidden_field_tag 'update[issuable_ids]', [] = hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event] = hidden_field_tag :state_event, params[:state_event]
---
title: Bulk edit health status
merge_request: 33065
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe 'Issues > Health status bulk assignment' do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
let!(:issue1) { create(:issue, project: project, title: "Issue 1") }
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
context 'as an allowed user', :js do
before do
allow(group).to receive(:feature_enabled?).and_return(true)
stub_licensed_features(issuable_health_status: true)
group.add_maintainer(user)
sign_in user
end
context 'sidebar' do
before do
enable_bulk_update
end
it 'is present when bulk edit is enabled' do
expect(page).to have_css('.issuable-sidebar')
end
it 'is not present when bulk edit is disabled' do
disable_bulk_update
expect(page).not_to have_css('.issuable-sidebar')
end
end
context 'can bulk assign' do
before do
enable_bulk_update
end
context 'health_status' do
context 'to all issues' do
before do
check 'check-all-issues'
open_health_status_dropdown ['On track']
update_issues
end
it do
expect(issue1.reload.health_status).to eq 'on_track'
expect(issue2.reload.health_status).to eq 'on_track'
end
end
context 'to a issue' do
before do
check "selected_issue_#{issue1.id}"
open_health_status_dropdown ['At risk']
update_issues
end
it do
expect(issue1.reload.health_status).to eq 'at_risk'
expect(issue2.reload.health_status).to eq nil
end
end
end
end
end
context 'as a guest' do
before do
sign_in user
allow(group).to receive(:feature_enabled?).and_return(true)
stub_licensed_features(issuable_health_status: true)
visit project_issues_path(project)
end
context 'cannot bulk assign health_status' do
it do
expect(page).not_to have_button 'Edit issues'
expect(page).not_to have_css '.check-all-issues'
expect(page).not_to have_css '.issue-check'
end
end
end
def open_health_status_dropdown(items = [])
page.within('.issues-bulk-update') do
click_button 'Select health status'
items.map do |item|
find('.gl-button-text', { text: item }).click
end
end
end
def check_issue(issue, uncheck = false)
page.within('.issues-list') do
if uncheck
uncheck "selected_issue_#{issue.id}"
else
check "selected_issue_#{issue.id}"
end
end
end
def uncheck_issue(issue)
check_issue(issue, true)
end
def update_issues
find('.update-selected-issues').click
wait_for_requests
end
def enable_bulk_update
visit project_issues_path(project)
click_button 'Edit issues'
end
def disable_bulk_update
click_button 'Cancel'
end
end
...@@ -11666,6 +11666,9 @@ msgstr "" ...@@ -11666,6 +11666,9 @@ msgstr ""
msgid "Health information can be retrieved from the following endpoints. More information is available" msgid "Health information can be retrieved from the following endpoints. More information is available"
msgstr "" msgstr ""
msgid "Health status"
msgstr ""
msgid "HealthCheck|Access token is" msgid "HealthCheck|Access token is"
msgstr "" msgstr ""
...@@ -15263,6 +15266,9 @@ msgstr "" ...@@ -15263,6 +15266,9 @@ msgstr ""
msgid "No start date" msgid "No start date"
msgstr "" msgstr ""
msgid "No status"
msgstr ""
msgid "No template" msgid "No template"
msgstr "" msgstr ""
......
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