Commit ea1185e2 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '227049-visual-review-is-hard-to-find-confused-with-review-app' into 'master'

Resolve "Visual review is hard to find / confused with review app"

See merge request gitlab-org/gitlab!66453
parents ef7cb02f 3599a84e
<script>
import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
......@@ -25,6 +26,9 @@ export default {
step3: s__(
`EnableReviewApp|%{stepStart}Step 3%{stepEnd}. Add it to the project %{linkStart}gitlab-ci.yml%{linkEnd} file.`,
),
step4: s__(
`EnableReviewApp|%{stepStart}Step 4 (optional)%{stepEnd}. Enable Visual Reviews by following the %{linkStart}setup instructions%{linkEnd}.`,
),
},
modalInfo: {
closeText: s__('EnableReviewApp|Close'),
......@@ -45,6 +49,9 @@ export default {
except:
- ${this.defaultBranchName}`;
},
visualReviewsDocs() {
return helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' });
},
},
};
</script>
......@@ -103,5 +110,15 @@ export default {
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf :message="$options.instructionText.step4">
<template #step="{ content }">
<strong>{{ content }}</strong>
</template>
<template #link="{ content }">
<gl-link :href="visualReviewsDocs" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</gl-modal>
</template>
......@@ -20,21 +20,6 @@ export default {
type: Boolean,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
},
computed: {
computedDeploymentStatus() {
......@@ -63,8 +48,6 @@ export default {
<deployment-actions
:deployment="deployment"
:computed-deployment-status="computedDeploymentStatus"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
</div>
......
......@@ -33,21 +33,6 @@ export default {
type: Object,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
},
data() {
return {
......@@ -178,8 +163,6 @@ export default {
v-if="hasExternalUrls"
:app-button-text="appButtonText"
:deployment="deployment"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-meta="visualReviewAppMeta"
/>
<deployment-action-button
v-if="stopUrl"
......
......@@ -32,11 +32,6 @@ export default {
appUrl: '',
}),
},
showVisualReviewAppLink: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
showCollapsedDeployments() {
......@@ -74,8 +69,6 @@ export default {
class="gl-bg-gray-50"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
:show-visual-review-app="showVisualReviewAppLink"
:visual-review-app-meta="visualReviewAppMeta"
/>
</mr-collapsible-extension>
<div v-else class="mr-widget-extension">
......@@ -85,8 +78,6 @@ export default {
:class="deploymentClass"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
:show-visual-review-app="showVisualReviewAppLink"
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
</template>
......@@ -20,8 +20,6 @@ export default {
GlLink,
GlSearchBoxByType,
ReviewAppLink,
VisualReviewAppLink: () =>
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
directives: {
autofocusonshow,
......@@ -35,21 +33,6 @@ export default {
type: Object,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
},
data() {
return { searchTerm: '' };
......@@ -114,12 +97,5 @@ export default {
size="small"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline gl-ml-3"
/>
<visual-review-app-link
v-if="showVisualReviewApp"
:view-app-display="appButtonText"
:link="deploymentExternalUrl"
:app-metadata="visualReviewAppMeta"
:changes="deployment.changes"
/>
</span>
</template>
......@@ -66,11 +66,6 @@ export default {
pipeline() {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
showVisualReviewAppLink() {
return Boolean(
this.mr.visualReviewAppAvailable && this.glFeatures.anonymousVisualReviewFeedback,
);
},
showMergeTrainPositionIndicator() {
return isNumber(this.mr.mergeTrainIndex);
},
......@@ -120,8 +115,6 @@ export default {
:deployments="deployments"
:deployment-class="deploymentClass"
:has-deployment-metrics="hasDeploymentMetrics"
:visual-review-app-meta="visualReviewAppMeta"
:show-visual-review-app-link="showVisualReviewAppLink"
/>
<merge-train-position-indicator
v-if="showMergeTrainPositionIndicator"
......
......@@ -224,14 +224,7 @@ To see Visual reviews in action, see the [Visual Reviews Walk through](https://y
### Configure Review Apps for Visual Reviews
The feedback form is served through a script you add to pages in your Review App.
If you have the [Developer role](../../user/permissions.md) in the project,
you can access it by clicking the **Review** button in the **Pipeline** section
of the merge request. The form modal also shows a dropdown for changed pages
if [route maps](#route-maps) are configured in the project.
![review button](img/review_button.png)
The provided script should be added to the `<head>` of your application and
It should be added to the `<head>` of your application and
consists of some project and merge request specific values. Here's how it
looks for a project with code hosted in a project on GitLab.com:
......
<script>
/* eslint-disable vue/no-v-html */
import {
GlButton,
GlDropdown,
GlDropdownItem,
GlModal,
GlSearchBoxByType,
GlModalDirective,
} from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
export default {
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlModal,
GlSearchBoxByType,
ReviewAppLink,
ModalCopyButton,
},
directives: {
'gl-modal': GlModalDirective,
},
props: {
appMetadata: {
type: Object,
required: true,
},
changes: {
type: Array,
required: false,
default: () => [],
},
cssClass: {
type: String,
required: false,
default: '',
},
link: {
type: String,
required: true,
},
viewAppDisplay: {
type: Object,
required: true,
},
},
data() {
return {
modalId: 'visual-review-app-info',
changesSearchTerm: '',
};
},
computed: {
copyToClipboard() {
return {
script: s__('VisualReviewApp|Copy script'),
mrId: s__('VisualReviewApp|Copy merge request ID'),
};
},
copyString() {
/* eslint-disable no-useless-escape */
return {
script: `<script defer
data-project-id='${this.appMetadata.sourceProjectId}'
data-project-path='${this.appMetadata.sourceProjectPath}'
<!-- Remove the following line to use the same script for multiple merge requests -->
data-merge-request-id='${this.appMetadata.mergeRequestId}'
data-mr-url='${this.appMetadata.appUrl}'
id='review-app-toolbar-script'
src='https://gitlab.com/assets/webpack/visual_review_toolbar.js'><\/script>`,
};
/* eslint-enable no-useless-escape */
},
filteredChanges() {
return this.changes.filter((change) => change.path.includes(this.changesSearchTerm));
},
instructionText() {
return {
intro: {
p1: s__(
'VisualReviewApp|Follow the steps below to enable Visual Reviews inside your application.',
),
p2: s__(
'VisualReviewApp|Steps 1 and 2 (and sometimes 3) are performed once by the developer before requesting feedback. Steps 3 (if necessary), 4 is performed by the reviewer each time they perform a review.',
),
},
step1: sprintf(
s__('VisualReviewApp|%{stepStart}Step 1%{stepEnd}. Copy the following script:'),
{
stepStart: '<strong>',
stepEnd: '</strong>',
},
false,
),
step2: sprintf(
s__(
'VisualReviewApp|%{stepStart}Step 2%{stepEnd}. Add it to the %{headTags} tags of every page of your application, ensuring the merge request ID is set or not set as required. ',
),
{
stepStart: '<strong>',
stepEnd: '</strong>',
headTags: `<code>&lt;head&gt;</code>`,
},
false,
),
step3: sprintf(
s__(
`VisualReviewApp|%{stepStart}Step 3%{stepEnd}. If not previously %{linkStart}configured%{linkEnd} by a developer, enter the merge request ID for the review when prompted. The ID of this merge request is %{stepStart}%{mrId}%{stepStart}.`,
),
{
stepStart: '<strong>',
stepEnd: '</strong>',
linkStart:
'<a href="https://docs.gitlab.com/ee/ci/review_apps/#configuring-visual-reviews">',
linkEnd: '</a>',
mrId: this.appMetadata.mergeRequestId,
},
false,
),
step4: sprintf(
s__('VisualReviewApp|%{stepStart}Step 4%{stepEnd}. Leave feedback in the Review App.'),
{
stepStart: '<strong>',
stepEnd: '</strong>',
},
false,
),
};
},
modalTitle() {
return s__('VisualReviewApp|Enable Visual Reviews');
},
shouldShowChanges() {
return this.changes.length > 0;
},
isSearchEmpty() {
return this.filteredChanges.length === 0;
},
},
methods: {
cancel() {
this.$refs.modal.cancel();
},
ok() {
this.$refs.modal.ok();
},
},
};
</script>
<template>
<div class="gl-display-inline-flex">
<gl-button
v-gl-modal="modalId"
category="secondary"
class="gl-ml-3 js-review-button"
size="small"
:class="cssClass"
type="button"
>
{{ s__('VisualReviewApp|Review') }}
</gl-button>
<gl-modal
ref="modal"
:modal-id="modalId"
:title="modalTitle"
lazy
static
size="lg"
class="text-2 ws-normal"
>
<p v-html="instructionText.intro.p1"></p>
<p v-html="instructionText.intro.p2"></p>
<div>
<p v-html="instructionText.step1"></p>
<div class="flex align-items-start">
<pre> {{ copyString.script }} </pre>
<modal-copy-button
:title="copyToClipboard.script"
:text="copyString.script"
:modal-id="modalId"
css-classes="border-0"
/>
</div>
</div>
<p v-html="instructionText.step2"></p>
<p>
<span v-html="instructionText.step3"></span>
<modal-copy-button
:title="copyToClipboard.mrId"
:text="appMetadata.mergeRequestId.toString()"
:modal-id="modalId"
css-classes="border-0 gl-pt-0 gl-pr-0 gl-pl-2 gl-pb-0"
/>
</p>
<p v-html="instructionText.step4"></p>
<template #modal-footer>
<gl-button category="secondary" @click="cancel">
{{ s__('VisualReviewApp|Cancel') }}
</gl-button>
<gl-dropdown
v-if="shouldShowChanges"
:text="s__('VisualReviewApp|Open review app')"
icon="external-link"
dropup
right
split
:split-href="link"
data-track-event="open_review_app"
data-track-label="review_app"
@click="ok"
>
<gl-search-box-by-type v-model.trim="changesSearchTerm" />
<gl-dropdown-item
v-for="change in filteredChanges"
:key="change.path"
:href="change.external_url"
data-track-event="open_review_app"
data-track-label="review_app"
>{{ change.path }}</gl-dropdown-item
>
<div v-show="isSearchEmpty" class="text-secondary p-2">
{{ s__('VisualReviewApp|No review app found or available.') }}
</div>
</gl-dropdown>
<review-app-link
v-else
:display="viewAppDisplay"
:link="link"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
/>
</template>
</gl-modal>
</div>
</template>
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Merge request > User sees deployment widget', :js do
describe 'when merge request has associated environments' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :merged, source_project: project) }
let(:environment) { create(:environment, project: project) }
let(:role) { :developer }
let(:ref) { merge_request.target_branch }
let(:sha) { project.commit(ref).id }
let(:pipeline) { create(:ci_pipeline, sha: sha, project: project, ref: ref) }
let!(:manual) { }
before do
merge_request.update!(merge_commit_sha: sha)
project.add_user(user, role)
sign_in(user)
end
context 'when deployment succeeded' do
let(:build) { create(:ci_build, :success, pipeline: pipeline) }
let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) }
context 'when the license flag is enabled' do
before do
stub_licensed_features(visual_review_app: true)
end
it 'displays the visual review button' do
visit project_merge_request_path(project, merge_request)
wait_for_requests
expect(page).to have_selector('.js-review-button')
end
end
context 'when the license flag is disabled' do
before do
stub_licensed_features(visual_review_app: false)
end
it 'does not display the button' do
visit project_merge_request_path(project, merge_request)
wait_for_requests
expect(page).not_to have_selector('.js-review-button')
end
end
end
end
end
import { mount, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import MergeTrainPositionIndicator from 'ee/vue_merge_request_widget/components/merge_train_position_indicator.vue';
import VisualReviewAppLink from 'ee/vue_merge_request_widget/components/visual_review_app_link.vue';
import { mockStore } from 'jest/vue_mr_widget/mock_data';
import axios from '~/lib/utils/axios_utils';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
......@@ -62,78 +61,4 @@ describe('MrWidgetPipelineContainer', () => {
expect(wrapper.find(MergeTrainPositionIndicator).exists()).toBe(false);
});
});
describe('with anonymous visual review feedback feature flag enabled', () => {
beforeEach(() => {
factory(
mount,
{
visualReviewAppAvailable: true,
appUrl: 'http://gitlab.example.com',
iid: 1,
sourceProjectId: 20,
sourceProjectFullPath: 'source/project',
},
{
glFeatures: {
anonymousVisualReviewFeedback: true,
},
},
);
// the visual review app link component is lazy loaded
// so we need to re-render the component
return wrapper.vm.$nextTick();
});
it('renders the visual review app link', (done) => {
// the visual review app link component is lazy loaded
// so we need to re-render the component again, as once
// apparently isn't enough.
wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.find(VisualReviewAppLink).exists()).toEqual(true);
})
.then(done)
.catch(done.fail);
});
});
describe('with anonymous visual review feedback feature flag disabled', () => {
beforeEach(() => {
factory(
mount,
{
visualReviewAppAvailable: true,
appUrl: 'http://gitlab.example.com',
iid: 1,
sourceProjectId: 20,
sourceProjectFullPath: 'source/project',
},
{
glFeatures: {
anonymousVisualReviewFeedback: false,
},
},
);
// the visual review app link component is lazy loaded
// so we need to re-render the component
return wrapper.vm.$nextTick();
});
it('does not render the visual review app link', (done) => {
// the visual review app link component is lazy loaded
// so we need to re-render the component again, as once
// apparently isn't enough.
wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.find(VisualReviewAppLink).exists()).toEqual(false);
})
.then(done)
.catch(done.fail);
});
});
});
import { GlButton, GlDropdown, GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import VisualReviewAppLink from 'ee/vue_merge_request_widget/components/visual_review_app_link.vue';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
const propsData = {
cssClass: 'button cool-button best-button',
appMetadata: {
mergeRequestId: 1,
sourceProjectId: 20,
appUrl: 'http://gitlab.example.com',
sourceProjectPath: 'source/project',
},
viewAppDisplay: {
text: 'View app',
tooltip: '',
},
link: 'http://example.com',
};
describe('Visual Review App Link', () => {
let wrapper;
const factory = (options = {}) => {
wrapper = mount(VisualReviewAppLink, {
...options,
});
};
const openModal = () => {
wrapper.find('.js-review-button').trigger('click');
};
const findModal = () => wrapper.find(GlModal);
beforeEach(() => {
factory({
propsData,
});
});
afterEach(() => {
wrapper.destroy();
});
describe('renders link and text', () => {
it('renders Review text', () => {
expect(wrapper.find(GlButton).text()).toBe('Review');
});
it('renders provided cssClass as class attribute', () => {
expect(wrapper.find(GlButton).attributes('class')).toEqual(
expect.stringContaining(propsData.cssClass),
);
});
});
describe('renders the modal', () => {
beforeEach(() => {
openModal();
});
it('with expected project Id', () => {
expect(findModal().text()).toEqual(
expect.stringContaining(`data-project-id='${propsData.appMetadata.sourceProjectId}'`),
);
});
it('with expected project path', () => {
expect(findModal().text()).toEqual(
expect.stringContaining(`data-project-path='${propsData.appMetadata.sourceProjectPath}'`),
);
});
it('with expected merge request id', () => {
expect(findModal().text()).toEqual(
expect.stringContaining(`data-merge-request-id='${propsData.appMetadata.mergeRequestId}'`),
);
});
it('with expected appUrl', () => {
expect(findModal().text()).toEqual(
expect.stringContaining(`data-mr-url='${propsData.appMetadata.appUrl}'`),
);
});
describe('renders the copyToClipboard button', () => {
it('within the modal', () => {
expect(wrapper.find(ModalCopyButton).exists()).toEqual(true);
});
it('with the expected modalId', () => {
const { modalId } = findModal().props();
expect(wrapper.find(ModalCopyButton).props().modalId).toBe(modalId);
});
});
describe('renders modal footer', () => {
describe('when no changes are listed', () => {
it('with review app link', () => {
expect(wrapper.find('a.js-deploy-url').attributes('href')).toEqual(propsData.link);
});
it('tracks an event when review app link is clicked', () => {
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
const appLink = findModal().find('a.js-deploy-url');
triggerEvent(appLink.element);
expect(spy).toHaveBeenCalledWith('_category_', 'open_review_app', {
label: 'review_app',
});
});
});
describe('when changes are listed', () => {
beforeEach(() => {
factory({
propsData: {
...propsData,
changes: [
{
path: '/example-path',
external_url: `${propsData.link}/example-path`,
},
],
},
});
openModal();
});
it('with review app split dropdown', () => {
expect(wrapper.find(GlDropdown).find(`a[href='${propsData.link}']`).exists()).toEqual(
true,
);
});
it('contains a list of changed pages', () => {
expect(
wrapper.find(GlDropdown).find(`a[href='${propsData.link}/example-path']`).exists(),
).toEqual(true);
});
it('tracks an event when review app link is clicked', () => {
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
const appLink = findModal().find(`a[href='${propsData.link}/example-path']`);
triggerEvent(appLink.element);
expect(spy).toHaveBeenCalledWith('_category_', 'open_review_app', {
label: 'review_app',
});
});
});
});
});
});
......@@ -12280,6 +12280,9 @@ msgstr ""
msgid "EnableReviewApp|%{stepStart}Step 3%{stepEnd}. Add it to the project %{linkStart}gitlab-ci.yml%{linkEnd} file."
msgstr ""
msgid "EnableReviewApp|%{stepStart}Step 4 (optional)%{stepEnd}. Enable Visual Reviews by following the %{linkStart}setup instructions%{linkEnd}."
msgstr ""
msgid "EnableReviewApp|Close"
msgstr ""
......@@ -36385,45 +36388,6 @@ msgstr ""
msgid "Visual Studio Code (SSH)"
msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 1%{stepEnd}. Copy the following script:"
msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 2%{stepEnd}. Add it to the %{headTags} tags of every page of your application, ensuring the merge request ID is set or not set as required. "
msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 3%{stepEnd}. If not previously %{linkStart}configured%{linkEnd} by a developer, enter the merge request ID for the review when prompted. The ID of this merge request is %{stepStart}%{mrId}%{stepStart}."
msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 4%{stepEnd}. Leave feedback in the Review App."
msgstr ""
msgid "VisualReviewApp|Cancel"
msgstr ""
msgid "VisualReviewApp|Copy merge request ID"
msgstr ""
msgid "VisualReviewApp|Copy script"
msgstr ""
msgid "VisualReviewApp|Enable Visual Reviews"
msgstr ""
msgid "VisualReviewApp|Follow the steps below to enable Visual Reviews inside your application."
msgstr ""
msgid "VisualReviewApp|No review app found or available."
msgstr ""
msgid "VisualReviewApp|Open review app"
msgstr ""
msgid "VisualReviewApp|Review"
msgstr ""
msgid "VisualReviewApp|Steps 1 and 2 (and sometimes 3) are performed once by the developer before requesting feedback. Steps 3 (if necessary), 4 is performed by the reviewer each time they perform a review."
msgstr ""
msgid "Vulnerabilities"
msgstr ""
......
......@@ -45,7 +45,6 @@ describe('DeploymentAction component', () => {
propsData: {
computedDeploymentStatus: CREATED,
deployment: deploymentMockData,
showVisualReviewApp: false,
},
});
});
......@@ -64,7 +63,6 @@ describe('DeploymentAction component', () => {
...deploymentMockData,
stop_url: null,
},
showVisualReviewApp: false,
},
});
});
......@@ -115,7 +113,6 @@ describe('DeploymentAction component', () => {
...deploymentMockData,
details: displayConditionChanges,
},
showVisualReviewApp: false,
},
});
});
......
......@@ -7,7 +7,6 @@ import MrCollapsibleExtension from '~/vue_merge_request_widget/components/mr_col
import { mockStore } from '../mock_data';
const DEFAULT_PROPS = {
showVisualReviewAppLink: false,
hasDeploymentMetrics: false,
deploymentClass: 'js-pre-deployment',
};
......@@ -46,7 +45,6 @@ describe('~/vue_merge_request_widget/components/deployment/deployment_list.vue',
([deploymentWrapper, deployment]) => {
expect(deploymentWrapper.props('deployment')).toEqual(deployment);
expect(deploymentWrapper.props()).toMatchObject({
showVisualReviewApp: DEFAULT_PROPS.showVisualReviewAppLink,
showMetrics: DEFAULT_PROPS.hasDeploymentMetrics,
});
expect(deploymentWrapper.classes(DEFAULT_PROPS.deploymentClass)).toBe(true);
......@@ -87,10 +85,6 @@ describe('~/vue_merge_request_widget/components/deployment/deployment_list.vue',
zip(deploymentWrappers.wrappers, propsData.deployments).forEach(
([deploymentWrapper, deployment]) => {
expect(deploymentWrapper.props('deployment')).toEqual(deployment);
expect(deploymentWrapper.props()).toMatchObject({
showVisualReviewApp: DEFAULT_PROPS.showVisualReviewAppLink,
showMetrics: DEFAULT_PROPS.hasDeploymentMetrics,
});
expect(deploymentWrapper.classes(DEFAULT_PROPS.deploymentClass)).toBe(true);
expect(deploymentWrapper.text()).toEqual(expect.any(String));
expect(deploymentWrapper.text()).not.toBe('');
......
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