Commit 5222fb59 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '231401-epic-board-sidebar-edit-start-and-due-date' into 'master'

Epic Boards - Edit start and due dates in sidebar

See merge request gitlab-org/gitlab!59446
parents 12c79809 57af7a1e
<script>
import { GlButton } from '@gitlab/ui';
export default {
components: {
GlButton,
},
inject: ['canUpdate'],
props: {
formattedDate: {
required: true,
type: String,
},
hasDate: {
required: true,
type: Boolean,
},
resetText: {
required: true,
type: String,
},
isLoading: {
required: true,
type: Boolean,
},
canDelete: {
required: false,
type: Boolean,
default: true,
},
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center hide-collapsed">
<span
:class="hasDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
data-testid="sidebar-date-value"
>
{{ formattedDate }}
</span>
<div v-if="hasDate && canUpdate && canDelete" class="gl-display-flex">
<span class="gl-px-2">-</span>
<gl-button
variant="link"
class="gl-text-gray-500!"
data-testid="reset-button"
:disabled="isLoading"
@click="$emit('reset-date', $event)"
>
{{ resetText }}
</gl-button>
</div>
</div>
</template>
<script>
import { GlFormRadio } from '@gitlab/ui';
import { dateInWords, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import { dateFields } from '../../constants';
import SidebarFormattedDate from './sidebar_formatted_date.vue';
export default {
components: {
GlFormRadio,
SidebarFormattedDate,
},
inject: ['canUpdate'],
props: {
issuable: {
required: true,
type: Object,
},
isLoading: {
required: true,
type: Boolean,
},
dateType: {
type: String,
required: true,
},
},
computed: {
dateIsFixed: {
get() {
return this.issuable?.[dateFields[this.dateType].isDateFixed] || false;
},
set(fixed) {
this.$emit('set-date', fixed);
},
},
hasFixedDate() {
return this.issuable[dateFields[this.dateType].dateFixed] !== null;
},
formattedFixedDate() {
const dateFixed = this.issuable[dateFields[this.dateType].dateFixed];
if (!dateFixed) {
return this.$options.i18n.noDate;
}
return dateInWords(parsePikadayDate(dateFixed), true);
},
formattedInheritedDate() {
const dateFromMilestones = this.issuable[dateFields[this.dateType].dateFromMilestones];
if (!dateFromMilestones) {
return this.$options.i18n.noDate;
}
return dateInWords(parsePikadayDate(dateFromMilestones), true);
},
},
i18n: {
fixed: __('Fixed:'),
inherited: __('Inherited:'),
remove: __('remove'),
noDate: __('None'),
},
};
</script>
<template>
<div class="hide-collapsed gl-mt-3">
<div class="gl-display-flex gl-align-items-baseline" data-testid="sidebar-fixed-date">
<gl-form-radio
v-model="dateIsFixed"
:value="true"
:disabled="!canUpdate || isLoading"
class="gl-pr-2"
>
<span :class="dateIsFixed ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'">
{{ $options.i18n.fixed }}
</span>
</gl-form-radio>
<sidebar-formatted-date
:has-date="dateIsFixed"
:formatted-date="formattedFixedDate"
:reset-text="$options.i18n.remove"
:is-loading="isLoading"
:can-delete="dateIsFixed && hasFixedDate"
class="gl-line-height-normal"
@reset-date="$emit('reset-date', $event)"
/>
</div>
<div class="gl-display-flex gl-align-items-baseline" data-testid="sidebar-inherited-date">
<gl-form-radio
v-model="dateIsFixed"
:value="false"
:disabled="!canUpdate || isLoading"
class="gl-pr-2"
>
<span :class="!dateIsFixed ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'">
{{ $options.i18n.inherited }}
</span>
</gl-form-radio>
<sidebar-formatted-date
:has-date="!dateIsFixed"
:formatted-date="formattedInheritedDate"
:reset-text="$options.i18n.remove"
:is-loading="isLoading"
:can-delete="false"
class="gl-line-height-normal"
/>
</div>
</div>
</template>
......@@ -103,6 +103,7 @@ export default {
<div>
<div class="gl-display-flex gl-align-items-center" @click.self="collapse">
<span class="hide-collapsed" data-testid="title" @click="collapse">{{ title }}</span>
<slot name="title-extra"></slot>
<gl-loading-icon v-if="loading || initialLoading" inline class="gl-ml-2 hide-collapsed" />
<gl-loading-icon
v-if="loading && isClassicSidebar"
......
import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
......@@ -34,7 +38,7 @@ export const confidentialityQueries = {
},
[IssuableType.Epic]: {
query: epicConfidentialQuery,
mutation: updateEpicMutation,
mutation: updateEpicConfidentialMutation,
},
};
......@@ -47,9 +51,38 @@ export const referenceQueries = {
},
};
export const dateTypes = {
start: 'startDate',
due: 'dueDate',
};
export const dateFields = {
[dateTypes.start]: {
isDateFixed: 'startDateIsFixed',
dateFixed: 'startDateFixed',
dateFromMilestones: 'startDateFromMilestones',
},
[dateTypes.due]: {
isDateFixed: 'dueDateIsFixed',
dateFixed: 'dueDateFixed',
dateFromMilestones: 'dueDateFromMilestones',
},
};
export const dueDateQueries = {
[IssuableType.Issue]: {
query: issueDueDateQuery,
mutation: updateIssueDueDateMutation,
},
[IssuableType.Epic]: {
query: epicDueDateQuery,
mutation: updateEpicDueDateMutation,
},
};
export const startDateQueries = {
[IssuableType.Epic]: {
query: epicStartDateQuery,
mutation: updateEpicStartDateMutation,
},
};
......@@ -13,7 +13,7 @@ import { __ } from '~/locale';
import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDueDateWidget from '~/sidebar/components/due_date/sidebar_due_date_widget.vue';
import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import { apolloProvider } from '~/sidebar/graphql';
import Translate from '../vue_shared/translate';
......@@ -225,14 +225,14 @@ function mountDueDateComponent() {
SidebarDueDateWidget,
},
provide: {
iid: String(iid),
fullPath,
canUpdate: editable,
},
render: (createElement) =>
createElement('sidebar-due-date-widget', {
props: {
iid: String(iid),
fullPath,
issuableType: IssuableType.Issue,
},
}),
......
query epicDueDate($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
dueDate
dueDateIsFixed
dueDateFixed
dueDateFromMilestones
}
}
}
query epicStartDate($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
startDate
startDateIsFixed
startDateFixed
startDateFromMilestones
}
}
}
mutation updateEpicDueDate($input: UpdateEpicInput!) {
issuableSetDate: updateEpic(input: $input) {
issuable: epic {
id
dueDateIsFixed
dueDateFixed
dueDateFromMilestones
}
errors
}
}
mutation updateEpicStartDate($input: UpdateEpicInput!) {
issuableSetDate: updateEpic(input: $input) {
issuable: epic {
id
startDateIsFixed
startDateFixed
startDateFromMilestones
}
errors
}
}
mutation updateIssueDueDate($input: UpdateIssueInput!) {
issuableSetDueDate: updateIssue(input: $input) {
issuableSetDate: updateIssue(input: $input) {
issuable: issue {
id
dueDate
......
......@@ -7,6 +7,7 @@ import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.v
import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
export default {
headerHeight: `${contentTop()}px`,
......@@ -16,6 +17,7 @@ export default {
BoardSidebarSubscription,
BoardSidebarTitle,
SidebarConfidentialityWidget,
SidebarDateWidget,
},
computed: {
...mapGetters(['isSidebarOpen', 'activeBoardItem']),
......@@ -46,6 +48,20 @@ export default {
<template #header>{{ __('Epic details') }}</template>
<template #default>
<board-sidebar-title data-testid="sidebar-title" />
<sidebar-date-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
date-type="startDate"
issuable-type="epic"
:can-inherit="true"
/>
<sidebar-date-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
date-type="dueDate"
issuable-type="epic"
:can-inherit="true"
/>
<board-sidebar-labels-select class="labels" />
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
......
......@@ -81,6 +81,50 @@ RSpec.describe 'Epic boards sidebar', :js do
end
end
context 'start date' do
it 'edits fixed start date' do
click_card(card)
wait_for_requests
page.within('[data-testid="start-date"]') do
edit_fixed_date
end
end
it 'removes fixed start date' do
click_card(card)
wait_for_requests
page.within('[data-testid="start-date"]') do
remove_fixed_date
end
end
end
context 'due date' do
it 'edits fixed due date' do
click_card(card)
wait_for_requests
page.within('[data-testid="due-date"]') do
edit_fixed_date
end
end
it 'removes fixed due date' do
click_card(card)
wait_for_requests
page.within('[data-testid="due-date"]') do
remove_fixed_date
end
end
end
context 'labels' do
it 'adds a single label' do
click_card(card)
......@@ -173,4 +217,48 @@ RSpec.describe 'Epic boards sidebar', :js do
click_card(card)
end
def pick_a_date
click_button 'Edit'
expect(page).to have_selector('.gl-datepicker')
page.within('.pika-lendar') do
click_button '25'
end
wait_for_requests
end
def edit_fixed_date
page.within('[data-testid="sidebar-inherited-date"]') do
expect(find_field('Inherited:')).to be_checked
end
pick_a_date
page.within('[data-testid="sidebar-fixed-date"]') do
expect(find('[data-testid="sidebar-date-value"]').text).to include('25')
expect(find_field('Fixed:')).to be_checked
end
end
def remove_fixed_date
expect(page).not_to have_button('remove')
page.within('[data-testid="sidebar-fixed-date"]') do
expect(find('[data-testid="sidebar-date-value"]').text).to include('None')
end
pick_a_date
page.within('[data-testid="sidebar-fixed-date"]') do
expect(find('[data-testid="sidebar-date-value"]').text).not_to include('None')
expect(page).to have_button('remove')
find_button('remove').click
wait_for_requests
expect(page).not_to have_button('remove')
expect(find('[data-testid="sidebar-date-value"]').text).to include('None')
end
end
end
......@@ -29924,10 +29924,10 @@ msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again."
msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgid "Something went wrong while setting %{issuableType} %{dateType} date."
msgstr ""
msgid "Something went wrong while setting %{issuableType} due date."
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
......@@ -32351,6 +32351,9 @@ msgstr ""
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
msgstr ""
msgid "These dates affect how your epics appear in the roadmap. Set a fixed date or one inherited from the milestones assigned to issues in this epic."
msgstr ""
msgid "These examples show how to trigger this project's pipeline for a branch or tag."
msgstr ""
......@@ -38470,6 +38473,9 @@ msgstr ""
msgid "remove due date"
msgstr ""
msgid "remove start date"
msgstr ""
msgid "remove weight"
msgstr ""
......
......@@ -417,7 +417,7 @@ RSpec.describe "Issues > User edits issue", :js do
wait_for_requests
expect(find('[data-testid="sidebar-duedate-value"]').text).to have_content date.strftime('%b %-d, %Y')
expect(find('[data-testid="sidebar-date-value"]').text).to have_content date.strftime('%b %-d, %Y')
end
end
......
......@@ -4,37 +4,48 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import SidebarDueDateWidget from '~/sidebar/components/due_date/sidebar_due_date_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue';
import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import { issueDueDateResponse } from '../../mock_data';
import { issuableDueDateResponse, issuableStartDateResponse } from '../../mock_data';
jest.mock('~/flash');
Vue.use(VueApollo);
describe('Sidebar Due date Widget', () => {
describe('Sidebar date Widget', () => {
let wrapper;
let fakeApollo;
const date = '2021-04-15';
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findFormattedDueDate = () => wrapper.find("[data-testid='sidebar-duedate-value']");
const createComponent = ({
dueDateQueryHandler = jest.fn().mockResolvedValue(issueDueDateResponse()),
dueDateQueryHandler = jest.fn().mockResolvedValue(issuableDueDateResponse()),
startDateQueryHandler = jest.fn().mockResolvedValue(issuableStartDateResponse()),
canInherit = false,
dateType = undefined,
issuableType = 'issue',
} = {}) => {
fakeApollo = createMockApollo([[issueDueDateQuery, dueDateQueryHandler]]);
fakeApollo = createMockApollo([
[issueDueDateQuery, dueDateQueryHandler],
[epicStartDateQuery, startDateQueryHandler],
]);
wrapper = shallowMount(SidebarDueDateWidget, {
wrapper = shallowMount(SidebarDateWidget, {
apolloProvider: fakeApollo,
provide: {
fullPath: 'group/project',
iid: '1',
canUpdate: true,
},
propsData: {
issuableType: 'issue',
fullPath: 'group/project',
iid: '1',
issuableType,
canInherit,
dateType,
},
stubs: {
SidebarEditableItem,
......@@ -53,10 +64,16 @@ describe('Sidebar Due date Widget', () => {
expect(findEditableItem().props('loading')).toBe(true);
});
describe('when issue has no due date', () => {
it('dateType is due date by default', () => {
createComponent();
expect(wrapper.text()).toContain('Due date');
});
describe('when issuable has no due date', () => {
beforeEach(async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockResolvedValue(issueDueDateResponse(null)),
dueDateQueryHandler: jest.fn().mockResolvedValue(issuableDueDateResponse(null)),
});
await waitForPromises();
});
......@@ -65,10 +82,6 @@ describe('Sidebar Due date Widget', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('dueDate is null by default', () => {
expect(findFormattedDueDate().text()).toBe('None');
});
it('emits `dueDateUpdated` event with a `null` payload', () => {
expect(wrapper.emitted('dueDateUpdated')).toEqual([[null]]);
});
......@@ -77,7 +90,7 @@ describe('Sidebar Due date Widget', () => {
describe('when issue has due date', () => {
beforeEach(async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockResolvedValue(issueDueDateResponse(date)),
dueDateQueryHandler: jest.fn().mockResolvedValue(issuableDueDateResponse(date)),
});
await waitForPromises();
});
......@@ -86,15 +99,26 @@ describe('Sidebar Due date Widget', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('has dueDate', () => {
expect(findFormattedDueDate().text()).toBe('Apr 15, 2021');
});
it('emits `dueDateUpdated` event with the date payload', () => {
expect(wrapper.emitted('dueDateUpdated')).toEqual([[date]]);
});
});
it.each`
canInherit | component | componentName | expected
${true} | ${SidebarFormattedDate} | ${'SidebarFormattedDate'} | ${false}
${true} | ${SidebarInheritDate} | ${'SidebarInheritDate'} | ${true}
${false} | ${SidebarFormattedDate} | ${'SidebarFormattedDate'} | ${true}
${false} | ${SidebarInheritDate} | ${'SidebarInheritDate'} | ${false}
`(
'when canInherit is $canInherit, $componentName display is $expected',
({ canInherit, component, expected }) => {
createComponent({ canInherit });
expect(wrapper.find(component).exists()).toBe(expected);
},
);
it('displays a flash message when query is rejected', async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
......@@ -103,4 +127,23 @@ describe('Sidebar Due date Widget', () => {
expect(createFlash).toHaveBeenCalled();
});
it.each`
dateType | text | event | mockedResponse | issuableType | queryHandler
${'dueDate'} | ${'Due date'} | ${'dueDateUpdated'} | ${issuableDueDateResponse} | ${'issue'} | ${'dueDateQueryHandler'}
${'startDate'} | ${'Start date'} | ${'startDateUpdated'} | ${issuableStartDateResponse} | ${'epic'} | ${'startDateQueryHandler'}
`(
'when dateType is $dateType, component renders $text and emits $event',
async ({ dateType, text, event, mockedResponse, issuableType, queryHandler }) => {
createComponent({
dateType,
issuableType,
[queryHandler]: jest.fn().mockResolvedValue(mockedResponse(date)),
});
await waitForPromises();
expect(wrapper.text()).toContain(text);
expect(wrapper.emitted(event)).toEqual([[date]]);
},
);
});
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue';
describe('SidebarFormattedDate', () => {
let wrapper;
const findFormattedDate = () => wrapper.find("[data-testid='sidebar-date-value']");
const findRemoveButton = () => wrapper.find(GlButton);
const createComponent = ({ hasDate = true } = {}) => {
wrapper = shallowMount(SidebarFormattedDate, {
provide: {
canUpdate: true,
},
propsData: {
formattedDate: 'Apr 15, 2021',
hasDate,
issuableType: 'issue',
resetText: 'remove',
isLoading: false,
canDelete: true,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('displays formatted date', () => {
expect(findFormattedDate().text()).toBe('Apr 15, 2021');
});
describe('when issue has due date', () => {
it('displays remove button', () => {
expect(findRemoveButton().exists()).toBe(true);
expect(findRemoveButton().children).toEqual(wrapper.props.resetText);
});
it('emits reset-date event on click on remove button', () => {
findRemoveButton().vm.$emit('click');
expect(wrapper.emitted('reset-date')).toEqual([[undefined]]);
});
});
describe('when issuable has no due date', () => {
beforeEach(() => {
createComponent({
hasDate: false,
});
});
it('does not display remove button', () => {
expect(findRemoveButton().exists()).toBe(false);
});
});
});
import { GlFormRadio } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue';
import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue';
describe('SidebarInheritDate', () => {
let wrapper;
const findFixedFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(0);
const findInheritFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(1);
const findFixedRadio = () => wrapper.findAll(GlFormRadio).at(0);
const findInheritRadio = () => wrapper.findAll(GlFormRadio).at(1);
const createComponent = () => {
wrapper = shallowMount(SidebarInheritDate, {
provide: {
canUpdate: true,
},
propsData: {
issuable: {
dueDate: '2021-04-15',
dueDateIsFixed: true,
dueDateFixed: '2021-04-15',
dueDateFromMilestones: '2021-05-15',
},
isLoading: false,
dateType: 'dueDate',
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('displays formatted fixed and inherited dates with radio buttons', () => {
expect(wrapper.findAll(SidebarFormattedDate)).toHaveLength(2);
expect(wrapper.findAll(GlFormRadio)).toHaveLength(2);
expect(findFixedFormattedDate().props('formattedDate')).toBe('Apr 15, 2021');
expect(findInheritFormattedDate().props('formattedDate')).toBe('May 15, 2021');
expect(findFixedRadio().text()).toBe('Fixed:');
expect(findInheritRadio().text()).toBe('Inherited:');
});
it('emits set-date event on click on radio button', () => {
findFixedRadio().vm.$emit('input', true);
expect(wrapper.emitted('set-date')).toEqual([[true]]);
});
});
......@@ -233,7 +233,7 @@ export const issueConfidentialityResponse = (confidential = false) => ({
},
});
export const issueDueDateResponse = (dueDate = null) => ({
export const issuableDueDateResponse = (dueDate = null) => ({
data: {
workspace: {
__typename: 'Project',
......@@ -246,6 +246,22 @@ export const issueDueDateResponse = (dueDate = null) => ({
},
});
export const issuableStartDateResponse = (startDate = null) => ({
data: {
workspace: {
__typename: 'Group',
issuable: {
__typename: 'Epic',
id: 'gid://gitlab/Epic/4',
startDate,
startDateIsFixed: true,
startDateFixed: startDate,
startDateFromMilestones: null,
},
},
},
});
export const issueReferenceResponse = (reference) => ({
data: {
workspace: {
......
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