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

View File

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

View File

@ -3,22 +3,21 @@ import VTooltip from 'v-tooltip'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
import FilterPosts from './FilterPosts.vue'
import FilterPostsMenuItem from './FilterPostsMenuItems.vue'
import { mutations, getters } from '~/store/posts'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(VTooltip)
localVue.use(Vuex)
let mutations
let getters
describe('FilterPosts.vue', () => {
let wrapper
let mocks
let propsData
let menuToggle
let allCategoriesButton
let environmentAndNatureButton
let consumptionAndSustainabiltyButton
let democracyAndPoliticsButton
beforeEach(() => {
@ -49,44 +48,28 @@ describe('FilterPosts.vue', () => {
})
describe('mount', () => {
const store = new Vuex.Store({
mutations: {
'posts/SET_POSTS': mutations.SET_POSTS,
'posts/SET_FILTERED_BY_CATEGORIES': mutations.SET_FILTERED_BY_CATEGORIES,
'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 })
mutations = {
'postsFilter/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'postsFilter/RESET_CATEGORIES': jest.fn(),
'postsFilter/TOGGLE_CATEGORY': jest.fn(),
}
beforeEach(() => {
store.replaceState({
filteredByCategories: false,
filteredByUsersFollowed: false,
usersFollowedFilter: {},
categoriesFilter: {},
selectedCategoryIds: [],
})
wrapper = Wrapper()
getters = {
'auth/user': () => {
return { id: 'u34' }
},
'postsFilter/filteredCategoryIds': jest.fn(() => []),
'postsFilter/filteredByUsersFollowed': jest.fn(),
}
const openFilterPosts = () => {
const store = new Vuex.Store({ mutations, getters })
const wrapper = mount(FilterPosts, { mocks, localVue, propsData, store })
menuToggle = wrapper.findAll('a').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' },
@ -97,95 +80,42 @@ describe('FilterPosts.vue', () => {
})
it('starts with all categories button active', () => {
const wrapper = openFilterPosts()
allCategoriesButton = wrapper.findAll('button').at(0)
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.trigger('click')
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem)
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual(['cat4'])
expect(mutations['postsFilter/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, '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.trigger('click')
expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary')
})
it('queries a post by its categories', () => {
consumptionAndSustainabiltyButton = wrapper.findAll('button').at(2)
consumptionAndSustainabiltyButton.trigger('click')
expect(mocks.$apollo.query).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
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([])
it('sets "filter-by-followed-authors-only" button attribute `primary`', () => {
getters['postsFilter/filteredByUsersFollowed'] = jest.fn(() => true)
const wrapper = openFilterPosts()
expect(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'),
).toBe(true)
})
describe('click "filter-by-followed-authors-only" button', () => {
let wrapper
beforeEach(() => {
wrapper = openFilterPosts()
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
})
it('calls the filterPost query', () => {
expect(mocks.$apollo.query).toHaveBeenCalledWith(
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)
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
expect(mutations['postsFilter/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
})
})
})

View File

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

View File

@ -15,8 +15,8 @@
<ds-flex-item width="100%">
<ds-button
icon="check"
@click.stop.prevent="toggleCategory()"
:primary="!filteredByCategories"
@click.stop.prevent="resetCategories"
:primary="!filteredCategoryIds.length"
/>
<ds-flex-item>
<label class="category-labels">{{ $t('filter-posts.categories.all') }}</label>
@ -40,7 +40,7 @@
<ds-flex-item width="100%" class="categories-menu-item">
<ds-button
:icon="category.icon"
:primary="isActive(category.id)"
:primary="filteredCategoryIds.includes(category.id)"
@click.stop.prevent="toggleCategory(category.id)"
/>
<ds-space margin-bottom="small" />
@ -78,7 +78,7 @@
name="filter-by-followed-authors-only"
icon="user-plus"
:primary="filteredByUsersFollowed"
@click="toggleOnlyFollowed"
@click="toggleFilteredByFollowed(user.id)"
/>
<ds-flex-item>
<label class="follow-label">{{ $t('filter-posts.followers.label') }}</label>
@ -107,56 +107,16 @@ export default {
},
computed: {
...mapGetters({
filteredByUsersFollowed: 'posts/filteredByUsersFollowed',
filteredByCategories: 'posts/filteredByCategories',
usersFollowedFilter: 'posts/usersFollowedFilter',
categoriesFilter: 'posts/categoriesFilter',
selectedCategoryIds: 'posts/selectedCategoryIds',
filteredCategoryIds: 'postsFilter/filteredCategoryIds',
filteredByUsersFollowed: 'postsFilter/filteredByUsersFollowed',
}),
},
methods: {
...mapMutations({
setFilteredByFollowers: 'posts/SET_FILTERED_BY_FOLLOWERS',
setFilteredByCategories: 'posts/SET_FILTERED_BY_CATEGORIES',
setUsersFollowedFilter: 'posts/SET_USERS_FOLLOWED_FILTER',
setCategoriesFilter: 'posts/SET_CATEGORIES_FILTER',
setSelectedCategoryIds: 'posts/SET_SELECTED_CATEGORY_IDS',
toggleFilteredByFollowed: 'postsFilter/TOGGLE_FILTER_BY_FOLLOWED',
resetCategories: 'postsFilter/RESET_CATEGORIES',
toggleCategory: 'postsFilter/TOGGLE_CATEGORY',
}),
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>

View File

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

View File

@ -16,7 +16,7 @@
</div>
</ds-flex-item>
<hc-post-card
v-for="(post, index) in posts"
v-for="post in posts"
:key="post.id"
:post="post"
: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 HcPostCard from '~/components/PostCard'
import HcLoadMore from '~/components/LoadMore.vue'
import { mapGetters, mapMutations } from 'vuex'
import { mapGetters } from 'vuex'
import { filterPosts } from '~/graphql/PostQuery.js'
export default {
@ -54,10 +54,10 @@ export default {
data() {
const { hashtag = null } = this.$route.query
return {
posts: [],
// Initialize your apollo data
page: 1,
offset: 0,
pageSize: 12,
filter: {},
hashtag,
placeholder: 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: {
...mapGetters({
currentUser: 'auth/user',
posts: 'posts/posts',
usersFollowedFilter: 'posts/usersFollowedFilter',
categoriesFilter: 'posts/categoriesFilter',
postsFilter: 'postsFilter/postsFilter',
}),
tags() {
return this.posts ? this.posts.tags.map(tag => tag.name) : '-'
},
offset() {
return (this.page - 1) * this.pageSize
},
},
methods: {
...mapMutations({
setPosts: 'posts/SET_POSTS',
}),
changeFilterBubble(filter) {
finalFilters() {
let filter = this.postsFilter
if (this.hashtag) {
filter = {
...filter,
tags_some: { name: this.hashtag },
}
}
this.filter = filter
this.$apollo.queries.Post.refetch()
return filter
},
},
watch: {
postsFilter() {
this.offset = 0
this.posts = []
},
},
methods: {
toggleOnlySorting(x) {
this.filter = {
...this.usersFollowedFilter,
...this.categoriesFilter,
}
this.offset = 0
this.posts = []
this.sortingIcon = x.icons
this.sorting = x.order
this.$apollo.queries.Post.refetch()
},
clearSearch() {
this.$router.push({ path: '/' })
this.hashtag = null
delete this.filter.tags_some
this.changeFilterBubble(this.filter)
},
uniq(items, field = 'id') {
return uniqBy(items, field)
},
href(post) {
return this.$router.resolve({
@ -155,31 +130,12 @@ export default {
}).href
},
showMoreContributions() {
// this.page++
// 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',
})
this.offset += this.pageSize
},
deletePost(_index, postId) {
this.Post = this.Post.filter(post => {
this.posts = this.posts.filter(post => {
return post.id !== postId
})
// Why "uniq(Post)" is used in the array for list creation?
// Ideal solution here:
// this.Post.splice(index, 1)
},
},
apollo: {
@ -188,16 +144,20 @@ export default {
return filterPosts(this.$i18n)
},
variables() {
return {
filter: {
...this.usersFollowedFilter,
...this.categoriesFilter,
...this.filter,
},
const result = {
filter: this.finalFilters,
first: this.pageSize,
offset: 0,
offset: this.offset,
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',
},