Commit 4a8b8929 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'health-status-dropdown' into 'master'

Use dropdown to change health status

See merge request gitlab-org/gitlab!28547
parents 3f474171 24198491
...@@ -210,3 +210,15 @@ ...@@ -210,3 +210,15 @@
} }
} }
} }
.health-status {
.dropdown-body {
.health-divider {
border-top-color: $gray-200;
}
.dropdown-item:not(.health-dropdown-item) {
padding: 0;
}
}
}
...@@ -177,7 +177,7 @@ that's progressing as planned or needs attention to keep on schedule: ...@@ -177,7 +177,7 @@ that's progressing as planned or needs attention to keep on schedule:
- **Needs attention** (amber) - **Needs attention** (amber)
- **At risk** (red) - **At risk** (red)
!["On track" health status on an issue](img/issue_health_status_v12_10.png) !["On track" health status on an issue](img/issue_health_status_dropdown_v12_10.png)
You can then see issue statuses on the You can then see issue statuses on the
[Epic tree](../../group/epics/index.md#issue-health-status-in-epic-tree-ultimate). [Epic tree](../../group/epics/index.md#issue-health-status-in-epic-tree-ultimate).
......
...@@ -17,7 +17,7 @@ export default { ...@@ -17,7 +17,7 @@ export default {
}, },
}, },
methods: { methods: {
handleFormSubmission(status) { handleDropdownClick(status) {
this.mediator.updateStatus(status).catch(() => { this.mediator.updateStatus(status).catch(() => {
Flash(__('Error occurred while updating the issue status')); Flash(__('Error occurred while updating the issue status'));
}); });
...@@ -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"
@onStatusChange="handleFormSubmission" @onDropdownClick="handleDropdownClick"
/> />
</template> </template>
<script> <script>
import Tracking from '~/tracking';
import { import {
GlDeprecatedButton,
GlFormGroup,
GlFormRadioGroup,
GlIcon, GlIcon,
GlNewButton,
GlLoadingIcon, GlLoadingIcon,
GlTooltip, GlTooltip,
GlDropdownItem,
GlDropdown,
GlDropdownDivider,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { healthStatusColorMap, healthStatusTextMap } from '../../constants'; import { healthStatusTextMap } from '../../constants';
export default { export default {
components: { components: {
GlDeprecatedButton,
GlIcon, GlIcon,
GlNewButton,
GlLoadingIcon, GlLoadingIcon,
GlFormGroup,
GlFormRadioGroup,
GlTooltip, GlTooltip,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
}, },
mixins: [Tracking.mixin()],
props: { props: {
isEditable: { isEditable: {
type: Boolean, type: Boolean,
...@@ -38,11 +42,11 @@ export default { ...@@ -38,11 +42,11 @@ export default {
}, },
data() { data() {
return { return {
isFormShowing: false, isDropdownShowing: false,
selectedStatus: this.status, selectedStatus: this.status,
statusOptions: Object.keys(healthStatusTextMap).map(key => ({ statusOptions: Object.keys(healthStatusTextMap).map(key => ({
value: key, key,
text: healthStatusTextMap[key], value: healthStatusTextMap[key],
})), })),
}; };
}, },
...@@ -53,11 +57,11 @@ export default { ...@@ -53,11 +57,11 @@ export default {
statusText() { statusText() {
return this.status ? healthStatusTextMap[this.status] : s__('Sidebar|None'); return this.status ? healthStatusTextMap[this.status] : s__('Sidebar|None');
}, },
statusColor() { dropdownText() {
return healthStatusColorMap[this.status]; return this.status ? healthStatusTextMap[this.status] : s__('Select health status');
}, },
tooltipText() { tooltipText() {
let tooltipText = s__('Sidebar|Status'); let tooltipText = s__('Sidebar|Health status');
if (this.status) { if (this.status) {
tooltipText += `: ${this.statusText}`; tooltipText += `: ${this.statusText}`;
...@@ -72,19 +76,33 @@ export default { ...@@ -72,19 +76,33 @@ export default {
}, },
}, },
methods: { methods: {
handleFormSubmission() { handleDropdownClick(status) {
this.$emit('onStatusChange', this.selectedStatus); this.selectedStatus = status;
this.hideForm(); this.$emit('onDropdownClick', status);
this.track('change_health_status', { property: status });
this.hideDropdown();
}, },
hideForm() { hideDropdown() {
this.isFormShowing = false; this.isDropdownShowing = false;
this.$refs.editButton.focus();
}, },
toggleFormDropdown() { toggleFormDropdown() {
this.isFormShowing = !this.isFormShowing; 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() { removeStatus() {
this.$emit('onStatusChange', null); this.handleDropdownClick(null);
},
isSelected(status) {
return this.status === status;
}, },
}, },
}; };
...@@ -104,62 +122,75 @@ export default { ...@@ -104,62 +122,75 @@ export default {
<div class="hide-collapsed"> <div class="hide-collapsed">
<p class="title d-flex justify-content-between"> <p class="title d-flex justify-content-between">
{{ s__('Sidebar|Status') }} {{ s__('Sidebar|Health status') }}
<a <a
v-if="isEditable" v-if="isEditable"
ref="editButton" ref="editButton"
class="btn-link" class="btn-link"
href="#" href="#"
@click="toggleFormDropdown" @click="toggleFormDropdown"
@keydown.esc="hideForm" @keydown.esc="hideDropdown"
> >
{{ __('Edit') }} {{ __('Edit') }}
</a> </a>
</p> </p>
<div v-if="isFormShowing" class="dropdown show"> <div
<form class="dropdown-menu p-3" @submit.prevent="handleFormSubmission"> class="dropdown dropdown-menu-selectable"
<p> :class="{ show: isDropdownShowing, 'd-none': !isDropdownShowing }"
{{ >
__('Choose which status most accurately reflects the current state of this issue:') <gl-dropdown
}} ref="dropdown"
</p> class="w-100"
<gl-form-group> :text="dropdownText"
<gl-form-radio-group @keydown.esc.native="hideDropdown"
v-model="selectedStatus" @hide="hideDropdown"
:checked="selectedStatus" >
:options="statusOptions" <div class="dropdown-title">
stacked <span class="health-title">{{ s__('Sidebar|Assign health status') }}</span>
@keydown.esc.native="hideForm" <gl-new-button
:aria-label="__('Close')"
variant="link"
class="dropdown-title-button dropdown-menu-close"
icon="close"
@click="hideDropdown"
/> />
</gl-form-group> </div>
<gl-form-group class="mb-0">
<gl-deprecated-button type="button" class="append-right-10" @click="hideForm"> <div class="dropdown-content dropdown-body">
{{ __('Cancel') }} <gl-dropdown-item @click="handleDropdownClick(null)">
</gl-deprecated-button> <gl-new-button
<gl-deprecated-button type="submit" variant="success"> variant="link"
{{ __('Save') }} class="dropdown-item health-dropdown-item"
</gl-deprecated-button> :class="{ 'is-active': isSelected(null) }"
</gl-form-group> >
</form> {{ s__('Sidebar|No status') }}
</gl-new-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-new-button
variant="link"
class="dropdown-item health-dropdown-item"
:class="{ 'is-active': isSelected(option.key) }"
>
{{ option.value }}
</gl-new-button>
</gl-dropdown-item>
</div>
</gl-dropdown>
</div> </div>
<gl-loading-icon v-if="isFetching" :inline="true" /> <gl-loading-icon v-if="isFetching" :inline="true" />
<p v-else class="value d-flex align-items-center m-0" :class="{ 'no-value': !status }"> <p v-else-if="!isDropdownShowing" class="value m-0" :class="{ 'no-value': !status }">
<gl-icon <span v-if="status" class="text-plain bold">{{ statusText }}</span>
v-if="status" <span v-else>{{ __('None') }}</span>
name="severity-low"
:size="14"
class="align-bottom append-right-10"
:class="statusColor"
/>
{{ statusText }}
<template v-if="canRemoveStatus">
<span class="text-secondary mx-1" aria-hidden="true">-</span>
<gl-deprecated-button variant="link" class="text-secondary" @click="removeStatus">
{{ __('remove status') }}
</gl-deprecated-button>
</template>
</p> </p>
</div> </div>
</div> </div>
......
...@@ -6,12 +6,6 @@ export const healthStatus = { ...@@ -6,12 +6,6 @@ export const healthStatus = {
AT_RISK: 'atRisk', AT_RISK: 'atRisk',
}; };
export const healthStatusColorMap = {
[healthStatus.ON_TRACK]: 'text-success',
[healthStatus.NEEDS_ATTENTION]: 'text-warning',
[healthStatus.AT_RISK]: 'text-danger',
};
export const healthStatusTextMap = { export const healthStatusTextMap = {
[healthStatus.ON_TRACK]: __('On track'), [healthStatus.ON_TRACK]: __('On track'),
[healthStatus.NEEDS_ATTENTION]: __('Needs attention'), [healthStatus.NEEDS_ATTENTION]: __('Needs attention'),
......
---
title: Use dropdown to change health status
merge_request: 28547
author:
type: changed
import { mount, shallowMount } from '@vue/test-utils'; import { 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; let wrapper;
let handleDropdownClickMock;
beforeEach(() => {
const mediator = {
store: {
isFetching: {
status: true,
},
status: '',
},
};
handleDropdownClickMock = jest.fn();
wrapper = shallowMount(SidebarStatus, {
propsData: {
mediator,
},
methods: {
handleDropdownClick: handleDropdownClickMock,
},
});
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -14,73 +34,22 @@ describe('SidebarStatus', () => { ...@@ -14,73 +34,22 @@ describe('SidebarStatus', () => {
}); });
describe('Status child component', () => { describe('Status child component', () => {
let handleFormSubmissionMock; beforeEach(() => {});
beforeEach(() => {
const mediator = {
store: {
isFetching: {
status: true,
},
status: '',
},
};
handleFormSubmissionMock = jest.fn();
wrapper = shallowMount(SidebarStatus, {
propsData: {
mediator,
},
methods: {
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 onStatusChange event from Status component', () => { it('calls handleFormSubmission when receiving an onDropdownClick event from Status component', () => {
wrapper.find(Status).vm.$emit('onStatusChange', 'onTrack'); wrapper.find(Status).vm.$emit('onDropdownClick', 'onTrack');
expect(handleFormSubmissionMock).toHaveBeenCalledWith('onTrack'); expect(handleDropdownClickMock).toHaveBeenCalledWith('onTrack');
}); });
}); });
it('removes status when user clicks on "remove status"', () => { it('calls handleFormSubmission when receiving an onFormSubmit event from Status component', () => {
const mediator = { wrapper.find(Status).vm.$emit('onDropdownClick', 'onTrack');
store: {
editable: true,
isFetching: {
status: false,
},
status: 'onTrack',
},
updateStatus(status) {
this.store.status = status;
wrapper.setProps({
mediator: {
...this,
},
});
return Promise.resolve();
},
};
wrapper = mount(SidebarStatus, { expect(handleDropdownClickMock).toHaveBeenCalledWith('onTrack');
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 { GlDeprecatedButton, GlFormRadioGroup, GlLoadingIcon, GlTooltip } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, 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';
import { healthStatus, healthStatusColorMap, healthStatusTextMap } from 'ee/sidebar/constants'; import { healthStatus, healthStatusTextMap } from 'ee/sidebar/constants';
const getStatusText = wrapper => wrapper.find('.value').text(); const getStatusText = wrapper => wrapper.find('.value .text-plain').text();
const getTooltipText = wrapper => wrapper.find(GlTooltip).text(); const getTooltipText = wrapper => wrapper.find(GlTooltip).text();
const getStatusIconCssClasses = wrapper => wrapper.find('[name="severity-low"]').classes();
const getEditButton = wrapper => wrapper.find({ ref: 'editButton' }); const getEditButton = wrapper => wrapper.find({ ref: 'editButton' });
const getRemoveStatusButton = wrapper => wrapper.find(GlDeprecatedButton); const getDropdownElement = wrapper => wrapper.find(GlDropdown);
const getEditForm = wrapper => wrapper.find('form');
const getRadioInputs = wrapper => wrapper.findAll('input[type="radio"]');
const getRadioComponent = wrapper => wrapper.find(GlFormRadioGroup); const getRemoveStatusItem = wrapper => wrapper.find(GlDropdownItem);
describe('Status', () => { describe('Status', () => {
let wrapper; let wrapper;
...@@ -41,8 +35,7 @@ describe('Status', () => { ...@@ -41,8 +35,7 @@ describe('Status', () => {
it('shows the text "Status"', () => { it('shows the text "Status"', () => {
shallowMountStatus(); shallowMountStatus();
expect(wrapper.find('.title').text()).toBe('Health status');
expect(wrapper.find('.title').text()).toBe('Status');
}); });
describe('loading icon', () => { describe('loading icon', () => {
...@@ -89,18 +82,7 @@ describe('Status', () => { ...@@ -89,18 +82,7 @@ describe('Status', () => {
}); });
}); });
describe('remove status button', () => { describe('remove status dropdown item', () => {
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', () => { it('is displayed when there is a status', () => {
const props = { const props = {
isEditable: true, isEditable: true,
...@@ -109,10 +91,14 @@ describe('Status', () => { ...@@ -109,10 +91,14 @@ describe('Status', () => {
shallowMountStatus(props); shallowMountStatus(props);
expect(getRemoveStatusButton(wrapper).exists()).toBe(true); wrapper.vm.isDropdownShowing = true;
wrapper.vm.$nextTick(() => {
expect(getRemoveStatusItem(wrapper).exists()).toBe(true);
});
}); });
it('emits an onStatusChange event with argument null when clicked', () => { it('emits an onDropdownClick event with argument null when clicked', () => {
const props = { const props = {
isEditable: true, isEditable: true,
status: healthStatus.AT_RISK, status: healthStatus.AT_RISK,
...@@ -120,9 +106,13 @@ describe('Status', () => { ...@@ -120,9 +106,13 @@ describe('Status', () => {
shallowMountStatus(props); shallowMountStatus(props);
getRemoveStatusButton(wrapper).vm.$emit('click'); wrapper.vm.isDropdownShowing = true;
wrapper.vm.$nextTick(() => {
getRemoveStatusItem(wrapper).vm.$emit('click', { preventDefault: () => null });
expect(wrapper.emitted().onStatusChange[0]).toEqual([null]); expect(wrapper.emitted().onDropdownClick[0]).toEqual([null]);
});
}); });
}); });
...@@ -137,11 +127,11 @@ describe('Status', () => { ...@@ -137,11 +127,11 @@ describe('Status', () => {
}); });
it('shows "None"', () => { it('shows "None"', () => {
expect(getStatusText(wrapper)).toBe('None'); expect(wrapper.find('.no-value').text()).toBe('None');
}); });
it('shows "Status" in the tooltip', () => { it('shows "Status" in the tooltip', () => {
expect(getTooltipText(wrapper)).toBe('Status'); expect(getTooltipText(wrapper)).toBe('Health status');
}); });
}); });
...@@ -159,24 +149,22 @@ describe('Status', () => { ...@@ -159,24 +149,22 @@ describe('Status', () => {
}); });
it(`shows "Status: ${healthStatusTextMap[statusValue]}" in the tooltip`, () => { it(`shows "Status: ${healthStatusTextMap[statusValue]}" in the tooltip`, () => {
expect(getTooltipText(wrapper)).toBe(`Status: ${healthStatusTextMap[statusValue]}`); expect(getTooltipText(wrapper)).toBe(`Health status: ${healthStatusTextMap[statusValue]}`);
});
it(`uses ${healthStatusColorMap[statusValue]} color for the status icon`, () => {
expect(getStatusIconCssClasses(wrapper)).toContain(healthStatusColorMap[statusValue]);
}); });
}); });
}); });
describe('status edit form', () => { describe('status dropdown', () => {
it('is hidden by default', () => { it('is hidden by default', () => {
const props = { const props = {
isEditable: true, isEditable: true,
}; };
shallowMountStatus(props); mountStatus(props);
const dropdown = wrapper.find('.dropdown');
expect(getEditForm(wrapper).exists()).toBe(false); expect(dropdown.classes()).toContain('d-none');
}); });
describe('when hidden', () => { describe('when hidden', () => {
...@@ -185,14 +173,14 @@ describe('Status', () => { ...@@ -185,14 +173,14 @@ describe('Status', () => {
isEditable: true, isEditable: true,
}; };
shallowMountStatus(props); mountStatus(props);
}); });
it('shows the form when the Edit button is clicked', () => { it('shows the dropdown when the Edit button is clicked', () => {
getEditButton(wrapper).trigger('click'); getEditButton(wrapper).trigger('click');
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(getEditForm(wrapper).exists()).toBe(true); expect(wrapper.find('.dropdown').classes()).toContain('show');
}); });
}); });
}); });
...@@ -205,47 +193,43 @@ describe('Status', () => { ...@@ -205,47 +193,43 @@ describe('Status', () => {
shallowMountStatus(props); shallowMountStatus(props);
wrapper.setData({ isFormShowing: true }); wrapper.setData({ isDropdownShowing: true });
}); });
it('shows text to ask the user to pick an option', () => { it('shows text to ask the user to pick an option', () => {
const message = const message = 'Assign health status';
'Choose which status most accurately reflects the current state of this issue:';
expect( expect(
getEditForm(wrapper) getDropdownElement(wrapper)
.find('p') .find('.health-title')
.text(), .text(),
).toContain(message); ).toContain(message);
}); });
it('hides form when the Edit button is clicked', () => { it('hides form when the `edit` button is clicked', () => {
getEditButton(wrapper).trigger('click'); getEditButton(wrapper).trigger('click');
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(getEditForm(wrapper).exists()).toBe(false); expect(wrapper.find('.dropdown').classes()).toContain('d-none');
}); });
}); });
it('hides form when the Cancel button is clicked', () => { it('hides form when a dropdown item is clicked', () => {
const button = getEditForm(wrapper).find('[type="button"]'); const dropdownItem = wrapper.findAll(GlDropdownItem).at(1);
button.vm.$emit('click'); dropdownItem.vm.$emit('click');
return Vue.nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(getEditForm(wrapper).exists()).toBe(false); expect(wrapper.find('.dropdown').classes()).toContain('d-none');
});
});
it('hides form when the form is submitted', () => {
getEditForm(wrapper).trigger('submit');
return Vue.nextTick().then(() => {
expect(getEditForm(wrapper).exists()).toBe(false);
}); });
}); });
}); });
describe('radio buttons', () => { describe('dropdown', () => {
const getIterableArray = arr => {
return arr.map((value, index) => [value, index]);
};
beforeEach(() => { beforeEach(() => {
const props = { const props = {
isEditable: true, isEditable: true,
...@@ -253,29 +237,37 @@ describe('Status', () => { ...@@ -253,29 +237,37 @@ describe('Status', () => {
mountStatus(props); mountStatus(props);
wrapper.setData({ isFormShowing: true }); wrapper.setData({ isDropdownShowing: true });
}); });
it('shows 3 radio buttons', () => { it('shows 4 dropdown items', () => {
expect(getRadioInputs(wrapper).length).toBe(3); expect(wrapper.findAll(GlDropdownItem).length).toBe(4);
}); });
// Test that "On track", "Needs attention", and "At risk" are displayed // Test that "On track", "Needs attention", and "At risk" are displayed
it.each(Object.values(healthStatusTextMap))('shows "%s" text', statusText => { it.each(getIterableArray(Object.values(healthStatusTextMap)))(
expect(getRadioComponent(wrapper).text()).toContain(statusText); 'shows "%s" text',
}); (statusText, index) => {
expect(
wrapper
.findAll(GlDropdownItem)
.at(index + 1) // +1 in index to account for 1st item as `No status`
.text(),
).toContain(statusText);
},
);
// 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(getIterableArray(Object.values(healthStatus)))(
'emits onStatusChange event with argument "%s" when user selects the option and submits form', 'emits onFormSubmit event with argument "%s" when user selects the option and submits form',
status => { (status, index) => {
getEditForm(wrapper) wrapper
.find(`input[value="${status}"]`) .findAll(GlDropdownItem)
.trigger('click'); .at(index + 1)
.vm.$emit('click', { preventDefault: () => null });
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
getEditForm(wrapper).trigger('submit'); expect(wrapper.emitted().onDropdownClick[0]).toEqual([status]);
expect(wrapper.emitted().onStatusChange[0]).toEqual([status]);
}); });
}, },
); );
......
...@@ -3850,9 +3850,6 @@ msgstr "" ...@@ -3850,9 +3850,6 @@ msgstr ""
msgid "Choose which shards you wish to synchronize to this secondary node" msgid "Choose which shards you wish to synchronize to this secondary node"
msgstr "" msgstr ""
msgid "Choose which status most accurately reflects the current state of this issue:"
msgstr ""
msgid "Choose your framework" msgid "Choose your framework"
msgstr "" msgstr ""
...@@ -18040,6 +18037,9 @@ msgstr "" ...@@ -18040,6 +18037,9 @@ msgstr ""
msgid "Select groups to replicate" msgid "Select groups to replicate"
msgstr "" msgstr ""
msgid "Select health status"
msgstr ""
msgid "Select labels" msgid "Select labels"
msgstr "" msgstr ""
...@@ -18576,16 +18576,22 @@ msgstr "" ...@@ -18576,16 +18576,22 @@ msgstr ""
msgid "Side-by-side" msgid "Side-by-side"
msgstr "" msgstr ""
msgid "Sidebar|Assign health status"
msgstr ""
msgid "Sidebar|Change weight" msgid "Sidebar|Change weight"
msgstr "" msgstr ""
msgid "Sidebar|None" msgid "Sidebar|Health status"
msgstr "" msgstr ""
msgid "Sidebar|Only numeral characters allowed" msgid "Sidebar|No status"
msgstr "" msgstr ""
msgid "Sidebar|Status" msgid "Sidebar|None"
msgstr ""
msgid "Sidebar|Only numeral characters allowed"
msgstr "" msgstr ""
msgid "Sidebar|Weight" msgid "Sidebar|Weight"
...@@ -24957,9 +24963,6 @@ msgstr "" ...@@ -24957,9 +24963,6 @@ 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