Commit 3756964b authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '216881-add-close-button-to-sidebar-labels-to-remove' into 'master'

Add close button to issue, MR and epic sidebar labels

See merge request gitlab-org/gitlab!42703
parents b6c0b3c8 bd8edb93
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { difference, union } from 'lodash'; import { difference, union } from 'lodash';
import { mapState, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -26,47 +25,49 @@ export default { ...@@ -26,47 +25,49 @@ export default {
'projectIssuesPath', 'projectIssuesPath',
'projectPath', 'projectPath',
], ],
data: () => ({ data() {
labelsSelectInProgress: false, return {
}), isLabelsSelectInProgress: false,
computed: {
...mapState(['selectedLabels']),
},
mounted() {
this.setInitialState({
selectedLabels: this.initiallySelectedLabels, selectedLabels: this.initiallySelectedLabels,
}); };
}, },
methods: { methods: {
...mapActions(['setInitialState', 'replaceSelectedLabels']),
handleDropdownClose() { handleDropdownClose() {
$(this.$el).trigger('hidden.gl.dropdown'); $(this.$el).trigger('hidden.gl.dropdown');
}, },
handleUpdateSelectedLabels(labels) { handleUpdateSelectedLabels(dropdownLabels) {
const currentLabelIds = this.selectedLabels.map(label => label.id); const currentLabelIds = this.selectedLabels.map(label => label.id);
const userAddedLabelIds = labels.filter(label => label.set).map(label => label.id); const userAddedLabelIds = dropdownLabels.filter(label => label.set).map(label => label.id);
const userRemovedLabelIds = labels.filter(label => !label.set).map(label => label.id); const userRemovedLabelIds = dropdownLabels.filter(label => !label.set).map(label => label.id);
const issuableLabels = difference( const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds);
union(currentLabelIds, userAddedLabelIds),
userRemovedLabelIds,
);
this.labelsSelectInProgress = true; this.updateSelectedLabels(labelIds);
},
handleLabelRemove(labelId) {
const currentLabelIds = this.selectedLabels.map(label => label.id);
const labelIds = difference(currentLabelIds, [labelId]);
this.updateSelectedLabels(labelIds);
},
updateSelectedLabels(labelIds) {
this.isLabelsSelectInProgress = true;
axios({ axios({
data: { data: {
[this.issuableType]: { [this.issuableType]: {
label_ids: issuableLabels, label_ids: labelIds,
}, },
}, },
method: 'put', method: 'put',
url: this.labelsUpdatePath, url: this.labelsUpdatePath,
}) })
.then(({ data }) => this.replaceSelectedLabels(data.labels)) .then(({ data }) => {
this.selectedLabels = data.labels;
})
.catch(() => flash(__('An error occurred while updating labels.'))) .catch(() => flash(__('An error occurred while updating labels.')))
.finally(() => { .finally(() => {
this.labelsSelectInProgress = false; this.isLabelsSelectInProgress = false;
}); });
}, },
}, },
...@@ -76,6 +77,7 @@ export default { ...@@ -76,6 +77,7 @@ export default {
<template> <template>
<labels-select <labels-select
class="block labels js-labels-block" class="block labels js-labels-block"
:allow-label-remove="true"
:allow-label-create="allowLabelCreate" :allow-label-create="allowLabelCreate"
:allow-label-edit="allowLabelEdit" :allow-label-edit="allowLabelEdit"
:allow-multiselect="true" :allow-multiselect="true"
...@@ -86,11 +88,12 @@ export default { ...@@ -86,11 +88,12 @@ export default {
:labels-fetch-path="labelsFetchPath" :labels-fetch-path="labelsFetchPath"
:labels-filter-base-path="projectIssuesPath" :labels-filter-base-path="projectIssuesPath"
:labels-manage-path="labelsManagePath" :labels-manage-path="labelsManagePath"
:labels-select-in-progress="labelsSelectInProgress" :labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels" :selected-labels="selectedLabels"
:variant="$options.sidebar" :variant="$options.sidebar"
data-qa-selector="labels_block" data-qa-selector="labels_block"
@onDropdownClose="handleDropdownClose" @onDropdownClose="handleDropdownClose"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels" @updateSelectedLabels="handleUpdateSelectedLabels"
> >
{{ __('None') }} {{ __('None') }}
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarLabels from './components/labels/sidebar_labels.vue'; import SidebarLabels from './components/labels/sidebar_labels.vue';
...@@ -17,11 +16,9 @@ import createDefaultClient from '~/lib/graphql'; ...@@ -17,11 +16,9 @@ import createDefaultClient from '~/lib/graphql';
import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils'; import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
Vue.use(Translate); Vue.use(Translate);
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(Vuex);
function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-options')) { function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-options')) {
return JSON.parse(sidebarOptEl.innerHTML); return JSON.parse(sidebarOptEl.innerHTML);
...@@ -94,8 +91,6 @@ export function mountSidebarLabels() { ...@@ -94,8 +91,6 @@ export function mountSidebarLabels() {
return false; return false;
} }
const labelsStore = new Vuex.Store(labelsSelectModule());
return new Vue({ return new Vue({
el, el,
provide: { provide: {
...@@ -105,7 +100,6 @@ export function mountSidebarLabels() { ...@@ -105,7 +100,6 @@ export function mountSidebarLabels() {
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels), allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels), initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
}, },
store: labelsStore,
render: createElement => createElement(SidebarLabels), render: createElement => createElement(SidebarLabels),
}); });
} }
......
...@@ -8,8 +8,20 @@ export default { ...@@ -8,8 +8,20 @@ export default {
components: { components: {
GlLabel, GlLabel,
}, },
props: {
disableLabels: {
type: Boolean,
required: false,
default: false,
},
},
computed: { computed: {
...mapState(['selectedLabels', 'allowScopedLabels', 'labelsFilterBasePath']), ...mapState([
'selectedLabels',
'allowLabelRemove',
'allowScopedLabels',
'labelsFilterBasePath',
]),
}, },
methods: { methods: {
labelFilterUrl(label) { labelFilterUrl(label) {
...@@ -42,7 +54,10 @@ export default { ...@@ -42,7 +54,10 @@ export default {
:background-color="label.color" :background-color="label.color"
:target="labelFilterUrl(label)" :target="labelFilterUrl(label)"
:scoped="scopedLabel(label)" :scoped="scopedLabel(label)"
:show-close-button="allowLabelRemove"
:disabled="disableLabels"
tooltip-placement="top" tooltip-placement="top"
@close="$emit('onLabelRemove', label.id)"
/> />
</template> </template>
</div> </div>
......
...@@ -28,6 +28,11 @@ export default { ...@@ -28,6 +28,11 @@ export default {
DropdownValueCollapsed, DropdownValueCollapsed,
}, },
props: { props: {
allowLabelRemove: {
type: Boolean,
required: false,
default: false,
},
allowLabelEdit: { allowLabelEdit: {
type: Boolean, type: Boolean,
required: true, required: true,
...@@ -130,6 +135,7 @@ export default { ...@@ -130,6 +135,7 @@ export default {
mounted() { mounted() {
this.setInitialState({ this.setInitialState({
variant: this.variant, variant: this.variant,
allowLabelRemove: this.allowLabelRemove,
allowLabelEdit: this.allowLabelEdit, allowLabelEdit: this.allowLabelEdit,
allowLabelCreate: this.allowLabelCreate, allowLabelCreate: this.allowLabelCreate,
allowMultiselect: this.allowMultiselect, allowMultiselect: this.allowMultiselect,
...@@ -252,7 +258,10 @@ export default { ...@@ -252,7 +258,10 @@ export default {
:allow-label-edit="allowLabelEdit" :allow-label-edit="allowLabelEdit"
:labels-select-in-progress="labelsSelectInProgress" :labels-select-in-progress="labelsSelectInProgress"
/> />
<dropdown-value> <dropdown-value
:disable-labels="labelsSelectInProgress"
@onLabelRemove="$emit('onLabelRemove', $event)"
>
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" /> <dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
......
...@@ -54,8 +54,5 @@ export const createLabel = ({ state, dispatch }, label) => { ...@@ -54,8 +54,5 @@ export const createLabel = ({ state, dispatch }, label) => {
}); });
}; };
export const replaceSelectedLabels = ({ commit }, selectedLabels) =>
commit(types.REPLACE_SELECTED_LABELS, selectedLabels);
export const updateSelectedLabels = ({ commit }, labels) => export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels }); commit(types.UPDATE_SELECTED_LABELS, { labels });
...@@ -15,7 +15,6 @@ export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE'; ...@@ -15,7 +15,6 @@ export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE';
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY'; export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS'; export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
export const REPLACE_SELECTED_LABELS = 'REPLACE_SELECTED_LABELS';
export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS'; export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS';
export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW'; export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW';
...@@ -57,10 +57,6 @@ export default { ...@@ -57,10 +57,6 @@ export default {
state.labelCreateInProgress = false; state.labelCreateInProgress = false;
}, },
[types.REPLACE_SELECTED_LABELS](state, selectedLabels = []) {
state.selectedLabels = selectedLabels;
},
[types.UPDATE_SELECTED_LABELS](state, { labels }) { [types.UPDATE_SELECTED_LABELS](state, { labels }) {
// Find the label to update from all the labels // Find the label to update from all the labels
// and change `set` prop value to represent their current state. // and change `set` prop value to represent their current state.
......
...@@ -15,6 +15,7 @@ export default () => ({ ...@@ -15,6 +15,7 @@ export default () => ({
// UI Flags // UI Flags
variant: '', variant: '',
allowLabelRemove: false,
allowLabelCreate: false, allowLabelCreate: false,
allowLabelEdit: false, allowLabelEdit: false,
allowScopedLabels: false, allowScopedLabels: false,
......
---
title: Add close button to issue, MR, and epic sidebar labels
merge_request: 42703
author:
type: added
...@@ -30,18 +30,25 @@ There are two types of labels in GitLab: ...@@ -30,18 +30,25 @@ There are two types of labels in GitLab:
## Assign and unassign labels ## Assign and unassign labels
Every issue, merge request and epic can be assigned any number of labels. The labels are > Unassigning labels with the **X** button [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216881) in GitLab 13.5.
Every issue, merge request, and epic can be assigned any number of labels. The labels are
managed in the right sidebar, where you can assign or unassign labels as needed. managed in the right sidebar, where you can assign or unassign labels as needed.
To assign a label to an issue, merge request or epic: To assign or unassign a label:
1. In the **Labels** section of the sidebar, click **Edit**.
1. In the **Assign labels** list, search for labels by typing their names.
You can search repeatedly to add more labels.
The selected labels are marked with a checkmark.
1. Click the labels you want to assign or unassign.
1. To apply your changes to labels, click **X** next to **Assign labels** or anywhere outside the
label section.
1. In the label section of the sidebar, click **Edit**, then: Alternatively, to unassign a label, click the **X** on the label you want to unassign.
- In the list, click the labels you want. Each label is flagged with a checkmark.
- Find labels by entering a search query and clicking search (**{search}**), then
click on them. You can search repeatedly and add more labels.
1. Click **X** or anywhere outside the label section and the labels are applied.
You can also assign a label with the [`/label ~label1 ~label2` quick action](quick_actions.md). You can also assign a label with the `/label` [quick action](quick_actions.md),
remove labels with `/unlabel`, and reassign labels (remove all and assign new ones) with `/relabel`.
## Label management ## Label management
......
...@@ -100,6 +100,10 @@ export default { ...@@ -100,6 +100,10 @@ export default {
} }
} }
}, },
handleLabelRemove(labelId) {
const labelToRemove = [{ id: labelId, set: false }];
this.updateEpicLabels(labelToRemove);
},
handleUpdateSelectedLabels(labels) { handleUpdateSelectedLabels(labels) {
// Iterate over selection and check if labels which were // Iterate over selection and check if labels which were
// either selected or removed aren't leading to same selection // either selected or removed aren't leading to same selection
...@@ -124,6 +128,7 @@ export default { ...@@ -124,6 +128,7 @@ export default {
<template> <template>
<labels-select-vue <labels-select-vue
:allow-label-remove="true"
:allow-label-edit="canUpdate" :allow-label-edit="canUpdate"
:allow-label-create="true" :allow-label-create="true"
:allow-multiselect="true" :allow-multiselect="true"
...@@ -137,6 +142,7 @@ export default { ...@@ -137,6 +142,7 @@ export default {
class="block labels js-labels-block" class="block labels js-labels-block"
@updateSelectedLabels="handleUpdateSelectedLabels" @updateSelectedLabels="handleUpdateSelectedLabels"
@onDropdownClose="handleDropdownClose" @onDropdownClose="handleDropdownClose"
@onLabelRemove="handleLabelRemove"
@toggleCollapse="toggleSidebarRevealLabelsDropdown" @toggleCollapse="toggleSidebarRevealLabelsDropdown"
>{{ __('None') }}</labels-select-vue >{{ __('None') }}</labels-select-vue
> >
......
...@@ -118,6 +118,22 @@ describe('SidebarLabelsComponent', () => { ...@@ -118,6 +118,22 @@ describe('SidebarLabelsComponent', () => {
}); });
}); });
describe('handleLabelRemove', () => {
it('calls action `updateEpicLabels` with the label ID to remove', () => {
const labelIdToRemove = 9;
jest.spyOn(wrapper.vm, 'updateEpicLabels').mockImplementation();
store.state.labels = mockLabels;
wrapper.vm.handleLabelRemove(labelIdToRemove);
expect(wrapper.vm.updateEpicLabels).toHaveBeenCalledWith(
expect.arrayContaining([{ id: labelIdToRemove, set: false }]),
);
});
});
describe('handleUpdateSelectedLabels', () => { describe('handleUpdateSelectedLabels', () => {
const updatingLabel = { const updatingLabel = {
id: 1, id: 1,
......
...@@ -6,9 +6,10 @@ RSpec.describe "Issues > User edits issue", :js do ...@@ -6,9 +6,10 @@ RSpec.describe "Issues > User edits issue", :js do
let_it_be(:project) { create(:project_empty_repo, :public) } let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:project_with_milestones) { create(:project_empty_repo, :public) } let_it_be(:project_with_milestones) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, project: project, author: user, assignees: [user]) } let_it_be(:label_assigned) { create(:label, project: project, title: 'verisimilitude') }
let_it_be(:label_unassigned) { create(:label, project: project, title: 'syzygy') }
let_it_be(:issue) { create(:issue, project: project, author: user, assignees: [user], labels: [label_assigned]) }
let_it_be(:issue_with_milestones) { create(:issue, project: project_with_milestones, author: user, assignees: [user]) } let_it_be(:issue_with_milestones) { create(:issue, project: project_with_milestones, author: user, assignees: [user]) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:milestones) { create_list(:milestone, 25, project: project_with_milestones) } let_it_be(:milestones) { create_list(:milestone, 25, project: project_with_milestones) }
...@@ -103,6 +104,39 @@ RSpec.describe "Issues > User edits issue", :js do ...@@ -103,6 +104,39 @@ RSpec.describe "Issues > User edits issue", :js do
expect(page).not_to have_selector('.gl-spinner') expect(page).not_to have_selector('.gl-spinner')
end end
end end
it 'can add label to issue' do
page.within '.block.labels' do
expect(page).to have_text('verisimilitude')
expect(page).not_to have_text('syzygy')
click_on 'Edit'
wait_for_requests
click_on 'syzygy'
find('.dropdown-header-button').click
wait_for_requests
expect(page).to have_text('verisimilitude')
expect(page).to have_text('syzygy')
end
end
it 'can remove label from issue by clicking on the label `x` button' do
page.within '.block.labels' do
expect(page).to have_text('verisimilitude')
within '.gl-label' do
click_button
end
wait_for_requests
expect(page).not_to have_text('verisimilitude')
end
end
end end
describe 'update assignee' do describe 'update assignee' do
......
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import { import {
mockLabels, mockLabels,
mockRegularLabel, mockRegularLabel,
...@@ -9,17 +8,11 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -9,17 +8,11 @@ import axios from '~/lib/utils/axios_utils';
import SidebarLabels from '~/sidebar/components/labels/sidebar_labels.vue'; import SidebarLabels from '~/sidebar/components/labels/sidebar_labels.vue';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants'; import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('sidebar labels', () => { describe('sidebar labels', () => {
let axiosMock; let axiosMock;
let wrapper; let wrapper;
const store = new Vuex.Store(labelsSelectModule());
const defaultProps = { const defaultProps = {
allowLabelCreate: true, allowLabelCreate: true,
allowLabelEdit: true, allowLabelEdit: true,
...@@ -39,11 +32,9 @@ describe('sidebar labels', () => { ...@@ -39,11 +32,9 @@ describe('sidebar labels', () => {
const mountComponent = () => { const mountComponent = () => {
wrapper = shallowMount(SidebarLabels, { wrapper = shallowMount(SidebarLabels, {
localVue,
provide: { provide: {
...defaultProps, ...defaultProps,
}, },
store,
}); });
}; };
...@@ -81,7 +72,7 @@ describe('sidebar labels', () => { ...@@ -81,7 +72,7 @@ describe('sidebar labels', () => {
}); });
}); });
describe('when labels are changed', () => { describe('when labels are updated', () => {
beforeEach(() => { beforeEach(() => {
mountComponent(); mountComponent();
}); });
...@@ -121,4 +112,24 @@ describe('sidebar labels', () => { ...@@ -121,4 +112,24 @@ describe('sidebar labels', () => {
expect(axiosMock.history.put[0].data).toEqual(JSON.stringify(expected)); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify(expected));
}); });
}); });
describe('when label `x` is clicked', () => {
beforeEach(() => {
mountComponent();
});
it('makes an API call to update labels', async () => {
findLabelsSelect().vm.$emit('onLabelRemove', 27);
await axios.waitForAll();
const expected = {
[defaultProps.issuableType]: {
label_ids: [26, 28, 29],
},
};
expect(axiosMock.history.put[0].data).toEqual(JSON.stringify(expected));
});
});
}); });
...@@ -259,21 +259,6 @@ describe('LabelsSelect Actions', () => { ...@@ -259,21 +259,6 @@ describe('LabelsSelect Actions', () => {
}); });
}); });
describe('replaceSelectedLabels', () => {
it('replaces `state.selectedLabels`', done => {
const selectedLabels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
testAction(
actions.replaceSelectedLabels,
selectedLabels,
state,
[{ type: types.REPLACE_SELECTED_LABELS, payload: selectedLabels }],
[],
done,
);
});
});
describe('updateSelectedLabels', () => { describe('updateSelectedLabels', () => {
it('updates `state.labels` based on provided `labels` param', done => { it('updates `state.labels` based on provided `labels` param', done => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
......
...@@ -152,19 +152,6 @@ describe('LabelsSelect Mutations', () => { ...@@ -152,19 +152,6 @@ describe('LabelsSelect Mutations', () => {
}); });
}); });
describe(`${types.REPLACE_SELECTED_LABELS}`, () => {
it('replaces `state.selectedLabels`', () => {
const state = {
selectedLabels: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }],
};
const newSelectedLabels = [{ id: 2 }, { id: 5 }];
mutations[types.REPLACE_SELECTED_LABELS](state, newSelectedLabels);
expect(state.selectedLabels).toEqual(newSelectedLabels);
});
});
describe(`${types.UPDATE_SELECTED_LABELS}`, () => { describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
......
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