refactor: CategoriesFilter to not use ds-flex

Co-authored-by: mattwr18 <mattwr18@gmail.com>

- introduce LabeledButton component
- rename FilterMenu to HashtagsFilter and FilterPosts to FilterMenu
This commit is contained in:
Alina Beck 2020-03-11 16:43:13 +01:00 committed by mattwr18
parent 33237d8803
commit 1ffde6bf10
23 changed files with 467 additions and 429 deletions

View File

@ -19,3 +19,10 @@ h6,
p { p {
margin: 0; margin: 0;
} }
ul,
ol {
list-style-type: none;
padding: 0;
margin: 0;
}

View File

@ -0,0 +1,125 @@
<template>
<section class="categories-filter">
<h4 class="title">{{ $t('filter-menu.categories.header') }}</h4>
<labeled-button
:filled="!filteredCategoryIds.length"
icon="check"
:label="$t('filter-menu.categories.all')"
@click="resetCategories"
/>
<div class="divider" />
<ul class="categories-list">
<li v-for="category in categories" :key="category.id" class="menu-item">
<labeled-button
:icon="category.icon"
:filled="filteredCategoryIds.includes(category.id)"
:label="$t(`contribution.category.name.${category.slug}`)"
@click="toggleCategory(category.id)"
/>
</li>
</ul>
</section>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
import CategoryQuery from '~/graphql/CategoryQuery.js'
import LabeledButton from '~/components/_new/generic/LabeledButton/LabeledButton'
export default {
components: {
LabeledButton,
},
data() {
return {
categories: [],
}
},
computed: {
...mapGetters({
filteredCategoryIds: 'posts/filteredCategoryIds',
}),
},
methods: {
...mapMutations({
resetCategories: 'posts/RESET_CATEGORIES',
toggleCategory: 'posts/TOGGLE_CATEGORY',
}),
},
apollo: {
Category: {
query() {
return CategoryQuery()
},
update({ Category }) {
if (!Category) return []
this.categories = Category
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>
<style lang="scss">
.categories-filter {
display: flex;
flex-wrap: wrap;
margin-top: $space-small;
> .title {
width: 100%;
}
> .labeled-button {
margin-top: $space-small;
}
> .divider {
border-left: $border-size-base solid $border-color-soft;
margin: $space-base;
}
> .categories-list {
display: flex;
flex-wrap: wrap;
flex-basis: 80%;
flex-grow: 1;
> .menu-item {
width: 12.5%;
margin: $space-small 0;
}
}
@media only screen and (max-width: 800px) {
.categories-list > .menu-item {
width: 16%;
}
}
@media only screen and (max-width: 630px) {
flex-direction: column;
> .categories-list > .menu-item {
width: 25%;
margin: $space-x-small 0;
}
> .title {
text-align: center;
}
> .divider {
border-top: $border-size-base solid $border-color-soft;
margin: $space-small;
}
}
@media only screen and (max-width: 440px) {
.categories-list > .menu-item {
width: 50%;
}
}
}
</style>

View File

@ -1,42 +1,166 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Vuex from 'vuex'
import FilterMenu from './FilterMenu.vue' import FilterMenu from './FilterMenu.vue'
import locales from '~/locales'
import orderBy from 'lodash/orderBy'
const localVue = global.localVue const localVue = global.localVue
let mutations
let getters
const languages = orderBy(locales, 'name')
describe('FilterMenu.vue', () => { describe('FilterMenu.vue', () => {
let wrapper
let mocks let mocks
let propsData let propsData
let menuToggle
let allCategoriesButton
let environmentAndNatureButton
let democracyAndPoliticsButton
let happyEmotionButton
let englishButton
let spanishButton
beforeEach(() => { beforeEach(() => {
mocks = { $t: () => {} } mocks = {
$apollo: {
query: jest
.fn()
.mockResolvedValueOnce({
data: { Post: { title: 'Post with Category', category: [{ id: 'cat4' }] } },
})
.mockRejectedValue({ message: 'We were unable to filter' }),
},
$t: jest.fn(),
$i18n: {
locale: () => 'en',
},
$toast: {
error: jest.fn(),
},
}
propsData = {
categories: [
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
{ id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart' },
{ id: 'cat9', name: 'Democracy & Politics', icon: 'university' },
],
}
}) })
describe('given a hashtag', () => { describe('mount', () => {
beforeEach(() => { mutations = {
propsData = { 'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
hashtag: 'Frieden', 'posts/RESET_CATEGORIES': jest.fn(),
} 'posts/TOGGLE_CATEGORY': jest.fn(),
'posts/TOGGLE_EMOTION': jest.fn(),
'posts/TOGGLE_LANGUAGE': jest.fn(),
'posts/RESET_LANGUAGES': jest.fn(),
}
getters = {
'posts/isActive': () => false,
'auth/isModerator': () => false,
'auth/user': () => {
return { id: 'u34' }
},
'posts/filteredCategoryIds': jest.fn(() => []),
'posts/filteredByUsersFollowed': jest.fn(),
'posts/filteredByEmotions': jest.fn(() => []),
'posts/filteredLanguageCodes': jest.fn(() => []),
}
const openFilterMenu = () => {
const store = new Vuex.Store({ mutations, getters })
const wrapper = mount(FilterMenu, { mocks, localVue, propsData, store })
menuToggle = wrapper.findAll('button').at(0)
menuToggle.trigger('click')
return wrapper
}
it('groups the categories by pair', () => {
const wrapper = openFilterMenu()
expect(wrapper.vm.chunk).toEqual([
[
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
{ id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart' },
],
[{ id: 'cat9', name: 'Democracy & Politics', icon: 'university' }],
])
}) })
describe('mount', () => { it('starts with all categories button active', () => {
const Wrapper = () => { const wrapper = openFilterMenu()
return mount(FilterMenu, { mocks, localVue, propsData }) allCategoriesButton = wrapper.findAll('button').at(1)
} expect(allCategoriesButton.attributes().class).toContain('--filled')
})
it('calls TOGGLE_CATEGORY when clicked', () => {
const wrapper = openFilterMenu()
environmentAndNatureButton = wrapper.findAll('button').at(2)
environmentAndNatureButton.trigger('click')
expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4')
})
it('calls TOGGLE_LANGUAGE when clicked', () => {
const wrapper = openFilterMenu()
englishButton = wrapper
.findAll('button.language-buttons')
.at(languages.findIndex(l => l.code === 'en'))
englishButton.trigger('click')
expect(mutations['posts/TOGGLE_LANGUAGE']).toHaveBeenCalledWith({}, 'en')
})
it('sets category button attribute `filled` when corresponding category is filtered', () => {
getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
const wrapper = openFilterMenu()
democracyAndPoliticsButton = wrapper.findAll('button').at(4)
expect(democracyAndPoliticsButton.attributes().class).toContain('--filled')
})
it('sets language button attribute `filled` when corresponding language is filtered', () => {
getters['posts/filteredLanguageCodes'] = jest.fn(() => ['es'])
const wrapper = openFilterMenu()
spanishButton = wrapper
.findAll('button.language-buttons')
.at(languages.findIndex(l => l.code === 'es'))
expect(spanishButton.attributes().class).toContain('--filled')
})
it('sets "filter-by-followed" button attribute `filled`', () => {
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
const wrapper = openFilterMenu()
expect(wrapper.find('.base-button[data-test="filter-by-followed"]').classes('--filled')).toBe(
true,
)
})
describe('click "filter-by-followed" button', () => {
let wrapper
beforeEach(() => { beforeEach(() => {
wrapper = Wrapper() wrapper = openFilterMenu()
wrapper.find('.base-button[data-test="filter-by-followed"]').trigger('click')
}) })
it('renders a card', () => { it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
wrapper = Wrapper() expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
expect(wrapper.is('.base-card')).toBe(true) })
})
describe('click on an "emotions-buttons" button', () => {
it('calls TOGGLE_EMOTION when clicked', () => {
const wrapper = openFilterMenu()
happyEmotionButton = wrapper.findAll('.emotion-button .base-button').at(1)
happyEmotionButton.trigger('click')
expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
}) })
describe('click clear search button', () => { it('sets the attribute `src` to colorized image', () => {
it('emits clearSearch', () => { getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
wrapper.find('.base-button').trigger('click') const wrapper = openFilterMenu()
expect(wrapper.emitted().clearSearch).toHaveLength(1) happyEmotionButton = wrapper.findAll('.emotion-button .base-button').at(1)
}) const happyEmotionButtonImage = happyEmotionButton.find('img')
expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg')
}) })
}) })
}) })

View File

@ -1,36 +1,46 @@
<template> <template>
<base-card class="filter-menu"> <dropdown ref="menu" :placement="placement" :offset="offset">
<h2>{{ $t('filter-menu.hashtag-search', { hashtag }) }}</h2>
<base-button <base-button
icon="close" slot="default"
circle icon="filter"
:title="this.$t('filter-menu.clearSearch')" :filled="filterActive"
@click="clearSearch" :ghost="!filterActive"
/> slot-scope="{ toggleMenu }"
</base-card> @click.prevent="toggleMenu()"
>
<base-icon class="dropdown-arrow" name="angle-down" />
</base-button>
<template slot="popover">
<ds-container>
<categories-filter />
<general-filter />
<language-filter />
</ds-container>
</template>
</dropdown>
</template> </template>
<script> <script>
import Dropdown from '~/components/Dropdown'
import { mapGetters } from 'vuex'
import CategoriesFilter from './CategoriesFilter'
import GeneralFilter from './GeneralFilter'
import LanguageFilter from './LanguageFilter'
export default { export default {
props: { components: {
hashtag: { Dropdown,
type: String, CategoriesFilter,
required: true, GeneralFilter,
}, LanguageFilter,
}, },
methods: { props: {
clearSearch() { placement: { type: String },
this.$emit('clearSearch') offset: { type: [String, Number] },
}, },
computed: {
...mapGetters({
filterActive: 'posts/isActive',
}),
}, },
} }
</script> </script>
<style lang="scss">
.filter-menu.base-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: $space-x-small $space-base;
}
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<ds-space> <ds-space>
<ds-flex id="filter-posts-by-followers-header"> <ds-flex id="filter-menu-by-followers-header">
<ds-heading tag="h4">{{ $t('filter-posts.general.header') }}</ds-heading> <ds-heading tag="h4">{{ $t('filter-menu.general.header') }}</ds-heading>
<ds-space margin-bottom="large" /> <ds-space margin-bottom="large" />
</ds-flex> </ds-flex>
<ds-flex :gutter="{ lg: 'large' }"> <ds-flex :gutter="{ lg: 'large' }">
@ -14,14 +14,14 @@
icon="user-plus" icon="user-plus"
circle circle
:filled="filteredByUsersFollowed" :filled="filteredByUsersFollowed"
@click="toggleFilteredByFollowed(user.id)" @click="toggleFilteredByFollowed(currentUser.id)"
v-tooltip="{ v-tooltip="{
content: this.$t('contribution.filterFollow'), content: this.$t('contribution.filterFollow'),
placement: 'left', placement: 'left',
delay: { show: 500 }, delay: { show: 500 },
}" }"
/> />
<label class="follow-label">{{ $t('filter-posts.followers.label') }}</label> <label class="follow-label">{{ $t('filter-menu.followers.label') }}</label>
</ds-flex-item> </ds-flex-item>
<emotion-button <emotion-button
v-for="emotion in emotionsArray" v-for="emotion in emotionsArray"
@ -42,9 +42,6 @@ export default {
components: { components: {
EmotionButton, EmotionButton,
}, },
props: {
user: { type: Object, required: true },
},
data() { data() {
return { return {
emotionsArray: ['funny', 'happy', 'surprised', 'cry', 'angry'], emotionsArray: ['funny', 'happy', 'surprised', 'cry', 'angry'],
@ -54,6 +51,7 @@ export default {
...mapGetters({ ...mapGetters({
filteredByUsersFollowed: 'posts/filteredByUsersFollowed', filteredByUsersFollowed: 'posts/filteredByUsersFollowed',
filteredByEmotions: 'posts/filteredByEmotions', filteredByEmotions: 'posts/filteredByEmotions',
currentUser: 'auth/user',
}), }),
}, },
methods: { methods: {
@ -71,11 +69,11 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss">
#filter-posts-header { #filter-menu-header {
display: block; display: block;
} }
#filter-posts-by-followers-header { #filter-menu-by-followers-header {
display: block; display: block;
} }
@ -92,7 +90,7 @@ export default {
} }
@media only screen and (max-width: 960px) { @media only screen and (max-width: 960px) {
#filter-posts-header { #filter-menu-header {
text-align: center; text-align: center;
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<ds-space margin-top="large"> <ds-space margin-top="large">
<ds-flex id="filter-posts-header"> <ds-flex id="filter-menu-header">
<ds-heading tag="h4">{{ $t('filter-posts.language.header') }}</ds-heading> <ds-heading tag="h4">{{ $t('filter-menu.language.header') }}</ds-heading>
<ds-space margin-bottom="large" /> <ds-space margin-bottom="large" />
</ds-flex> </ds-flex>
<ds-flex :gutter="{ lg: 'small' }"> <ds-flex :gutter="{ lg: 'small' }">
@ -19,7 +19,7 @@
@click="resetLanguages" @click="resetLanguages"
/> />
<ds-flex-item> <ds-flex-item>
<label class="language-labels">{{ $t('filter-posts.language.all') }}</label> <label class="language-labels">{{ $t('filter-menu.language.all') }}</label>
</ds-flex-item> </ds-flex-item>
<ds-space /> <ds-space />
</ds-flex-item> </ds-flex-item>
@ -62,9 +62,6 @@ import orderBy from 'lodash/orderBy'
import { mapGetters, mapMutations } from 'vuex' import { mapGetters, mapMutations } from 'vuex'
export default { export default {
props: {
chunk: { type: Array, default: () => [] },
},
computed: { computed: {
...mapGetters({ ...mapGetters({
filteredLanguageCodes: 'posts/filteredLanguageCodes', filteredLanguageCodes: 'posts/filteredLanguageCodes',

View File

@ -1,103 +0,0 @@
<template>
<ds-space margin-top="large">
<ds-flex id="filter-posts-header">
<ds-heading tag="h4">{{ $t('filter-posts.categories.header') }}</ds-heading>
<ds-space margin-bottom="large" />
</ds-flex>
<ds-flex :gutter="{ lg: 'small' }">
<ds-flex-item
:width="{ base: '100%', sm: '100%', md: '100%', lg: '5%' }"
class="categories-menu-item"
>
<ds-flex>
<ds-flex-item width="10%" />
<ds-flex-item width="100%">
<base-button
circle
icon="check"
@click="resetCategories"
:filled="!filteredCategoryIds.length"
/>
<ds-flex-item>
<label class="category-labels">{{ $t('filter-posts.categories.all') }}</label>
</ds-flex-item>
<ds-space />
</ds-flex-item>
</ds-flex>
</ds-flex-item>
<ds-flex-item :width="{ base: '0%', sm: '0%', md: '0%', lg: '4%' }" />
<ds-flex-item
:width="{ base: '0%', sm: '0%', md: '0%', lg: '3%' }"
id="categories-menu-divider"
/>
<ds-flex-item
:width="{ base: '50%', sm: '50%', md: '50%', lg: '11%' }"
v-for="index in chunk.length"
:key="index"
>
<ds-flex v-for="category in chunk[index - 1]" :key="category.id" class="categories-menu">
<ds-flex class="categories-menu">
<ds-flex-item width="100%" class="categories-menu-item">
<base-button
circle
:icon="category.icon"
:filled="filteredCategoryIds.includes(category.id)"
@click="toggleCategory(category.id)"
/>
<ds-space margin-bottom="small" />
</ds-flex-item>
<ds-flex>
<ds-flex-item class="categories-menu-item">
<label class="category-labels">
{{ $t(`contribution.category.name.${category.slug}`) }}
</label>
</ds-flex-item>
<ds-space margin-bottom="xx-large" />
</ds-flex>
</ds-flex>
</ds-flex>
</ds-flex-item>
</ds-flex>
</ds-space>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
export default {
props: {
chunk: { type: Array, default: () => [] },
},
computed: {
...mapGetters({
filteredCategoryIds: 'posts/filteredCategoryIds',
}),
},
methods: {
...mapMutations({
resetCategories: 'posts/RESET_CATEGORIES',
toggleCategory: 'posts/TOGGLE_CATEGORY',
}),
},
}
</script>
<style lang="scss">
.categories-menu-item {
text-align: center;
}
.categories-menu {
justify-content: center;
}
.category-labels,
.follow-label {
font-size: $font-size-small;
}
@media only screen and (min-width: 960px) {
#categories-menu-divider {
border-left: 1px solid $border-color-soft;
margin: 9px 0px 40px 0px;
}
}
</style>

View File

@ -1,167 +0,0 @@
import { mount } from '@vue/test-utils'
import Vuex from 'vuex'
import FilterPosts from './FilterPosts.vue'
import locales from '~/locales'
import orderBy from 'lodash/orderBy'
const localVue = global.localVue
let mutations
let getters
const languages = orderBy(locales, 'name')
describe('FilterPosts.vue', () => {
let mocks
let propsData
let menuToggle
let allCategoriesButton
let environmentAndNatureButton
let democracyAndPoliticsButton
let happyEmotionButton
let englishButton
let spanishButton
beforeEach(() => {
mocks = {
$apollo: {
query: jest
.fn()
.mockResolvedValueOnce({
data: { Post: { title: 'Post with Category', category: [{ id: 'cat4' }] } },
})
.mockRejectedValue({ message: 'We were unable to filter' }),
},
$t: jest.fn(),
$i18n: {
locale: () => 'en',
},
$toast: {
error: jest.fn(),
},
}
propsData = {
categories: [
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
{ id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart' },
{ id: 'cat9', name: 'Democracy & Politics', icon: 'university' },
],
}
})
describe('mount', () => {
mutations = {
'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'posts/RESET_CATEGORIES': jest.fn(),
'posts/TOGGLE_CATEGORY': jest.fn(),
'posts/TOGGLE_EMOTION': jest.fn(),
'posts/TOGGLE_LANGUAGE': jest.fn(),
'posts/RESET_LANGUAGES': jest.fn(),
}
getters = {
'posts/isActive': () => false,
'auth/isModerator': () => false,
'auth/user': () => {
return { id: 'u34' }
},
'posts/filteredCategoryIds': jest.fn(() => []),
'posts/filteredByUsersFollowed': jest.fn(),
'posts/filteredByEmotions': jest.fn(() => []),
'posts/filteredLanguageCodes': jest.fn(() => []),
}
const openFilterPosts = () => {
const store = new Vuex.Store({ mutations, getters })
const wrapper = mount(FilterPosts, { mocks, localVue, propsData, store })
menuToggle = wrapper.findAll('button').at(0)
menuToggle.trigger('click')
return wrapper
}
it('groups the categories by pair', () => {
const wrapper = openFilterPosts()
expect(wrapper.vm.chunk).toEqual([
[
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
{ id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart' },
],
[{ id: 'cat9', name: 'Democracy & Politics', icon: 'university' }],
])
})
it('starts with all categories button active', () => {
const wrapper = openFilterPosts()
allCategoriesButton = wrapper.findAll('button').at(1)
expect(allCategoriesButton.attributes().class).toContain('--filled')
})
it('calls TOGGLE_CATEGORY when clicked', () => {
const wrapper = openFilterPosts()
environmentAndNatureButton = wrapper.findAll('button').at(2)
environmentAndNatureButton.trigger('click')
expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4')
})
it('calls TOGGLE_LANGUAGE when clicked', () => {
const wrapper = openFilterPosts()
englishButton = wrapper
.findAll('button.language-buttons')
.at(languages.findIndex((l) => l.code === 'en'))
englishButton.trigger('click')
expect(mutations['posts/TOGGLE_LANGUAGE']).toHaveBeenCalledWith({}, 'en')
})
it('sets category button attribute `filled` when corresponding category is filtered', () => {
getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
const wrapper = openFilterPosts()
democracyAndPoliticsButton = wrapper.findAll('button').at(4)
expect(democracyAndPoliticsButton.attributes().class).toContain('--filled')
})
it('sets language button attribute `filled` when corresponding language is filtered', () => {
getters['posts/filteredLanguageCodes'] = jest.fn(() => ['es'])
const wrapper = openFilterPosts()
spanishButton = wrapper
.findAll('button.language-buttons')
.at(languages.findIndex((l) => l.code === 'es'))
expect(spanishButton.attributes().class).toContain('--filled')
})
it('sets "filter-by-followed" button attribute `filled`', () => {
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
const wrapper = openFilterPosts()
expect(wrapper.find('.base-button[data-test="filter-by-followed"]').classes('--filled')).toBe(
true,
)
})
describe('click "filter-by-followed" button', () => {
let wrapper
beforeEach(() => {
wrapper = openFilterPosts()
wrapper.find('.base-button[data-test="filter-by-followed"]').trigger('click')
})
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
})
})
describe('click on an "emotions-buttons" button', () => {
it('calls TOGGLE_EMOTION when clicked', () => {
const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('.emotion-button .base-button').at(1)
happyEmotionButton.trigger('click')
expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
})
it('sets the attribute `src` to colorized image', () => {
getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('.emotion-button .base-button').at(1)
const happyEmotionButtonImage = happyEmotionButton.find('img')
expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg')
})
})
})
})

View File

@ -1,52 +0,0 @@
<template>
<dropdown ref="menu" :placement="placement" :offset="offset">
<template #default="{ toggleMenu }">
<base-button
icon="filter"
:filled="filterActive"
:ghost="!filterActive"
@click.prevent="toggleMenu()"
>
<base-icon class="dropdown-arrow" name="angle-down" />
</base-button>
</template>
<template slot="popover">
<ds-container>
<categories-filter-menu-items :chunk="chunk" />
<general-filter-menu-items :user="currentUser" />
<language-filter-menu-items :user="currentUser" />
</ds-container>
</template>
</dropdown>
</template>
<script>
import { chunk } from 'lodash'
import Dropdown from '~/components/Dropdown'
import { mapGetters } from 'vuex'
import CategoriesFilterMenuItems from './CategoriesFilterMenuItems'
import GeneralFilterMenuItems from './GeneralFilterMenuItems'
import LanguageFilterMenuItems from './LanguageFilterMenuItems'
export default {
components: {
Dropdown,
CategoriesFilterMenuItems,
GeneralFilterMenuItems,
LanguageFilterMenuItems,
},
props: {
placement: { type: String },
offset: { type: [String, Number] },
categories: { type: Array, default: () => [] },
},
computed: {
...mapGetters({
currentUser: 'auth/user',
filterActive: 'posts/isActive',
}),
chunk() {
return chunk(this.categories, 2)
},
},
}
</script>

View File

@ -0,0 +1,43 @@
import { mount } from '@vue/test-utils'
import FilterCard from './FilterCard.vue'
const localVue = global.localVue
describe('FilterCard.vue', () => {
let wrapper
let mocks
let propsData
beforeEach(() => {
mocks = { $t: () => {} }
})
describe('given a hashtag', () => {
beforeEach(() => {
propsData = {
hashtag: 'Frieden',
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(FilterCard, { mocks, localVue, propsData })
}
beforeEach(() => {
wrapper = Wrapper()
})
it('renders a card', () => {
wrapper = Wrapper()
expect(wrapper.is('.base-card')).toBe(true)
})
describe('click clear search button', () => {
it('emits clearSearch', () => {
wrapper.find('.base-button').trigger('click')
expect(wrapper.emitted().clearSearch).toHaveLength(1)
})
})
})
})
})

View File

@ -0,0 +1,36 @@
<template>
<base-card class="hashtags-filter">
<h2>{{ $t('hashtags-filter.hashtag-search', { hashtag }) }}</h2>
<base-button
icon="close"
circle
:title="this.$t('hashtags-filter.clearSearch')"
@click="clearSearch"
/>
</base-card>
</template>
<script>
export default {
props: {
hashtag: {
type: String,
required: true,
},
},
methods: {
clearSearch() {
this.$emit('clearSearch')
},
},
}
</script>
<style lang="scss">
.hashtags-filter.base-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: $space-x-small $space-base;
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<div class="labeled-button">
<base-button circle :icon="icon" :filled="filled" @click="event => $emit('click', event)" />
<label class="label">{{ label }}</label>
</div>
</template>
<script>
export default {
props: {
filled: {
type: Boolean,
default: false,
},
icon: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
},
}
</script>
<style lang="scss">
.labeled-button {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 $space-x-small;
> .label {
margin-top: $space-x-small;
font-size: $font-size-small;
text-align: center;
}
}
</style>

View File

@ -30,12 +30,7 @@
style="flex-grow: 0; flex-basis: auto;" style="flex-grow: 0; flex-basis: auto;"
> >
<client-only> <client-only>
<filter-posts <filter-menu v-show="showFilterMenuDropdown" placement="top-start" offset="8" />
v-show="showFilterPostsDropdown"
placement="top-start"
offset="8"
:categories="categories"
/>
</client-only> </client-only>
</ds-flex-item> </ds-flex-item>
<ds-flex-item <ds-flex-item
@ -85,8 +80,7 @@ import SearchField from '~/components/features/SearchField/SearchField.vue'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu' import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
import seo from '~/mixins/seo' import seo from '~/mixins/seo'
import FilterPosts from '~/components/FilterPosts/FilterPosts.vue' import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import CategoryQuery from '~/graphql/CategoryQuery.js'
import PageFooter from '~/components/PageFooter/PageFooter' import PageFooter from '~/components/PageFooter/PageFooter'
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu' import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
@ -97,7 +91,7 @@ export default {
Modal, Modal,
NotificationMenu, NotificationMenu,
AvatarMenu, AvatarMenu,
FilterPosts, FilterMenu,
PageFooter, PageFooter,
}, },
mixins: [seo], mixins: [seo],
@ -105,36 +99,22 @@ export default {
return { return {
mobileSearchVisible: false, mobileSearchVisible: false,
toggleMobileMenu: false, toggleMobileMenu: false,
categories: [],
} }
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
isLoggedIn: 'auth/isLoggedIn', isLoggedIn: 'auth/isLoggedIn',
}), }),
showFilterPostsDropdown() { showFilterMenuDropdown() {
const [firstRoute] = this.$route.matched const [firstRoute] = this.$route.matched
return firstRoute && firstRoute.name === 'index' return firstRoute && firstRoute.name === 'index'
}, },
}, },
watch: {
Category(category) {
this.categories = category || []
},
},
methods: { methods: {
toggleMobileMenuView() { toggleMobileMenuView() {
this.toggleMobileMenu = !this.toggleMobileMenu this.toggleMobileMenu = !this.toggleMobileMenu
}, },
}, },
apollo: {
Category: {
query() {
return CategoryQuery()
},
fetchPolicy: 'cache-and-network',
},
},
} }
</script> </script>

View File

@ -331,12 +331,12 @@
"post-not-found": "Dieser Beitrag konnte nicht gefunden werden", "post-not-found": "Dieser Beitrag konnte nicht gefunden werden",
"profile-not-found": "Dieses Profil konnte nicht gefunden werden" "profile-not-found": "Dieses Profil konnte nicht gefunden werden"
}, },
"filter-menu": { "hashtags-filter": {
"clearSearch": "Suche löschen", "clearSearch": "Suche löschen",
"hashtag-search": "Suche nach #{hashtag}", "hashtag-search": "Suche nach #{hashtag}",
"title": "Deine Filterblase" "title": "Deine Filterblase"
}, },
"filter-posts": { "filter-menu": {
"categories": { "categories": {
"all": "Alle", "all": "Alle",
"header": "Themenkategorien" "header": "Themenkategorien"

View File

@ -331,12 +331,12 @@
"post-not-found": "This post could not be found", "post-not-found": "This post could not be found",
"profile-not-found": "This profile could not be found" "profile-not-found": "This profile could not be found"
}, },
"filter-menu": { "hashtags-filter": {
"clearSearch": "Clear search", "clearSearch": "Clear search",
"hashtag-search": "Searching for #{hashtag}", "hashtag-search": "Searching for #{hashtag}",
"title": "Your filter bubble" "title": "Your filter bubble"
}, },
"filter-posts": { "filter-menu": {
"categories": { "categories": {
"all": "All", "all": "All",
"header": "Categories of Content" "header": "Categories of Content"

View File

@ -329,12 +329,12 @@
"post-not-found": "Esta contribución no se pudo encontrar", "post-not-found": "Esta contribución no se pudo encontrar",
"profile-not-found": "Este perfil no se pudo encontrar" "profile-not-found": "Este perfil no se pudo encontrar"
}, },
"filter-menu": { "hashtags-filter": {
"clearSearch": "Borrar búsqueda", "clearSearch": "Borrar búsqueda",
"hashtag-search": "Buscando a #{hashtag}", "hashtag-search": "Buscando a #{hashtag}",
"title": "Su burbuja de filtro" "title": "Su burbuja de filtro"
}, },
"filter-posts": { "filter-menu": {
"categories": { "categories": {
"all": "Todas", "all": "Todas",
"header": "Categorías de contenido" "header": "Categorías de contenido"

View File

@ -318,12 +318,12 @@
}, },
"placeholder": "Écrivez quelque chose d'inspirant..." "placeholder": "Écrivez quelque chose d'inspirant..."
}, },
"filter-menu": { "hashtags-filter": {
"clearSearch": "Réinitialiser la recherche", "clearSearch": "Réinitialiser la recherche",
"hashtag-search": "Recherche de #{hashtag}", "hashtag-search": "Recherche de #{hashtag}",
"title": "Votre bulle de filtre" "title": "Votre bulle de filtre"
}, },
"filter-posts": { "filter-menu": {
"categories": { "categories": {
"all": "Toutes", "all": "Toutes",
"header": "Catégories de contenu" "header": "Catégories de contenu"

View File

@ -323,12 +323,12 @@
}, },
"placeholder": "" "placeholder": ""
}, },
"filter-menu": { "hashtags-filter": {
"clearSearch": "", "clearSearch": "",
"hashtag-search": "", "hashtag-search": "",
"title": "" "title": ""
}, },
"filter-posts": { "filter-menu": {
"categories": { "categories": {
"all": "", "all": "",
"header": "" "header": ""

View File

@ -160,7 +160,7 @@
"editor": { "editor": {
"placeholder": "Napisz coś inspirującego..." "placeholder": "Napisz coś inspirującego..."
}, },
"filter-menu": { "hashtags-filter": {
"title": "Twoja bańka filtrująca" "title": "Twoja bańka filtrująca"
}, },
"followButton": { "followButton": {

View File

@ -314,12 +314,12 @@
}, },
"placeholder": " Escreva algo inspirador…" "placeholder": " Escreva algo inspirador…"
}, },
"filter-menu": { "hashtags-filter": {
"clearSearch": "Limpar pesquisa", "clearSearch": "Limpar pesquisa",
"hashtag-search": "Procurando por #{hashtag}", "hashtag-search": "Procurando por #{hashtag}",
"title": "Sua bolha de filtro" "title": "Sua bolha de filtro"
}, },
"filter-posts": { "filter-menu": {
"categories": { "categories": {
"all": "Todos", "all": "Todos",
"header": "Categorias de Conteúdo" "header": "Categorias de Conteúdo"

View File

@ -329,12 +329,12 @@
"post-not-found": "Этот пост не удалось найти", "post-not-found": "Этот пост не удалось найти",
"profile-not-found": "Этот профиль не удалось найти" "profile-not-found": "Этот профиль не удалось найти"
}, },
"filter-menu": { "hashtags-filter": {
"clearSearch": "Очистить поиск", "clearSearch": "Очистить поиск",
"hashtag-search": "Поиск по #{hashtag}", "hashtag-search": "Поиск по #{hashtag}",
"title": "Ваш фильтр пузыря" "title": "Ваш фильтр пузыря"
}, },
"filter-posts": { "filter-menu": {
"categories": { "categories": {
"all": "Все", "all": "Все",
"header": "Категории" "header": "Категории"

View File

@ -1,7 +1,7 @@
import { config, shallowMount, mount } from '@vue/test-utils' import { config, shallowMount, mount } from '@vue/test-utils'
import PostIndex from './index.vue' import PostIndex from './index.vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import FilterMenu from '~/components/FilterMenu/FilterMenu' import FilterCard from '~/components/FilterCard/FilterCard'
const localVue = global.localVue const localVue = global.localVue
@ -106,7 +106,7 @@ describe('PostIndex', () => {
it('clears the search when the filter menu emits clearSearch', () => { it('clears the search when the filter menu emits clearSearch', () => {
mocks.$route.query.hashtag = '#samplehashtag' mocks.$route.query.hashtag = '#samplehashtag'
wrapper = Wrapper() wrapper = Wrapper()
wrapper.find(FilterMenu).vm.$emit('clearSearch') wrapper.find(FilterCard).vm.$emit('clearSearch')
expect(wrapper.vm.hashtag).toBeNull() expect(wrapper.vm.hashtag).toBeNull()
}) })

View File

@ -2,7 +2,7 @@
<div> <div>
<masonry-grid> <masonry-grid>
<ds-grid-item v-if="hashtag" :row-span="2" column-span="fullWidth"> <ds-grid-item v-if="hashtag" :row-span="2" column-span="fullWidth">
<filter-menu :hashtag="hashtag" @clearSearch="clearSearch" /> <hashtags-filter :hashtag="hashtag" @clearSearch="clearSearch" />
</ds-grid-item> </ds-grid-item>
<ds-grid-item :row-span="2" column-span="fullWidth" class="top-info-bar"> <ds-grid-item :row-span="2" column-span="fullWidth" class="top-info-bar">
<!--<donation-info /> --> <!--<donation-info /> -->
@ -65,7 +65,7 @@
<script> <script>
// import DonationInfo from '~/components/DonationInfo/DonationInfo.vue' // import DonationInfo from '~/components/DonationInfo/DonationInfo.vue'
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue' import HashtagsFilter from '~/components/HashtagsFilter/HashtagsFilter.vue'
import HcEmpty from '~/components/Empty/Empty' import HcEmpty from '~/components/Empty/Empty'
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue' import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue' import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
@ -78,7 +78,7 @@ import UpdateQuery from '~/components/utils/UpdateQuery'
export default { export default {
components: { components: {
// DonationInfo, // DonationInfo,
FilterMenu, HashtagsFilter,
PostTeaser, PostTeaser,
HcEmpty, HcEmpty,
MasonryGrid, MasonryGrid,