Commit 126e1816 authored by Alper Akgun's avatar Alper Akgun

Merge branch 'revert-016b6e6a' into 'master'

Revert "Merge branch 'jswain_whats_new_tabs' into 'master'"

See merge request gitlab-org/gitlab!49233
parents ad1e207a 92013227
......@@ -2,15 +2,13 @@
import { mapState, mapActions } from 'vuex';
import {
GlDrawer,
GlBadge,
GlIcon,
GlLink,
GlInfiniteScroll,
GlResizeObserverDirective,
GlTabs,
GlTab,
GlBadge,
GlLoadingIcon,
} from '@gitlab/ui';
import SkeletonLoader from './skeleton_loader.vue';
import Feature from './feature.vue';
import Tracking from '~/tracking';
import { getDrawerBodyHeight } from '../utils/get_drawer_body_height';
......@@ -19,13 +17,11 @@ const trackingMixin = Tracking.mixin();
export default {
components: {
GlDrawer,
GlBadge,
GlIcon,
GlLink,
GlInfiniteScroll,
GlTabs,
GlTab,
SkeletonLoader,
Feature,
GlBadge,
GlLoadingIcon,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
......@@ -35,19 +31,11 @@ export default {
storageKey: {
type: String,
required: true,
},
versions: {
type: Array,
required: true,
},
gitlabDotCom: {
type: Boolean,
required: false,
default: false,
default: null,
},
},
computed: {
...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight', 'fetching']),
...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight']),
},
mounted() {
this.openDrawer(this.storageKey);
......@@ -61,25 +49,14 @@ export default {
methods: {
...mapActions(['openDrawer', 'closeDrawer', 'fetchItems', 'setDrawerBodyHeight']),
bottomReached() {
const page = this.pageInfo.nextPage;
if (page) {
this.fetchItems({ page });
if (this.pageInfo.nextPage) {
this.fetchItems(this.pageInfo.nextPage);
}
},
handleResize() {
const height = getDrawerBodyHeight(this.$refs.drawer.$el);
this.setDrawerBodyHeight(height);
},
featuresForVersion(version) {
return this.features.filter(feature => {
return feature.release === parseFloat(version);
});
},
fetchVersion(version) {
if (this.featuresForVersion(version).length === 0) {
this.fetchItems({ version });
}
},
},
};
</script>
......@@ -96,39 +73,64 @@ export default {
<template #header>
<h4 class="page-title gl-my-2">{{ __("What's new at GitLab") }}</h4>
</template>
<template v-if="features.length">
<gl-infinite-scroll
v-if="gitlabDotCom"
v-if="features.length"
:fetched-items="features.length"
:max-list-height="drawerBodyHeight"
class="gl-p-0"
@bottomReached="bottomReached"
>
<template #items>
<feature v-for="feature in features" :key="feature.title" :feature="feature" />
</template>
</gl-infinite-scroll>
<gl-tabs v-else :style="{ height: `${drawerBodyHeight}px` }" class="gl-p-0">
<gl-tab
v-for="(version, index) in versions"
:key="version"
@click="fetchVersion(version)"
>
<template #title>
<span>{{ version }}</span>
<gl-badge v-if="index === 0">{{ __('Your Version') }}</gl-badge>
</template>
<gl-loading-icon v-if="fetching" size="lg" class="text-center" />
<template v-else>
<feature
v-for="feature in featuresForVersion(version)"
<div
v-for="feature in features"
:key="feature.title"
:feature="feature"
class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
>
<gl-link
:href="feature.url"
target="_blank"
class="whats-new-item-title-link"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<h5 class="gl-font-lg">{{ feature.title }}</h5>
</gl-link>
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="package_name in feature.packages"
:key="package_name"
size="sm"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ package_name }}
</gl-badge>
</div>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<img
:alt="feature.title"
:src="feature.image_url"
class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
/>
</gl-link>
<p class="gl-pt-3">{{ feature.body }}</p>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>{{ __('Learn more') }}</gl-link
>
</div>
</template>
</gl-tab>
</gl-tabs>
</template>
</gl-infinite-scroll>
<div v-else class="gl-mt-5">
<skeleton-loader />
<skeleton-loader />
......
<script>
import { GlBadge, GlIcon, GlLink } from '@gitlab/ui';
export default {
components: {
GlBadge,
GlIcon,
GlLink,
},
props: {
feature: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<gl-link
:href="feature.url"
target="_blank"
class="whats-new-item-title-link"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<h5 class="gl-font-lg" data-test-id="feature-title">{{ feature.title }}</h5>
</gl-link>
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="packageName in feature.packages"
:key="packageName"
size="sm"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ packageName }}
</gl-badge>
</div>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<img
:alt="feature.title"
:src="feature.image_url"
class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
/>
</gl-link>
<p class="gl-pt-3">{{ feature.body }}</p>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>{{ __('Learn more') }}</gl-link
>
</div>
</template>
......@@ -10,6 +10,8 @@ export default el => {
if (whatsNewApp) {
store.dispatch('openDrawer');
} else {
const storageKey = getStorageKey(el);
whatsNewApp = new Vue({
el,
store,
......@@ -26,11 +28,7 @@ export default el => {
},
render(createElement) {
return createElement('app', {
props: {
storageKey: getStorageKey(el),
versions: JSON.parse(el.getAttribute('data-versions')),
gitlabDotCom: el.getAttribute('data-gitlab-dot-com'),
},
props: { storageKey },
});
},
});
......
......@@ -13,7 +13,7 @@ export default {
localStorage.setItem(storageKey, JSON.stringify(false));
}
},
fetchItems({ commit, state }, { page, version } = { page: null, version: null }) {
fetchItems({ commit, state }, page) {
if (state.fetching) {
return false;
}
......@@ -24,7 +24,6 @@ export default {
.get('/-/whats_new', {
params: {
page,
version,
},
})
.then(({ data, headers }) => {
......
......@@ -6,32 +6,6 @@
.gl-infinite-scroll-legend {
@include gl-display-none;
}
.gl-tabs {
@include gl-overflow-y-auto;
}
.gl-tabs-nav {
flex-wrap: nowrap;
overflow-x: scroll;
align-items: stretch;
.nav-item {
@include gl-flex-shrink-0;
a {
@include gl-h-full;
line-height: 1.5;
}
}
}
.gl-spinner-container {
@include gl-w-full;
@include gl-absolute;
top: 50%;
transform: translateY(-50%);
}
}
.with-performance-bar .whats-new-drawer {
......
# frozen_string_literal: true
class WhatsNewController < ApplicationController
include Gitlab::Utils::StrongMemoize
skip_before_action :authenticate_user!
before_action :check_feature_flag
before_action :check_valid_page_param, :set_pagination_headers, unless: -> { has_version_param? }
before_action :check_feature_flag, :check_valid_page_param, :set_pagination_headers
feature_category :navigation
def index
respond_to do |format|
format.js do
render json: highlight_items
render json: most_recent_items
end
end
end
......@@ -32,25 +29,15 @@ class WhatsNewController < ApplicationController
params[:page]&.to_i || 1
end
def highlights
strong_memoize(:highlights) do
if has_version_param?
ReleaseHighlight.for_version(version: params[:version])
else
ReleaseHighlight.paginated(page: current_page)
end
end
def most_recent
@most_recent ||= ReleaseHighlight.paginated(page: current_page)
end
def highlight_items
highlights.map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) }
def most_recent_items
most_recent[:items].map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) }
end
def set_pagination_headers
response.set_header('X-Next-Page', highlights.next_page)
end
def has_version_param?
params[:version].present?
response.set_header('X-Next-Page', most_recent[:next_page])
end
end
......@@ -6,14 +6,10 @@ module WhatsNewHelper
end
def whats_new_storage_key
most_recent_version = ReleaseHighlight.versions&.first
most_recent_version = ReleaseHighlight.most_recent_version
return unless most_recent_version
['display-whats-new-notification', most_recent_version].join('-')
end
def whats_new_versions
ReleaseHighlight.versions
end
end
......@@ -3,17 +3,6 @@
class ReleaseHighlight
CACHE_DURATION = 1.hour
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
RELEASE_VERSIONS_IN_A_YEAR = 12
def self.for_version(version:)
index = self.versions.index(version)
return if index.nil?
page = index + 1
self.paginated(page: page)
end
def self.paginated(page: 1)
Rails.cache.fetch(cache_key(page), expires_in: CACHE_DURATION) do
......@@ -21,7 +10,10 @@ class ReleaseHighlight
next if items.nil?
QueryResult.new(items: items, next_page: next_page(current_page: page))
{
items: items,
next_page: next_page(current_page: page)
}
end
end
......@@ -61,25 +53,15 @@ class ReleaseHighlight
next_page if self.file_paths[next_index]
end
def self.most_recent_item_count
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do
self.paginated&.items&.count
def self.most_recent_version
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:release_version', expires_in: CACHE_DURATION) do
self.paginated&.[](:items)&.first&.[]('release')
end
end
def self.versions
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:versions', expires_in: CACHE_DURATION) do
versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path|
/\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".")
end
versions.uniq
end
def self.most_recent_item_count
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do
self.paginated&.[](:items)&.count
end
QueryResult = Struct.new(:items, :next_page, keyword_init: true) do
include Enumerable
delegate :each, to: :items
end
end
......@@ -102,7 +102,7 @@
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if ::Feature.enabled?(:whats_new_drawer, current_user)
#whats-new-app{ data: { storage_key: whats_new_storage_key, versions: whats_new_versions, gitlab_dot_com: Gitlab.dev_env_org_or_com? } }
#whats-new-app{ data: { storage_key: whats_new_storage_key } }
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: user_status_data }
......@@ -31747,9 +31747,6 @@ msgstr ""
msgid "Your U2F device was registered!"
msgstr ""
msgid "Your Version"
msgstr ""
msgid "Your WebAuthn device did not send a valid JSON response."
msgstr ""
......
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlDrawer, GlInfiniteScroll, GlTabs } from '@gitlab/ui';
import { GlDrawer, GlInfiniteScroll } from '@gitlab/ui';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import App from '~/whats_new/components/app.vue';
......@@ -16,18 +16,12 @@ const localVue = createLocalVue();
localVue.use(Vuex);
describe('App', () => {
const propsData = { storageKey: 'storage-key' };
let wrapper;
let store;
let actions;
let state;
let trackingSpy;
let gitlabDotCom = true;
const buildProps = () => ({
storageKey: 'storage-key',
versions: ['3.11', '3.10'],
gitlabDotCom,
});
const buildWrapper = () => {
actions = {
......@@ -51,7 +45,7 @@ describe('App', () => {
wrapper = mount(App, {
localVue,
store,
propsData: buildProps(),
propsData,
directives: {
GlResizeObserver: createMockDirective(),
},
......@@ -59,31 +53,25 @@ describe('App', () => {
};
const findInfiniteScroll = () => wrapper.find(GlInfiniteScroll);
const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached');
const setup = async () => {
beforeEach(async () => {
document.body.dataset.page = 'test-page';
document.body.dataset.namespaceId = 'namespace-840';
trackingSpy = mockTracking('_category_', null, jest.spyOn);
buildWrapper();
wrapper.vm.$store.state.features = [
{ title: 'Whats New Drawer', url: 'www.url.com', release: 3.11 },
];
wrapper.vm.$store.state.features = [{ title: 'Whats New Drawer', url: 'www.url.com' }];
wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT;
await wrapper.vm.$nextTick();
};
});
afterEach(() => {
wrapper.destroy();
unmockTracking();
});
describe('gitlab.com', () => {
beforeEach(() => {
setup();
});
const getDrawer = () => wrapper.find(GlDrawer);
it('contains a drawer', () => {
......@@ -113,7 +101,7 @@ describe('App', () => {
it('renders features when provided via ajax', () => {
expect(actions.fetchItems).toHaveBeenCalled();
expect(wrapper.find('[data-test-id="feature-title"]').text()).toBe('Whats New Drawer');
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
});
it('send an event when feature item is clicked', () => {
......@@ -142,8 +130,6 @@ describe('App', () => {
});
describe('bottomReached', () => {
const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached');
beforeEach(() => {
actions.fetchItems.mockClear();
});
......@@ -152,7 +138,7 @@ describe('App', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: 840 };
emitBottomReached();
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { page: 840 });
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), 840);
});
it('when nextPage does not exist it does not call fetchItems', () => {
......@@ -175,55 +161,4 @@ describe('App', () => {
MOCK_DRAWER_BODY_HEIGHT,
);
});
});
describe('self managed', () => {
const findTabs = () => wrapper.find(GlTabs);
const clickSecondTab = async () => {
const secondTab = wrapper.findAll('.nav-link').at(1);
await secondTab.trigger('click');
await new Promise(resolve => requestAnimationFrame(resolve));
};
beforeEach(() => {
gitlabDotCom = false;
setup();
});
it('renders tabs with drawer body height and content', () => {
const scroll = findInfiniteScroll();
const tabs = findTabs();
expect(scroll.exists()).toBe(false);
expect(tabs.attributes().style).toBe(`height: ${MOCK_DRAWER_BODY_HEIGHT}px;`);
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
});
describe('fetchVersion', () => {
beforeEach(() => {
actions.fetchItems.mockClear();
});
it('when version isnt fetched, clicking a tab calls fetchItems', async () => {
const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
await clickSecondTab();
expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { version: '3.10' });
});
it('when version has been fetched, clicking a tab calls fetchItems', async () => {
wrapper.vm.$store.state.features.push({ title: 'GitLab Stories', release: 3.1 });
await wrapper.vm.$nextTick();
const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
await clickSecondTab();
expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
expect(actions.fetchItems).not.toHaveBeenCalled();
expect(wrapper.find('.tab-pane.active h5').text()).toBe('GitLab Stories');
});
});
});
});
......@@ -41,23 +41,6 @@ describe('whats new actions', () => {
axiosMock.restore();
});
it('passes arguments', () => {
axiosMock.reset();
axiosMock
.onGet('/-/whats_new', { params: { page: 8, version: 40 } })
.replyOnce(200, [{ title: 'GitLab Stories' }]);
testAction(
actions.fetchItems,
{ page: 8, version: 40 },
{},
expect.arrayContaining([
{ type: types.ADD_FEATURES, payload: [{ title: 'GitLab Stories' }] },
]),
);
});
it('if already fetching, does not fetch', () => {
testAction(actions.fetchItems, {}, { fetching: true }, []);
});
......
......@@ -10,7 +10,7 @@ RSpec.describe WhatsNewHelper do
let(:release_item) { double(:item) }
before do
allow(ReleaseHighlight).to receive(:versions).and_return([84.0])
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(84.0)
end
it { is_expected.to eq('display-whats-new-notification-84.0') }
......@@ -18,7 +18,7 @@ RSpec.describe WhatsNewHelper do
context 'when most recent release highlights do NOT exist' do
before do
allow(ReleaseHighlight).to receive(:versions).and_return(nil)
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(nil)
end
it { is_expected.to be_nil }
......@@ -44,14 +44,4 @@ RSpec.describe WhatsNewHelper do
end
end
end
describe '#whats_new_versions' do
let(:versions) { [84.0] }
it 'returns ReleaseHighlight.versions' do
expect(ReleaseHighlight).to receive(:versions).and_return(versions)
expect(helper.whats_new_versions).to eq(versions)
end
end
end
......@@ -3,46 +3,23 @@
require 'spec_helper'
RSpec.describe ReleaseHighlight do
describe '#paginated' do
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
let(:cache_mock) { double(:cache_mock) }
let(:dot_com) { false }
before do
allow(Gitlab).to receive(:com?).and_return(dot_com)
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
allow(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
expect(Rails).to receive(:cache).twice.and_return(cache_mock)
expect(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
end
after do
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
end
describe '.for_version' do
subject { ReleaseHighlight.for_version(version: version) }
let(:version) { '1.1' }
context 'with version param that exists' do
it 'returns items from that version' do
expect(subject.items.first['title']).to eq("It's gonna be a bright")
end
end
context 'with version param that does NOT exist' do
let(:version) { '84.0' }
it 'returns nil' do
expect(subject).to be_nil
end
end
end
describe '.paginated' do
let(:dot_com) { false }
before do
allow(Gitlab).to receive(:com?).and_return(dot_com)
expect(Rails).to receive(:cache).twice.and_return(cache_mock)
end
context 'with page param' do
subject { ReleaseHighlight.paginated(page: page) }
......@@ -113,51 +90,46 @@ RSpec.describe ReleaseHighlight do
end
end
describe '.most_recent_item_count' do
subject { ReleaseHighlight.most_recent_item_count }
describe '.most_recent_version' do
subject { ReleaseHighlight.most_recent_version }
context 'when recent release items exist' do
it 'returns the count from the most recent file' do
allow(ReleaseHighlight).to receive(:paginated).and_return(double(:paginated, items: [double(:item)]))
context 'when version exist' do
let(:release_item) { double(:item) }
expect(subject).to eq(1)
before do
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [release_item] })
allow(release_item).to receive(:[]).with('release').and_return(84.0)
end
it { is_expected.to eq(84.0) }
end
context 'when recent release items do NOT exist' do
it 'returns nil' do
context 'when most recent release highlights do NOT exist' do
before do
allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
expect(subject).to be_nil
end
it { is_expected.to be_nil }
end
end
describe '.versions' do
it 'returns versions from the file paths' do
expect(ReleaseHighlight.versions).to eq(['1.5', '1.2', '1.1'])
end
describe '#most_recent_item_count' do
subject { ReleaseHighlight.most_recent_item_count }
context 'when there are more than 12 versions' do
let(:file_paths) do
i = 0
Array.new(20) { "20201225_01_#{i += 1}.yml" }
end
context 'when recent release items exist' do
it 'returns the count from the most recent file' do
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [double(:item)] })
it 'limits to 12 versions' do
allow(ReleaseHighlight).to receive(:file_paths).and_return(file_paths)
expect(ReleaseHighlight.versions.count).to eq(12)
end
expect(subject).to eq(1)
end
end
describe 'QueryResult' do
subject { ReleaseHighlight::QueryResult.new(items: items, next_page: 2) }
let(:items) { [:item] }
context 'when recent release items do NOT exist' do
it 'returns nil' do
allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
it 'responds to map' do
expect(subject.map(&:to_s)).to eq(items.map(&:to_s))
expect(subject).to be_nil
end
end
end
end
......@@ -4,22 +4,22 @@ require 'spec_helper'
RSpec.describe WhatsNewController do
describe 'whats_new_path' do
let(:item) { double(:item) }
let(:highlights) { double(:highlight, items: [item], map: [item].map, next_page: 2) }
context 'with whats_new_drawer feature enabled' do
before do
stub_feature_flags(whats_new_drawer: true)
end
context 'with no page param' do
let(:most_recent) { { items: [item], next_page: 2 } }
let(:item) { double(:item) }
it 'responds with paginated data and headers' do
allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(highlights)
allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(most_recent)
allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
get whats_new_path, xhr: true
expect(response.body).to eq(highlights.items.to_json)
expect(response.body).to eq(most_recent[:items].to_json)
expect(response.headers['X-Next-Page']).to eq(2)
end
end
......@@ -37,18 +37,6 @@ RSpec.describe WhatsNewController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with version param' do
it 'returns items without pagination headers' do
allow(ReleaseHighlight).to receive(:for_version).with(version: '42').and_return(highlights)
allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
get whats_new_path(version: 42), xhr: true
expect(response.body).to eq(highlights.items.to_json)
expect(response.headers['X-Next-Page']).to be_nil
end
end
end
context 'with whats_new_drawer feature disabled' do
......
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