Commit ef4bc6df authored by Winnie Hellmann's avatar Winnie Hellmann Committed by Fatih Acet

Adjust position and wording for related issues in merge requests

parent 025cbc2a
export default { export default {
name: 'MRWidgetRelatedLinks', name: 'MRWidgetRelatedLinks',
props: { props: {
isMerged: { type: Boolean, required: true },
relatedLinks: { type: Object, required: true }, relatedLinks: { type: Object, required: true },
}, },
computed: { computed: {
// TODO: the following should be handled by i18n
closingText() {
if (this.isMerged) {
return `Closed ${this.issueLabel('closing')}`;
}
return `Closes ${this.issueLabel('closing')}`;
},
hasLinks() { hasLinks() {
const { closing, mentioned, assignToMe } = this.relatedLinks; const { closing, mentioned, assignToMe } = this.relatedLinks;
return closing || mentioned || assignToMe; return closing || mentioned || assignToMe;
}, },
// TODO: the following should be handled by i18n
mentionedText() {
if (this.isMerged) {
if (this.hasMultipleIssues(this.relatedLinks.mentioned)) {
return 'are mentioned but were not closed';
}
return 'is mentioned but was not closed';
}
if (this.hasMultipleIssues(this.relatedLinks.mentioned)) {
return 'are mentioned but will not be closed';
}
return 'is mentioned but will not be closed';
},
}, },
methods: { methods: {
hasMultipleIssues(text) { hasMultipleIssues(text) {
return !text ? false : text.match(/<\/a> and <a/); return /<\/a>,? and <a/.test(text);
}, },
// TODO: the following should be handled by i18n
issueLabel(field) { issueLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue'; return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue';
}, },
verbLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
},
}, },
template: ` template: `
<section <div v-if="hasLinks">
v-if="hasLinks"
class="mr-info-list mr-links">
<div class="legend"></div> <div class="legend"></div>
<p v-if="relatedLinks.closing"> <p v-if="relatedLinks.closing">
Closes {{issueLabel('closing')}} {{closingText}}
<span v-html="relatedLinks.closing"></span>. <span v-html="relatedLinks.closing"></span>.
</p> </p>
<p v-if="relatedLinks.mentioned"> <p v-if="relatedLinks.mentioned">
<span class="capitalize">{{issueLabel('mentioned')}}</span> <span class="capitalize">{{issueLabel('mentioned')}}</span>
<span v-html="relatedLinks.mentioned"></span> <span v-html="relatedLinks.mentioned"></span>
{{verbLabel('mentioned')}} mentioned but will not be closed. {{mentionedText}}
</p> </p>
<p v-if="relatedLinks.assignToMe"> <p v-if="relatedLinks.assignToMe">
<span v-html="relatedLinks.assignToMe"></span> <span v-html="relatedLinks.assignToMe"></span>
</p> </p>
</section> </div>
`, `,
}; };
/* global Flash */ /* global Flash */
import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import mrWidgetRelatedLinks from '../../components/mr_widget_related_links';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import '../../../flash';
export default { export default {
name: 'MRWidgetMerged', name: 'MRWidgetMerged',
...@@ -11,6 +13,7 @@ export default { ...@@ -11,6 +13,7 @@ export default {
}, },
components: { components: {
'mr-widget-author-and-time': mrWidgetAuthorTime, 'mr-widget-author-and-time': mrWidgetAuthorTime,
'mr-widget-related-links': mrWidgetRelatedLinks,
}, },
data() { data() {
return { return {
...@@ -18,6 +21,9 @@ export default { ...@@ -18,6 +21,9 @@ export default {
}; };
}, },
computed: { computed: {
shouldRenderRelatedLinks() {
return this.mr.relatedLinks && this.mr.isMerged;
},
shouldShowRemoveSourceBranch() { shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr; const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
...@@ -86,6 +92,10 @@ export default { ...@@ -86,6 +92,10 @@ export default {
aria-hidden="true" /> aria-hidden="true" />
The source branch is being removed. The source branch is being removed.
</p> </p>
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:is-merged="mr.isMerged()"
:related-links="mr.relatedLinks" />
</section> </section>
<div <div
v-if="shouldShowMergedButtons" v-if="shouldShowMergedButtons"
......
...@@ -48,7 +48,7 @@ export default { ...@@ -48,7 +48,7 @@ export default {
return stateMaps.stateToComponentMap[this.mr.state]; return stateMaps.stateToComponentMap[this.mr.state];
}, },
shouldRenderMergeHelp() { shouldRenderMergeHelp() {
return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1; return !this.mr.isMerged;
}, },
shouldRenderPipelines() { shouldRenderPipelines() {
return Object.keys(this.mr.pipeline).length || this.mr.hasCI; return Object.keys(this.mr.pipeline).length || this.mr.hasCI;
...@@ -238,9 +238,14 @@ export default { ...@@ -238,9 +238,14 @@ export default {
:is="componentName" :is="componentName"
:mr="mr" :mr="mr"
:service="service" /> :service="service" />
<mr-widget-related-links <section
v-if="shouldRenderRelatedLinks" v-if="shouldRenderRelatedLinks"
:related-links="mr.relatedLinks" /> class="mr-info-list mr-links">
<div class="legend"></div>
<mr-widget-related-links
:is-merged="mr.isMerged"
:related-links="mr.relatedLinks" />
</section>
<mr-widget-merge-help v-if="shouldRenderMergeHelp" /> <mr-widget-merge-help v-if="shouldRenderMergeHelp" />
</div> </div>
`, `,
......
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies'; import { getStateKey } from '../dependencies';
const unmergedStates = [
'locked',
'conflicts',
'workInProgress',
'readyToMerge',
'checking',
'unresolvedDiscussions',
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
];
export default class MergeRequestStore { export default class MergeRequestStore {
constructor(data) { constructor(data) {
...@@ -65,6 +77,7 @@ export default class MergeRequestStore { ...@@ -65,6 +77,7 @@ export default class MergeRequestStore {
this.mergeActionsContentPath = data.commit_change_content_path; this.mergeActionsContentPath = data.commit_change_content_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened' || data.state === 'reopened' || false; this.isOpen = data.state === 'opened' || data.state === 'reopened' || false;
this.isMerged = unmergedStates.indexOf(data.state) === -1;
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false; this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
this.canMerge = !!data.merge_path; this.canMerge = !!data.merge_path;
......
...@@ -19,19 +19,6 @@ const stateToComponentMap = { ...@@ -19,19 +19,6 @@ const stateToComponentMap = {
shaMismatch: 'mr-widget-sha-mismatch', shaMismatch: 'mr-widget-sha-mismatch',
}; };
const statesToShowHelpWidget = [
'locked',
'conflicts',
'workInProgress',
'readyToMerge',
'checking',
'unresolvedDiscussions',
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
];
export default { export default {
stateToComponentMap, stateToComponentMap,
statesToShowHelpWidget,
}; };
...@@ -372,6 +372,10 @@ ...@@ -372,6 +372,10 @@
margin-left: 12px; margin-left: 12px;
} }
&.mr-state-locked + .mr-info-list.mr-links {
margin-top: -16px;
}
&.empty-state { &.empty-state {
.artwork { .artwork {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
......
...@@ -36,7 +36,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -36,7 +36,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" } let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}") expect(page).to have_content("Closed issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end end
end end
...@@ -44,7 +44,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -44,7 +44,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" } let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.") expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but were not closed")
end end
end end
...@@ -52,8 +52,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -52,8 +52,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Closes issue #{issue_1.to_reference}.") expect(page).to have_content("Closed issue #{issue_1.to_reference}")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.") expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but was not closed")
end end
end end
...@@ -61,7 +61,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -61,7 +61,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" } let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}") expect(page).to have_content("Closed issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end end
end end
...@@ -69,7 +69,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -69,7 +69,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" } let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.") expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but were not closed")
end end
end end
...@@ -77,8 +77,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do ...@@ -77,8 +77,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Closes issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.") expect(page).to have_content("Closed issue #{issue_1.to_reference}")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.") expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but was not closed")
end end
end end
end end
import Vue from 'vue'; import Vue from 'vue';
import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links'; import MRWidgetRelatedLinks from '~/vue_merge_request_widget/components/mr_widget_related_links';
const createComponent = (data) => { describe('MRWidgetRelatedLinks', () => {
const Component = Vue.extend(relatedLinksComponent); let vm;
beforeEach(() => {
const Component = Vue.extend(MRWidgetRelatedLinks);
vm = new Component({
el: document.createElement('div'),
propsData: {
isMerged: false,
relatedLinks: {},
},
});
});
return new Component({ afterEach(() => {
el: document.createElement('div'), vm.$destroy();
propsData: data,
}); });
};
describe('MRWidgetRelatedLinks', () => {
describe('props', () => { describe('props', () => {
it('should have props', () => { it('should have props', () => {
const { relatedLinks } = relatedLinksComponent.props; const { isMerged, relatedLinks } = MRWidgetRelatedLinks.props;
expect(isMerged).toBeDefined();
expect(isMerged.type).toBe(Boolean);
expect(isMerged.required).toBeTruthy();
expect(relatedLinks).toBeDefined(); expect(relatedLinks).toBeDefined();
expect(relatedLinks.type instanceof Object).toBeTruthy(); expect(relatedLinks.type instanceof Object).toBeTruthy();
expect(relatedLinks.required).toBeTruthy(); expect(relatedLinks.required).toBeTruthy();
...@@ -22,16 +33,38 @@ describe('MRWidgetRelatedLinks', () => { ...@@ -22,16 +33,38 @@ describe('MRWidgetRelatedLinks', () => {
}); });
describe('computed', () => { describe('computed', () => {
describe('closingText', () => {
const dummyIssueLabel = 'dummy label';
beforeEach(() => {
spyOn(vm, 'issueLabel').and.returnValue(dummyIssueLabel);
});
it('outputs text for closing issues', () => {
vm.isMerged = false;
const text = vm.closingText;
expect(text).toBe(`Closes ${dummyIssueLabel}`);
});
it('outputs text for closed issues', () => {
vm.isMerged = true;
const text = vm.closingText;
expect(text).toBe(`Closed ${dummyIssueLabel}`);
});
});
describe('hasLinks', () => { describe('hasLinks', () => {
it('should return correct value when we have links reference', () => { it('should return correct value when we have links reference', () => {
const data = { vm.relatedLinks = {
relatedLinks: { closing: '/foo',
closing: '/foo', mentioned: '/foo',
mentioned: '/foo', assignToMe: '/foo',
assignToMe: '/foo',
},
}; };
const vm = createComponent(data);
expect(vm.hasLinks).toBeTruthy(); expect(vm.hasLinks).toBeTruthy();
vm.relatedLinks.closing = null; vm.relatedLinks.closing = null;
...@@ -44,95 +77,160 @@ describe('MRWidgetRelatedLinks', () => { ...@@ -44,95 +77,160 @@ describe('MRWidgetRelatedLinks', () => {
expect(vm.hasLinks).toBeFalsy(); expect(vm.hasLinks).toBeFalsy();
}); });
}); });
});
describe('methods', () => { describe('mentionedText', () => {
const data = { it('outputs text for one mentioned issue before merging', () => {
relatedLinks: { vm.isMerged = false;
closing: '<a href="#">#23</a> and <a>#42</a>', spyOn(vm, 'hasMultipleIssues').and.returnValue(false);
mentioned: '<a href="#">#7</a>',
},
};
const vm = createComponent(data);
describe('hasMultipleIssues', () => { const text = vm.mentionedText;
it('should return true if the given text has multiple issues', () => {
expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy(); expect(text).toBe('is mentioned but will not be closed');
}); });
it('should return false if the given text has one issue', () => { it('outputs text for one mentioned issue after merging', () => {
expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy(); vm.isMerged = true;
spyOn(vm, 'hasMultipleIssues').and.returnValue(false);
const text = vm.mentionedText;
expect(text).toBe('is mentioned but was not closed');
});
it('outputs text for multiple mentioned issue before merging', () => {
vm.isMerged = false;
spyOn(vm, 'hasMultipleIssues').and.returnValue(true);
const text = vm.mentionedText;
expect(text).toBe('are mentioned but will not be closed');
});
it('outputs text for multiple mentioned issue after merging', () => {
vm.isMerged = true;
spyOn(vm, 'hasMultipleIssues').and.returnValue(true);
const text = vm.mentionedText;
expect(text).toBe('are mentioned but were not closed');
}); });
}); });
});
describe('issueLabel', () => { describe('methods', () => {
const relatedLinks = {
oneIssue: '<a href="#">#7</a>',
twoIssues: '<a href="#">#23</a> and <a>#42</a>',
threeIssues: '<a href="#">#1</a>, <a>#2</a>, and <a>#3</a>',
};
beforeEach(() => {
vm.relatedLinks = relatedLinks;
});
describe('hasMultipleIssues', () => {
it('should return true if the given text has multiple issues', () => { it('should return true if the given text has multiple issues', () => {
expect(vm.issueLabel('closing')).toEqual('issues'); expect(vm.hasMultipleIssues(relatedLinks.twoIssues)).toBeTruthy();
expect(vm.hasMultipleIssues(relatedLinks.threeIssues)).toBeTruthy();
}); });
it('should return false if the given text has one issue', () => { it('should return false if the given text has one issue', () => {
expect(vm.issueLabel('mentioned')).toEqual('issue'); expect(vm.hasMultipleIssues(relatedLinks.oneIssue)).toBeFalsy();
}); });
}); });
describe('verbLabel', () => { describe('issueLabel', () => {
it('should return true if the given text has multiple issues', () => { it('should return true if the given text has multiple issues', () => {
expect(vm.verbLabel('closing')).toEqual('are'); expect(vm.issueLabel('twoIssues')).toEqual('issues');
expect(vm.issueLabel('threeIssues')).toEqual('issues');
}); });
it('should return false if the given text has one issue', () => { it('should return false if the given text has one issue', () => {
expect(vm.verbLabel('mentioned')).toEqual('is'); expect(vm.issueLabel('oneIssue')).toEqual('issue');
}); });
}); });
}); });
describe('template', () => { describe('template', () => {
it('should have only have closing issues text', () => { it('should have only have closing issues text', (done) => {
const vm = createComponent({ vm.relatedLinks = {
relatedLinks: { closing: '<a href="#">#23</a> and <a>#42</a>',
closing: '<a href="#">#23</a> and <a>#42</a>', };
},
});
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closes issues #23 and #42');
expect(content).not.toContain('mentioned');
});
it('should have only have mentioned issues text', () => { Vue.nextTick()
const vm = createComponent({ .then(() => {
relatedLinks: { const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
mentioned: '<a href="#">#7</a>',
},
});
expect(vm.$el.innerText).toContain('issue #7'); expect(content).toContain('Closes issues #23 and #42');
expect(vm.$el.innerText).toContain('is mentioned but will not be closed.'); expect(content).not.toContain('mentioned');
expect(vm.$el.innerText).not.toContain('Closes'); })
.then(done)
.catch(done.fail);
}); });
it('should have closing and mentioned issues at the same time', () => { it('should have only have mentioned issues text', (done) => {
const vm = createComponent({ vm.relatedLinks = {
relatedLinks: { mentioned: '<a href="#">#7</a>',
closing: '<a href="#">#7</a>', };
mentioned: '<a href="#">#23</a> and <a>#42</a>',
}, Vue.nextTick()
}); .then(() => {
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); expect(vm.$el.innerText).toContain('issue #7');
expect(vm.$el.innerText).toContain('is mentioned but will not be closed');
expect(vm.$el.innerText).not.toContain('Closes');
})
.then(done)
.catch(done.fail);
});
expect(content).toContain('Closes issue #7.'); it('should have closing and mentioned issues at the same time', (done) => {
expect(content).toContain('issues #23 and #42'); vm.relatedLinks = {
expect(content).toContain('are mentioned but will not be closed.'); closing: '<a href="#">#7</a>',
mentioned: '<a href="#">#23</a> and <a>#42</a>',
};
Vue.nextTick()
.then(() => {
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closes issue #7.');
expect(content).toContain('issues #23 and #42');
expect(content).toContain('are mentioned but will not be closed');
})
.then(done)
.catch(done.fail);
}); });
it('should have assing issues link', () => { it('should have assing issues link', (done) => {
const vm = createComponent({ vm.relatedLinks = {
relatedLinks: { assignToMe: '<a href="#">Assign yourself to these issues</a>',
assignToMe: '<a href="#">Assign yourself to these issues</a>', };
},
}); Vue.nextTick()
.then(() => {
expect(vm.$el.innerText).toContain('Assign yourself to these issues');
})
.then(done)
.catch(done.fail);
});
expect(vm.$el.innerText).toContain('Assign yourself to these issues'); it('should use different wording after merging', (done) => {
vm.isMerged = true;
vm.relatedLinks = {
closing: '<a href="#">#7</a>',
mentioned: '<a href="#">#23</a> and <a>#42</a>',
};
Vue.nextTick()
.then(() => {
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closed issue #7.');
expect(content).toContain('issues #23 and #42');
expect(content).toContain('are mentioned but were not closed');
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
...@@ -48,12 +48,13 @@ describe('mrWidgetOptions', () => { ...@@ -48,12 +48,13 @@ describe('mrWidgetOptions', () => {
}); });
describe('shouldRenderMergeHelp', () => { describe('shouldRenderMergeHelp', () => {
it('should return false for the initial merged state', () => { it('should return false after merging', () => {
vm.mr.isMerged = true;
expect(vm.shouldRenderMergeHelp).toBeFalsy(); expect(vm.shouldRenderMergeHelp).toBeFalsy();
}); });
it('should return true for a state which requires help widget', () => { it('should return true before merging', () => {
vm.mr.state = 'conflicts'; vm.mr.isMerged = false;
expect(vm.shouldRenderMergeHelp).toBeTruthy(); expect(vm.shouldRenderMergeHelp).toBeTruthy();
}); });
}); });
......
...@@ -18,5 +18,17 @@ describe('MergeRequestStore', () => { ...@@ -18,5 +18,17 @@ describe('MergeRequestStore', () => {
store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress }); store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
expect(store.hasSHAChanged).toBe(false); expect(store.hasSHAChanged).toBe(false);
}); });
it('sets isMerged to true for merged state', () => {
store.setData({ ...mockData, state: 'merged' });
expect(store.isMerged).toBe(true);
});
it('sets isMerged to false for readyToMerge state', () => {
store.setData({ ...mockData, state: 'readyToMerge' });
expect(store.isMerged).toBe(false);
});
}); });
}); });
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