Massive refactoring

Fix #1231 the filter menu once and for all...
This commit is contained in:
Robert Schäfer 2019-08-10 03:02:08 +02:00
parent 196cced8cf
commit 0e707cdd4c
7 changed files with 86 additions and 283 deletions

View File

@ -20,7 +20,7 @@ describe('FilterMenu.vue', () => {
describe('given a user', () => { describe('given a user', () => {
beforeEach(() => { beforeEach(() => {
propsData = { propsData = {
hashtag: {}, hashtag: null,
} }
}) })
@ -37,7 +37,7 @@ describe('FilterMenu.vue', () => {
}) })
it('renders a card if there are hashtags', () => { it('renders a card if there are hashtags', () => {
propsData.hashtag = { hashtag: 'Frieden' } propsData.hashtag = 'Frieden'
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.is('.ds-card')).toBe(true) expect(wrapper.is('.ds-card')).toBe(true)
}) })

View File

@ -28,7 +28,7 @@
<script> <script>
export default { export default {
props: { props: {
hashtag: { type: Object, default: null }, hashtag: { type: String, default: null },
}, },
methods: { methods: {
clearSearch() { clearSearch() {

View File

@ -3,22 +3,21 @@ import VTooltip from 'v-tooltip'
import Styleguide from '@human-connection/styleguide' import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex' import Vuex from 'vuex'
import FilterPosts from './FilterPosts.vue' import FilterPosts from './FilterPosts.vue'
import FilterPostsMenuItem from './FilterPostsMenuItems.vue'
import { mutations, getters } from '~/store/posts'
const localVue = createLocalVue() const localVue = createLocalVue()
localVue.use(Styleguide) localVue.use(Styleguide)
localVue.use(VTooltip) localVue.use(VTooltip)
localVue.use(Vuex) localVue.use(Vuex)
let mutations
let getters
describe('FilterPosts.vue', () => { describe('FilterPosts.vue', () => {
let wrapper
let mocks let mocks
let propsData let propsData
let menuToggle let menuToggle
let allCategoriesButton let allCategoriesButton
let environmentAndNatureButton let environmentAndNatureButton
let consumptionAndSustainabiltyButton
let democracyAndPoliticsButton let democracyAndPoliticsButton
beforeEach(() => { beforeEach(() => {
@ -49,44 +48,28 @@ describe('FilterPosts.vue', () => {
}) })
describe('mount', () => { describe('mount', () => {
const store = new Vuex.Store({ mutations = {
mutations: { 'postsFilter/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'posts/SET_POSTS': mutations.SET_POSTS, 'postsFilter/RESET_CATEGORIES': jest.fn(),
'posts/SET_FILTERED_BY_CATEGORIES': mutations.SET_FILTERED_BY_CATEGORIES, 'postsFilter/TOGGLE_CATEGORY': jest.fn(),
'posts/SET_FILTERED_BY_FOLLOWERS': mutations.SET_FILTERED_BY_FOLLOWERS,
'posts/SET_USERS_FOLLOWED_FILTER': mutations.SET_USERS_FOLLOWED_FILTER,
'posts/SET_CATEGORIES_FILTER': mutations.SET_CATEGORIES_FILTER,
'posts/SET_SELECTED_CATEGORY_IDS': mutations.SET_SELECTED_CATEGORY_IDS,
},
getters: {
'auth/user': () => {
return { id: 'u34' }
},
'posts/filteredByCategories': getters.filteredByCategories,
'posts/filteredByUsersFollowed': getters.filteredByUsersFollowed,
'posts/usersFollowedFilter': getters.usersFollowedFilter,
'posts/categoriesFilter': getters.categoriesFilter,
'posts/selectedCategoryIds': getters.selectedCategoryIds,
},
})
const Wrapper = () => {
return mount(FilterPosts, { mocks, localVue, propsData, store })
} }
getters = {
beforeEach(() => { 'auth/user': () => {
store.replaceState({ return { id: 'u34' }
filteredByCategories: false, },
filteredByUsersFollowed: false, 'postsFilter/filteredCategoryIds': jest.fn(() => []),
usersFollowedFilter: {}, 'postsFilter/filteredByUsersFollowed': jest.fn(),
categoriesFilter: {}, }
selectedCategoryIds: [], const openFilterPosts = () => {
}) const store = new Vuex.Store({ mutations, getters })
wrapper = Wrapper() const wrapper = mount(FilterPosts, { mocks, localVue, propsData, store })
menuToggle = wrapper.findAll('a').at(0) menuToggle = wrapper.findAll('a').at(0)
menuToggle.trigger('click') menuToggle.trigger('click')
}) return wrapper
}
it('groups the categories by pair', () => { it('groups the categories by pair', () => {
const wrapper = openFilterPosts()
expect(wrapper.vm.chunk).toEqual([ expect(wrapper.vm.chunk).toEqual([
[ [
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' }, { id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
@ -97,95 +80,42 @@ describe('FilterPosts.vue', () => {
}) })
it('starts with all categories button active', () => { it('starts with all categories button active', () => {
const wrapper = openFilterPosts()
allCategoriesButton = wrapper.findAll('button').at(0) allCategoriesButton = wrapper.findAll('button').at(0)
expect(allCategoriesButton.attributes().class).toContain('ds-button-primary') expect(allCategoriesButton.attributes().class).toContain('ds-button-primary')
}) })
it('adds a categories id to selectedCategoryIds when clicked', () => { it('calls TOGGLE_CATEGORY when clicked', () => {
const wrapper = openFilterPosts()
environmentAndNatureButton = wrapper.findAll('button').at(1) environmentAndNatureButton = wrapper.findAll('button').at(1)
environmentAndNatureButton.trigger('click') environmentAndNatureButton.trigger('click')
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem) expect(mutations['postsFilter/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4')
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual(['cat4'])
}) })
it('sets primary to true when the button is clicked', () => { it('sets category button attribute `primary` when corresponding category is filtered', () => {
getters['postsFilter/filteredCategoryIds'] = jest.fn(() => ['cat9'])
const wrapper = openFilterPosts()
democracyAndPoliticsButton = wrapper.findAll('button').at(3) democracyAndPoliticsButton = wrapper.findAll('button').at(3)
democracyAndPoliticsButton.trigger('click')
expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary') expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary')
}) })
it('queries a post by its categories', () => { it('sets "filter-by-followed-authors-only" button attribute `primary`', () => {
consumptionAndSustainabiltyButton = wrapper.findAll('button').at(2) getters['postsFilter/filteredByUsersFollowed'] = jest.fn(() => true)
consumptionAndSustainabiltyButton.trigger('click') const wrapper = openFilterPosts()
expect(mocks.$apollo.query).toHaveBeenCalledWith( expect(
expect.objectContaining({ wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'),
variables: { ).toBe(true)
filter: { categories_some: { id_in: ['cat15'] } },
first: expect.any(Number),
offset: expect.any(Number),
},
}),
)
})
it('supports a query of multiple categories', () => {
environmentAndNatureButton = wrapper.findAll('button').at(1)
environmentAndNatureButton.trigger('click')
consumptionAndSustainabiltyButton = wrapper.findAll('button').at(2)
consumptionAndSustainabiltyButton.trigger('click')
expect(mocks.$apollo.query).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
filter: { categories_some: { id_in: ['cat4', 'cat15'] } },
first: expect.any(Number),
offset: expect.any(Number),
},
}),
)
})
it('toggles the categoryIds when clicked more than once', () => {
environmentAndNatureButton = wrapper.findAll('button').at(1)
environmentAndNatureButton.trigger('click')
environmentAndNatureButton.trigger('click')
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem)
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual([])
}) })
describe('click "filter-by-followed-authors-only" button', () => { describe('click "filter-by-followed-authors-only" button', () => {
let wrapper
beforeEach(() => { beforeEach(() => {
wrapper = openFilterPosts()
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click') wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
}) })
it('calls the filterPost query', () => { it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
expect(mocks.$apollo.query).toHaveBeenCalledWith( expect(mutations['postsFilter/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
expect.objectContaining({
variables: {
filter: { author: { followedBy_some: { id: 'u34' } } },
first: expect.any(Number),
offset: expect.any(Number),
},
}),
)
})
it('toggles filterBubble.author property', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(mocks.$apollo.query).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
filter: {},
first: expect.any(Number),
offset: expect.any(Number),
},
}),
)
})
it("sets the button's class to primary when clicked", async () => {
expect(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'),
).toBe(true)
}) })
}) })
}) })

View File

@ -5,15 +5,14 @@
<ds-icon style="margin: 7px 0px 0px 2px" size="xx-small" name="angle-down" /> <ds-icon style="margin: 7px 0px 0px 2px" size="xx-small" name="angle-down" />
</a> </a>
<template slot="popover"> <template slot="popover">
<filter-posts-menu-items :chunk="chunk" @filterPosts="filterPosts" :user="currentUser" /> <filter-posts-menu-items :chunk="chunk" :user="currentUser" />
</template> </template>
</dropdown> </dropdown>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import Dropdown from '~/components/Dropdown' import Dropdown from '~/components/Dropdown'
import { filterPosts } from '~/graphql/PostQuery.js' import { mapGetters } from 'vuex'
import { mapGetters, mapMutations } from 'vuex'
import FilterPostsMenuItems from '~/components/FilterPosts/FilterPostsMenuItems' import FilterPostsMenuItems from '~/components/FilterPosts/FilterPostsMenuItems'
export default { export default {
@ -26,11 +25,6 @@ export default {
offset: { type: [String, Number] }, offset: { type: [String, Number] },
categories: { type: Array, default: () => [] }, categories: { type: Array, default: () => [] },
}, },
data() {
return {
pageSize: 12,
}
},
computed: { computed: {
...mapGetters({ ...mapGetters({
currentUser: 'auth/user', currentUser: 'auth/user',
@ -39,25 +33,5 @@ export default {
return _.chunk(this.categories, 2) return _.chunk(this.categories, 2)
}, },
}, },
methods: {
...mapMutations({
setPosts: 'posts/SET_POSTS',
}),
filterPosts(filter) {
this.$apollo
.query({
query: filterPosts(this.$i18n),
variables: {
filter,
first: this.pageSize,
offset: 0,
},
})
.then(({ data: { Post } }) => {
this.setPosts(Post)
})
.catch(error => this.$toast.error(error.message))
},
},
} }
</script> </script>

View File

@ -15,8 +15,8 @@
<ds-flex-item width="100%"> <ds-flex-item width="100%">
<ds-button <ds-button
icon="check" icon="check"
@click.stop.prevent="toggleCategory()" @click.stop.prevent="resetCategories"
:primary="!filteredByCategories" :primary="!filteredCategoryIds.length"
/> />
<ds-flex-item> <ds-flex-item>
<label class="category-labels">{{ $t('filter-posts.categories.all') }}</label> <label class="category-labels">{{ $t('filter-posts.categories.all') }}</label>
@ -40,7 +40,7 @@
<ds-flex-item width="100%" class="categories-menu-item"> <ds-flex-item width="100%" class="categories-menu-item">
<ds-button <ds-button
:icon="category.icon" :icon="category.icon"
:primary="isActive(category.id)" :primary="filteredCategoryIds.includes(category.id)"
@click.stop.prevent="toggleCategory(category.id)" @click.stop.prevent="toggleCategory(category.id)"
/> />
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
@ -78,7 +78,7 @@
name="filter-by-followed-authors-only" name="filter-by-followed-authors-only"
icon="user-plus" icon="user-plus"
:primary="filteredByUsersFollowed" :primary="filteredByUsersFollowed"
@click="toggleOnlyFollowed" @click="toggleFilteredByFollowed(user.id)"
/> />
<ds-flex-item> <ds-flex-item>
<label class="follow-label">{{ $t('filter-posts.followers.label') }}</label> <label class="follow-label">{{ $t('filter-posts.followers.label') }}</label>
@ -107,56 +107,16 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
filteredByUsersFollowed: 'posts/filteredByUsersFollowed', filteredCategoryIds: 'postsFilter/filteredCategoryIds',
filteredByCategories: 'posts/filteredByCategories', filteredByUsersFollowed: 'postsFilter/filteredByUsersFollowed',
usersFollowedFilter: 'posts/usersFollowedFilter',
categoriesFilter: 'posts/categoriesFilter',
selectedCategoryIds: 'posts/selectedCategoryIds',
}), }),
}, },
methods: { methods: {
...mapMutations({ ...mapMutations({
setFilteredByFollowers: 'posts/SET_FILTERED_BY_FOLLOWERS', toggleFilteredByFollowed: 'postsFilter/TOGGLE_FILTER_BY_FOLLOWED',
setFilteredByCategories: 'posts/SET_FILTERED_BY_CATEGORIES', resetCategories: 'postsFilter/RESET_CATEGORIES',
setUsersFollowedFilter: 'posts/SET_USERS_FOLLOWED_FILTER', toggleCategory: 'postsFilter/TOGGLE_CATEGORY',
setCategoriesFilter: 'posts/SET_CATEGORIES_FILTER',
setSelectedCategoryIds: 'posts/SET_SELECTED_CATEGORY_IDS',
}), }),
isActive(id) {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1 && this.setFilteredByCategories) {
return true
}
return false
},
toggleCategory(categoryId) {
if (!categoryId) {
this.setSelectedCategoryIds()
} else {
this.setSelectedCategoryIds(categoryId)
}
this.setFilteredByCategories(!!this.selectedCategoryIds.length)
this.setCategoriesFilter(
this.selectedCategoryIds.length
? { categories_some: { id_in: this.selectedCategoryIds } }
: {},
)
this.toggleFilter()
},
toggleOnlyFollowed() {
this.setFilteredByFollowers(!this.filteredByUsersFollowed)
this.setUsersFollowedFilter(
this.filteredByUsersFollowed ? { author: { followedBy_some: { id: this.user.id } } } : {},
)
this.toggleFilter()
},
toggleFilter() {
this.filter = {
...this.usersFollowedFilter,
...this.categoriesFilter,
}
this.$emit('filterPosts', this.filter)
},
}, },
} }
</script> </script>

View File

@ -26,22 +26,10 @@ describe('PostIndex', () => {
beforeEach(() => { beforeEach(() => {
store = new Vuex.Store({ store = new Vuex.Store({
getters: { getters: {
'posts/posts': () => { 'postsFilter/postsFilter': () => ({}),
return [
{
id: 'p23',
name: 'It is a post',
author: {
id: 'u1',
},
},
]
},
'auth/user': () => { 'auth/user': () => {
return { id: 'u23' } return { id: 'u23' }
}, },
'posts/usersFollowedFilter': () => {},
'posts/categoriesFilter': () => {},
}, },
}) })
mocks = { mocks = {
@ -102,12 +90,6 @@ describe('PostIndex', () => {
expect(wrapper.vm.hashtag).toBeNull() expect(wrapper.vm.hashtag).toBeNull()
}) })
it('calls the changeFilterBubble if there are hasthags in the route query', () => {
mocks.$route.query.hashtag = { id: 'hashtag' }
wrapper = Wrapper()
expect(mocks.$apollo.queries.Post.refetch).toHaveBeenCalledTimes(1)
})
describe('mount', () => { describe('mount', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mount(PostIndex, { wrapper = mount(PostIndex, {
@ -125,12 +107,9 @@ describe('PostIndex', () => {
expect(wrapper.vm.sorting).toEqual('createdAt_desc') expect(wrapper.vm.sorting).toEqual('createdAt_desc')
}) })
it('loads more posts when a user clicks on the load more button', () => { it('updates offset when a user clicks on the load more button', () => {
wrapper wrapper.find('.load-more button').trigger('click')
.findAll('button') expect(wrapper.vm.offset).toEqual(12)
.at(2)
.trigger('click')
expect(mocks.$apollo.queries.Post.fetchMore).toHaveBeenCalledTimes(1)
}) })
}) })
}) })

View File

@ -16,7 +16,7 @@
</div> </div>
</ds-flex-item> </ds-flex-item>
<hc-post-card <hc-post-card
v-for="(post, index) in posts" v-for="post in posts"
:key="post.id" :key="post.id"
:post="post" :post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }" :width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
@ -42,7 +42,7 @@ import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import uniqBy from 'lodash/uniqBy' import uniqBy from 'lodash/uniqBy'
import HcPostCard from '~/components/PostCard' import HcPostCard from '~/components/PostCard'
import HcLoadMore from '~/components/LoadMore.vue' import HcLoadMore from '~/components/LoadMore.vue'
import { mapGetters, mapMutations } from 'vuex' import { mapGetters } from 'vuex'
import { filterPosts } from '~/graphql/PostQuery.js' import { filterPosts } from '~/graphql/PostQuery.js'
export default { export default {
@ -54,10 +54,10 @@ export default {
data() { data() {
const { hashtag = null } = this.$route.query const { hashtag = null } = this.$route.query
return { return {
posts: [],
// Initialize your apollo data // Initialize your apollo data
page: 1, offset: 0,
pageSize: 12, pageSize: 12,
filter: {},
hashtag, hashtag,
placeholder: this.$t('sorting.newest'), placeholder: this.$t('sorting.newest'),
selected: this.$t('sorting.newest'), selected: this.$t('sorting.newest'),
@ -91,62 +91,37 @@ export default {
], ],
} }
}, },
mounted() {
if (this.hashtag) {
this.changeFilterBubble({ tags_some: { name: this.hashtag } })
}
},
watch: {
Post(post) {
this.setPosts(this.Post)
},
},
computed: { computed: {
...mapGetters({ ...mapGetters({
currentUser: 'auth/user', postsFilter: 'postsFilter/postsFilter',
posts: 'posts/posts',
usersFollowedFilter: 'posts/usersFollowedFilter',
categoriesFilter: 'posts/categoriesFilter',
}), }),
tags() { finalFilters() {
return this.posts ? this.posts.tags.map(tag => tag.name) : '-' let filter = this.postsFilter
},
offset() {
return (this.page - 1) * this.pageSize
},
},
methods: {
...mapMutations({
setPosts: 'posts/SET_POSTS',
}),
changeFilterBubble(filter) {
if (this.hashtag) { if (this.hashtag) {
filter = { filter = {
...filter, ...filter,
tags_some: { name: this.hashtag }, tags_some: { name: this.hashtag },
} }
} }
this.filter = filter return filter
this.$apollo.queries.Post.refetch()
}, },
},
watch: {
postsFilter() {
this.offset = 0
this.posts = []
},
},
methods: {
toggleOnlySorting(x) { toggleOnlySorting(x) {
this.filter = { this.offset = 0
...this.usersFollowedFilter, this.posts = []
...this.categoriesFilter,
}
this.sortingIcon = x.icons this.sortingIcon = x.icons
this.sorting = x.order this.sorting = x.order
this.$apollo.queries.Post.refetch()
}, },
clearSearch() { clearSearch() {
this.$router.push({ path: '/' }) this.$router.push({ path: '/' })
this.hashtag = null this.hashtag = null
delete this.filter.tags_some
this.changeFilterBubble(this.filter)
},
uniq(items, field = 'id') {
return uniqBy(items, field)
}, },
href(post) { href(post) {
return this.$router.resolve({ return this.$router.resolve({
@ -155,31 +130,12 @@ export default {
}).href }).href
}, },
showMoreContributions() { showMoreContributions() {
// this.page++ this.offset += this.pageSize
// Fetch more data and transform the original result
this.page++
this.$apollo.queries.Post.fetchMore({
variables: {
filter: this.filter,
first: this.pageSize,
offset: this.offset,
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
let output = { Post: this.Post }
output.Post = [...previousResult.Post, ...fetchMoreResult.Post]
return output
},
fetchPolicy: 'cache-and-network',
})
}, },
deletePost(_index, postId) { deletePost(_index, postId) {
this.Post = this.Post.filter(post => { this.posts = this.posts.filter(post => {
return post.id !== postId return post.id !== postId
}) })
// Why "uniq(Post)" is used in the array for list creation?
// Ideal solution here:
// this.Post.splice(index, 1)
}, },
}, },
apollo: { apollo: {
@ -188,16 +144,20 @@ export default {
return filterPosts(this.$i18n) return filterPosts(this.$i18n)
}, },
variables() { variables() {
return { const result = {
filter: { filter: this.finalFilters,
...this.usersFollowedFilter,
...this.categoriesFilter,
...this.filter,
},
first: this.pageSize, first: this.pageSize,
offset: 0, offset: this.offset,
orderBy: this.sorting, orderBy: this.sorting,
} }
return result
},
update({ Post }) {
// TODO: find out why `update` gets called twice initially.
// We have to filter for uniq posts only because we get the same
// result set twice.
const posts = uniqBy([...this.posts, ...Post], 'id')
this.posts = posts
}, },
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
}, },