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. ...@@ -168,15 +168,15 @@ requires [GraphQL](../../../api/graphql/index.md) to be enabled.
### Status **(ULTIMATE)** ### 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: 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) - **On track** (green)
- `Needs attention` (amber) - **Needs attention** (amber)
- `At risk` (red) - **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 { ...@@ -31,6 +31,6 @@ export default {
:is-editable="mediator.store.editable" :is-editable="mediator.store.editable"
:is-fetching="mediator.store.isFetching.status" :is-fetching="mediator.store.isFetching.status"
:status="mediator.store.status" :status="mediator.store.status"
@onFormSubmit="handleFormSubmission" @onStatusChange="handleFormSubmission"
/> />
</template> </template>
...@@ -47,6 +47,9 @@ export default { ...@@ -47,6 +47,9 @@ export default {
}; };
}, },
computed: { computed: {
canRemoveStatus() {
return this.isEditable && this.status;
},
statusText() { statusText() {
return this.status ? healthStatusTextMap[this.status] : s__('Sidebar|None'); return this.status ? healthStatusTextMap[this.status] : s__('Sidebar|None');
}, },
...@@ -70,7 +73,7 @@ export default { ...@@ -70,7 +73,7 @@ export default {
}, },
methods: { methods: {
handleFormSubmission() { handleFormSubmission() {
this.$emit('onFormSubmit', this.selectedStatus); this.$emit('onStatusChange', this.selectedStatus);
this.hideForm(); this.hideForm();
}, },
hideForm() { hideForm() {
...@@ -80,6 +83,9 @@ export default { ...@@ -80,6 +83,9 @@ export default {
toggleFormDropdown() { toggleFormDropdown() {
this.isFormShowing = !this.isFormShowing; this.isFormShowing = !this.isFormShowing;
}, },
removeStatus() {
this.$emit('onStatusChange', null);
},
}, },
}; };
</script> </script>
...@@ -139,15 +145,21 @@ export default { ...@@ -139,15 +145,21 @@ export default {
</div> </div>
<gl-loading-icon v-if="isFetching" :inline="true" /> <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 <gl-icon
v-if="status" v-if="status"
name="severity-low" name="severity-low"
:size="14" :size="14"
class="align-bottom mr-2" class="align-bottom append-right-10"
:class="statusColor" :class="statusColor"
/> />
{{ statusText }} {{ 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> </p>
</div> </div>
</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 SidebarStatus from 'ee/sidebar/components/status/sidebar_status.vue';
import Status from 'ee/sidebar/components/status/status.vue'; import Status from 'ee/sidebar/components/status/status.vue';
const getStatusText = wrapper => wrapper.find('.value').text();
describe('SidebarStatus', () => { describe('SidebarStatus', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('Status child component', () => {
let handleFormSubmissionMock;
beforeEach(() => {
const mediator = { const mediator = {
store: { store: {
isFetching: { isFetching: {
...@@ -12,9 +26,9 @@ describe('SidebarStatus', () => { ...@@ -12,9 +26,9 @@ describe('SidebarStatus', () => {
}, },
}; };
const handleFormSubmissionMock = jest.fn(); handleFormSubmissionMock = jest.fn();
const wrapper = shallowMount(SidebarStatus, { wrapper = shallowMount(SidebarStatus, {
propsData: { propsData: {
mediator, mediator,
}, },
...@@ -22,14 +36,51 @@ describe('SidebarStatus', () => { ...@@ -22,14 +36,51 @@ describe('SidebarStatus', () => {
handleFormSubmission: handleFormSubmissionMock, handleFormSubmission: handleFormSubmissionMock,
}, },
}); });
});
it('renders Status component', () => { it('renders Status component', () => {
expect(wrapper.contains(Status)).toBe(true); expect(wrapper.contains(Status)).toBe(true);
}); });
it('calls handleFormSubmission when receiving an onFormSubmit event from Status component', () => { it('calls handleFormSubmission when receiving an onStatusChange event from Status component', () => {
wrapper.find(Status).vm.$emit('onFormSubmit', 'onTrack'); wrapper.find(Status).vm.$emit('onStatusChange', 'onTrack');
expect(handleFormSubmissionMock).toHaveBeenCalledWith('onTrack'); expect(handleFormSubmissionMock).toHaveBeenCalledWith('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');
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 { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import Status from 'ee/sidebar/components/status/status.vue'; import Status from 'ee/sidebar/components/status/status.vue';
...@@ -12,6 +12,8 @@ const getStatusIconCssClasses = wrapper => wrapper.find('[name="severity-low"]') ...@@ -12,6 +12,8 @@ const getStatusIconCssClasses = wrapper => wrapper.find('[name="severity-low"]')
const getEditButton = wrapper => wrapper.find({ ref: 'editButton' }); const getEditButton = wrapper => wrapper.find({ ref: 'editButton' });
const getRemoveStatusButton = wrapper => wrapper.find(GlButton);
const getEditForm = wrapper => wrapper.find('form'); const getEditForm = wrapper => wrapper.find('form');
const getRadioInputs = wrapper => wrapper.findAll('input[type="radio"]'); const getRadioInputs = wrapper => wrapper.findAll('input[type="radio"]');
...@@ -87,6 +89,43 @@ describe('Status', () => { ...@@ -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('status text', () => {
describe('when no value is provided for status', () => { describe('when no value is provided for status', () => {
beforeEach(() => { beforeEach(() => {
...@@ -228,7 +267,7 @@ describe('Status', () => { ...@@ -228,7 +267,7 @@ describe('Status', () => {
// Test that "onTrack", "needsAttention", and "atRisk" values are emitted when form is submitted // Test that "onTrack", "needsAttention", and "atRisk" values are emitted when form is submitted
it.each(Object.values(healthStatus))( 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 => { status => {
getEditForm(wrapper) getEditForm(wrapper)
.find(`input[value="${status}"]`) .find(`input[value="${status}"]`)
...@@ -236,7 +275,7 @@ describe('Status', () => { ...@@ -236,7 +275,7 @@ describe('Status', () => {
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
getEditForm(wrapper).trigger('submit'); getEditForm(wrapper).trigger('submit');
expect(wrapper.emitted().onFormSubmit[0]).toEqual([status]); expect(wrapper.emitted().onStatusChange[0]).toEqual([status]);
}); });
}, },
); );
......
...@@ -24701,6 +24701,9 @@ msgstr "" ...@@ -24701,6 +24701,9 @@ msgstr ""
msgid "remove due date" msgid "remove due date"
msgstr "" msgstr ""
msgid "remove status"
msgstr ""
msgid "remove weight" msgid "remove weight"
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