Commit fe0f60e5 authored by Nathan Friend's avatar Nathan Friend

Merge branch '36427-add-ability-to-remove-health-status-in-issue-sidebar' into 'master'

Add ability to remove health status in Issue sidebar"

See merge request gitlab-org/gitlab!27917
parents fd7a8b30 06ad62cd
......@@ -168,15 +168,15 @@ requires [GraphQL](../../../api/graphql/index.md) to be enabled.
### Status **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/36427) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.9.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/36427) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
To help you track the status of your issues, you can assign a status to each issue to flag work that's progressing as planned or needs attention to keep on schedule:
- `On track` (green)
- `Needs attention` (amber)
- `At risk` (red)
- **On track** (green)
- **Needs attention** (amber)
- **At risk** (red)
!["On track" health status on an issue](img/issue_health_status_v12_9.png)
!["On track" health status on an issue](img/issue_health_status_v12_10.png)
---
......
......@@ -31,6 +31,6 @@ export default {
:is-editable="mediator.store.editable"
:is-fetching="mediator.store.isFetching.status"
:status="mediator.store.status"
@onFormSubmit="handleFormSubmission"
@onStatusChange="handleFormSubmission"
/>
</template>
......@@ -47,6 +47,9 @@ export default {
};
},
computed: {
canRemoveStatus() {
return this.isEditable && this.status;
},
statusText() {
return this.status ? healthStatusTextMap[this.status] : s__('Sidebar|None');
},
......@@ -70,7 +73,7 @@ export default {
},
methods: {
handleFormSubmission() {
this.$emit('onFormSubmit', this.selectedStatus);
this.$emit('onStatusChange', this.selectedStatus);
this.hideForm();
},
hideForm() {
......@@ -80,6 +83,9 @@ export default {
toggleFormDropdown() {
this.isFormShowing = !this.isFormShowing;
},
removeStatus() {
this.$emit('onStatusChange', null);
},
},
};
</script>
......@@ -139,15 +145,21 @@ export default {
</div>
<gl-loading-icon v-if="isFetching" :inline="true" />
<p v-else class="value m-0" :class="{ 'no-value': !status }">
<p v-else class="value d-flex align-items-center m-0" :class="{ 'no-value': !status }">
<gl-icon
v-if="status"
name="severity-low"
:size="14"
class="align-bottom mr-2"
class="align-bottom append-right-10"
:class="statusColor"
/>
{{ statusText }}
<template v-if="canRemoveStatus">
<span class="text-secondary mx-1" aria-hidden="true">-</span>
<gl-button variant="link" class="text-secondary" @click="removeStatus">
{{ __('remove status') }}
</gl-button>
</template>
</p>
</div>
</div>
......
import { shallowMount } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import SidebarStatus from 'ee/sidebar/components/status/sidebar_status.vue';
import Status from 'ee/sidebar/components/status/status.vue';
const getStatusText = wrapper => wrapper.find('.value').text();
describe('SidebarStatus', () => {
const mediator = {
store: {
isFetching: {
status: true,
},
status: '',
},
};
const handleFormSubmissionMock = jest.fn();
const wrapper = shallowMount(SidebarStatus, {
propsData: {
mediator,
},
methods: {
handleFormSubmission: handleFormSubmissionMock,
},
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders Status component', () => {
expect(wrapper.contains(Status)).toBe(true);
describe('Status child component', () => {
let handleFormSubmissionMock;
beforeEach(() => {
const mediator = {
store: {
isFetching: {
status: true,
},
status: '',
},
};
handleFormSubmissionMock = jest.fn();
wrapper = shallowMount(SidebarStatus, {
propsData: {
mediator,
},
methods: {
handleFormSubmission: handleFormSubmissionMock,
},
});
});
it('renders Status component', () => {
expect(wrapper.contains(Status)).toBe(true);
});
it('calls handleFormSubmission when receiving an onStatusChange event from Status component', () => {
wrapper.find(Status).vm.$emit('onStatusChange', 'onTrack');
expect(handleFormSubmissionMock).toHaveBeenCalledWith('onTrack');
});
});
it('calls handleFormSubmission when receiving an onFormSubmit event from Status component', () => {
wrapper.find(Status).vm.$emit('onFormSubmit', 'onTrack');
it('removes status when user clicks on "remove status"', () => {
const mediator = {
store: {
editable: true,
isFetching: {
status: false,
},
status: 'onTrack',
},
updateStatus(status) {
this.store.status = status;
wrapper.setProps({
mediator: {
...this,
},
});
return Promise.resolve();
},
};
wrapper = mount(SidebarStatus, {
propsData: {
mediator,
},
});
expect(getStatusText(wrapper)).toContain('On track');
wrapper.find('button.btn-link').trigger('click');
expect(handleFormSubmissionMock).toHaveBeenCalledWith('onTrack');
return Vue.nextTick().then(() => {
expect(getStatusText(wrapper)).toBe('None');
});
});
});
import { GlFormRadioGroup, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { GlButton, GlFormRadioGroup, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Status from 'ee/sidebar/components/status/status.vue';
......@@ -12,6 +12,8 @@ const getStatusIconCssClasses = wrapper => wrapper.find('[name="severity-low"]')
const getEditButton = wrapper => wrapper.find({ ref: 'editButton' });
const getRemoveStatusButton = wrapper => wrapper.find(GlButton);
const getEditForm = wrapper => wrapper.find('form');
const getRadioInputs = wrapper => wrapper.findAll('input[type="radio"]');
......@@ -87,6 +89,43 @@ describe('Status', () => {
});
});
describe('remove status button', () => {
it('is hidden when there is no status', () => {
const props = {
isEditable: true,
status: '',
};
shallowMountStatus(props);
expect(getRemoveStatusButton(wrapper).exists()).toBe(false);
});
it('is displayed when there is a status', () => {
const props = {
isEditable: true,
status: healthStatus.AT_RISK,
};
shallowMountStatus(props);
expect(getRemoveStatusButton(wrapper).exists()).toBe(true);
});
it('emits an onStatusChange event with argument null when clicked', () => {
const props = {
isEditable: true,
status: healthStatus.AT_RISK,
};
shallowMountStatus(props);
getRemoveStatusButton(wrapper).vm.$emit('click');
expect(wrapper.emitted().onStatusChange[0]).toEqual([null]);
});
});
describe('status text', () => {
describe('when no value is provided for status', () => {
beforeEach(() => {
......@@ -228,7 +267,7 @@ describe('Status', () => {
// Test that "onTrack", "needsAttention", and "atRisk" values are emitted when form is submitted
it.each(Object.values(healthStatus))(
'emits onFormSubmit event with argument "%s" when user selects the option and submits form',
'emits onStatusChange event with argument "%s" when user selects the option and submits form',
status => {
getEditForm(wrapper)
.find(`input[value="${status}"]`)
......@@ -236,7 +275,7 @@ describe('Status', () => {
return Vue.nextTick().then(() => {
getEditForm(wrapper).trigger('submit');
expect(wrapper.emitted().onFormSubmit[0]).toEqual([status]);
expect(wrapper.emitted().onStatusChange[0]).toEqual([status]);
});
},
);
......
......@@ -24701,6 +24701,9 @@ msgstr ""
msgid "remove due date"
msgstr ""
msgid "remove status"
msgstr ""
msgid "remove weight"
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