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 {
*/
getFormDataAsObject() {
const healthStatusValue = this.form.find('input[name="update[health_status]"]').val();
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
......@@ -86,6 +88,7 @@ export default {
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
health_status: healthStatusValue === 'null' ? null : healthStatusValue,
add_label_ids: [],
remove_label_ids: [],
},
......
......@@ -7,6 +7,8 @@ import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_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';
const HIDDEN_CLASS = 'hidden';
......@@ -63,6 +65,7 @@ export default class IssuableBulkUpdateSidebar {
new MilestoneSelect();
issueStatusSelect();
subscriptionSelect();
HealthStatusSelect();
}
setupBulkUpdateActions() {
......
- 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? } }
.issuable-sidebar.hidden
......@@ -36,6 +37,13 @@
= _('Labels')
.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
- 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
.title
= _('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 = {
noIterationItem: [{ title: __('No iteration'), id: null }],
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
EE_PERMITTED_KEYS = %w[
weight
health_status
].freeze
private
......
- group = local_assigns.fetch(:group)
- 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? } }
.issuable-sidebar.hidden
......@@ -19,6 +20,13 @@
= _('Labels')
.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
- 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 :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 ""
msgid "Health information can be retrieved from the following endpoints. More information is available"
msgstr ""
msgid "Health status"
msgstr ""
msgid "HealthCheck|Access token is"
msgstr ""
......@@ -15263,6 +15266,9 @@ msgstr ""
msgid "No start date"
msgstr ""
msgid "No status"
msgstr ""
msgid "No template"
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